From d5d642493433facc6723ca0ecfba6c690b4ffa74 Mon Sep 17 00:00:00 2001 From: Lauren Qurashi Date: Wed, 22 Nov 2023 10:28:57 +0000 Subject: [PATCH 001/118] Add reference_documents app --- reference_documents/__init__.py | 0 reference_documents/admin.py | 1 + reference_documents/apps.py | 6 ++++++ reference_documents/migrations/__init__.py | 0 reference_documents/models.py | 1 + reference_documents/tests.py | 1 + reference_documents/urls.py | 0 reference_documents/views.py | 1 + 8 files changed, 10 insertions(+) create mode 100644 reference_documents/__init__.py create mode 100644 reference_documents/admin.py create mode 100644 reference_documents/apps.py create mode 100644 reference_documents/migrations/__init__.py create mode 100644 reference_documents/models.py create mode 100644 reference_documents/tests.py create mode 100644 reference_documents/urls.py create mode 100644 reference_documents/views.py diff --git a/reference_documents/__init__.py b/reference_documents/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/reference_documents/admin.py b/reference_documents/admin.py new file mode 100644 index 000000000..846f6b406 --- /dev/null +++ b/reference_documents/admin.py @@ -0,0 +1 @@ +# Register your models here. diff --git a/reference_documents/apps.py b/reference_documents/apps.py new file mode 100644 index 000000000..c8e519933 --- /dev/null +++ b/reference_documents/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class ReferenceDocumentsConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "reference_documents" diff --git a/reference_documents/migrations/__init__.py b/reference_documents/migrations/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/reference_documents/models.py b/reference_documents/models.py new file mode 100644 index 000000000..6b2021999 --- /dev/null +++ b/reference_documents/models.py @@ -0,0 +1 @@ +# Create your models here. diff --git a/reference_documents/tests.py b/reference_documents/tests.py new file mode 100644 index 000000000..a39b155ac --- /dev/null +++ b/reference_documents/tests.py @@ -0,0 +1 @@ +# Create your tests here. diff --git a/reference_documents/urls.py b/reference_documents/urls.py new file mode 100644 index 000000000..e69de29bb diff --git a/reference_documents/views.py b/reference_documents/views.py new file mode 100644 index 000000000..60f00ef0e --- /dev/null +++ b/reference_documents/views.py @@ -0,0 +1 @@ +# Create your views here. From 3de7ad2a45178900937d74bb881c25b5512afa1c Mon Sep 17 00:00:00 2001 From: Lauren Qurashi Date: Wed, 22 Nov 2023 15:05:30 +0000 Subject: [PATCH 002/118] WIP - Templates >:( --- .../jinja2/reference_documents/list.jinja | 3 +++ reference_documents/urls.py | 11 +++++++++++ reference_documents/views.py | 5 +++++ urls.py | 3 ++- 4 files changed, 21 insertions(+), 1 deletion(-) create mode 100644 reference_documents/jinja2/reference_documents/list.jinja diff --git a/reference_documents/jinja2/reference_documents/list.jinja b/reference_documents/jinja2/reference_documents/list.jinja new file mode 100644 index 000000000..0c88cf04e --- /dev/null +++ b/reference_documents/jinja2/reference_documents/list.jinja @@ -0,0 +1,3 @@ +{% extends "layouts/list.jinja" %} + +

hello!

diff --git a/reference_documents/urls.py b/reference_documents/urls.py index e69de29bb..181144c8a 100644 --- a/reference_documents/urls.py +++ b/reference_documents/urls.py @@ -0,0 +1,11 @@ +from django.urls import path + +from reference_documents import views + +urlpatterns = [ + path( + "reference-documents/", + views.ReferenceDocumentsListView.as_view(), + name="reference_documents-ui-list", + ), +] diff --git a/reference_documents/views.py b/reference_documents/views.py index 60f00ef0e..fe94cd938 100644 --- a/reference_documents/views.py +++ b/reference_documents/views.py @@ -1 +1,6 @@ # Create your views here. +from django.views.generic import TemplateView + + +class ReferenceDocumentsListView(TemplateView): + template_name = "reference_documents/list.jinja" diff --git a/urls.py b/urls.py index 7e6de7866..c9063d278 100644 --- a/urls.py +++ b/urls.py @@ -1,5 +1,5 @@ """ -tamato URL Configuration. +Tamato URL Configuration. The `urlpatterns` list routes URLs to views. For more information please see: https://docs.djangoproject.com/en/3.0/topics/http/urls/ @@ -33,6 +33,7 @@ path("", include("measures.urls")), path("", include("publishing.urls", namespace="publishing")), path("", include("quotas.urls")), + path("", include("reference_documents.urls")), path("", include("regulations.urls")), path("", include("reports.urls")), path("", include("workbaskets.urls", namespace="workbaskets")), From 60a332bb50369a00c0f144fb55697a27f24f9401 Mon Sep 17 00:00:00 2001 From: Lauren Qurashi Date: Wed, 22 Nov 2023 16:44:28 +0000 Subject: [PATCH 003/118] Get a template of some sort working --- reference_documents/jinja2/reference_documents/list.jinja | 2 +- settings/common.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/reference_documents/jinja2/reference_documents/list.jinja b/reference_documents/jinja2/reference_documents/list.jinja index 0c88cf04e..8f915b5bd 100644 --- a/reference_documents/jinja2/reference_documents/list.jinja +++ b/reference_documents/jinja2/reference_documents/list.jinja @@ -1,3 +1,3 @@ -{% extends "layouts/list.jinja" %} +

hello!

diff --git a/settings/common.py b/settings/common.py index f0ef2cb83..68e33e0b1 100644 --- a/settings/common.py +++ b/settings/common.py @@ -124,6 +124,7 @@ "publishing", "taric", "workbaskets", + "reference_documents", "exporter.apps.ExporterConfig", "crispy_forms", "crispy_forms_gds", From 6e60fba8bccc305308ccbe238f960e235a728b5e Mon Sep 17 00:00:00 2001 From: Lauren Qurashi Date: Fri, 1 Dec 2023 15:33:32 +0000 Subject: [PATCH 004/118] Add reference document detail path and templates --- .../jinja2/includes/tabs/core_data.jinja | 31 ++++++++++++++ .../jinja2/includes/tabs/tariff_quotas.jinja | 0 .../jinja2/reference_documents/detail.jinja | 41 +++++++++++++++++++ .../jinja2/reference_documents/list.jinja | 36 +++++++++++++++- reference_documents/urls.py | 5 +++ reference_documents/views.py | 38 +++++++++++++++++ 6 files changed, 150 insertions(+), 1 deletion(-) create mode 100644 reference_documents/jinja2/includes/tabs/core_data.jinja create mode 100644 reference_documents/jinja2/includes/tabs/tariff_quotas.jinja create mode 100644 reference_documents/jinja2/reference_documents/detail.jinja diff --git a/reference_documents/jinja2/includes/tabs/core_data.jinja b/reference_documents/jinja2/includes/tabs/core_data.jinja new file mode 100644 index 000000000..487fcff6b --- /dev/null +++ b/reference_documents/jinja2/includes/tabs/core_data.jinja @@ -0,0 +1,31 @@ +{% from "components/table/macro.njk" import govukTable %} +{% from 'macros/create_link.jinja' import create_link %} + +{%- set table_rows = [] -%} +{% for measure in ref_doc.measure_list %} + {{ table_rows.append([ + {"text": create_link(url("commodity-ui-detail", kwargs={"sid": measure.goods_nomenclature.sid}), measure.goods_nomenclature.item_id) if measure.goods_nomenclature else '-'}, + {"text": measure.duty_sentence}, + ]) or "" }} +{% endfor %} + +
+
+ {{ govukTable({ + "head": [ + {"text": "Commodity code"}, + {"text": "Preferential duty rates"}, + ], + "rows": table_rows + }) }} +
+ +
+ +
+
diff --git a/reference_documents/jinja2/includes/tabs/tariff_quotas.jinja b/reference_documents/jinja2/includes/tabs/tariff_quotas.jinja new file mode 100644 index 000000000..e69de29bb diff --git a/reference_documents/jinja2/reference_documents/detail.jinja b/reference_documents/jinja2/reference_documents/detail.jinja new file mode 100644 index 000000000..dda4bcc16 --- /dev/null +++ b/reference_documents/jinja2/reference_documents/detail.jinja @@ -0,0 +1,41 @@ +{% extends "layouts/layout.jinja" %} + +{% from "components/table/macro.njk" import govukTable %} +{% from "components/tabs/macro.njk" import govukTabs %} + +{% set page_title = "Preferential duty rates" %} + +{% set core_data_tab_html %}{% include "includes/tabs/core_data.jinja" %}{% endset %} +{% set tariff_quotas_html %}{% include "includes/tabs/tariff_quotas.jinja" %}{% endset %} + +{% set tabs = { + "items": [ + { + "label": "Preferential duty rates", + "id": "core-data", + "panel": { + "html": core_data_tab_html + } + }, + { + "label": "Tariff quotas", + "id": "tariff-quotas", + "panel": { + "html": tariff_quotas_html + } + }, + ] + }%} + +{% block content %} +

{{ page_title }}

+

Title:

+

{{ref_doc.name}}

+

Version:

+

{{ref_doc.version}}

+

Date published:

+

{{ref_doc.date_published}}

+ + {{ govukTabs(tabs) }} + +{% endblock %} diff --git a/reference_documents/jinja2/reference_documents/list.jinja b/reference_documents/jinja2/reference_documents/list.jinja index 8f915b5bd..70a49fa35 100644 --- a/reference_documents/jinja2/reference_documents/list.jinja +++ b/reference_documents/jinja2/reference_documents/list.jinja @@ -1,3 +1,37 @@ +{% extends "layouts/layout.jinja" %} +{% from "components/table/macro.njk" import govukTable %} -

hello!

+{% set page_title = "View reference documents" %} + +{% block content %} +

{{ page_title }}

+ + {%- set table_rows = [] -%} + {% for ref_doc in object_list %} + {% set ref_doc_link -%} + {{ ref_doc.name }} + {%- endset %} + {{ table_rows.append([ + {"html": ref_doc_link}, + {"text": ref_doc.version}, + {"text": ref_doc.date_published}, + {"text": ref_doc.regulation_id }, + {"text": ref_doc.geo_area_id }, + ]) or "" }} + {% endfor %} + + + + {{ govukTable({ + "head": [ + {"text": "Reference document"}, + {"text": "Version"}, + {"text": "Date published"}, + {"text": "Regulation ID"}, + {"text": "Geo area ID"}, + ], + "rows": table_rows +}) }} + +{% endblock %} diff --git a/reference_documents/urls.py b/reference_documents/urls.py index 181144c8a..3c823912b 100644 --- a/reference_documents/urls.py +++ b/reference_documents/urls.py @@ -8,4 +8,9 @@ views.ReferenceDocumentsListView.as_view(), name="reference_documents-ui-list", ), + path( + f"reference-documents/albania/", + views.ReferenceDocumentsDetailView.as_view(), + name="reference_documents-ui-detail", + ), ] diff --git a/reference_documents/views.py b/reference_documents/views.py index fe94cd938..dd72a486a 100644 --- a/reference_documents/views.py +++ b/reference_documents/views.py @@ -1,6 +1,44 @@ # Create your views here. +from datetime import date + from django.views.generic import TemplateView +from geo_areas.models import GeographicalArea +from measures.models import Measure + class ReferenceDocumentsListView(TemplateView): template_name = "reference_documents/list.jinja" + + def get_context_data(self, *args, **kwargs): + context = super().get_context_data(*args, **kwargs) + context["object_list"] = [ + { + "name": "The Albania Preferential Tariff", + "version": 1.4, + "date_published": date.today().strftime("%d %b %Y"), + "regulation_id": "TBC", + "geo_area_id": GeographicalArea.objects.get(area_id="AL").area_id, + }, + ] + return context + + +class ReferenceDocumentsDetailView(TemplateView): + template_name = "reference_documents/detail.jinja" + + def get_context_data(self, *args, **kwargs): + context = super().get_context_data(*args, **kwargs) + + context["ref_doc"] = { + "name": "The Albania Preferential Tariff", + "version": 1.4, + "date_published": date.today().strftime("%d %b %Y"), + "regulation_id": "TBC", + "geo_area_id": GeographicalArea.objects.get(area_id="AL").area_id, + "measure_list": Measure.objects.filter( + measure_type__sid="142", + geographical_area__area_id="AL", + )[:10], + } + return context From 6bcfc51ea6240feb0af8d997ae83d2402f5abc8c Mon Sep 17 00:00:00 2001 From: Lauren Qurashi Date: Tue, 5 Dec 2023 11:29:57 +0000 Subject: [PATCH 005/118] add date filters to the get_context_data function --- reference_documents/views.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/reference_documents/views.py b/reference_documents/views.py index dd72a486a..62e73d533 100644 --- a/reference_documents/views.py +++ b/reference_documents/views.py @@ -1,6 +1,7 @@ # Create your views here. from datetime import date +from django.db.models import Q from django.views.generic import TemplateView from geo_areas.models import GeographicalArea @@ -16,7 +17,7 @@ def get_context_data(self, *args, **kwargs): { "name": "The Albania Preferential Tariff", "version": 1.4, - "date_published": date.today().strftime("%d %b %Y"), + "date_published": date(2023, 4, 12).strftime("%d %b %Y"), "regulation_id": "TBC", "geo_area_id": GeographicalArea.objects.get(area_id="AL").area_id, }, @@ -30,15 +31,20 @@ class ReferenceDocumentsDetailView(TemplateView): def get_context_data(self, *args, **kwargs): context = super().get_context_data(*args, **kwargs) + date_filter_query = Q(valid_between__contains=date(2023, 4, 12)) | Q( + valid_between__startswith__lt=date(2023, 4, 12), + ) + context["ref_doc"] = { "name": "The Albania Preferential Tariff", "version": 1.4, - "date_published": date.today().strftime("%d %b %Y"), + "date_published": date(2023, 4, 12).strftime("%d %b %Y"), "regulation_id": "TBC", "geo_area_id": GeographicalArea.objects.get(area_id="AL").area_id, "measure_list": Measure.objects.filter( + date_filter_query, measure_type__sid="142", geographical_area__area_id="AL", - )[:10], + )[:20], } return context From 512ff93078dd958ee301925dc03a1e2d86803ba3 Mon Sep 17 00:00:00 2001 From: Lauren Qurashi Date: Fri, 8 Dec 2023 13:45:16 +0000 Subject: [PATCH 006/118] WiP --- reference_documents/views.py | 39 +++++++++++++++++++++++++++++++----- 1 file changed, 34 insertions(+), 5 deletions(-) diff --git a/reference_documents/views.py b/reference_documents/views.py index 62e73d533..8ca11becf 100644 --- a/reference_documents/views.py +++ b/reference_documents/views.py @@ -6,6 +6,7 @@ from geo_areas.models import GeographicalArea from measures.models import Measure +from quotas.models import QuotaDefinition class ReferenceDocumentsListView(TemplateView): @@ -28,6 +29,31 @@ def get_context_data(self, *args, **kwargs): class ReferenceDocumentsDetailView(TemplateView): template_name = "reference_documents/detail.jinja" + def get_quota_data(self): + # Get all the order numbers from measures with type 143, and location of Albania + quotas = [] + for measure in Measure.objects.filter( + measure_type__sid="143", + geographical_area__area_id="AL", + ).exclude(order_number=None)[:20]: + quotas.append(measure.order_number) + + # For each order number.. + current_definitions = [] + for order_number in quotas: + for definition in QuotaDefinition.objects.filter( + valid_between__contains=date.today(), + order_number=order_number, + ): + current_definitions.append( + { + "order_number": order_number, + "definition_start_date": definition.valid_between.lower.year, + "definition_end_date": definition.valid_between.upper.year, + }, + ) + return current_definitions + def get_context_data(self, *args, **kwargs): context = super().get_context_data(*args, **kwargs) @@ -35,16 +61,19 @@ def get_context_data(self, *args, **kwargs): valid_between__startswith__lt=date(2023, 4, 12), ) + measure_list = Measure.objects.filter( + date_filter_query, + measure_type__sid="142", + geographical_area__area_id="AL", + )[:20] + context["ref_doc"] = { "name": "The Albania Preferential Tariff", "version": 1.4, "date_published": date(2023, 4, 12).strftime("%d %b %Y"), "regulation_id": "TBC", "geo_area_id": GeographicalArea.objects.get(area_id="AL").area_id, - "measure_list": Measure.objects.filter( - date_filter_query, - measure_type__sid="142", - geographical_area__area_id="AL", - )[:20], + "measure_list": measure_list, + "quotas": self.get_quota_data(), } return context From e668d14257a41c687abb5a53dd555f4f432f4eb3 Mon Sep 17 00:00:00 2001 From: Lauren Qurashi Date: Wed, 13 Dec 2023 13:01:15 +0000 Subject: [PATCH 007/118] Rough commit - working tabs - need cleaning up --- .../jinja2/includes/tabs/core_data.jinja | 2 +- .../jinja2/includes/tabs/tariff_quotas.jinja | 43 ++++++++++ reference_documents/views.py | 86 +++++++++++-------- 3 files changed, 96 insertions(+), 35 deletions(-) diff --git a/reference_documents/jinja2/includes/tabs/core_data.jinja b/reference_documents/jinja2/includes/tabs/core_data.jinja index 487fcff6b..45b5c088a 100644 --- a/reference_documents/jinja2/includes/tabs/core_data.jinja +++ b/reference_documents/jinja2/includes/tabs/core_data.jinja @@ -2,7 +2,7 @@ {% from 'macros/create_link.jinja' import create_link %} {%- set table_rows = [] -%} -{% for measure in ref_doc.measure_list %} +{% for measure in ref_doc.pref_duty_measure_list %} {{ table_rows.append([ {"text": create_link(url("commodity-ui-detail", kwargs={"sid": measure.goods_nomenclature.sid}), measure.goods_nomenclature.item_id) if measure.goods_nomenclature else '-'}, {"text": measure.duty_sentence}, diff --git a/reference_documents/jinja2/includes/tabs/tariff_quotas.jinja b/reference_documents/jinja2/includes/tabs/tariff_quotas.jinja index e69de29bb..a4248e9c4 100644 --- a/reference_documents/jinja2/includes/tabs/tariff_quotas.jinja +++ b/reference_documents/jinja2/includes/tabs/tariff_quotas.jinja @@ -0,0 +1,43 @@ +{% from "components/table/macro.njk" import govukTable %} +{% from 'macros/create_link.jinja' import create_link %} + +{%- set table_rows = [] -%} +{% for quota in ref_doc.quotas %} + {{ table_rows.append([ + {"text": quota["order_number"].structure_code}, + {"text": quota["order_number"].is_origin_quota}, + {"text": quota["comm_codes"]}, + {"text": "-"}, + {"text": quota["definition"].volume}, + {"text": quota["definition"].valid_between.upper}, + {"text": quota["definition"]["measurement_unit"].description}, + + ]) or "" }} +{% endfor %} + +
+
+ {{ govukTable({ + "head": [ + {"text": "Quota order no."}, + {"text": "Origin quota"}, + {"text": "Commodity codes"}, + {"text": "Quote duty rate"}, + {"text": "Volume"}, + {"text": "Quote period closed"}, + {"text": "Unit"}, + + ], + "rows": table_rows + }) }} +
+ +
+ +
+
diff --git a/reference_documents/views.py b/reference_documents/views.py index 8ca11becf..08c27d9a9 100644 --- a/reference_documents/views.py +++ b/reference_documents/views.py @@ -6,7 +6,6 @@ from geo_areas.models import GeographicalArea from measures.models import Measure -from quotas.models import QuotaDefinition class ReferenceDocumentsListView(TemplateView): @@ -29,51 +28,70 @@ def get_context_data(self, *args, **kwargs): class ReferenceDocumentsDetailView(TemplateView): template_name = "reference_documents/detail.jinja" - def get_quota_data(self): - # Get all the order numbers from measures with type 143, and location of Albania - quotas = [] - for measure in Measure.objects.filter( - measure_type__sid="143", - geographical_area__area_id="AL", - ).exclude(order_number=None)[:20]: - quotas.append(measure.order_number) - - # For each order number.. - current_definitions = [] - for order_number in quotas: - for definition in QuotaDefinition.objects.filter( - valid_between__contains=date.today(), - order_number=order_number, - ): - current_definitions.append( - { - "order_number": order_number, - "definition_start_date": definition.valid_between.lower.year, - "definition_end_date": definition.valid_between.upper.year, - }, - ) - return current_definitions - - def get_context_data(self, *args, **kwargs): - context = super().get_context_data(*args, **kwargs) + def get_pref_duty_rates(self): + """Returns a list of measures associated with the Albania Preferential + Tariff.""" + # Measures with type 142, for Albania, Valid up to and including 12th April 2023 date_filter_query = Q(valid_between__contains=date(2023, 4, 12)) | Q( valid_between__startswith__lt=date(2023, 4, 12), ) - - measure_list = Measure.objects.filter( + pref_duty_measure_list = Measure.objects.filter( date_filter_query, measure_type__sid="142", geographical_area__area_id="AL", )[:20] + return pref_duty_measure_list + + def get_tariff_quota_data(self): + """Returns a dict of quota order numbers, and their linked definitions + that are associated with the Albania Preferential Tariff.""" + # Measures with type 143, for Albania, with descriptions that are valid for 2023 only. + + # measures + albanian_measures = ( + Measure.objects.filter( + measure_type__sid="143", + geographical_area__area_id="AL", + ) + .exclude(order_number=None) + .order_by("-valid_between")[:30] + ) + + # order_numbers of measures + albanian_order_numbers = [] + for measure in albanian_measures: + albanian_order_numbers.append(measure.order_number) + + # remove the duplicates + albanian_order_numbers = list(dict.fromkeys(albanian_order_numbers)) + + quotas = [] + + for order_number in albanian_order_numbers: + comm_codes = [] + for measure in albanian_measures: + if measure.order_number == order_number: + comm_codes.append(measure.goods_nomenclature.id) + + quotas.append({"order_number": order_number, "comm_codes": comm_codes}) + + # Get the current definition for each order number in the quotas list + for quota in quotas: + for definition in quota["order_number"].definitions.current(): + if definition.valid_between.upper.year == 2023: + quota["definition"] = definition + return quotas + + def get_context_data(self, *args, **kwargs): + context = super().get_context_data(*args, **kwargs) + context["ref_doc"] = { "name": "The Albania Preferential Tariff", "version": 1.4, "date_published": date(2023, 4, 12).strftime("%d %b %Y"), - "regulation_id": "TBC", - "geo_area_id": GeographicalArea.objects.get(area_id="AL").area_id, - "measure_list": measure_list, - "quotas": self.get_quota_data(), + "pref_duty_measure_list": self.get_pref_duty_rates(), + "quotas": self.get_tariff_quota_data(), } return context From b8b73db81bf40508c4454a95fd6aac1e3f2e1690 Mon Sep 17 00:00:00 2001 From: Lauren Qurashi Date: Wed, 13 Dec 2023 13:12:17 +0000 Subject: [PATCH 008/118] Add button to home form --- common/forms.py | 3 ++- common/views.py | 2 ++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/common/forms.py b/common/forms.py index e651f16c7..b51023f98 100644 --- a/common/forms.py +++ b/common/forms.py @@ -251,6 +251,7 @@ class HMRCCDSManagerActions(TextChoices): class CommonUserActions(TextChoices): SEARCH = "SEARCH", "Search the tariff" # Change this to be dependent on permissions later + VIEW_REF_DOCS = "VIEW_REF_DOCS", "View reference documents" class ImportUserActions(TextChoices): @@ -276,7 +277,7 @@ def __init__(self, *args, **kwargs): choices += CommonUserActions.choices if self.user.has_perm("common.add_trackedmodel") or self.user.has_perm( - "common.change_trackedmodel" + "common.change_trackedmodel", ): choices += ImportUserActions.choices diff --git a/common/views.py b/common/views.py index e7f162bf4..18e4e1edd 100644 --- a/common/views.py +++ b/common/views.py @@ -69,6 +69,8 @@ def form_valid(self, form): return redirect(reverse("search-page")) elif form.cleaned_data["workbasket_action"] == "IMPORT": return redirect(reverse("commodity_importer-ui-list")) + elif form.cleaned_data["workbasket_action"] == "VIEW_REF_DOCS": + return redirect(reverse("reference_documents-ui-list")) class SearchPageView(TemplateView): From 93adf29462aa265882e5dae0dd1c1bc47c3911d3 Mon Sep 17 00:00:00 2001 From: Lauren Qurashi Date: Mon, 18 Dec 2023 15:45:59 +0000 Subject: [PATCH 009/118] Add comm code links to table --- .../jinja2/includes/tabs/tariff_quotas.jinja | 19 ++++++++++++++----- reference_documents/views.py | 2 +- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/reference_documents/jinja2/includes/tabs/tariff_quotas.jinja b/reference_documents/jinja2/includes/tabs/tariff_quotas.jinja index a4248e9c4..3c5cebd93 100644 --- a/reference_documents/jinja2/includes/tabs/tariff_quotas.jinja +++ b/reference_documents/jinja2/includes/tabs/tariff_quotas.jinja @@ -3,13 +3,22 @@ {%- set table_rows = [] -%} {% for quota in ref_doc.quotas %} + {% set commodity_codes -%} + + {%- endset %} {{ table_rows.append([ - {"text": quota["order_number"].structure_code}, - {"text": quota["order_number"].is_origin_quota}, - {"text": quota["comm_codes"]}, + {"text": create_link(url("quota-ui-detail", kwargs={"sid": quota["order_number"].sid}), quota["order_number"].structure_code) if quota["order_number"].structure_code else '-'}, + {"text": "Yes" if quota["order_number"].is_origin_quota else "No"}, + {"html": commodity_codes}, {"text": "-"}, - {"text": quota["definition"].volume}, - {"text": quota["definition"].valid_between.upper}, + {"text": "{:.2f}".format(quota["definition"].volume)}, + {"text": quota["definition"].valid_between.upper.strftime("%d %b %Y")}, {"text": quota["definition"]["measurement_unit"].description}, ]) or "" }} diff --git a/reference_documents/views.py b/reference_documents/views.py index 08c27d9a9..e0b090905 100644 --- a/reference_documents/views.py +++ b/reference_documents/views.py @@ -73,7 +73,7 @@ def get_tariff_quota_data(self): comm_codes = [] for measure in albanian_measures: if measure.order_number == order_number: - comm_codes.append(measure.goods_nomenclature.id) + comm_codes.append(measure.goods_nomenclature) quotas.append({"order_number": order_number, "comm_codes": comm_codes}) From 71da90ca9c3c4b8081a3f18ed48e8986cb96f9e4 Mon Sep 17 00:00:00 2001 From: Doug Mills Date: Tue, 6 Feb 2024 08:29:21 +0000 Subject: [PATCH 010/118] initial commit - ref doc data model --- reference_documents/__init__.py | 0 reference_documents/admin.py | 1 + reference_documents/apps.py | 5 + .../jinja2/reference_documents/index.jinja | 24 ++++ .../jinja2/reference_documents/overview.jinja | 24 ++++ .../migrations/0001_initial.py | 132 ++++++++++++++++++ .../0002_referencedocument_area_id.py | 19 +++ .../migrations/0003_auto_20240201_0940.py | 33 +++++ .../migrations/0004_auto_20240202_0936.py | 23 +++ .../migrations/0005_auto_20240202_1431.py | 41 ++++++ .../0006_alter_preferentialquota_volume.py | 18 +++ reference_documents/migrations/__init__.py | 0 reference_documents/models.py | 100 +++++++++++++ reference_documents/tests.py | 1 + reference_documents/urls.py | 12 ++ reference_documents/views.py | 72 ++++++++++ settings/common.py | 1 + urls.py | 1 + 18 files changed, 507 insertions(+) create mode 100644 reference_documents/__init__.py create mode 100644 reference_documents/admin.py create mode 100644 reference_documents/apps.py create mode 100644 reference_documents/jinja2/reference_documents/index.jinja create mode 100644 reference_documents/jinja2/reference_documents/overview.jinja create mode 100644 reference_documents/migrations/0001_initial.py create mode 100644 reference_documents/migrations/0002_referencedocument_area_id.py create mode 100644 reference_documents/migrations/0003_auto_20240201_0940.py create mode 100644 reference_documents/migrations/0004_auto_20240202_0936.py create mode 100644 reference_documents/migrations/0005_auto_20240202_1431.py create mode 100644 reference_documents/migrations/0006_alter_preferentialquota_volume.py create mode 100644 reference_documents/migrations/__init__.py create mode 100644 reference_documents/models.py create mode 100644 reference_documents/tests.py create mode 100644 reference_documents/urls.py create mode 100644 reference_documents/views.py diff --git a/reference_documents/__init__.py b/reference_documents/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/reference_documents/admin.py b/reference_documents/admin.py new file mode 100644 index 000000000..846f6b406 --- /dev/null +++ b/reference_documents/admin.py @@ -0,0 +1 @@ +# Register your models here. diff --git a/reference_documents/apps.py b/reference_documents/apps.py new file mode 100644 index 000000000..1d11db6f1 --- /dev/null +++ b/reference_documents/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class ReferenceDocumentsConfig(AppConfig): + name = "reference_documents" diff --git a/reference_documents/jinja2/reference_documents/index.jinja b/reference_documents/jinja2/reference_documents/index.jinja new file mode 100644 index 000000000..b281f6a3d --- /dev/null +++ b/reference_documents/jinja2/reference_documents/index.jinja @@ -0,0 +1,24 @@ +{% extends "layouts/layout.jinja" %} +{% from "components/table/macro.njk" import govukTable %} + +{% set page_title = 'Reference Documents Index' %} + +{% block breadcrumb %} + {{ breadcrumbs(request, [ + {'text': "Reference Documents"} + ]) }} +{% endblock %} + +{% block content %} +

+ Reference Documents +

+ You will find a list of reference documents below that can be viewed. + +
+ {{ govukTable({ "head": reference_document_headers, "rows": reference_documents }) }} +
+{% endblock %} + + + diff --git a/reference_documents/jinja2/reference_documents/overview.jinja b/reference_documents/jinja2/reference_documents/overview.jinja new file mode 100644 index 000000000..fc442ceb7 --- /dev/null +++ b/reference_documents/jinja2/reference_documents/overview.jinja @@ -0,0 +1,24 @@ +{% extends "layouts/layout.jinja" %} +{% from "components/table/macro.njk" import govukTable %} + +{% set page_title = 'Reference Documents versions Overview' %} + +{% block breadcrumb %} + {{ breadcrumbs(request, [ + {'text': "Reference Documents"} + ]) }} +{% endblock %} + +{% block content %} +

+ Reference Document Overview +

+ You will find a list of reference document versions below that can be viewed. + +
+ {{ govukTable({ "head": reference_document_versions_headers, "rows": reference_documents_versions }) }} +
+{% endblock %} + + + diff --git a/reference_documents/migrations/0001_initial.py b/reference_documents/migrations/0001_initial.py new file mode 100644 index 000000000..64b49e847 --- /dev/null +++ b/reference_documents/migrations/0001_initial.py @@ -0,0 +1,132 @@ +# Generated by Django 3.2.23 on 2024-01-29 14:52 + +import django.db.models.deletion +import django_fsm +from django.db import migrations +from django.db import models + + +class Migration(migrations.Migration): + initial = True + + dependencies = [] + + operations = [ + migrations.CreateModel( + name="ReferenceDocument", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "title", + models.CharField( + db_index=True, + help_text="Short name for this workbasket", + max_length=255, + unique=True, + ), + ), + ], + ), + migrations.CreateModel( + name="ReferenceDocumentVersion", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("version", models.FloatField()), + ("published_date", models.DateField()), + ("entry_into_force_date", models.DateField()), + ( + "status", + django_fsm.FSMField( + choices=[ + ("EDITING", "Editing"), + ("IN_REVIEW", "In Review"), + ("PUBLISHED", "Published"), + ], + db_index=True, + default="EDITING", + editable=False, + max_length=50, + ), + ), + ( + "reference_document", + models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, + related_name="reference_document_versions", + to="reference_documents.referencedocument", + ), + ), + ], + ), + migrations.CreateModel( + name="PreferentialRate", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("commodity_code", models.CharField(db_index=True, max_length=10)), + ("duty_rate", models.CharField(max_length=255)), + ("order", models.IntegerField()), + ( + "reference_document_version", + models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, + related_name="preferential_rates", + to="reference_documents.referencedocumentversion", + ), + ), + ], + ), + migrations.CreateModel( + name="PreferentialQuota", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("quota_order_number", models.CharField(db_index=True, max_length=6)), + ("commodity_code", models.CharField(db_index=True, max_length=10)), + ("quota_duty_rate", models.CharField(max_length=255)), + ("volume", models.FloatField()), + ("quota_period_open", models.DateField()), + ("quota_period_close", models.DateField()), + ("measurement", models.CharField(max_length=255)), + ("order", models.IntegerField()), + ( + "reference_document_version", + models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, + related_name="preferential_quotas", + to="reference_documents.referencedocumentversion", + ), + ), + ], + ), + ] diff --git a/reference_documents/migrations/0002_referencedocument_area_id.py b/reference_documents/migrations/0002_referencedocument_area_id.py new file mode 100644 index 000000000..18f449577 --- /dev/null +++ b/reference_documents/migrations/0002_referencedocument_area_id.py @@ -0,0 +1,19 @@ +# Generated by Django 3.2.23 on 2024-01-30 16:02 + +from django.db import migrations +from django.db import models + + +class Migration(migrations.Migration): + dependencies = [ + ("reference_documents", "0001_initial"), + ] + + operations = [ + migrations.AddField( + model_name="referencedocument", + name="area_id", + field=models.CharField(db_index=True, default="ZZ", max_length=4), + preserve_default=False, + ), + ] diff --git a/reference_documents/migrations/0003_auto_20240201_0940.py b/reference_documents/migrations/0003_auto_20240201_0940.py new file mode 100644 index 000000000..f7b8ec51d --- /dev/null +++ b/reference_documents/migrations/0003_auto_20240201_0940.py @@ -0,0 +1,33 @@ +# Generated by Django 3.2.23 on 2024-02-01 09:40 + +from django.db import migrations +from django.db import models + + +class Migration(migrations.Migration): + dependencies = [ + ("reference_documents", "0002_referencedocument_area_id"), + ] + + operations = [ + migrations.AddField( + model_name="preferentialrate", + name="valid_end_day", + field=models.IntegerField(blank=True, null=True), + ), + migrations.AddField( + model_name="preferentialrate", + name="valid_end_month", + field=models.IntegerField(blank=True, null=True), + ), + migrations.AddField( + model_name="preferentialrate", + name="valid_start_day", + field=models.IntegerField(blank=True, null=True), + ), + migrations.AddField( + model_name="preferentialrate", + name="valid_start_month", + field=models.IntegerField(blank=True, null=True), + ), + ] diff --git a/reference_documents/migrations/0004_auto_20240202_0936.py b/reference_documents/migrations/0004_auto_20240202_0936.py new file mode 100644 index 000000000..c59bbfbdb --- /dev/null +++ b/reference_documents/migrations/0004_auto_20240202_0936.py @@ -0,0 +1,23 @@ +# Generated by Django 3.2.23 on 2024-02-02 09:36 + +from django.db import migrations +from django.db import models + + +class Migration(migrations.Migration): + dependencies = [ + ("reference_documents", "0003_auto_20240201_0940"), + ] + + operations = [ + migrations.AlterField( + model_name="referencedocumentversion", + name="entry_into_force_date", + field=models.DateField(blank=True, null=True), + ), + migrations.AlterField( + model_name="referencedocumentversion", + name="published_date", + field=models.DateField(blank=True, null=True), + ), + ] diff --git a/reference_documents/migrations/0005_auto_20240202_1431.py b/reference_documents/migrations/0005_auto_20240202_1431.py new file mode 100644 index 000000000..7787303df --- /dev/null +++ b/reference_documents/migrations/0005_auto_20240202_1431.py @@ -0,0 +1,41 @@ +# Generated by Django 3.2.23 on 2024-02-02 14:31 + +from django.db import migrations +from django.db import models + + +class Migration(migrations.Migration): + dependencies = [ + ("reference_documents", "0004_auto_20240202_0936"), + ] + + operations = [ + migrations.RemoveField( + model_name="preferentialquota", + name="quota_period_close", + ), + migrations.RemoveField( + model_name="preferentialquota", + name="quota_period_open", + ), + migrations.AddField( + model_name="preferentialquota", + name="valid_end_day", + field=models.IntegerField(blank=True, null=True), + ), + migrations.AddField( + model_name="preferentialquota", + name="valid_end_month", + field=models.IntegerField(blank=True, null=True), + ), + migrations.AddField( + model_name="preferentialquota", + name="valid_start_day", + field=models.IntegerField(blank=True, null=True), + ), + migrations.AddField( + model_name="preferentialquota", + name="valid_start_month", + field=models.IntegerField(blank=True, null=True), + ), + ] diff --git a/reference_documents/migrations/0006_alter_preferentialquota_volume.py b/reference_documents/migrations/0006_alter_preferentialquota_volume.py new file mode 100644 index 000000000..1f898a734 --- /dev/null +++ b/reference_documents/migrations/0006_alter_preferentialquota_volume.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.23 on 2024-02-02 14:48 + +from django.db import migrations +from django.db import models + + +class Migration(migrations.Migration): + dependencies = [ + ("reference_documents", "0005_auto_20240202_1431"), + ] + + operations = [ + migrations.AlterField( + model_name="preferentialquota", + name="volume", + field=models.CharField(max_length=255), + ), + ] diff --git a/reference_documents/migrations/__init__.py b/reference_documents/migrations/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/reference_documents/models.py b/reference_documents/models.py new file mode 100644 index 000000000..253119f0b --- /dev/null +++ b/reference_documents/models.py @@ -0,0 +1,100 @@ +from django.db import models +from django_fsm import FSMField + + +class ReferenceDocumentVersionStatus(models.TextChoices): + # Reference document version can be edited + EDITING = "EDITING", "Editing" + # Reference document version ius locked and in review + IN_REVIEW = "IN_REVIEW", "In Review" + # reference document version has been approved and published + PUBLISHED = "PUBLISHED", "Published" + + +class ReferenceDocument(models.Model): + title = models.CharField( + max_length=255, + help_text="Short name for this workbasket", + db_index=True, + unique=True, + ) + + area_id = models.CharField( + max_length=4, + db_index=True, + ) + + +class ReferenceDocumentVersion(models.Model): + version = models.FloatField() + published_date = models.DateField(blank=True, null=True) + entry_into_force_date = models.DateField(blank=True, null=True) + + reference_document = models.ForeignKey( + "reference_documents.ReferenceDocument", + on_delete=models.PROTECT, + related_name="reference_document_versions", + ) + status = FSMField( + default=ReferenceDocumentVersionStatus.EDITING, + choices=ReferenceDocumentVersionStatus.choices, + db_index=True, + protected=False, + editable=False, + ) + + +class PreferentialRate(models.Model): + commodity_code = models.CharField( + max_length=10, + db_index=True, + ) + duty_rate = models.CharField( + max_length=255, + ) + order = models.IntegerField() + + reference_document_version = models.ForeignKey( + "reference_documents.ReferenceDocumentVersion", + on_delete=models.PROTECT, + related_name="preferential_rates", + ) + + valid_start_day = models.IntegerField(blank=True, null=True) + valid_start_month = models.IntegerField(blank=True, null=True) + valid_end_day = models.IntegerField(blank=True, null=True) + valid_end_month = models.IntegerField(blank=True, null=True) + + +class PreferentialQuota(models.Model): + quota_order_number = models.CharField( + max_length=6, + db_index=True, + ) + commodity_code = models.CharField( + max_length=10, + db_index=True, + ) + quota_duty_rate = models.CharField( + max_length=255, + ) + volume = models.CharField( + max_length=255, + ) + + valid_start_day = models.IntegerField(blank=True, null=True) + valid_start_month = models.IntegerField(blank=True, null=True) + valid_end_day = models.IntegerField(blank=True, null=True) + valid_end_month = models.IntegerField(blank=True, null=True) + + measurement = models.CharField( + max_length=255, + ) + + order = models.IntegerField() + + reference_document_version = models.ForeignKey( + "reference_documents.ReferenceDocumentVersion", + on_delete=models.PROTECT, + related_name="preferential_quotas", + ) diff --git a/reference_documents/tests.py b/reference_documents/tests.py new file mode 100644 index 000000000..a39b155ac --- /dev/null +++ b/reference_documents/tests.py @@ -0,0 +1 @@ +# Create your tests here. diff --git a/reference_documents/urls.py b/reference_documents/urls.py new file mode 100644 index 000000000..3574a43b2 --- /dev/null +++ b/reference_documents/urls.py @@ -0,0 +1,12 @@ +from django.urls import path +from rest_framework import routers + +import reference_documents.views as views + +app_name = "reference_documents" + +api_router = routers.DefaultRouter() + +urlpatterns = [ + path("reference_documents/", views.ReferenceDocumentList.as_view(), name="index"), +] diff --git a/reference_documents/views.py b/reference_documents/views.py new file mode 100644 index 000000000..d447ac90c --- /dev/null +++ b/reference_documents/views.py @@ -0,0 +1,72 @@ +from django.contrib.auth.mixins import PermissionRequiredMixin +from django.views.generic import ListView + +from geo_areas.models import GeographicalArea +from geo_areas.models import GeographicalAreaDescription +from reference_documents.models import ReferenceDocument + + +class ReferenceDocumentList(PermissionRequiredMixin, ListView): + """UI endpoint for viewing and filtering workbaskets.""" + + template_name = "reference_documents/index.jinja" + permission_required = "reference_documents.view_reference_document" + model = ReferenceDocument + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + reference_documents = [] + + for reference in ReferenceDocument.objects.all().order_by("area_id"): + if reference.reference_document_versions.count() == 0: + reference_documents.append( + [ + {"text": "None"}, + { + "text": f"{reference.area_id} - ({self.get_name_by_area_id(reference.area_id)})", + }, + {"text": 0}, + {"text": 0}, + {"text": ""}, + ], + ) + + else: + reference_documents.append( + [ + {"text": reference.reference_document_versions.last().version}, + { + "text": f"{reference.area_id} - ({self.get_name_by_area_id(reference.area_id)})", + }, + { + "text": reference.reference_document_versions.last().preferential_rates.count(), + }, + { + "text": reference.reference_document_versions.last().preferential_quotas.count(), + }, + {"text": ""}, + ], + ) + + context["reference_documents"] = reference_documents + context["reference_document_headers"] = [ + {"text": "Latest Version"}, + {"text": "Country"}, + {"text": "Duties"}, + {"text": "Quotas"}, + {"text": "Actions"}, + ] + return context + + def get_name_by_area_id(self, area_id): + geo_area = ( + GeographicalArea.objects.latest_approved().filter(area_id=area_id).first() + ) + if geo_area: + geo_area_name = ( + GeographicalAreaDescription.objects.latest_approved() + .filter(described_geographicalarea_id=geo_area.trackedmodel_ptr_id) + .last() + ) + return geo_area_name.description if geo_area_name else "None" + return "None" diff --git a/settings/common.py b/settings/common.py index f9993a449..e6c193a6a 100644 --- a/settings/common.py +++ b/settings/common.py @@ -124,6 +124,7 @@ "importer", "notifications", # XXX need to keep this for migrations. delete later. + "reference_documents", "publishing", "taric", "workbaskets", diff --git a/urls.py b/urls.py index 08482dc3f..ab9650a14 100644 --- a/urls.py +++ b/urls.py @@ -37,6 +37,7 @@ path("", include("reports.urls")), path("", include("taric_parsers.urls")), path("", include("workbaskets.urls", namespace="workbaskets")), + path("", include("reference_documents.urls", namespace="reference_documents")), ] if not settings.MAINTENANCE_MODE: From eb2cd34817136d5cbbeb7849180389562c748441 Mon Sep 17 00:00:00 2001 From: Doug Mills Date: Tue, 6 Feb 2024 08:29:21 +0000 Subject: [PATCH 011/118] initial commit - ref doc data model --- reference_documents/__init__.py | 0 reference_documents/admin.py | 1 + reference_documents/apps.py | 5 + .../jinja2/reference_documents/index.jinja | 24 ++++ .../jinja2/reference_documents/overview.jinja | 24 ++++ .../migrations/0001_initial.py | 132 ++++++++++++++++++ .../0002_referencedocument_area_id.py | 19 +++ .../migrations/0003_auto_20240201_0940.py | 33 +++++ .../migrations/0004_auto_20240202_0936.py | 23 +++ .../migrations/0005_auto_20240202_1431.py | 41 ++++++ .../0006_alter_preferentialquota_volume.py | 18 +++ reference_documents/migrations/__init__.py | 0 reference_documents/models.py | 100 +++++++++++++ reference_documents/tests.py | 1 + reference_documents/urls.py | 12 ++ reference_documents/views.py | 72 ++++++++++ settings/common.py | 1 + urls.py | 1 + 18 files changed, 507 insertions(+) create mode 100644 reference_documents/__init__.py create mode 100644 reference_documents/admin.py create mode 100644 reference_documents/apps.py create mode 100644 reference_documents/jinja2/reference_documents/index.jinja create mode 100644 reference_documents/jinja2/reference_documents/overview.jinja create mode 100644 reference_documents/migrations/0001_initial.py create mode 100644 reference_documents/migrations/0002_referencedocument_area_id.py create mode 100644 reference_documents/migrations/0003_auto_20240201_0940.py create mode 100644 reference_documents/migrations/0004_auto_20240202_0936.py create mode 100644 reference_documents/migrations/0005_auto_20240202_1431.py create mode 100644 reference_documents/migrations/0006_alter_preferentialquota_volume.py create mode 100644 reference_documents/migrations/__init__.py create mode 100644 reference_documents/models.py create mode 100644 reference_documents/tests.py create mode 100644 reference_documents/urls.py create mode 100644 reference_documents/views.py diff --git a/reference_documents/__init__.py b/reference_documents/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/reference_documents/admin.py b/reference_documents/admin.py new file mode 100644 index 000000000..846f6b406 --- /dev/null +++ b/reference_documents/admin.py @@ -0,0 +1 @@ +# Register your models here. diff --git a/reference_documents/apps.py b/reference_documents/apps.py new file mode 100644 index 000000000..1d11db6f1 --- /dev/null +++ b/reference_documents/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class ReferenceDocumentsConfig(AppConfig): + name = "reference_documents" diff --git a/reference_documents/jinja2/reference_documents/index.jinja b/reference_documents/jinja2/reference_documents/index.jinja new file mode 100644 index 000000000..b281f6a3d --- /dev/null +++ b/reference_documents/jinja2/reference_documents/index.jinja @@ -0,0 +1,24 @@ +{% extends "layouts/layout.jinja" %} +{% from "components/table/macro.njk" import govukTable %} + +{% set page_title = 'Reference Documents Index' %} + +{% block breadcrumb %} + {{ breadcrumbs(request, [ + {'text': "Reference Documents"} + ]) }} +{% endblock %} + +{% block content %} +

+ Reference Documents +

+ You will find a list of reference documents below that can be viewed. + +
+ {{ govukTable({ "head": reference_document_headers, "rows": reference_documents }) }} +
+{% endblock %} + + + diff --git a/reference_documents/jinja2/reference_documents/overview.jinja b/reference_documents/jinja2/reference_documents/overview.jinja new file mode 100644 index 000000000..fc442ceb7 --- /dev/null +++ b/reference_documents/jinja2/reference_documents/overview.jinja @@ -0,0 +1,24 @@ +{% extends "layouts/layout.jinja" %} +{% from "components/table/macro.njk" import govukTable %} + +{% set page_title = 'Reference Documents versions Overview' %} + +{% block breadcrumb %} + {{ breadcrumbs(request, [ + {'text': "Reference Documents"} + ]) }} +{% endblock %} + +{% block content %} +

+ Reference Document Overview +

+ You will find a list of reference document versions below that can be viewed. + +
+ {{ govukTable({ "head": reference_document_versions_headers, "rows": reference_documents_versions }) }} +
+{% endblock %} + + + diff --git a/reference_documents/migrations/0001_initial.py b/reference_documents/migrations/0001_initial.py new file mode 100644 index 000000000..64b49e847 --- /dev/null +++ b/reference_documents/migrations/0001_initial.py @@ -0,0 +1,132 @@ +# Generated by Django 3.2.23 on 2024-01-29 14:52 + +import django.db.models.deletion +import django_fsm +from django.db import migrations +from django.db import models + + +class Migration(migrations.Migration): + initial = True + + dependencies = [] + + operations = [ + migrations.CreateModel( + name="ReferenceDocument", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "title", + models.CharField( + db_index=True, + help_text="Short name for this workbasket", + max_length=255, + unique=True, + ), + ), + ], + ), + migrations.CreateModel( + name="ReferenceDocumentVersion", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("version", models.FloatField()), + ("published_date", models.DateField()), + ("entry_into_force_date", models.DateField()), + ( + "status", + django_fsm.FSMField( + choices=[ + ("EDITING", "Editing"), + ("IN_REVIEW", "In Review"), + ("PUBLISHED", "Published"), + ], + db_index=True, + default="EDITING", + editable=False, + max_length=50, + ), + ), + ( + "reference_document", + models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, + related_name="reference_document_versions", + to="reference_documents.referencedocument", + ), + ), + ], + ), + migrations.CreateModel( + name="PreferentialRate", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("commodity_code", models.CharField(db_index=True, max_length=10)), + ("duty_rate", models.CharField(max_length=255)), + ("order", models.IntegerField()), + ( + "reference_document_version", + models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, + related_name="preferential_rates", + to="reference_documents.referencedocumentversion", + ), + ), + ], + ), + migrations.CreateModel( + name="PreferentialQuota", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("quota_order_number", models.CharField(db_index=True, max_length=6)), + ("commodity_code", models.CharField(db_index=True, max_length=10)), + ("quota_duty_rate", models.CharField(max_length=255)), + ("volume", models.FloatField()), + ("quota_period_open", models.DateField()), + ("quota_period_close", models.DateField()), + ("measurement", models.CharField(max_length=255)), + ("order", models.IntegerField()), + ( + "reference_document_version", + models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, + related_name="preferential_quotas", + to="reference_documents.referencedocumentversion", + ), + ), + ], + ), + ] diff --git a/reference_documents/migrations/0002_referencedocument_area_id.py b/reference_documents/migrations/0002_referencedocument_area_id.py new file mode 100644 index 000000000..18f449577 --- /dev/null +++ b/reference_documents/migrations/0002_referencedocument_area_id.py @@ -0,0 +1,19 @@ +# Generated by Django 3.2.23 on 2024-01-30 16:02 + +from django.db import migrations +from django.db import models + + +class Migration(migrations.Migration): + dependencies = [ + ("reference_documents", "0001_initial"), + ] + + operations = [ + migrations.AddField( + model_name="referencedocument", + name="area_id", + field=models.CharField(db_index=True, default="ZZ", max_length=4), + preserve_default=False, + ), + ] diff --git a/reference_documents/migrations/0003_auto_20240201_0940.py b/reference_documents/migrations/0003_auto_20240201_0940.py new file mode 100644 index 000000000..f7b8ec51d --- /dev/null +++ b/reference_documents/migrations/0003_auto_20240201_0940.py @@ -0,0 +1,33 @@ +# Generated by Django 3.2.23 on 2024-02-01 09:40 + +from django.db import migrations +from django.db import models + + +class Migration(migrations.Migration): + dependencies = [ + ("reference_documents", "0002_referencedocument_area_id"), + ] + + operations = [ + migrations.AddField( + model_name="preferentialrate", + name="valid_end_day", + field=models.IntegerField(blank=True, null=True), + ), + migrations.AddField( + model_name="preferentialrate", + name="valid_end_month", + field=models.IntegerField(blank=True, null=True), + ), + migrations.AddField( + model_name="preferentialrate", + name="valid_start_day", + field=models.IntegerField(blank=True, null=True), + ), + migrations.AddField( + model_name="preferentialrate", + name="valid_start_month", + field=models.IntegerField(blank=True, null=True), + ), + ] diff --git a/reference_documents/migrations/0004_auto_20240202_0936.py b/reference_documents/migrations/0004_auto_20240202_0936.py new file mode 100644 index 000000000..c59bbfbdb --- /dev/null +++ b/reference_documents/migrations/0004_auto_20240202_0936.py @@ -0,0 +1,23 @@ +# Generated by Django 3.2.23 on 2024-02-02 09:36 + +from django.db import migrations +from django.db import models + + +class Migration(migrations.Migration): + dependencies = [ + ("reference_documents", "0003_auto_20240201_0940"), + ] + + operations = [ + migrations.AlterField( + model_name="referencedocumentversion", + name="entry_into_force_date", + field=models.DateField(blank=True, null=True), + ), + migrations.AlterField( + model_name="referencedocumentversion", + name="published_date", + field=models.DateField(blank=True, null=True), + ), + ] diff --git a/reference_documents/migrations/0005_auto_20240202_1431.py b/reference_documents/migrations/0005_auto_20240202_1431.py new file mode 100644 index 000000000..7787303df --- /dev/null +++ b/reference_documents/migrations/0005_auto_20240202_1431.py @@ -0,0 +1,41 @@ +# Generated by Django 3.2.23 on 2024-02-02 14:31 + +from django.db import migrations +from django.db import models + + +class Migration(migrations.Migration): + dependencies = [ + ("reference_documents", "0004_auto_20240202_0936"), + ] + + operations = [ + migrations.RemoveField( + model_name="preferentialquota", + name="quota_period_close", + ), + migrations.RemoveField( + model_name="preferentialquota", + name="quota_period_open", + ), + migrations.AddField( + model_name="preferentialquota", + name="valid_end_day", + field=models.IntegerField(blank=True, null=True), + ), + migrations.AddField( + model_name="preferentialquota", + name="valid_end_month", + field=models.IntegerField(blank=True, null=True), + ), + migrations.AddField( + model_name="preferentialquota", + name="valid_start_day", + field=models.IntegerField(blank=True, null=True), + ), + migrations.AddField( + model_name="preferentialquota", + name="valid_start_month", + field=models.IntegerField(blank=True, null=True), + ), + ] diff --git a/reference_documents/migrations/0006_alter_preferentialquota_volume.py b/reference_documents/migrations/0006_alter_preferentialquota_volume.py new file mode 100644 index 000000000..1f898a734 --- /dev/null +++ b/reference_documents/migrations/0006_alter_preferentialquota_volume.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.23 on 2024-02-02 14:48 + +from django.db import migrations +from django.db import models + + +class Migration(migrations.Migration): + dependencies = [ + ("reference_documents", "0005_auto_20240202_1431"), + ] + + operations = [ + migrations.AlterField( + model_name="preferentialquota", + name="volume", + field=models.CharField(max_length=255), + ), + ] diff --git a/reference_documents/migrations/__init__.py b/reference_documents/migrations/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/reference_documents/models.py b/reference_documents/models.py new file mode 100644 index 000000000..253119f0b --- /dev/null +++ b/reference_documents/models.py @@ -0,0 +1,100 @@ +from django.db import models +from django_fsm import FSMField + + +class ReferenceDocumentVersionStatus(models.TextChoices): + # Reference document version can be edited + EDITING = "EDITING", "Editing" + # Reference document version ius locked and in review + IN_REVIEW = "IN_REVIEW", "In Review" + # reference document version has been approved and published + PUBLISHED = "PUBLISHED", "Published" + + +class ReferenceDocument(models.Model): + title = models.CharField( + max_length=255, + help_text="Short name for this workbasket", + db_index=True, + unique=True, + ) + + area_id = models.CharField( + max_length=4, + db_index=True, + ) + + +class ReferenceDocumentVersion(models.Model): + version = models.FloatField() + published_date = models.DateField(blank=True, null=True) + entry_into_force_date = models.DateField(blank=True, null=True) + + reference_document = models.ForeignKey( + "reference_documents.ReferenceDocument", + on_delete=models.PROTECT, + related_name="reference_document_versions", + ) + status = FSMField( + default=ReferenceDocumentVersionStatus.EDITING, + choices=ReferenceDocumentVersionStatus.choices, + db_index=True, + protected=False, + editable=False, + ) + + +class PreferentialRate(models.Model): + commodity_code = models.CharField( + max_length=10, + db_index=True, + ) + duty_rate = models.CharField( + max_length=255, + ) + order = models.IntegerField() + + reference_document_version = models.ForeignKey( + "reference_documents.ReferenceDocumentVersion", + on_delete=models.PROTECT, + related_name="preferential_rates", + ) + + valid_start_day = models.IntegerField(blank=True, null=True) + valid_start_month = models.IntegerField(blank=True, null=True) + valid_end_day = models.IntegerField(blank=True, null=True) + valid_end_month = models.IntegerField(blank=True, null=True) + + +class PreferentialQuota(models.Model): + quota_order_number = models.CharField( + max_length=6, + db_index=True, + ) + commodity_code = models.CharField( + max_length=10, + db_index=True, + ) + quota_duty_rate = models.CharField( + max_length=255, + ) + volume = models.CharField( + max_length=255, + ) + + valid_start_day = models.IntegerField(blank=True, null=True) + valid_start_month = models.IntegerField(blank=True, null=True) + valid_end_day = models.IntegerField(blank=True, null=True) + valid_end_month = models.IntegerField(blank=True, null=True) + + measurement = models.CharField( + max_length=255, + ) + + order = models.IntegerField() + + reference_document_version = models.ForeignKey( + "reference_documents.ReferenceDocumentVersion", + on_delete=models.PROTECT, + related_name="preferential_quotas", + ) diff --git a/reference_documents/tests.py b/reference_documents/tests.py new file mode 100644 index 000000000..a39b155ac --- /dev/null +++ b/reference_documents/tests.py @@ -0,0 +1 @@ +# Create your tests here. diff --git a/reference_documents/urls.py b/reference_documents/urls.py new file mode 100644 index 000000000..3574a43b2 --- /dev/null +++ b/reference_documents/urls.py @@ -0,0 +1,12 @@ +from django.urls import path +from rest_framework import routers + +import reference_documents.views as views + +app_name = "reference_documents" + +api_router = routers.DefaultRouter() + +urlpatterns = [ + path("reference_documents/", views.ReferenceDocumentList.as_view(), name="index"), +] diff --git a/reference_documents/views.py b/reference_documents/views.py new file mode 100644 index 000000000..d447ac90c --- /dev/null +++ b/reference_documents/views.py @@ -0,0 +1,72 @@ +from django.contrib.auth.mixins import PermissionRequiredMixin +from django.views.generic import ListView + +from geo_areas.models import GeographicalArea +from geo_areas.models import GeographicalAreaDescription +from reference_documents.models import ReferenceDocument + + +class ReferenceDocumentList(PermissionRequiredMixin, ListView): + """UI endpoint for viewing and filtering workbaskets.""" + + template_name = "reference_documents/index.jinja" + permission_required = "reference_documents.view_reference_document" + model = ReferenceDocument + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + reference_documents = [] + + for reference in ReferenceDocument.objects.all().order_by("area_id"): + if reference.reference_document_versions.count() == 0: + reference_documents.append( + [ + {"text": "None"}, + { + "text": f"{reference.area_id} - ({self.get_name_by_area_id(reference.area_id)})", + }, + {"text": 0}, + {"text": 0}, + {"text": ""}, + ], + ) + + else: + reference_documents.append( + [ + {"text": reference.reference_document_versions.last().version}, + { + "text": f"{reference.area_id} - ({self.get_name_by_area_id(reference.area_id)})", + }, + { + "text": reference.reference_document_versions.last().preferential_rates.count(), + }, + { + "text": reference.reference_document_versions.last().preferential_quotas.count(), + }, + {"text": ""}, + ], + ) + + context["reference_documents"] = reference_documents + context["reference_document_headers"] = [ + {"text": "Latest Version"}, + {"text": "Country"}, + {"text": "Duties"}, + {"text": "Quotas"}, + {"text": "Actions"}, + ] + return context + + def get_name_by_area_id(self, area_id): + geo_area = ( + GeographicalArea.objects.latest_approved().filter(area_id=area_id).first() + ) + if geo_area: + geo_area_name = ( + GeographicalAreaDescription.objects.latest_approved() + .filter(described_geographicalarea_id=geo_area.trackedmodel_ptr_id) + .last() + ) + return geo_area_name.description if geo_area_name else "None" + return "None" diff --git a/settings/common.py b/settings/common.py index 3e12d02ec..ee3125dce 100644 --- a/settings/common.py +++ b/settings/common.py @@ -124,6 +124,7 @@ "importer", "notifications", # XXX need to keep this for migrations. delete later. + "reference_documents", "publishing", "taric", "workbaskets", diff --git a/urls.py b/urls.py index 08482dc3f..ab9650a14 100644 --- a/urls.py +++ b/urls.py @@ -37,6 +37,7 @@ path("", include("reports.urls")), path("", include("taric_parsers.urls")), path("", include("workbaskets.urls", namespace="workbaskets")), + path("", include("reference_documents.urls", namespace="reference_documents")), ] if not settings.MAINTENANCE_MODE: From 1246355093cae193cca2edcfcfb753064c7ffa42 Mon Sep 17 00:00:00 2001 From: Doug Mills Date: Thu, 8 Feb 2024 08:34:04 +0000 Subject: [PATCH 012/118] wip commit --- reference_documents/alignment_checks.py | 36 +++++ .../reference_document_versions/details.jinja | 27 ++++ .../{overview.jinja => details.jinja} | 2 +- reference_documents/models.py | 50 +++++- reference_documents/urls.py | 10 ++ reference_documents/views.py | 147 +++++++++++++++++- 6 files changed, 267 insertions(+), 5 deletions(-) create mode 100644 reference_documents/alignment_checks.py create mode 100644 reference_documents/jinja2/reference_document_versions/details.jinja rename reference_documents/jinja2/reference_documents/{overview.jinja => details.jinja} (93%) diff --git a/reference_documents/alignment_checks.py b/reference_documents/alignment_checks.py new file mode 100644 index 000000000..fd25204e9 --- /dev/null +++ b/reference_documents/alignment_checks.py @@ -0,0 +1,36 @@ +from commodities.models import GoodsNomenclature +from geo_areas.models import GeographicalArea +from reference_documents.models import PreferentialRate + + +class BaseCheck: + def run_check(self): + raise NotImplemented("Please implement on child classes") + + +class BasePreferentialRateCheck(BaseCheck): + def __init__(self, preferential_rate: PreferentialRate): + self.preferential_rate = preferential_rate + + def comm_code(self): + return GoodsNomenclature.objects.latest_approved().get( + item_id=self.preferential_rate.commodity_code, + ) + + def geo_area(self): + return GeographicalArea.objects.latest_approved().get( + area_id=self.preferential_rate.reference_document_version.reference_document.area_id, + ) + + def run_check(self): + raise NotImplementedError("Please implement on child classes") + + +class CheckPreferentialRateCommCode(BasePreferentialRateCheck): + def run_check(self): + measures = self.comm_code().measures.get(geographical_area=self.geo_area()) + + return ( + len(measures) > 0, + f"{len(measures)} measures matched required preferential rate", + ) diff --git a/reference_documents/jinja2/reference_document_versions/details.jinja b/reference_documents/jinja2/reference_document_versions/details.jinja new file mode 100644 index 000000000..c2cc712d8 --- /dev/null +++ b/reference_documents/jinja2/reference_document_versions/details.jinja @@ -0,0 +1,27 @@ +{% extends "layouts/layout.jinja" %} +{% from "components/table/macro.njk" import govukTable %} + +{% set page_title = 'Reference Documents version details' %} + +{% block breadcrumb %} + {{ breadcrumbs(request, [ + {'text': "Reference Documents"} + ]) }} +{% endblock %} + +{% block content %} +

+ Reference Document version Overview +

+ Reference document version details. + +
+ {{ govukTable({ "head": reference_document_version_duties_headers, "rows": reference_document_version_duties }) }} +
+
+ {{ govukTable({ "head": reference_document_version_quotas_headers, "rows": reference_document_version_quotas }) }} +
+{% endblock %} + + + diff --git a/reference_documents/jinja2/reference_documents/overview.jinja b/reference_documents/jinja2/reference_documents/details.jinja similarity index 93% rename from reference_documents/jinja2/reference_documents/overview.jinja rename to reference_documents/jinja2/reference_documents/details.jinja index fc442ceb7..0c301c051 100644 --- a/reference_documents/jinja2/reference_documents/overview.jinja +++ b/reference_documents/jinja2/reference_documents/details.jinja @@ -16,7 +16,7 @@ You will find a list of reference document versions below that can be viewed.
- {{ govukTable({ "head": reference_document_versions_headers, "rows": reference_documents_versions }) }} + {{ govukTable({ "head": reference_document_versions_headers, "rows": reference_document_versions }) }}
{% endblock %} diff --git a/reference_documents/models.py b/reference_documents/models.py index 253119f0b..94c712c05 100644 --- a/reference_documents/models.py +++ b/reference_documents/models.py @@ -1,6 +1,9 @@ from django.db import models +from django.db.models import fields from django_fsm import FSMField +from common.models import TimestampedMixin + class ReferenceDocumentVersionStatus(models.TextChoices): # Reference document version can be edited @@ -11,7 +14,7 @@ class ReferenceDocumentVersionStatus(models.TextChoices): PUBLISHED = "PUBLISHED", "Published" -class ReferenceDocument(models.Model): +class ReferenceDocument(models.Model, TimestampedMixin): title = models.CharField( max_length=255, help_text="Short name for this workbasket", @@ -25,7 +28,7 @@ class ReferenceDocument(models.Model): ) -class ReferenceDocumentVersion(models.Model): +class ReferenceDocumentVersion(models.Model, TimestampedMixin): version = models.FloatField() published_date = models.DateField(blank=True, null=True) entry_into_force_date = models.DateField(blank=True, null=True) @@ -98,3 +101,46 @@ class PreferentialQuota(models.Model): on_delete=models.PROTECT, related_name="preferential_quotas", ) + + +class AlignmentReport(models.Model, TimestampedMixin): + reference_document_version = models.ForeignKey( + "reference_documents.ReferenceDocumentVersion", + on_delete=models.PROTECT, + related_name="alignment_reports", + ) + + +class AlignmentReportCheck(models.Model): + created_at = models.DateTimeField(auto_now_add=True) + + alignment_report = models.ForeignKey( + "reference_documents.AlignmentReport", + on_delete=models.PROTECT, + related_name="alignment_report_checks", + ) + + check_name = fields.CharField(max_length=255) + """A string identifying the type of check carried out.""" + + successful = fields.BooleanField() + """True if the check was successful.""" + + message = fields.TextField(null=True) + """The text content returned by the check, if any.""" + + preferential_quota = models.ForeignKey( + "reference_documents.PreferentialQuota", + on_delete=models.PROTECT, + related_name="alignment_report_checks", + blank=True, + null=True, + ) + + preferential_rate = models.ForeignKey( + "reference_documents.PreferentialRate", + on_delete=models.PROTECT, + related_name="alignment_report_checks", + blank=True, + null=True, + ) diff --git a/reference_documents/urls.py b/reference_documents/urls.py index 3574a43b2..cd7a7006c 100644 --- a/reference_documents/urls.py +++ b/reference_documents/urls.py @@ -9,4 +9,14 @@ urlpatterns = [ path("reference_documents/", views.ReferenceDocumentList.as_view(), name="index"), + path( + "reference_documents//", + views.ReferenceDocumentDetails.as_view(), + name="details", + ), + path( + "reference_document_versions//", + views.ReferenceDocumentVersionDetails.as_view(), + name="version_details", + ), ] diff --git a/reference_documents/views.py b/reference_documents/views.py index d447ac90c..4fc8d2c65 100644 --- a/reference_documents/views.py +++ b/reference_documents/views.py @@ -1,9 +1,11 @@ from django.contrib.auth.mixins import PermissionRequiredMixin +from django.views.generic import DetailView from django.views.generic import ListView from geo_areas.models import GeographicalArea from geo_areas.models import GeographicalAreaDescription from reference_documents.models import ReferenceDocument +from reference_documents.models import ReferenceDocumentVersion class ReferenceDocumentList(PermissionRequiredMixin, ListView): @@ -27,7 +29,9 @@ def get_context_data(self, **kwargs): }, {"text": 0}, {"text": 0}, - {"text": ""}, + { + "html": f'Details', + }, ], ) @@ -44,7 +48,9 @@ def get_context_data(self, **kwargs): { "text": reference.reference_document_versions.last().preferential_quotas.count(), }, - {"text": ""}, + { + "html": f'Details', + }, ], ) @@ -70,3 +76,140 @@ def get_name_by_area_id(self, area_id): ) return geo_area_name.description if geo_area_name else "None" return "None" + + +class ReferenceDocumentDetails(PermissionRequiredMixin, DetailView): + template_name = "reference_documents/details.jinja" + permission_required = "reference_documents.view_reference_document" + model = ReferenceDocument + + def get_context_data(self, *args, **kwargs): + context = super(ReferenceDocumentDetails, self).get_context_data( + *args, + **kwargs, + ) + + context["reference_document_versions_headers"] = [ + {"text": "Version"}, + {"text": "Duties"}, + {"text": "Quotas"}, + {"text": "Actions"}, + ] + reference_document_versions = [] + + print(self.request) + + for version in context["object"].reference_document_versions.order_by( + "version", + ): + reference_document_versions.append( + [ + { + "text": version.version, + }, + { + "text": version.preferential_rates.count(), + }, + { + "text": version.preferential_quotas.count(), + }, + { + "html": f'version details', + }, + ], + ) + + context["reference_document_versions"] = reference_document_versions + + return context + + +class ReferenceDocumentVersionDetails(PermissionRequiredMixin, DetailView): + template_name = "reference_document_versions/details.jinja" + permission_required = "reference_documents.view_reference_document" + model = ReferenceDocumentVersion + + def get_context_data(self, *args, **kwargs): + context = super(ReferenceDocumentVersionDetails, self).get_context_data( + *args, + **kwargs, + ) + + context["reference_document_version_duties_headers"] = [ + {"text": "Comm Code"}, + {"text": "Duty Rate"}, + {"text": "Validity"}, + {"text": "Actions"}, + ] + + context["reference_document_version_quotas_headers"] = [ + {"text": "Order Number"}, + {"text": "Comm Code"}, + {"text": "Rate"}, + {"text": "Volume"}, + {"text": "Validity"}, + {"text": "Actions"}, + ] + + reference_document_version_duties = [] + reference_document_version_quotas = [] + + print(self.request) + + for duty in context["object"].preferential_rates.order_by("order"): + validity = "" + + if duty.valid_start_day: + validity = f"{duty.valid_start_day}/{duty.valid_start_month} - {duty.valid_end_day}/{duty.valid_end_month}" + + reference_document_version_duties.append( + [ + { + "text": duty.commodity_code, + }, + { + "text": duty.duty_rate, + }, + { + "text": validity, + }, + { + "text": "", + }, + ], + ) + + for quota in context["object"].preferential_quotas.order_by("order"): + validity = "" + + if quota.valid_start_day: + validity = f"{quota.valid_start_day}/{quota.valid_start_month} - {quota.valid_end_day}/{quota.valid_end_month}" + + reference_document_version_quotas.append( + [ + { + "text": quota.quota_order_number, + }, + { + "text": quota.commodity_code, + }, + { + "text": quota.quota_duty_rate, + }, + { + "text": f"{quota.volume} {quota.measurement}", + }, + { + "text": validity, + }, + { + "text": "", + }, + ], + ) + + context["reference_document_version_duties"] = reference_document_version_duties + + context["reference_document_version_quotas"] = reference_document_version_quotas + + return context From 22f444f29d398d02fb5adccc62bf55b9da186f1c Mon Sep 17 00:00:00 2001 From: Doug Mills Date: Tue, 6 Feb 2024 08:29:21 +0000 Subject: [PATCH 013/118] initial commit - ref doc data model --- reference_documents/__init__.py | 0 reference_documents/admin.py | 1 + reference_documents/apps.py | 5 + .../jinja2/reference_documents/index.jinja | 24 ++++ .../jinja2/reference_documents/overview.jinja | 24 ++++ .../migrations/0001_initial.py | 132 ++++++++++++++++++ .../0002_referencedocument_area_id.py | 19 +++ .../migrations/0003_auto_20240201_0940.py | 33 +++++ .../migrations/0004_auto_20240202_0936.py | 23 +++ .../migrations/0005_auto_20240202_1431.py | 41 ++++++ .../0006_alter_preferentialquota_volume.py | 18 +++ reference_documents/migrations/__init__.py | 0 reference_documents/models.py | 100 +++++++++++++ reference_documents/tests.py | 1 + reference_documents/urls.py | 12 ++ reference_documents/views.py | 72 ++++++++++ settings/common.py | 1 + urls.py | 1 + 18 files changed, 507 insertions(+) create mode 100644 reference_documents/__init__.py create mode 100644 reference_documents/admin.py create mode 100644 reference_documents/apps.py create mode 100644 reference_documents/jinja2/reference_documents/index.jinja create mode 100644 reference_documents/jinja2/reference_documents/overview.jinja create mode 100644 reference_documents/migrations/0001_initial.py create mode 100644 reference_documents/migrations/0002_referencedocument_area_id.py create mode 100644 reference_documents/migrations/0003_auto_20240201_0940.py create mode 100644 reference_documents/migrations/0004_auto_20240202_0936.py create mode 100644 reference_documents/migrations/0005_auto_20240202_1431.py create mode 100644 reference_documents/migrations/0006_alter_preferentialquota_volume.py create mode 100644 reference_documents/migrations/__init__.py create mode 100644 reference_documents/models.py create mode 100644 reference_documents/tests.py create mode 100644 reference_documents/urls.py create mode 100644 reference_documents/views.py diff --git a/reference_documents/__init__.py b/reference_documents/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/reference_documents/admin.py b/reference_documents/admin.py new file mode 100644 index 000000000..846f6b406 --- /dev/null +++ b/reference_documents/admin.py @@ -0,0 +1 @@ +# Register your models here. diff --git a/reference_documents/apps.py b/reference_documents/apps.py new file mode 100644 index 000000000..1d11db6f1 --- /dev/null +++ b/reference_documents/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class ReferenceDocumentsConfig(AppConfig): + name = "reference_documents" diff --git a/reference_documents/jinja2/reference_documents/index.jinja b/reference_documents/jinja2/reference_documents/index.jinja new file mode 100644 index 000000000..b281f6a3d --- /dev/null +++ b/reference_documents/jinja2/reference_documents/index.jinja @@ -0,0 +1,24 @@ +{% extends "layouts/layout.jinja" %} +{% from "components/table/macro.njk" import govukTable %} + +{% set page_title = 'Reference Documents Index' %} + +{% block breadcrumb %} + {{ breadcrumbs(request, [ + {'text': "Reference Documents"} + ]) }} +{% endblock %} + +{% block content %} +

+ Reference Documents +

+ You will find a list of reference documents below that can be viewed. + +
+ {{ govukTable({ "head": reference_document_headers, "rows": reference_documents }) }} +
+{% endblock %} + + + diff --git a/reference_documents/jinja2/reference_documents/overview.jinja b/reference_documents/jinja2/reference_documents/overview.jinja new file mode 100644 index 000000000..fc442ceb7 --- /dev/null +++ b/reference_documents/jinja2/reference_documents/overview.jinja @@ -0,0 +1,24 @@ +{% extends "layouts/layout.jinja" %} +{% from "components/table/macro.njk" import govukTable %} + +{% set page_title = 'Reference Documents versions Overview' %} + +{% block breadcrumb %} + {{ breadcrumbs(request, [ + {'text': "Reference Documents"} + ]) }} +{% endblock %} + +{% block content %} +

+ Reference Document Overview +

+ You will find a list of reference document versions below that can be viewed. + +
+ {{ govukTable({ "head": reference_document_versions_headers, "rows": reference_documents_versions }) }} +
+{% endblock %} + + + diff --git a/reference_documents/migrations/0001_initial.py b/reference_documents/migrations/0001_initial.py new file mode 100644 index 000000000..64b49e847 --- /dev/null +++ b/reference_documents/migrations/0001_initial.py @@ -0,0 +1,132 @@ +# Generated by Django 3.2.23 on 2024-01-29 14:52 + +import django.db.models.deletion +import django_fsm +from django.db import migrations +from django.db import models + + +class Migration(migrations.Migration): + initial = True + + dependencies = [] + + operations = [ + migrations.CreateModel( + name="ReferenceDocument", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "title", + models.CharField( + db_index=True, + help_text="Short name for this workbasket", + max_length=255, + unique=True, + ), + ), + ], + ), + migrations.CreateModel( + name="ReferenceDocumentVersion", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("version", models.FloatField()), + ("published_date", models.DateField()), + ("entry_into_force_date", models.DateField()), + ( + "status", + django_fsm.FSMField( + choices=[ + ("EDITING", "Editing"), + ("IN_REVIEW", "In Review"), + ("PUBLISHED", "Published"), + ], + db_index=True, + default="EDITING", + editable=False, + max_length=50, + ), + ), + ( + "reference_document", + models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, + related_name="reference_document_versions", + to="reference_documents.referencedocument", + ), + ), + ], + ), + migrations.CreateModel( + name="PreferentialRate", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("commodity_code", models.CharField(db_index=True, max_length=10)), + ("duty_rate", models.CharField(max_length=255)), + ("order", models.IntegerField()), + ( + "reference_document_version", + models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, + related_name="preferential_rates", + to="reference_documents.referencedocumentversion", + ), + ), + ], + ), + migrations.CreateModel( + name="PreferentialQuota", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("quota_order_number", models.CharField(db_index=True, max_length=6)), + ("commodity_code", models.CharField(db_index=True, max_length=10)), + ("quota_duty_rate", models.CharField(max_length=255)), + ("volume", models.FloatField()), + ("quota_period_open", models.DateField()), + ("quota_period_close", models.DateField()), + ("measurement", models.CharField(max_length=255)), + ("order", models.IntegerField()), + ( + "reference_document_version", + models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, + related_name="preferential_quotas", + to="reference_documents.referencedocumentversion", + ), + ), + ], + ), + ] diff --git a/reference_documents/migrations/0002_referencedocument_area_id.py b/reference_documents/migrations/0002_referencedocument_area_id.py new file mode 100644 index 000000000..18f449577 --- /dev/null +++ b/reference_documents/migrations/0002_referencedocument_area_id.py @@ -0,0 +1,19 @@ +# Generated by Django 3.2.23 on 2024-01-30 16:02 + +from django.db import migrations +from django.db import models + + +class Migration(migrations.Migration): + dependencies = [ + ("reference_documents", "0001_initial"), + ] + + operations = [ + migrations.AddField( + model_name="referencedocument", + name="area_id", + field=models.CharField(db_index=True, default="ZZ", max_length=4), + preserve_default=False, + ), + ] diff --git a/reference_documents/migrations/0003_auto_20240201_0940.py b/reference_documents/migrations/0003_auto_20240201_0940.py new file mode 100644 index 000000000..f7b8ec51d --- /dev/null +++ b/reference_documents/migrations/0003_auto_20240201_0940.py @@ -0,0 +1,33 @@ +# Generated by Django 3.2.23 on 2024-02-01 09:40 + +from django.db import migrations +from django.db import models + + +class Migration(migrations.Migration): + dependencies = [ + ("reference_documents", "0002_referencedocument_area_id"), + ] + + operations = [ + migrations.AddField( + model_name="preferentialrate", + name="valid_end_day", + field=models.IntegerField(blank=True, null=True), + ), + migrations.AddField( + model_name="preferentialrate", + name="valid_end_month", + field=models.IntegerField(blank=True, null=True), + ), + migrations.AddField( + model_name="preferentialrate", + name="valid_start_day", + field=models.IntegerField(blank=True, null=True), + ), + migrations.AddField( + model_name="preferentialrate", + name="valid_start_month", + field=models.IntegerField(blank=True, null=True), + ), + ] diff --git a/reference_documents/migrations/0004_auto_20240202_0936.py b/reference_documents/migrations/0004_auto_20240202_0936.py new file mode 100644 index 000000000..c59bbfbdb --- /dev/null +++ b/reference_documents/migrations/0004_auto_20240202_0936.py @@ -0,0 +1,23 @@ +# Generated by Django 3.2.23 on 2024-02-02 09:36 + +from django.db import migrations +from django.db import models + + +class Migration(migrations.Migration): + dependencies = [ + ("reference_documents", "0003_auto_20240201_0940"), + ] + + operations = [ + migrations.AlterField( + model_name="referencedocumentversion", + name="entry_into_force_date", + field=models.DateField(blank=True, null=True), + ), + migrations.AlterField( + model_name="referencedocumentversion", + name="published_date", + field=models.DateField(blank=True, null=True), + ), + ] diff --git a/reference_documents/migrations/0005_auto_20240202_1431.py b/reference_documents/migrations/0005_auto_20240202_1431.py new file mode 100644 index 000000000..7787303df --- /dev/null +++ b/reference_documents/migrations/0005_auto_20240202_1431.py @@ -0,0 +1,41 @@ +# Generated by Django 3.2.23 on 2024-02-02 14:31 + +from django.db import migrations +from django.db import models + + +class Migration(migrations.Migration): + dependencies = [ + ("reference_documents", "0004_auto_20240202_0936"), + ] + + operations = [ + migrations.RemoveField( + model_name="preferentialquota", + name="quota_period_close", + ), + migrations.RemoveField( + model_name="preferentialquota", + name="quota_period_open", + ), + migrations.AddField( + model_name="preferentialquota", + name="valid_end_day", + field=models.IntegerField(blank=True, null=True), + ), + migrations.AddField( + model_name="preferentialquota", + name="valid_end_month", + field=models.IntegerField(blank=True, null=True), + ), + migrations.AddField( + model_name="preferentialquota", + name="valid_start_day", + field=models.IntegerField(blank=True, null=True), + ), + migrations.AddField( + model_name="preferentialquota", + name="valid_start_month", + field=models.IntegerField(blank=True, null=True), + ), + ] diff --git a/reference_documents/migrations/0006_alter_preferentialquota_volume.py b/reference_documents/migrations/0006_alter_preferentialquota_volume.py new file mode 100644 index 000000000..1f898a734 --- /dev/null +++ b/reference_documents/migrations/0006_alter_preferentialquota_volume.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.23 on 2024-02-02 14:48 + +from django.db import migrations +from django.db import models + + +class Migration(migrations.Migration): + dependencies = [ + ("reference_documents", "0005_auto_20240202_1431"), + ] + + operations = [ + migrations.AlterField( + model_name="preferentialquota", + name="volume", + field=models.CharField(max_length=255), + ), + ] diff --git a/reference_documents/migrations/__init__.py b/reference_documents/migrations/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/reference_documents/models.py b/reference_documents/models.py new file mode 100644 index 000000000..253119f0b --- /dev/null +++ b/reference_documents/models.py @@ -0,0 +1,100 @@ +from django.db import models +from django_fsm import FSMField + + +class ReferenceDocumentVersionStatus(models.TextChoices): + # Reference document version can be edited + EDITING = "EDITING", "Editing" + # Reference document version ius locked and in review + IN_REVIEW = "IN_REVIEW", "In Review" + # reference document version has been approved and published + PUBLISHED = "PUBLISHED", "Published" + + +class ReferenceDocument(models.Model): + title = models.CharField( + max_length=255, + help_text="Short name for this workbasket", + db_index=True, + unique=True, + ) + + area_id = models.CharField( + max_length=4, + db_index=True, + ) + + +class ReferenceDocumentVersion(models.Model): + version = models.FloatField() + published_date = models.DateField(blank=True, null=True) + entry_into_force_date = models.DateField(blank=True, null=True) + + reference_document = models.ForeignKey( + "reference_documents.ReferenceDocument", + on_delete=models.PROTECT, + related_name="reference_document_versions", + ) + status = FSMField( + default=ReferenceDocumentVersionStatus.EDITING, + choices=ReferenceDocumentVersionStatus.choices, + db_index=True, + protected=False, + editable=False, + ) + + +class PreferentialRate(models.Model): + commodity_code = models.CharField( + max_length=10, + db_index=True, + ) + duty_rate = models.CharField( + max_length=255, + ) + order = models.IntegerField() + + reference_document_version = models.ForeignKey( + "reference_documents.ReferenceDocumentVersion", + on_delete=models.PROTECT, + related_name="preferential_rates", + ) + + valid_start_day = models.IntegerField(blank=True, null=True) + valid_start_month = models.IntegerField(blank=True, null=True) + valid_end_day = models.IntegerField(blank=True, null=True) + valid_end_month = models.IntegerField(blank=True, null=True) + + +class PreferentialQuota(models.Model): + quota_order_number = models.CharField( + max_length=6, + db_index=True, + ) + commodity_code = models.CharField( + max_length=10, + db_index=True, + ) + quota_duty_rate = models.CharField( + max_length=255, + ) + volume = models.CharField( + max_length=255, + ) + + valid_start_day = models.IntegerField(blank=True, null=True) + valid_start_month = models.IntegerField(blank=True, null=True) + valid_end_day = models.IntegerField(blank=True, null=True) + valid_end_month = models.IntegerField(blank=True, null=True) + + measurement = models.CharField( + max_length=255, + ) + + order = models.IntegerField() + + reference_document_version = models.ForeignKey( + "reference_documents.ReferenceDocumentVersion", + on_delete=models.PROTECT, + related_name="preferential_quotas", + ) diff --git a/reference_documents/tests.py b/reference_documents/tests.py new file mode 100644 index 000000000..a39b155ac --- /dev/null +++ b/reference_documents/tests.py @@ -0,0 +1 @@ +# Create your tests here. diff --git a/reference_documents/urls.py b/reference_documents/urls.py new file mode 100644 index 000000000..3574a43b2 --- /dev/null +++ b/reference_documents/urls.py @@ -0,0 +1,12 @@ +from django.urls import path +from rest_framework import routers + +import reference_documents.views as views + +app_name = "reference_documents" + +api_router = routers.DefaultRouter() + +urlpatterns = [ + path("reference_documents/", views.ReferenceDocumentList.as_view(), name="index"), +] diff --git a/reference_documents/views.py b/reference_documents/views.py new file mode 100644 index 000000000..d447ac90c --- /dev/null +++ b/reference_documents/views.py @@ -0,0 +1,72 @@ +from django.contrib.auth.mixins import PermissionRequiredMixin +from django.views.generic import ListView + +from geo_areas.models import GeographicalArea +from geo_areas.models import GeographicalAreaDescription +from reference_documents.models import ReferenceDocument + + +class ReferenceDocumentList(PermissionRequiredMixin, ListView): + """UI endpoint for viewing and filtering workbaskets.""" + + template_name = "reference_documents/index.jinja" + permission_required = "reference_documents.view_reference_document" + model = ReferenceDocument + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + reference_documents = [] + + for reference in ReferenceDocument.objects.all().order_by("area_id"): + if reference.reference_document_versions.count() == 0: + reference_documents.append( + [ + {"text": "None"}, + { + "text": f"{reference.area_id} - ({self.get_name_by_area_id(reference.area_id)})", + }, + {"text": 0}, + {"text": 0}, + {"text": ""}, + ], + ) + + else: + reference_documents.append( + [ + {"text": reference.reference_document_versions.last().version}, + { + "text": f"{reference.area_id} - ({self.get_name_by_area_id(reference.area_id)})", + }, + { + "text": reference.reference_document_versions.last().preferential_rates.count(), + }, + { + "text": reference.reference_document_versions.last().preferential_quotas.count(), + }, + {"text": ""}, + ], + ) + + context["reference_documents"] = reference_documents + context["reference_document_headers"] = [ + {"text": "Latest Version"}, + {"text": "Country"}, + {"text": "Duties"}, + {"text": "Quotas"}, + {"text": "Actions"}, + ] + return context + + def get_name_by_area_id(self, area_id): + geo_area = ( + GeographicalArea.objects.latest_approved().filter(area_id=area_id).first() + ) + if geo_area: + geo_area_name = ( + GeographicalAreaDescription.objects.latest_approved() + .filter(described_geographicalarea_id=geo_area.trackedmodel_ptr_id) + .last() + ) + return geo_area_name.description if geo_area_name else "None" + return "None" diff --git a/settings/common.py b/settings/common.py index 8203950f0..b5f87e259 100644 --- a/settings/common.py +++ b/settings/common.py @@ -124,6 +124,7 @@ "importer", "notifications", # XXX need to keep this for migrations. delete later. + "reference_documents", "publishing", "taric", "workbaskets", diff --git a/urls.py b/urls.py index 08482dc3f..ab9650a14 100644 --- a/urls.py +++ b/urls.py @@ -37,6 +37,7 @@ path("", include("reports.urls")), path("", include("taric_parsers.urls")), path("", include("workbaskets.urls", namespace="workbaskets")), + path("", include("reference_documents.urls", namespace="reference_documents")), ] if not settings.MAINTENANCE_MODE: From d1d49cf5d5e541596e55d8e3a6051260a7722488 Mon Sep 17 00:00:00 2001 From: Doug Mills Date: Thu, 8 Feb 2024 08:34:04 +0000 Subject: [PATCH 014/118] wip commit --- reference_documents/alignment_checks.py | 36 +++++ .../reference_document_versions/details.jinja | 27 ++++ .../{overview.jinja => details.jinja} | 2 +- reference_documents/models.py | 50 +++++- reference_documents/urls.py | 10 ++ reference_documents/views.py | 147 +++++++++++++++++- 6 files changed, 267 insertions(+), 5 deletions(-) create mode 100644 reference_documents/alignment_checks.py create mode 100644 reference_documents/jinja2/reference_document_versions/details.jinja rename reference_documents/jinja2/reference_documents/{overview.jinja => details.jinja} (93%) diff --git a/reference_documents/alignment_checks.py b/reference_documents/alignment_checks.py new file mode 100644 index 000000000..fd25204e9 --- /dev/null +++ b/reference_documents/alignment_checks.py @@ -0,0 +1,36 @@ +from commodities.models import GoodsNomenclature +from geo_areas.models import GeographicalArea +from reference_documents.models import PreferentialRate + + +class BaseCheck: + def run_check(self): + raise NotImplemented("Please implement on child classes") + + +class BasePreferentialRateCheck(BaseCheck): + def __init__(self, preferential_rate: PreferentialRate): + self.preferential_rate = preferential_rate + + def comm_code(self): + return GoodsNomenclature.objects.latest_approved().get( + item_id=self.preferential_rate.commodity_code, + ) + + def geo_area(self): + return GeographicalArea.objects.latest_approved().get( + area_id=self.preferential_rate.reference_document_version.reference_document.area_id, + ) + + def run_check(self): + raise NotImplementedError("Please implement on child classes") + + +class CheckPreferentialRateCommCode(BasePreferentialRateCheck): + def run_check(self): + measures = self.comm_code().measures.get(geographical_area=self.geo_area()) + + return ( + len(measures) > 0, + f"{len(measures)} measures matched required preferential rate", + ) diff --git a/reference_documents/jinja2/reference_document_versions/details.jinja b/reference_documents/jinja2/reference_document_versions/details.jinja new file mode 100644 index 000000000..c2cc712d8 --- /dev/null +++ b/reference_documents/jinja2/reference_document_versions/details.jinja @@ -0,0 +1,27 @@ +{% extends "layouts/layout.jinja" %} +{% from "components/table/macro.njk" import govukTable %} + +{% set page_title = 'Reference Documents version details' %} + +{% block breadcrumb %} + {{ breadcrumbs(request, [ + {'text': "Reference Documents"} + ]) }} +{% endblock %} + +{% block content %} +

+ Reference Document version Overview +

+ Reference document version details. + +
+ {{ govukTable({ "head": reference_document_version_duties_headers, "rows": reference_document_version_duties }) }} +
+
+ {{ govukTable({ "head": reference_document_version_quotas_headers, "rows": reference_document_version_quotas }) }} +
+{% endblock %} + + + diff --git a/reference_documents/jinja2/reference_documents/overview.jinja b/reference_documents/jinja2/reference_documents/details.jinja similarity index 93% rename from reference_documents/jinja2/reference_documents/overview.jinja rename to reference_documents/jinja2/reference_documents/details.jinja index fc442ceb7..0c301c051 100644 --- a/reference_documents/jinja2/reference_documents/overview.jinja +++ b/reference_documents/jinja2/reference_documents/details.jinja @@ -16,7 +16,7 @@ You will find a list of reference document versions below that can be viewed.
- {{ govukTable({ "head": reference_document_versions_headers, "rows": reference_documents_versions }) }} + {{ govukTable({ "head": reference_document_versions_headers, "rows": reference_document_versions }) }}
{% endblock %} diff --git a/reference_documents/models.py b/reference_documents/models.py index 253119f0b..94c712c05 100644 --- a/reference_documents/models.py +++ b/reference_documents/models.py @@ -1,6 +1,9 @@ from django.db import models +from django.db.models import fields from django_fsm import FSMField +from common.models import TimestampedMixin + class ReferenceDocumentVersionStatus(models.TextChoices): # Reference document version can be edited @@ -11,7 +14,7 @@ class ReferenceDocumentVersionStatus(models.TextChoices): PUBLISHED = "PUBLISHED", "Published" -class ReferenceDocument(models.Model): +class ReferenceDocument(models.Model, TimestampedMixin): title = models.CharField( max_length=255, help_text="Short name for this workbasket", @@ -25,7 +28,7 @@ class ReferenceDocument(models.Model): ) -class ReferenceDocumentVersion(models.Model): +class ReferenceDocumentVersion(models.Model, TimestampedMixin): version = models.FloatField() published_date = models.DateField(blank=True, null=True) entry_into_force_date = models.DateField(blank=True, null=True) @@ -98,3 +101,46 @@ class PreferentialQuota(models.Model): on_delete=models.PROTECT, related_name="preferential_quotas", ) + + +class AlignmentReport(models.Model, TimestampedMixin): + reference_document_version = models.ForeignKey( + "reference_documents.ReferenceDocumentVersion", + on_delete=models.PROTECT, + related_name="alignment_reports", + ) + + +class AlignmentReportCheck(models.Model): + created_at = models.DateTimeField(auto_now_add=True) + + alignment_report = models.ForeignKey( + "reference_documents.AlignmentReport", + on_delete=models.PROTECT, + related_name="alignment_report_checks", + ) + + check_name = fields.CharField(max_length=255) + """A string identifying the type of check carried out.""" + + successful = fields.BooleanField() + """True if the check was successful.""" + + message = fields.TextField(null=True) + """The text content returned by the check, if any.""" + + preferential_quota = models.ForeignKey( + "reference_documents.PreferentialQuota", + on_delete=models.PROTECT, + related_name="alignment_report_checks", + blank=True, + null=True, + ) + + preferential_rate = models.ForeignKey( + "reference_documents.PreferentialRate", + on_delete=models.PROTECT, + related_name="alignment_report_checks", + blank=True, + null=True, + ) diff --git a/reference_documents/urls.py b/reference_documents/urls.py index 3574a43b2..cd7a7006c 100644 --- a/reference_documents/urls.py +++ b/reference_documents/urls.py @@ -9,4 +9,14 @@ urlpatterns = [ path("reference_documents/", views.ReferenceDocumentList.as_view(), name="index"), + path( + "reference_documents//", + views.ReferenceDocumentDetails.as_view(), + name="details", + ), + path( + "reference_document_versions//", + views.ReferenceDocumentVersionDetails.as_view(), + name="version_details", + ), ] diff --git a/reference_documents/views.py b/reference_documents/views.py index d447ac90c..4fc8d2c65 100644 --- a/reference_documents/views.py +++ b/reference_documents/views.py @@ -1,9 +1,11 @@ from django.contrib.auth.mixins import PermissionRequiredMixin +from django.views.generic import DetailView from django.views.generic import ListView from geo_areas.models import GeographicalArea from geo_areas.models import GeographicalAreaDescription from reference_documents.models import ReferenceDocument +from reference_documents.models import ReferenceDocumentVersion class ReferenceDocumentList(PermissionRequiredMixin, ListView): @@ -27,7 +29,9 @@ def get_context_data(self, **kwargs): }, {"text": 0}, {"text": 0}, - {"text": ""}, + { + "html": f'Details', + }, ], ) @@ -44,7 +48,9 @@ def get_context_data(self, **kwargs): { "text": reference.reference_document_versions.last().preferential_quotas.count(), }, - {"text": ""}, + { + "html": f'Details', + }, ], ) @@ -70,3 +76,140 @@ def get_name_by_area_id(self, area_id): ) return geo_area_name.description if geo_area_name else "None" return "None" + + +class ReferenceDocumentDetails(PermissionRequiredMixin, DetailView): + template_name = "reference_documents/details.jinja" + permission_required = "reference_documents.view_reference_document" + model = ReferenceDocument + + def get_context_data(self, *args, **kwargs): + context = super(ReferenceDocumentDetails, self).get_context_data( + *args, + **kwargs, + ) + + context["reference_document_versions_headers"] = [ + {"text": "Version"}, + {"text": "Duties"}, + {"text": "Quotas"}, + {"text": "Actions"}, + ] + reference_document_versions = [] + + print(self.request) + + for version in context["object"].reference_document_versions.order_by( + "version", + ): + reference_document_versions.append( + [ + { + "text": version.version, + }, + { + "text": version.preferential_rates.count(), + }, + { + "text": version.preferential_quotas.count(), + }, + { + "html": f'version details', + }, + ], + ) + + context["reference_document_versions"] = reference_document_versions + + return context + + +class ReferenceDocumentVersionDetails(PermissionRequiredMixin, DetailView): + template_name = "reference_document_versions/details.jinja" + permission_required = "reference_documents.view_reference_document" + model = ReferenceDocumentVersion + + def get_context_data(self, *args, **kwargs): + context = super(ReferenceDocumentVersionDetails, self).get_context_data( + *args, + **kwargs, + ) + + context["reference_document_version_duties_headers"] = [ + {"text": "Comm Code"}, + {"text": "Duty Rate"}, + {"text": "Validity"}, + {"text": "Actions"}, + ] + + context["reference_document_version_quotas_headers"] = [ + {"text": "Order Number"}, + {"text": "Comm Code"}, + {"text": "Rate"}, + {"text": "Volume"}, + {"text": "Validity"}, + {"text": "Actions"}, + ] + + reference_document_version_duties = [] + reference_document_version_quotas = [] + + print(self.request) + + for duty in context["object"].preferential_rates.order_by("order"): + validity = "" + + if duty.valid_start_day: + validity = f"{duty.valid_start_day}/{duty.valid_start_month} - {duty.valid_end_day}/{duty.valid_end_month}" + + reference_document_version_duties.append( + [ + { + "text": duty.commodity_code, + }, + { + "text": duty.duty_rate, + }, + { + "text": validity, + }, + { + "text": "", + }, + ], + ) + + for quota in context["object"].preferential_quotas.order_by("order"): + validity = "" + + if quota.valid_start_day: + validity = f"{quota.valid_start_day}/{quota.valid_start_month} - {quota.valid_end_day}/{quota.valid_end_month}" + + reference_document_version_quotas.append( + [ + { + "text": quota.quota_order_number, + }, + { + "text": quota.commodity_code, + }, + { + "text": quota.quota_duty_rate, + }, + { + "text": f"{quota.volume} {quota.measurement}", + }, + { + "text": validity, + }, + { + "text": "", + }, + ], + ) + + context["reference_document_version_duties"] = reference_document_version_duties + + context["reference_document_version_quotas"] = reference_document_version_quotas + + return context From 627ae6e94dee3e50a4bbf72e7a30216f039a106d Mon Sep 17 00:00:00 2001 From: Doug Mills Date: Tue, 6 Feb 2024 08:29:21 +0000 Subject: [PATCH 015/118] initial commit - ref doc data model --- .../jinja2/reference_documents/overview.jinja | 24 +++++++++++++++++++ reference_documents/models.py | 1 - 2 files changed, 24 insertions(+), 1 deletion(-) create mode 100644 reference_documents/jinja2/reference_documents/overview.jinja diff --git a/reference_documents/jinja2/reference_documents/overview.jinja b/reference_documents/jinja2/reference_documents/overview.jinja new file mode 100644 index 000000000..fc442ceb7 --- /dev/null +++ b/reference_documents/jinja2/reference_documents/overview.jinja @@ -0,0 +1,24 @@ +{% extends "layouts/layout.jinja" %} +{% from "components/table/macro.njk" import govukTable %} + +{% set page_title = 'Reference Documents versions Overview' %} + +{% block breadcrumb %} + {{ breadcrumbs(request, [ + {'text': "Reference Documents"} + ]) }} +{% endblock %} + +{% block content %} +

+ Reference Document Overview +

+ You will find a list of reference document versions below that can be viewed. + +
+ {{ govukTable({ "head": reference_document_versions_headers, "rows": reference_documents_versions }) }} +
+{% endblock %} + + + diff --git a/reference_documents/models.py b/reference_documents/models.py index 94c712c05..ccb1eb64b 100644 --- a/reference_documents/models.py +++ b/reference_documents/models.py @@ -1,7 +1,6 @@ from django.db import models from django.db.models import fields from django_fsm import FSMField - from common.models import TimestampedMixin From 24ede2f438456b8578f29d822b3eee1ea95a6bdc Mon Sep 17 00:00:00 2001 From: Doug Mills Date: Tue, 13 Feb 2024 11:33:34 +0000 Subject: [PATCH 016/118] wip commit --- reference_documents/alignment_checks.py | 80 ++++++++++++-- .../migrations/0001_initial.py | 101 ++++++++++++++++-- .../0002_referencedocument_area_id.py | 19 ---- .../migrations/0003_auto_20240201_0940.py | 33 ------ .../migrations/0004_auto_20240202_0936.py | 23 ---- .../migrations/0005_auto_20240202_1431.py | 41 ------- .../0006_alter_preferentialquota_volume.py | 18 ---- reference_documents/models.py | 7 +- 8 files changed, 168 insertions(+), 154 deletions(-) delete mode 100644 reference_documents/migrations/0002_referencedocument_area_id.py delete mode 100644 reference_documents/migrations/0003_auto_20240201_0940.py delete mode 100644 reference_documents/migrations/0004_auto_20240202_0936.py delete mode 100644 reference_documents/migrations/0005_auto_20240202_1431.py delete mode 100644 reference_documents/migrations/0006_alter_preferentialquota_volume.py diff --git a/reference_documents/alignment_checks.py b/reference_documents/alignment_checks.py index fd25204e9..c7f43eb59 100644 --- a/reference_documents/alignment_checks.py +++ b/reference_documents/alignment_checks.py @@ -1,5 +1,8 @@ +from datetime import date + from commodities.models import GoodsNomenclature from geo_areas.models import GeographicalArea +from geo_areas.models import GeographicalAreaDescription from reference_documents.models import PreferentialRate @@ -13,13 +16,53 @@ def __init__(self, preferential_rate: PreferentialRate): self.preferential_rate = preferential_rate def comm_code(self): - return GoodsNomenclature.objects.latest_approved().get( + goods = GoodsNomenclature.objects.latest_approved().filter( item_id=self.preferential_rate.commodity_code, + valid_between__contains=self.ref_doc_version_eif_date(), + suffix=80, ) + if len(goods) == 0: + return None + + return goods.first() + def geo_area(self): - return GeographicalArea.objects.latest_approved().get( - area_id=self.preferential_rate.reference_document_version.reference_document.area_id, + return ( + GeographicalArea.objects.latest_approved() + .filter( + area_id=self.preferential_rate.reference_document_version.reference_document.area_id, + ) + .first() + ) + + def geo_area_description(self): + geo_area_desc = ( + GeographicalAreaDescription.objects.latest_approved() + .filter(described_geographicalarea=self.geo_area()) + .last() + ) + return geo_area_desc.description + + def ref_doc_version_eif_date(self): + eif_date = ( + self.preferential_rate.reference_document_version.entry_into_force_date + ) + + # todo : make sure EIf dates are all populated correctly - and remove this + if eif_date is None: + eif_date = date.today() + + return eif_date + + def related_measures(self): + return ( + self.comm_code() + .measures.latest_approved() + .filter( + geographical_area=self.geo_area(), + valid_between__contains=self.ref_doc_version_eif_date(), + ) ) def run_check(self): @@ -28,9 +71,30 @@ def run_check(self): class CheckPreferentialRateCommCode(BasePreferentialRateCheck): def run_check(self): - measures = self.comm_code().measures.get(geographical_area=self.geo_area()) + # comm code live on EIF date + if not self.comm_code(): + print( + "FAIL", + self.preferential_rate.commodity_code, + self.geo_area_description(), + "comm code not live", + ) + return False - return ( - len(measures) > 0, - f"{len(measures)} measures matched required preferential rate", - ) + measures = self.related_measures() + + if len(measures) == 1: + print("PASS", self.comm_code(), self.geo_area_description()) + return True + + if len(measures) == 0: + print("FAIL", self.comm_code(), self.geo_area_description()) + return False + + if len(measures) > 1: + print( + "WARNING - multiple measures", + self.comm_code(), + self.geo_area_description(), + ) + return True diff --git a/reference_documents/migrations/0001_initial.py b/reference_documents/migrations/0001_initial.py index 64b49e847..d94be24d1 100644 --- a/reference_documents/migrations/0001_initial.py +++ b/reference_documents/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 3.2.23 on 2024-01-29 14:52 +# Generated by Django 3.2.23 on 2024-02-08 09:34 import django.db.models.deletion import django_fsm @@ -12,18 +12,35 @@ class Migration(migrations.Migration): dependencies = [] operations = [ + migrations.CreateModel( + name="AlignmentReport", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("created_at", models.DateTimeField(auto_now_add=True)), + ], + ), migrations.CreateModel( name="ReferenceDocument", fields=[ ( "id", - models.BigAutoField( + models.AutoField( auto_created=True, primary_key=True, serialize=False, verbose_name="ID", ), ), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), ( "title", models.CharField( @@ -33,6 +50,7 @@ class Migration(migrations.Migration): unique=True, ), ), + ("area_id", models.CharField(db_index=True, max_length=4)), ], ), migrations.CreateModel( @@ -40,16 +58,18 @@ class Migration(migrations.Migration): fields=[ ( "id", - models.BigAutoField( + models.AutoField( auto_created=True, primary_key=True, serialize=False, verbose_name="ID", ), ), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), ("version", models.FloatField()), - ("published_date", models.DateField()), - ("entry_into_force_date", models.DateField()), + ("published_date", models.DateField(blank=True, null=True)), + ("entry_into_force_date", models.DateField(blank=True, null=True)), ( "status", django_fsm.FSMField( @@ -79,7 +99,7 @@ class Migration(migrations.Migration): fields=[ ( "id", - models.BigAutoField( + models.AutoField( auto_created=True, primary_key=True, serialize=False, @@ -89,6 +109,10 @@ class Migration(migrations.Migration): ("commodity_code", models.CharField(db_index=True, max_length=10)), ("duty_rate", models.CharField(max_length=255)), ("order", models.IntegerField()), + ("valid_start_day", models.IntegerField(blank=True, null=True)), + ("valid_start_month", models.IntegerField(blank=True, null=True)), + ("valid_end_day", models.IntegerField(blank=True, null=True)), + ("valid_end_month", models.IntegerField(blank=True, null=True)), ( "reference_document_version", models.ForeignKey( @@ -104,7 +128,7 @@ class Migration(migrations.Migration): fields=[ ( "id", - models.BigAutoField( + models.AutoField( auto_created=True, primary_key=True, serialize=False, @@ -114,9 +138,11 @@ class Migration(migrations.Migration): ("quota_order_number", models.CharField(db_index=True, max_length=6)), ("commodity_code", models.CharField(db_index=True, max_length=10)), ("quota_duty_rate", models.CharField(max_length=255)), - ("volume", models.FloatField()), - ("quota_period_open", models.DateField()), - ("quota_period_close", models.DateField()), + ("volume", models.CharField(max_length=255)), + ("valid_start_day", models.IntegerField(blank=True, null=True)), + ("valid_start_month", models.IntegerField(blank=True, null=True)), + ("valid_end_day", models.IntegerField(blank=True, null=True)), + ("valid_end_month", models.IntegerField(blank=True, null=True)), ("measurement", models.CharField(max_length=255)), ("order", models.IntegerField()), ( @@ -129,4 +155,59 @@ class Migration(migrations.Migration): ), ], ), + migrations.CreateModel( + name="AlignmentReportCheck", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("check_name", models.CharField(max_length=255)), + ("successful", models.BooleanField()), + ("message", models.TextField(null=True)), + ( + "alignment_report", + models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, + related_name="alignment_report_checks", + to="reference_documents.alignmentreport", + ), + ), + ( + "preferential_quota", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.PROTECT, + related_name="alignment_report_checks", + to="reference_documents.preferentialquota", + ), + ), + ( + "preferential_rate", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.PROTECT, + related_name="alignment_report_checks", + to="reference_documents.preferentialrate", + ), + ), + ], + ), + migrations.AddField( + model_name="alignmentreport", + name="reference_document_version", + field=models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, + related_name="alignment_reports", + to="reference_documents.referencedocumentversion", + ), + ), ] diff --git a/reference_documents/migrations/0002_referencedocument_area_id.py b/reference_documents/migrations/0002_referencedocument_area_id.py deleted file mode 100644 index 18f449577..000000000 --- a/reference_documents/migrations/0002_referencedocument_area_id.py +++ /dev/null @@ -1,19 +0,0 @@ -# Generated by Django 3.2.23 on 2024-01-30 16:02 - -from django.db import migrations -from django.db import models - - -class Migration(migrations.Migration): - dependencies = [ - ("reference_documents", "0001_initial"), - ] - - operations = [ - migrations.AddField( - model_name="referencedocument", - name="area_id", - field=models.CharField(db_index=True, default="ZZ", max_length=4), - preserve_default=False, - ), - ] diff --git a/reference_documents/migrations/0003_auto_20240201_0940.py b/reference_documents/migrations/0003_auto_20240201_0940.py deleted file mode 100644 index f7b8ec51d..000000000 --- a/reference_documents/migrations/0003_auto_20240201_0940.py +++ /dev/null @@ -1,33 +0,0 @@ -# Generated by Django 3.2.23 on 2024-02-01 09:40 - -from django.db import migrations -from django.db import models - - -class Migration(migrations.Migration): - dependencies = [ - ("reference_documents", "0002_referencedocument_area_id"), - ] - - operations = [ - migrations.AddField( - model_name="preferentialrate", - name="valid_end_day", - field=models.IntegerField(blank=True, null=True), - ), - migrations.AddField( - model_name="preferentialrate", - name="valid_end_month", - field=models.IntegerField(blank=True, null=True), - ), - migrations.AddField( - model_name="preferentialrate", - name="valid_start_day", - field=models.IntegerField(blank=True, null=True), - ), - migrations.AddField( - model_name="preferentialrate", - name="valid_start_month", - field=models.IntegerField(blank=True, null=True), - ), - ] diff --git a/reference_documents/migrations/0004_auto_20240202_0936.py b/reference_documents/migrations/0004_auto_20240202_0936.py deleted file mode 100644 index c59bbfbdb..000000000 --- a/reference_documents/migrations/0004_auto_20240202_0936.py +++ /dev/null @@ -1,23 +0,0 @@ -# Generated by Django 3.2.23 on 2024-02-02 09:36 - -from django.db import migrations -from django.db import models - - -class Migration(migrations.Migration): - dependencies = [ - ("reference_documents", "0003_auto_20240201_0940"), - ] - - operations = [ - migrations.AlterField( - model_name="referencedocumentversion", - name="entry_into_force_date", - field=models.DateField(blank=True, null=True), - ), - migrations.AlterField( - model_name="referencedocumentversion", - name="published_date", - field=models.DateField(blank=True, null=True), - ), - ] diff --git a/reference_documents/migrations/0005_auto_20240202_1431.py b/reference_documents/migrations/0005_auto_20240202_1431.py deleted file mode 100644 index 7787303df..000000000 --- a/reference_documents/migrations/0005_auto_20240202_1431.py +++ /dev/null @@ -1,41 +0,0 @@ -# Generated by Django 3.2.23 on 2024-02-02 14:31 - -from django.db import migrations -from django.db import models - - -class Migration(migrations.Migration): - dependencies = [ - ("reference_documents", "0004_auto_20240202_0936"), - ] - - operations = [ - migrations.RemoveField( - model_name="preferentialquota", - name="quota_period_close", - ), - migrations.RemoveField( - model_name="preferentialquota", - name="quota_period_open", - ), - migrations.AddField( - model_name="preferentialquota", - name="valid_end_day", - field=models.IntegerField(blank=True, null=True), - ), - migrations.AddField( - model_name="preferentialquota", - name="valid_end_month", - field=models.IntegerField(blank=True, null=True), - ), - migrations.AddField( - model_name="preferentialquota", - name="valid_start_day", - field=models.IntegerField(blank=True, null=True), - ), - migrations.AddField( - model_name="preferentialquota", - name="valid_start_month", - field=models.IntegerField(blank=True, null=True), - ), - ] diff --git a/reference_documents/migrations/0006_alter_preferentialquota_volume.py b/reference_documents/migrations/0006_alter_preferentialquota_volume.py deleted file mode 100644 index 1f898a734..000000000 --- a/reference_documents/migrations/0006_alter_preferentialquota_volume.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 3.2.23 on 2024-02-02 14:48 - -from django.db import migrations -from django.db import models - - -class Migration(migrations.Migration): - dependencies = [ - ("reference_documents", "0005_auto_20240202_1431"), - ] - - operations = [ - migrations.AlterField( - model_name="preferentialquota", - name="volume", - field=models.CharField(max_length=255), - ), - ] diff --git a/reference_documents/models.py b/reference_documents/models.py index ccb1eb64b..1214d8b55 100644 --- a/reference_documents/models.py +++ b/reference_documents/models.py @@ -27,7 +27,9 @@ class ReferenceDocument(models.Model, TimestampedMixin): ) -class ReferenceDocumentVersion(models.Model, TimestampedMixin): +class ReferenceDocumentVersion(models.Model): + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) version = models.FloatField() published_date = models.DateField(blank=True, null=True) entry_into_force_date = models.DateField(blank=True, null=True) @@ -102,7 +104,8 @@ class PreferentialQuota(models.Model): ) -class AlignmentReport(models.Model, TimestampedMixin): +class AlignmentReport(models.Model): + created_at = models.DateTimeField(auto_now_add=True) reference_document_version = models.ForeignKey( "reference_documents.ReferenceDocumentVersion", on_delete=models.PROTECT, From b54738140b8efe3cf8fb4ec2b57cd57fa678b586 Mon Sep 17 00:00:00 2001 From: Doug Mills Date: Wed, 14 Feb 2024 11:42:17 +0000 Subject: [PATCH 017/118] wip commit --- .../migrations/0001_initial.py | 213 ------------------ reference_documents/models.py | 5 +- 2 files changed, 3 insertions(+), 215 deletions(-) delete mode 100644 reference_documents/migrations/0001_initial.py diff --git a/reference_documents/migrations/0001_initial.py b/reference_documents/migrations/0001_initial.py deleted file mode 100644 index d94be24d1..000000000 --- a/reference_documents/migrations/0001_initial.py +++ /dev/null @@ -1,213 +0,0 @@ -# Generated by Django 3.2.23 on 2024-02-08 09:34 - -import django.db.models.deletion -import django_fsm -from django.db import migrations -from django.db import models - - -class Migration(migrations.Migration): - initial = True - - dependencies = [] - - operations = [ - migrations.CreateModel( - name="AlignmentReport", - fields=[ - ( - "id", - models.AutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ("created_at", models.DateTimeField(auto_now_add=True)), - ], - ), - migrations.CreateModel( - name="ReferenceDocument", - fields=[ - ( - "id", - models.AutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ("created_at", models.DateTimeField(auto_now_add=True)), - ("updated_at", models.DateTimeField(auto_now=True)), - ( - "title", - models.CharField( - db_index=True, - help_text="Short name for this workbasket", - max_length=255, - unique=True, - ), - ), - ("area_id", models.CharField(db_index=True, max_length=4)), - ], - ), - migrations.CreateModel( - name="ReferenceDocumentVersion", - fields=[ - ( - "id", - models.AutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ("created_at", models.DateTimeField(auto_now_add=True)), - ("updated_at", models.DateTimeField(auto_now=True)), - ("version", models.FloatField()), - ("published_date", models.DateField(blank=True, null=True)), - ("entry_into_force_date", models.DateField(blank=True, null=True)), - ( - "status", - django_fsm.FSMField( - choices=[ - ("EDITING", "Editing"), - ("IN_REVIEW", "In Review"), - ("PUBLISHED", "Published"), - ], - db_index=True, - default="EDITING", - editable=False, - max_length=50, - ), - ), - ( - "reference_document", - models.ForeignKey( - on_delete=django.db.models.deletion.PROTECT, - related_name="reference_document_versions", - to="reference_documents.referencedocument", - ), - ), - ], - ), - migrations.CreateModel( - name="PreferentialRate", - fields=[ - ( - "id", - models.AutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ("commodity_code", models.CharField(db_index=True, max_length=10)), - ("duty_rate", models.CharField(max_length=255)), - ("order", models.IntegerField()), - ("valid_start_day", models.IntegerField(blank=True, null=True)), - ("valid_start_month", models.IntegerField(blank=True, null=True)), - ("valid_end_day", models.IntegerField(blank=True, null=True)), - ("valid_end_month", models.IntegerField(blank=True, null=True)), - ( - "reference_document_version", - models.ForeignKey( - on_delete=django.db.models.deletion.PROTECT, - related_name="preferential_rates", - to="reference_documents.referencedocumentversion", - ), - ), - ], - ), - migrations.CreateModel( - name="PreferentialQuota", - fields=[ - ( - "id", - models.AutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ("quota_order_number", models.CharField(db_index=True, max_length=6)), - ("commodity_code", models.CharField(db_index=True, max_length=10)), - ("quota_duty_rate", models.CharField(max_length=255)), - ("volume", models.CharField(max_length=255)), - ("valid_start_day", models.IntegerField(blank=True, null=True)), - ("valid_start_month", models.IntegerField(blank=True, null=True)), - ("valid_end_day", models.IntegerField(blank=True, null=True)), - ("valid_end_month", models.IntegerField(blank=True, null=True)), - ("measurement", models.CharField(max_length=255)), - ("order", models.IntegerField()), - ( - "reference_document_version", - models.ForeignKey( - on_delete=django.db.models.deletion.PROTECT, - related_name="preferential_quotas", - to="reference_documents.referencedocumentversion", - ), - ), - ], - ), - migrations.CreateModel( - name="AlignmentReportCheck", - fields=[ - ( - "id", - models.AutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ("created_at", models.DateTimeField(auto_now_add=True)), - ("check_name", models.CharField(max_length=255)), - ("successful", models.BooleanField()), - ("message", models.TextField(null=True)), - ( - "alignment_report", - models.ForeignKey( - on_delete=django.db.models.deletion.PROTECT, - related_name="alignment_report_checks", - to="reference_documents.alignmentreport", - ), - ), - ( - "preferential_quota", - models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.PROTECT, - related_name="alignment_report_checks", - to="reference_documents.preferentialquota", - ), - ), - ( - "preferential_rate", - models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.PROTECT, - related_name="alignment_report_checks", - to="reference_documents.preferentialrate", - ), - ), - ], - ), - migrations.AddField( - model_name="alignmentreport", - name="reference_document_version", - field=models.ForeignKey( - on_delete=django.db.models.deletion.PROTECT, - related_name="alignment_reports", - to="reference_documents.referencedocumentversion", - ), - ), - ] diff --git a/reference_documents/models.py b/reference_documents/models.py index 1214d8b55..9e7987320 100644 --- a/reference_documents/models.py +++ b/reference_documents/models.py @@ -1,7 +1,6 @@ from django.db import models from django.db.models import fields from django_fsm import FSMField -from common.models import TimestampedMixin class ReferenceDocumentVersionStatus(models.TextChoices): @@ -13,7 +12,9 @@ class ReferenceDocumentVersionStatus(models.TextChoices): PUBLISHED = "PUBLISHED", "Published" -class ReferenceDocument(models.Model, TimestampedMixin): +class ReferenceDocument(models.Model): + created_at = models.DateTimeField(auto_now_add=True) + title = models.CharField( max_length=255, help_text="Short name for this workbasket", From a622c35b24b4086ee0225753a2b24ec645718fcc Mon Sep 17 00:00:00 2001 From: Doug Mills Date: Wed, 14 Feb 2024 12:58:21 +0000 Subject: [PATCH 018/118] wip commit --- .../migrations/0001_initial.py | 100 ++++++++++++++++-- .../0002_referencedocument_area_id.py | 19 ---- .../migrations/0003_auto_20240201_0940.py | 33 ------ .../migrations/0004_auto_20240202_0936.py | 23 ---- .../migrations/0005_auto_20240202_1431.py | 41 ------- .../0006_alter_preferentialquota_volume.py | 18 ---- 6 files changed, 90 insertions(+), 144 deletions(-) delete mode 100644 reference_documents/migrations/0002_referencedocument_area_id.py delete mode 100644 reference_documents/migrations/0003_auto_20240201_0940.py delete mode 100644 reference_documents/migrations/0004_auto_20240202_0936.py delete mode 100644 reference_documents/migrations/0005_auto_20240202_1431.py delete mode 100644 reference_documents/migrations/0006_alter_preferentialquota_volume.py diff --git a/reference_documents/migrations/0001_initial.py b/reference_documents/migrations/0001_initial.py index 64b49e847..c56b2b4c0 100644 --- a/reference_documents/migrations/0001_initial.py +++ b/reference_documents/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 3.2.23 on 2024-01-29 14:52 +# Generated by Django 3.2.23 on 2024-02-14 11:45 import django.db.models.deletion import django_fsm @@ -12,18 +12,34 @@ class Migration(migrations.Migration): dependencies = [] operations = [ + migrations.CreateModel( + name="AlignmentReport", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("created_at", models.DateTimeField(auto_now_add=True)), + ], + ), migrations.CreateModel( name="ReferenceDocument", fields=[ ( "id", - models.BigAutoField( + models.AutoField( auto_created=True, primary_key=True, serialize=False, verbose_name="ID", ), ), + ("created_at", models.DateTimeField(auto_now_add=True)), ( "title", models.CharField( @@ -33,6 +49,7 @@ class Migration(migrations.Migration): unique=True, ), ), + ("area_id", models.CharField(db_index=True, max_length=4)), ], ), migrations.CreateModel( @@ -40,16 +57,18 @@ class Migration(migrations.Migration): fields=[ ( "id", - models.BigAutoField( + models.AutoField( auto_created=True, primary_key=True, serialize=False, verbose_name="ID", ), ), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), ("version", models.FloatField()), - ("published_date", models.DateField()), - ("entry_into_force_date", models.DateField()), + ("published_date", models.DateField(blank=True, null=True)), + ("entry_into_force_date", models.DateField(blank=True, null=True)), ( "status", django_fsm.FSMField( @@ -79,7 +98,7 @@ class Migration(migrations.Migration): fields=[ ( "id", - models.BigAutoField( + models.AutoField( auto_created=True, primary_key=True, serialize=False, @@ -89,6 +108,10 @@ class Migration(migrations.Migration): ("commodity_code", models.CharField(db_index=True, max_length=10)), ("duty_rate", models.CharField(max_length=255)), ("order", models.IntegerField()), + ("valid_start_day", models.IntegerField(blank=True, null=True)), + ("valid_start_month", models.IntegerField(blank=True, null=True)), + ("valid_end_day", models.IntegerField(blank=True, null=True)), + ("valid_end_month", models.IntegerField(blank=True, null=True)), ( "reference_document_version", models.ForeignKey( @@ -104,7 +127,7 @@ class Migration(migrations.Migration): fields=[ ( "id", - models.BigAutoField( + models.AutoField( auto_created=True, primary_key=True, serialize=False, @@ -114,9 +137,11 @@ class Migration(migrations.Migration): ("quota_order_number", models.CharField(db_index=True, max_length=6)), ("commodity_code", models.CharField(db_index=True, max_length=10)), ("quota_duty_rate", models.CharField(max_length=255)), - ("volume", models.FloatField()), - ("quota_period_open", models.DateField()), - ("quota_period_close", models.DateField()), + ("volume", models.CharField(max_length=255)), + ("valid_start_day", models.IntegerField(blank=True, null=True)), + ("valid_start_month", models.IntegerField(blank=True, null=True)), + ("valid_end_day", models.IntegerField(blank=True, null=True)), + ("valid_end_month", models.IntegerField(blank=True, null=True)), ("measurement", models.CharField(max_length=255)), ("order", models.IntegerField()), ( @@ -129,4 +154,59 @@ class Migration(migrations.Migration): ), ], ), + migrations.CreateModel( + name="AlignmentReportCheck", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("check_name", models.CharField(max_length=255)), + ("successful", models.BooleanField()), + ("message", models.TextField(null=True)), + ( + "alignment_report", + models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, + related_name="alignment_report_checks", + to="reference_documents.alignmentreport", + ), + ), + ( + "preferential_quota", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.PROTECT, + related_name="alignment_report_checks", + to="reference_documents.preferentialquota", + ), + ), + ( + "preferential_rate", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.PROTECT, + related_name="alignment_report_checks", + to="reference_documents.preferentialrate", + ), + ), + ], + ), + migrations.AddField( + model_name="alignmentreport", + name="reference_document_version", + field=models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, + related_name="alignment_reports", + to="reference_documents.referencedocumentversion", + ), + ), ] diff --git a/reference_documents/migrations/0002_referencedocument_area_id.py b/reference_documents/migrations/0002_referencedocument_area_id.py deleted file mode 100644 index 18f449577..000000000 --- a/reference_documents/migrations/0002_referencedocument_area_id.py +++ /dev/null @@ -1,19 +0,0 @@ -# Generated by Django 3.2.23 on 2024-01-30 16:02 - -from django.db import migrations -from django.db import models - - -class Migration(migrations.Migration): - dependencies = [ - ("reference_documents", "0001_initial"), - ] - - operations = [ - migrations.AddField( - model_name="referencedocument", - name="area_id", - field=models.CharField(db_index=True, default="ZZ", max_length=4), - preserve_default=False, - ), - ] diff --git a/reference_documents/migrations/0003_auto_20240201_0940.py b/reference_documents/migrations/0003_auto_20240201_0940.py deleted file mode 100644 index f7b8ec51d..000000000 --- a/reference_documents/migrations/0003_auto_20240201_0940.py +++ /dev/null @@ -1,33 +0,0 @@ -# Generated by Django 3.2.23 on 2024-02-01 09:40 - -from django.db import migrations -from django.db import models - - -class Migration(migrations.Migration): - dependencies = [ - ("reference_documents", "0002_referencedocument_area_id"), - ] - - operations = [ - migrations.AddField( - model_name="preferentialrate", - name="valid_end_day", - field=models.IntegerField(blank=True, null=True), - ), - migrations.AddField( - model_name="preferentialrate", - name="valid_end_month", - field=models.IntegerField(blank=True, null=True), - ), - migrations.AddField( - model_name="preferentialrate", - name="valid_start_day", - field=models.IntegerField(blank=True, null=True), - ), - migrations.AddField( - model_name="preferentialrate", - name="valid_start_month", - field=models.IntegerField(blank=True, null=True), - ), - ] diff --git a/reference_documents/migrations/0004_auto_20240202_0936.py b/reference_documents/migrations/0004_auto_20240202_0936.py deleted file mode 100644 index c59bbfbdb..000000000 --- a/reference_documents/migrations/0004_auto_20240202_0936.py +++ /dev/null @@ -1,23 +0,0 @@ -# Generated by Django 3.2.23 on 2024-02-02 09:36 - -from django.db import migrations -from django.db import models - - -class Migration(migrations.Migration): - dependencies = [ - ("reference_documents", "0003_auto_20240201_0940"), - ] - - operations = [ - migrations.AlterField( - model_name="referencedocumentversion", - name="entry_into_force_date", - field=models.DateField(blank=True, null=True), - ), - migrations.AlterField( - model_name="referencedocumentversion", - name="published_date", - field=models.DateField(blank=True, null=True), - ), - ] diff --git a/reference_documents/migrations/0005_auto_20240202_1431.py b/reference_documents/migrations/0005_auto_20240202_1431.py deleted file mode 100644 index 7787303df..000000000 --- a/reference_documents/migrations/0005_auto_20240202_1431.py +++ /dev/null @@ -1,41 +0,0 @@ -# Generated by Django 3.2.23 on 2024-02-02 14:31 - -from django.db import migrations -from django.db import models - - -class Migration(migrations.Migration): - dependencies = [ - ("reference_documents", "0004_auto_20240202_0936"), - ] - - operations = [ - migrations.RemoveField( - model_name="preferentialquota", - name="quota_period_close", - ), - migrations.RemoveField( - model_name="preferentialquota", - name="quota_period_open", - ), - migrations.AddField( - model_name="preferentialquota", - name="valid_end_day", - field=models.IntegerField(blank=True, null=True), - ), - migrations.AddField( - model_name="preferentialquota", - name="valid_end_month", - field=models.IntegerField(blank=True, null=True), - ), - migrations.AddField( - model_name="preferentialquota", - name="valid_start_day", - field=models.IntegerField(blank=True, null=True), - ), - migrations.AddField( - model_name="preferentialquota", - name="valid_start_month", - field=models.IntegerField(blank=True, null=True), - ), - ] diff --git a/reference_documents/migrations/0006_alter_preferentialquota_volume.py b/reference_documents/migrations/0006_alter_preferentialquota_volume.py deleted file mode 100644 index 1f898a734..000000000 --- a/reference_documents/migrations/0006_alter_preferentialquota_volume.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 3.2.23 on 2024-02-02 14:48 - -from django.db import migrations -from django.db import models - - -class Migration(migrations.Migration): - dependencies = [ - ("reference_documents", "0005_auto_20240202_1431"), - ] - - operations = [ - migrations.AlterField( - model_name="preferentialquota", - name="volume", - field=models.CharField(max_length=255), - ), - ] From 94181990bbc58fcb01a0d9de3f914476e2b8dfad Mon Sep 17 00:00:00 2001 From: Doug Mills Date: Tue, 20 Feb 2024 11:26:44 +0000 Subject: [PATCH 019/118] added alignment report, reference document and reference document version views, refactored the checks and ran the checks several times against reference document versions. --- common/static/common/scss/application.scss | 1 + reference_documents/alignment_checks.py | 100 ---------- reference_documents/checks/base.py | 175 ++++++++++++++++++ reference_documents/checks/check_runner.py | 43 +++++ .../checks/preferential_quotas.py | 1 + .../checks/preferential_rates.py | 45 +++++ reference_documents/checks/utils.py | 21 +++ .../jinja2/alignment_reports/details.jinja | 25 +++ .../alignment_reports.jinja | 25 +++ .../reference_document_versions/details.jinja | 2 +- .../migrations/0002_auto_20240215_1056.py | 32 ++++ .../migrations/0003_auto_20240219_0951.py | 36 ++++ reference_documents/models.py | 23 ++- .../scss/_reference_documents.scss | 8 + reference_documents/urls.py | 16 +- reference_documents/views.py | 136 +++++++++++++- webpack.config.js | 1 + 17 files changed, 581 insertions(+), 109 deletions(-) delete mode 100644 reference_documents/alignment_checks.py create mode 100644 reference_documents/checks/base.py create mode 100644 reference_documents/checks/check_runner.py create mode 100644 reference_documents/checks/preferential_quotas.py create mode 100644 reference_documents/checks/preferential_rates.py create mode 100644 reference_documents/checks/utils.py create mode 100644 reference_documents/jinja2/alignment_reports/details.jinja create mode 100644 reference_documents/jinja2/reference_document_versions/alignment_reports.jinja create mode 100644 reference_documents/migrations/0002_auto_20240215_1056.py create mode 100644 reference_documents/migrations/0003_auto_20240219_0951.py create mode 100644 reference_documents/static/reference_documents/scss/_reference_documents.scss diff --git a/common/static/common/scss/application.scss b/common/static/common/scss/application.scss index 3ac96053c..b9cc920eb 100644 --- a/common/static/common/scss/application.scss +++ b/common/static/common/scss/application.scss @@ -27,6 +27,7 @@ $govuk-image-url-function: frontend-image-url; @import "publishing"; @import "regulations"; @import "workbaskets"; +@import "reference_documents"; @import "versions"; @import "fake-link"; @import "violations"; diff --git a/reference_documents/alignment_checks.py b/reference_documents/alignment_checks.py deleted file mode 100644 index c7f43eb59..000000000 --- a/reference_documents/alignment_checks.py +++ /dev/null @@ -1,100 +0,0 @@ -from datetime import date - -from commodities.models import GoodsNomenclature -from geo_areas.models import GeographicalArea -from geo_areas.models import GeographicalAreaDescription -from reference_documents.models import PreferentialRate - - -class BaseCheck: - def run_check(self): - raise NotImplemented("Please implement on child classes") - - -class BasePreferentialRateCheck(BaseCheck): - def __init__(self, preferential_rate: PreferentialRate): - self.preferential_rate = preferential_rate - - def comm_code(self): - goods = GoodsNomenclature.objects.latest_approved().filter( - item_id=self.preferential_rate.commodity_code, - valid_between__contains=self.ref_doc_version_eif_date(), - suffix=80, - ) - - if len(goods) == 0: - return None - - return goods.first() - - def geo_area(self): - return ( - GeographicalArea.objects.latest_approved() - .filter( - area_id=self.preferential_rate.reference_document_version.reference_document.area_id, - ) - .first() - ) - - def geo_area_description(self): - geo_area_desc = ( - GeographicalAreaDescription.objects.latest_approved() - .filter(described_geographicalarea=self.geo_area()) - .last() - ) - return geo_area_desc.description - - def ref_doc_version_eif_date(self): - eif_date = ( - self.preferential_rate.reference_document_version.entry_into_force_date - ) - - # todo : make sure EIf dates are all populated correctly - and remove this - if eif_date is None: - eif_date = date.today() - - return eif_date - - def related_measures(self): - return ( - self.comm_code() - .measures.latest_approved() - .filter( - geographical_area=self.geo_area(), - valid_between__contains=self.ref_doc_version_eif_date(), - ) - ) - - def run_check(self): - raise NotImplementedError("Please implement on child classes") - - -class CheckPreferentialRateCommCode(BasePreferentialRateCheck): - def run_check(self): - # comm code live on EIF date - if not self.comm_code(): - print( - "FAIL", - self.preferential_rate.commodity_code, - self.geo_area_description(), - "comm code not live", - ) - return False - - measures = self.related_measures() - - if len(measures) == 1: - print("PASS", self.comm_code(), self.geo_area_description()) - return True - - if len(measures) == 0: - print("FAIL", self.comm_code(), self.geo_area_description()) - return False - - if len(measures) > 1: - print( - "WARNING - multiple measures", - self.comm_code(), - self.geo_area_description(), - ) - return True diff --git a/reference_documents/checks/base.py b/reference_documents/checks/base.py new file mode 100644 index 000000000..2f1c6ceb0 --- /dev/null +++ b/reference_documents/checks/base.py @@ -0,0 +1,175 @@ +from datetime import date + +from commodities.models import GoodsNomenclature +from commodities.models.dc import CommodityCollectionLoader +from commodities.models.dc import CommodityTreeSnapshot +from commodities.models.dc import SnapshotMoment +from common.models import Transaction +from geo_areas.models import GeographicalArea +from geo_areas.models import GeographicalAreaDescription +from reference_documents.models import PreferentialQuota +from reference_documents.models import PreferentialRate + + +class BaseCheck: + def __init__(self): + self.dependent_on_passing_check = None + + def run_check(self): + raise NotImplemented("Please implement on child classes") + + +class BasePreferentialQuotaCheck(BaseCheck): + def __init__(self, preferential_quota: PreferentialQuota): + super().__init__() + self.preferential_quota = preferential_quota + + +class BasePreferentialRateCheck(BaseCheck): + def __init__(self, preferential_rate: PreferentialRate): + super().__init__() + self.preferential_rate = preferential_rate + + def get_snapshot(self) -> CommodityTreeSnapshot: + # not liking having to use CommodityTreeSnapshot, but it does to the job + item_id = self.comm_code().item_id + while item_id[-2:] == "00": + item_id = item_id[0 : len(item_id) - 2] + + commodities_collection = CommodityCollectionLoader( + prefix=item_id, + ).load(current_only=True) + + latest_transaction = Transaction.objects.order_by("created_at").last() + + snapshot = CommodityTreeSnapshot( + commodities=commodities_collection.commodities, + moment=SnapshotMoment(transaction=latest_transaction), + ) + + return snapshot + + def comm_code(self): + goods = GoodsNomenclature.objects.latest_approved().filter( + item_id=self.preferential_rate.commodity_code, + valid_between__contains=self.ref_doc_version_eif_date(), + suffix=80, + ) + + if len(goods) == 0: + return None + + return goods.first() + + def geo_area(self): + return ( + GeographicalArea.objects.latest_approved() + .filter( + area_id=self.preferential_rate.reference_document_version.reference_document.area_id, + ) + .first() + ) + + def geo_area_description(self): + geo_area_desc = ( + GeographicalAreaDescription.objects.latest_approved() + .filter(described_geographicalarea=self.geo_area()) + .last() + ) + return geo_area_desc.description + + def ref_doc_version_eif_date(self): + eif_date = ( + self.preferential_rate.reference_document_version.entry_into_force_date + ) + + # todo : make sure EIf dates are all populated correctly - and remove this + if eif_date is None: + eif_date = date.today() + + return eif_date + + def related_measures(self, comm_code_item_id=None): + if comm_code_item_id: + good = GoodsNomenclature.objects.latest_approved().filter( + item_id=comm_code_item_id, + valid_between__contains=self.ref_doc_version_eif_date(), + suffix=80, + ) + + if len(good) == 1: + return ( + good.first() + .measures.latest_approved() + .filter( + geographical_area=self.geo_area(), + valid_between__contains=self.ref_doc_version_eif_date(), + measure_type__sid__in=[ + 142, + 143, + ], # note : these are the measure types used to identify preferential tariffs + ) + ) + else: + return [] + else: + return ( + self.comm_code() + .measures.latest_approved() + .filter( + geographical_area=self.geo_area(), + valid_between__contains=self.ref_doc_version_eif_date(), + measure_type__sid__in=[ + 142, + 143, + ], # note : these are the measure types used to identify preferential tariffs + ) + ) + + def recursive_comm_code_check( + self, + snapshot: CommodityTreeSnapshot, + parent_item_id, + parent_item_suffix, + level=1, + ): + # find comm code from snapshot + child_commodities = [] + for commodity in snapshot.commodities: + if ( + commodity.item_id == parent_item_id + and commodity.suffix == parent_item_suffix + ): + child_commodities = snapshot.get_children(commodity) + break + + if len(child_commodities) == 0: + print(f'{"-" * level} no more children') + return False + + results = [] + for child_commodity in child_commodities: + related_measures = self.related_measures(child_commodity.item_id) + + if len(related_measures) == 0: + print(f'{"-" * level} FAIL : {child_commodity.item_id}') + results.append( + self.recursive_comm_code_check( + snapshot, + child_commodity.item_id, + child_commodity.suffix, + level + 1, + ), + ) + elif len(related_measures) == 1: + print(f'{"-" * level} PASS : {child_commodity.item_id}') + results.append(True) + else: + # Multiple measures + print(f'{"-" * level} PASS : multiple : {child_commodity.item_id}') + results.append(True) + + return False in results + + def run_check(self): + raise NotImplementedError("Please implement on child classes") diff --git a/reference_documents/checks/check_runner.py b/reference_documents/checks/check_runner.py new file mode 100644 index 000000000..43d6c1223 --- /dev/null +++ b/reference_documents/checks/check_runner.py @@ -0,0 +1,43 @@ +from reference_documents.checks.base import BasePreferentialQuotaCheck +from reference_documents.checks.base import BasePreferentialRateCheck +from reference_documents.checks.preferential_quotas import * # noqa +from reference_documents.checks.preferential_rates import * # noqa +from reference_documents.checks.utils import utils +from reference_documents.models import AlignmentReport +from reference_documents.models import AlignmentReportCheck +from reference_documents.models import ReferenceDocumentVersion + + +class Checks: + def __init__(self, reference_document_version: ReferenceDocumentVersion): + self.reference_document_version = reference_document_version + self.alignment_report = AlignmentReport.objects.create( + reference_document_version=self.reference_document_version, + ) + + @staticmethod + def get_checks_for(check_class): + return utils().get_child_checks(check_class) + + def run(self): + for check in Checks.get_checks_for(BasePreferentialRateCheck): + for pref_rate in self.reference_document_version.preferential_rates.all(): + self.capture_check_result(check(pref_rate), pref_rate=pref_rate) + + for check in Checks.get_checks_for(BasePreferentialQuotaCheck): + for pref_quota in self.reference_document_version.preferential_quotas.all(): + self.capture_check_result(check(pref_quota), pref_quota=pref_quota) + + def capture_check_result(self, check, pref_rate=None, pref_quota=None): + status, message = check.run_check() + + kwargs = { + "alignment_report": self.alignment_report, + "check_name": check.__class__.__name__, + "preferential_rate": pref_rate, + "preferential_quota": pref_quota, + "status": status, + "message": message, + } + + AlignmentReportCheck.objects.create(**kwargs) diff --git a/reference_documents/checks/preferential_quotas.py b/reference_documents/checks/preferential_quotas.py new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/reference_documents/checks/preferential_quotas.py @@ -0,0 +1 @@ + diff --git a/reference_documents/checks/preferential_rates.py b/reference_documents/checks/preferential_rates.py new file mode 100644 index 000000000..840b33b21 --- /dev/null +++ b/reference_documents/checks/preferential_rates.py @@ -0,0 +1,45 @@ +from reference_documents.checks.base import BasePreferentialRateCheck +from reference_documents.models import AlignmentReportCheckStatus + + +class MeasureExists(BasePreferentialRateCheck): + def run_check(self): + # comm code live on EIF date + if not self.comm_code(): + message = f"{self.preferential_rate.commodity_code} {self.geo_area_description()} comm code not live" + print("FAIL", message) + return AlignmentReportCheckStatus.FAIL, message + + # query measures + measures = self.related_measures() + + # this is ok - there is a single measure matching the expected query + if len(measures) == 1: + return AlignmentReportCheckStatus.PASS, "" + + # this is not inline with expected measures presence - check comm code children + if len(measures) == 0: + # check 1 level down for presence of measures + match = self.recursive_comm_code_check( + self.get_snapshot(), + self.preferential_rate.commodity_code, + 80, + ) + + message = f"{self.comm_code()}, {self.geo_area_description()}" + + if match: + message += f"\nmatched with children" + print("PASS", message) + + return AlignmentReportCheckStatus.PASS, message + else: + message += f"\nno expected measures found on good code or children" + print("FAIL", message) + + return AlignmentReportCheckStatus.FAIL, message + + if len(measures) > 1: + message = f"multiple measures match {self.comm_code()}, {self.geo_area_description()}" + print("WARNING", message) + return AlignmentReportCheckStatus.WARNING, message diff --git a/reference_documents/checks/utils.py b/reference_documents/checks/utils.py new file mode 100644 index 000000000..242f15289 --- /dev/null +++ b/reference_documents/checks/utils.py @@ -0,0 +1,21 @@ +from reference_documents.checks.base import BaseCheck + + +class utils: + def get_child_checks(self, check_class: BaseCheck.__class__): + result = [] + + check_classes = self.subclasses_for(check_class) + for check_class in check_classes: + result.append(check_class) + + return result + + def subclasses_for(self, cls) -> list: + all_subclasses = [] + + for subclass in cls.__subclasses__(): + all_subclasses.append(subclass) + all_subclasses.extend(self.subclasses_for(subclass)) + + return all_subclasses diff --git a/reference_documents/jinja2/alignment_reports/details.jinja b/reference_documents/jinja2/alignment_reports/details.jinja new file mode 100644 index 000000000..59de1a733 --- /dev/null +++ b/reference_documents/jinja2/alignment_reports/details.jinja @@ -0,0 +1,25 @@ +{% extends "layouts/layout.jinja" %} +{% from "components/table/macro.njk" import govukTable %} + +{% set page_title = 'Reference Documents version details' %} + +{% block breadcrumb %} + {{ breadcrumbs(request, [ + {'text': "Reference Document Version Overview"} + ]) }} +{% endblock %} + +{% block content %} +

+ Alignment reports +

+ Reference document version details. + +
+ {{ govukTable({ "head": alignment_report_headers, "rows": alignment_reports }) }} +
+ +{% endblock %} + + + diff --git a/reference_documents/jinja2/reference_document_versions/alignment_reports.jinja b/reference_documents/jinja2/reference_document_versions/alignment_reports.jinja new file mode 100644 index 000000000..59de1a733 --- /dev/null +++ b/reference_documents/jinja2/reference_document_versions/alignment_reports.jinja @@ -0,0 +1,25 @@ +{% extends "layouts/layout.jinja" %} +{% from "components/table/macro.njk" import govukTable %} + +{% set page_title = 'Reference Documents version details' %} + +{% block breadcrumb %} + {{ breadcrumbs(request, [ + {'text': "Reference Document Version Overview"} + ]) }} +{% endblock %} + +{% block content %} +

+ Alignment reports +

+ Reference document version details. + +
+ {{ govukTable({ "head": alignment_report_headers, "rows": alignment_reports }) }} +
+ +{% endblock %} + + + diff --git a/reference_documents/jinja2/reference_document_versions/details.jinja b/reference_documents/jinja2/reference_document_versions/details.jinja index c2cc712d8..7f32bbb29 100644 --- a/reference_documents/jinja2/reference_document_versions/details.jinja +++ b/reference_documents/jinja2/reference_document_versions/details.jinja @@ -5,7 +5,7 @@ {% block breadcrumb %} {{ breadcrumbs(request, [ - {'text': "Reference Documents"} + {'text': "Reference Document Version"} ]) }} {% endblock %} diff --git a/reference_documents/migrations/0002_auto_20240215_1056.py b/reference_documents/migrations/0002_auto_20240215_1056.py new file mode 100644 index 000000000..f2d8b7bb1 --- /dev/null +++ b/reference_documents/migrations/0002_auto_20240215_1056.py @@ -0,0 +1,32 @@ +# Generated by Django 3.2.23 on 2024-02-15 10:56 + +import django_fsm +from django.db import migrations + + +class Migration(migrations.Migration): + dependencies = [ + ("reference_documents", "0001_initial"), + ] + + operations = [ + migrations.RemoveField( + model_name="alignmentreportcheck", + name="successful", + ), + migrations.AddField( + model_name="alignmentreportcheck", + name="status", + field=django_fsm.FSMField( + choices=[ + ("PASS", "Passing"), + ("FAIL", "Failed"), + ("WARNING", "Warning"), + ], + db_index=True, + default="FAIL", + editable=False, + max_length=50, + ), + ), + ] diff --git a/reference_documents/migrations/0003_auto_20240219_0951.py b/reference_documents/migrations/0003_auto_20240219_0951.py new file mode 100644 index 000000000..5d621f081 --- /dev/null +++ b/reference_documents/migrations/0003_auto_20240219_0951.py @@ -0,0 +1,36 @@ +# Generated by Django 3.2.23 on 2024-02-19 09:51 + +import django.db.models.deletion +from django.db import migrations +from django.db import models + + +class Migration(migrations.Migration): + dependencies = [ + ("reference_documents", "0002_auto_20240215_1056"), + ] + + operations = [ + migrations.AlterField( + model_name="alignmentreportcheck", + name="preferential_quota", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.PROTECT, + related_name="preferential_quota_checks", + to="reference_documents.preferentialquota", + ), + ), + migrations.AlterField( + model_name="alignmentreportcheck", + name="preferential_rate", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.PROTECT, + related_name="preferential_rate_checks", + to="reference_documents.preferentialrate", + ), + ), + ] diff --git a/reference_documents/models.py b/reference_documents/models.py index 9e7987320..6e691056a 100644 --- a/reference_documents/models.py +++ b/reference_documents/models.py @@ -12,6 +12,15 @@ class ReferenceDocumentVersionStatus(models.TextChoices): PUBLISHED = "PUBLISHED", "Published" +class AlignmentReportCheckStatus(models.TextChoices): + # Reference document version can be edited + PASS = "PASS", "Passing" + # Reference document version ius locked and in review + FAIL = "FAIL", "Failed" + # reference document version has been approved and published + WARNING = "WARNING", "Warning" + + class ReferenceDocument(models.Model): created_at = models.DateTimeField(auto_now_add=True) @@ -126,16 +135,20 @@ class AlignmentReportCheck(models.Model): check_name = fields.CharField(max_length=255) """A string identifying the type of check carried out.""" - successful = fields.BooleanField() - """True if the check was successful.""" - + status = FSMField( + default=AlignmentReportCheckStatus.FAIL, + choices=AlignmentReportCheckStatus.choices, + db_index=True, + protected=False, + editable=False, + ) message = fields.TextField(null=True) """The text content returned by the check, if any.""" preferential_quota = models.ForeignKey( "reference_documents.PreferentialQuota", on_delete=models.PROTECT, - related_name="alignment_report_checks", + related_name="preferential_quota_checks", blank=True, null=True, ) @@ -143,7 +156,7 @@ class AlignmentReportCheck(models.Model): preferential_rate = models.ForeignKey( "reference_documents.PreferentialRate", on_delete=models.PROTECT, - related_name="alignment_report_checks", + related_name="preferential_rate_checks", blank=True, null=True, ) diff --git a/reference_documents/static/reference_documents/scss/_reference_documents.scss b/reference_documents/static/reference_documents/scss/_reference_documents.scss new file mode 100644 index 000000000..59f254e15 --- /dev/null +++ b/reference_documents/static/reference_documents/scss/_reference_documents.scss @@ -0,0 +1,8 @@ +.check-passing { + color: #1d640f; + font-weight: bold; +} +.check-failing { + color: #671111; + font-weight: bold; +} \ No newline at end of file diff --git a/reference_documents/urls.py b/reference_documents/urls.py index cd7a7006c..42d4994c1 100644 --- a/reference_documents/urls.py +++ b/reference_documents/urls.py @@ -8,7 +8,11 @@ api_router = routers.DefaultRouter() urlpatterns = [ - path("reference_documents/", views.ReferenceDocumentList.as_view(), name="index"), + path( + "reference_documents/", + views.ReferenceDocumentList.as_view(), + name="index", + ), path( "reference_documents//", views.ReferenceDocumentDetails.as_view(), @@ -19,4 +23,14 @@ views.ReferenceDocumentVersionDetails.as_view(), name="version_details", ), + path( + "reference_document_version_alignment_reports//", + views.ReferenceDocumentVersionAlignmentReportsDetailsView.as_view(), + name="reference_document_version_alignment_reports", + ), + path( + "alignment_reports//", + views.AlignmentReportsDetailsView.as_view(), + name="alignment_reports", + ), ] diff --git a/reference_documents/views.py b/reference_documents/views.py index 4fc8d2c65..0cee3d04c 100644 --- a/reference_documents/views.py +++ b/reference_documents/views.py @@ -4,6 +4,8 @@ from geo_areas.models import GeographicalArea from geo_areas.models import GeographicalAreaDescription +from reference_documents.models import AlignmentReport +from reference_documents.models import AlignmentReportCheckStatus from reference_documents.models import ReferenceDocument from reference_documents.models import ReferenceDocumentVersion @@ -114,7 +116,8 @@ def get_context_data(self, *args, **kwargs): "text": version.preferential_quotas.count(), }, { - "html": f'version details', + "html": f'version details
' + f'Alignment reports', }, ], ) @@ -139,6 +142,7 @@ def get_context_data(self, *args, **kwargs): {"text": "Comm Code"}, {"text": "Duty Rate"}, {"text": "Validity"}, + {"text": "Checks"}, {"text": "Actions"}, ] @@ -148,13 +152,14 @@ def get_context_data(self, *args, **kwargs): {"text": "Rate"}, {"text": "Volume"}, {"text": "Validity"}, + {"text": "Checks"}, {"text": "Actions"}, ] reference_document_version_duties = [] reference_document_version_quotas = [] - print(self.request) + latest_alignment_report = context["object"].alignment_reports.last() for duty in context["object"].preferential_rates.order_by("order"): validity = "" @@ -162,6 +167,25 @@ def get_context_data(self, *args, **kwargs): if duty.valid_start_day: validity = f"{duty.valid_start_day}/{duty.valid_start_month} - {duty.valid_end_day}/{duty.valid_end_month}" + failure_count = ( + duty.preferential_rate_checks.all() + .filter( + alignment_report=latest_alignment_report, + status=AlignmentReportCheckStatus.FAIL, + ) + .count() + ) + check_count = ( + duty.preferential_rate_checks.all() + .filter(alignment_report=latest_alignment_report) + .count() + ) + + if failure_count > 0: + checks_output = f'
FAILED {failure_count} of {check_count}
' + else: + checks_output = f'
PASSED {check_count} of {check_count}
' + reference_document_version_duties.append( [ { @@ -173,6 +197,9 @@ def get_context_data(self, *args, **kwargs): { "text": validity, }, + { + "html": checks_output, + }, { "text": "", }, @@ -185,6 +212,25 @@ def get_context_data(self, *args, **kwargs): if quota.valid_start_day: validity = f"{quota.valid_start_day}/{quota.valid_start_month} - {quota.valid_end_day}/{quota.valid_end_month}" + failure_count = ( + quota.preferential_quota_checks.all() + .filter( + alignment_report=latest_alignment_report, + status=AlignmentReportCheckStatus.FAIL, + ) + .count() + ) + check_count = ( + quota.preferential_quota_checks.all() + .filter(alignment_report=latest_alignment_report) + .count() + ) + + if failure_count > 0: + checks_output = f'
FAILED {failure_count} of {check_count}
' + else: + checks_output = f'
PASSED {check_count} of {check_count}
' + reference_document_version_quotas.append( [ { @@ -202,6 +248,9 @@ def get_context_data(self, *args, **kwargs): { "text": validity, }, + { + "html": checks_output, + }, { "text": "", }, @@ -213,3 +262,86 @@ def get_context_data(self, *args, **kwargs): context["reference_document_version_quotas"] = reference_document_version_quotas return context + + +class ReferenceDocumentVersionAlignmentReportsDetailsView( + PermissionRequiredMixin, + DetailView, +): + template_name = "reference_document_versions/alignment_reports.jinja" + permission_required = "reference_documents.view_reference_document" + model = ReferenceDocumentVersion + + def get_context_data(self, *args, **kwargs): + context = super( + ReferenceDocumentVersionAlignmentReportsDetailsView, + self, + ).get_context_data( + *args, + **kwargs, + ) + + context["alignment_report_headers"] = [ + {"text": "Created"}, + {"text": "Passed"}, + {"text": "failed"}, + {"text": "Percent"}, + {"text": "Actions"}, + ] + + alignment_reports = [] + for report in context["object"].alignment_reports.order_by("-created_at"): + failure_count = ( + report.alignment_report_checks.all() + .filter(status=AlignmentReportCheckStatus.FAIL) + .count() + ) + pass_count = ( + report.alignment_report_checks.all() + .filter(status=AlignmentReportCheckStatus.PASS) + .count() + ) + + if pass_count > 0: + pass_percentage = round( + (pass_count / (pass_count + failure_count)) * 100, + 2, + ) + else: + pass_percentage = 100 + + alignment_reports.append( + [ + { + "text": report.created_at.strftime("%d/%m/%Y %H:%M"), + }, + { + "text": pass_count, + }, + { + "text": failure_count, + }, + { + "text": f"{pass_percentage} %", + }, + { + "html": f"Details", + }, + ], + ) + + context["alignment_reports"] = alignment_reports + + return context + + +class AlignmentReportsDetailsView(PermissionRequiredMixin, DetailView): + template_name = "alignment_reports/details.jinja" + permission_required = "reference_documents.view_reference_document" + model = AlignmentReport + + def get_context_data(self, *args, **kwargs): + context = super(AlignmentReportsDetailsView, self).get_context_data( + *args, + **kwargs, + ) diff --git a/webpack.config.js b/webpack.config.js index 2b6e1b1bb..449be3ce9 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -68,6 +68,7 @@ module.exports = { 'publishing/static/publishing/scss', 'regulations/static/regulations/scss', 'workbaskets/static/workbaskets/scss', + 'reference_documents/static/reference_documents/scss', ], }, }, From c34ef7659d910bb3fcb46210412237117cd44852 Mon Sep 17 00:00:00 2001 From: Dale Cannon <118175145+dalecannon@users.noreply.github.com> Date: Wed, 17 Jan 2024 11:46:17 +0000 Subject: [PATCH 020/118] TP2000-1168 Add sub-quota, blocking period & suspension period nested review tabs (#1133) * Add sub-quotas nested review tab * Add quota blocking periods nested review tab * Add quota suspension periods nested review tab * Use tab title instead of model verbose name * Add blocking period and suspension period SID to table --- .../review-quota-blocking-periods.jinja | 37 +++++++++++++ .../review-quota-suspension-periods.jinja | 35 ++++++++++++ .../workbaskets/review-sub-quotas.jinja | 35 ++++++++++++ .../jinja2/workbaskets/review-quotas.jinja | 15 +++++ workbaskets/jinja2/workbaskets/review.jinja | 2 +- workbaskets/tests/test_views.py | 15 +++++ workbaskets/urls.py | 15 +++++ workbaskets/views/ui.py | 55 +++++++++++++++++++ 8 files changed, 208 insertions(+), 1 deletion(-) create mode 100644 workbaskets/jinja2/includes/workbaskets/review-quota-blocking-periods.jinja create mode 100644 workbaskets/jinja2/includes/workbaskets/review-quota-suspension-periods.jinja create mode 100644 workbaskets/jinja2/includes/workbaskets/review-sub-quotas.jinja diff --git a/workbaskets/jinja2/includes/workbaskets/review-quota-blocking-periods.jinja b/workbaskets/jinja2/includes/workbaskets/review-quota-blocking-periods.jinja new file mode 100644 index 000000000..e587f5a0d --- /dev/null +++ b/workbaskets/jinja2/includes/workbaskets/review-quota-blocking-periods.jinja @@ -0,0 +1,37 @@ +{% set table_rows = [] %} +{% for obj in object_list %} + {% set quota_link %} + + {{ obj.quota_definition.order_number.order_number }} + + {% endset %} + + {% set quota_definition_link %} + + {{ obj.quota_definition.sid }} + + {% endset %} + + {{ table_rows.append([ + {"text": obj.sid}, + {"html": quota_link}, + {"html": quota_definition_link}, + {"text": "{:%d %b %Y}".format(obj.valid_between.lower)}, + {"text": "{:%d %b %Y}".format(obj.valid_between.upper) if obj.valid_between.upper else "-"}, + {"text": obj.blocking_period_type}, + {"text": obj.description if obj.description else "-", "classes": "govuk-!-width-one-quarter"}, + ]) or "" }} +{% endfor %} + +{{ govukTable({ + "head": [ + {"text": "Blocking period SID"}, + {"text": "Order number"}, + {"text": "Quota definition SID"}, + {"text": "Start date"}, + {"text": "End date"}, + {"text": "Blocking period type"}, + {"text": "Description"}, + ], + "rows": table_rows +}) }} diff --git a/workbaskets/jinja2/includes/workbaskets/review-quota-suspension-periods.jinja b/workbaskets/jinja2/includes/workbaskets/review-quota-suspension-periods.jinja new file mode 100644 index 000000000..fd5885c14 --- /dev/null +++ b/workbaskets/jinja2/includes/workbaskets/review-quota-suspension-periods.jinja @@ -0,0 +1,35 @@ +{% set table_rows = [] %} +{% for obj in object_list %} + {% set quota_link %} + + {{ obj.quota_definition.order_number.order_number }} + + {% endset %} + + {% set quota_definition_link %} + + {{ obj.quota_definition.sid }} + + {% endset %} + + {{ table_rows.append([ + {"text": obj.sid}, + {"html": quota_link}, + {"html": quota_definition_link}, + {"text": "{:%d %b %Y}".format(obj.valid_between.lower)}, + {"text": "{:%d %b %Y}".format(obj.valid_between.upper) if obj.valid_between.upper else "-"}, + {"text": obj.description if obj.description else "-", "classes": "govuk-!-width-one-quarter"}, + ]) or "" }} +{% endfor %} + +{{ govukTable({ + "head": [ + {"text": "Suspension period SID"}, + {"text": "Order number"}, + {"text": "Quota definition SID"}, + {"text": "Start date"}, + {"text": "End date"}, + {"text": "Description"}, + ], + "rows": table_rows +}) }} diff --git a/workbaskets/jinja2/includes/workbaskets/review-sub-quotas.jinja b/workbaskets/jinja2/includes/workbaskets/review-sub-quotas.jinja new file mode 100644 index 000000000..0c02a8bbd --- /dev/null +++ b/workbaskets/jinja2/includes/workbaskets/review-sub-quotas.jinja @@ -0,0 +1,35 @@ +{% set table_rows = [] %} +{% for obj in object_list %} + {% set sub_quota_link %} + + {{ obj.sub_quota.order_number.order_number }} + + {% endset %} + + {% set main_quota_link %} + + {{ obj.main_quota.order_number.order_number }} + + {% endset %} + + {{ table_rows.append([ + {"html": main_quota_link}, + {"html": sub_quota_link}, + {"text": "{:%d %b %Y}".format(obj.sub_quota.valid_between.lower) }, + {"text": "{:%d %b %Y}".format(obj.sub_quota.valid_between.upper) if obj.sub_quota.valid_between.upper else "-"}, + {"text": obj.sub_quota_relation_type}, + {"text": obj.coefficient}, + ]) or "" }} +{% endfor %} + +{{ govukTable({ + "head": [ + {"text": "Order number"}, + {"text": "Sub-quota order number"}, + {"text": "Start date"}, + {"text": "End date"}, + {"text": "Relation type"}, + {"text": "Co-efficient"}, + ], + "rows": table_rows +}) }} diff --git a/workbaskets/jinja2/workbaskets/review-quotas.jinja b/workbaskets/jinja2/workbaskets/review-quotas.jinja index b45313a2b..3f5114d3f 100644 --- a/workbaskets/jinja2/workbaskets/review-quotas.jinja +++ b/workbaskets/jinja2/workbaskets/review-quotas.jinja @@ -11,6 +11,21 @@ "href": url("workbaskets:workbasket-ui-review-quota-definitions", kwargs={"pk": workbasket.pk}), "selected": selected_nested_tab == "quota-definitions", }, + { + "text": "Sub-quota associations", + "href": url("workbaskets:workbasket-ui-review-sub-quotas", kwargs={"pk": workbasket.pk}), + "selected": selected_nested_tab == "sub-quotas", + }, + { + "text": "Blocking periods", + "href": url("workbaskets:workbasket-ui-review-quota-blocking-periods", kwargs={"pk": workbasket.pk}), + "selected": selected_nested_tab == "blocking-periods", + }, + { + "text": "Suspension periods", + "href": url("workbaskets:workbasket-ui-review-quota-suspension-periods", kwargs={"pk": workbasket.pk}), + "selected": selected_nested_tab == "suspension-periods", + }, ] %} diff --git a/workbaskets/jinja2/workbaskets/review.jinja b/workbaskets/jinja2/workbaskets/review.jinja index d817ec569..2fd0e00eb 100644 --- a/workbaskets/jinja2/workbaskets/review.jinja +++ b/workbaskets/jinja2/workbaskets/review.jinja @@ -114,7 +114,7 @@ {% if object_list %} {% include tab_template %} {% else %} -

0 {{ view.model._meta.verbose_name_plural }} available to review.

+

0 {{ selected_nested_tab.replace("-", " ") if selected_nested_tab else selected_tab }} available to review.

{% endif %} {% include "includes/common/pagination.jinja" %} {% endblock %} diff --git a/workbaskets/tests/test_views.py b/workbaskets/tests/test_views.py index 667f390c2..8bc4e0170 100644 --- a/workbaskets/tests/test_views.py +++ b/workbaskets/tests/test_views.py @@ -454,6 +454,21 @@ def test_workbasket_review_tabs_without_permission(url, client): lambda: factories.QuotaDefinitionFactory.create(), 11, ), + ( + "workbaskets:workbasket-ui-review-sub-quotas", + lambda: factories.QuotaAssociationFactory.create(), + 6, + ), + ( + "workbaskets:workbasket-ui-review-quota-blocking-periods", + lambda: factories.QuotaBlockingFactory.create(), + 7, + ), + ( + "workbaskets:workbasket-ui-review-quota-suspension-periods", + lambda: factories.QuotaSuspensionFactory.create(), + 6, + ), ( "workbaskets:workbasket-ui-review-regulations", lambda: factories.RegulationFactory.create(), diff --git a/workbaskets/urls.py b/workbaskets/urls.py index 0defdcd0e..6a13b872d 100644 --- a/workbaskets/urls.py +++ b/workbaskets/urls.py @@ -96,6 +96,21 @@ ui_views.WorkBasketReviewQuotaDefinitionsView.as_view(), name="workbasket-ui-review-quota-definitions", ), + path( + f"/review-sub-quotas/", + ui_views.WorkBasketReviewSubQuotasView.as_view(), + name="workbasket-ui-review-sub-quotas", + ), + path( + f"/review-quota-blocking-periods/", + ui_views.WorkBasketReviewQuotaBlockingView.as_view(), + name="workbasket-ui-review-quota-blocking-periods", + ), + path( + f"/review-quota-suspension-periods/", + ui_views.WorkBasketReviewQuotaSuspensionView.as_view(), + name="workbasket-ui-review-quota-suspension-periods", + ), path( f"/review-regulations/", ui_views.WorkBasketReviewRegulationsView.as_view(), diff --git a/workbaskets/views/ui.py b/workbaskets/views/ui.py index 57ce51d91..6d55a709d 100644 --- a/workbaskets/views/ui.py +++ b/workbaskets/views/ui.py @@ -49,8 +49,11 @@ from notifications.models import Notification from notifications.models import NotificationTypeChoices from publishing.models import PackagedWorkBasket +from quotas.models import QuotaAssociation +from quotas.models import QuotaBlocking from quotas.models import QuotaDefinition from quotas.models import QuotaOrderNumber +from quotas.models import QuotaSuspension from regulations.models import Regulation from workbaskets import forms from workbaskets.models import DataRow @@ -1478,6 +1481,58 @@ def get_context_data(self, *args, **kwargs): return context +class WorkBasketReviewSubQuotasView(WorkBasketReviewView): + """UI endpoint for reviewing sub-quota association changes in a + workbasket.""" + + model = QuotaAssociation + template_name = "workbaskets/review-quotas.jinja" + + def get_context_data(self, *args, **kwargs): + context = super().get_context_data(*args, **kwargs) + context["tab_page_title"] = "Review sub-quota associations" + context["selected_tab"] = "quotas" + context["selected_nested_tab"] = "sub-quotas" + context["tab_template"] = "includes/workbaskets/review-sub-quotas.jinja" + return context + + +class WorkBasketReviewQuotaBlockingView(WorkBasketReviewView): + """UI endpoint for reviewing quota blocking period changes in a + workbasket.""" + + model = QuotaBlocking + template_name = "workbaskets/review-quotas.jinja" + + def get_context_data(self, *args, **kwargs): + context = super().get_context_data(*args, **kwargs) + context["tab_page_title"] = "Review quota blocking periods" + context["selected_tab"] = "quotas" + context["selected_nested_tab"] = "blocking-periods" + context[ + "tab_template" + ] = "includes/workbaskets/review-quota-blocking-periods.jinja" + return context + + +class WorkBasketReviewQuotaSuspensionView(WorkBasketReviewView): + """UI endpoint for reviewing quota suspension period changes in a + workbasket.""" + + model = QuotaSuspension + template_name = "workbaskets/review-quotas.jinja" + + def get_context_data(self, *args, **kwargs): + context = super().get_context_data(*args, **kwargs) + context["tab_page_title"] = "Review quota suspension periods" + context["selected_tab"] = "quotas" + context["selected_nested_tab"] = "suspension-periods" + context[ + "tab_template" + ] = "includes/workbaskets/review-quota-suspension-periods.jinja" + return context + + class WorkBasketReviewRegulationsView(WorkBasketReviewView): """UI endpoint for reviewing regulation changes in a workbasket.""" From b34a5cd8ba1a53fcb27eb00a6522e6bc7afc4204 Mon Sep 17 00:00:00 2001 From: Tash Boyse <57753415+nboyse@users.noreply.github.com> Date: Wed, 17 Jan 2024 13:10:41 +0000 Subject: [PATCH 021/118] Feat: expand expiring quotas report to include tabs (#1131) --- reports/jinja2/generics/table.jinja | 65 +++++- reports/reports/base_table.py | 36 ++- ...piring_quotas_with_no_definition_period.py | 211 +++++++++++++++--- ...piring_quotas_with_no_definition_period.py | 130 ++++++----- 4 files changed, 352 insertions(+), 90 deletions(-) diff --git a/reports/jinja2/generics/table.jinja b/reports/jinja2/generics/table.jinja index 8f190f2cf..ae72f44e6 100644 --- a/reports/jinja2/generics/table.jinja +++ b/reports/jinja2/generics/table.jinja @@ -1,6 +1,63 @@ {% from "components/table/macro.njk" import govukTable -%} -{{ govukTable({ - "head": report.headers(), - "rows": report.rows() -}) }} +{% if report.tabular_reports -%} + +
+ +
+

{{ report.tab_name }}

+ {{ govukTable({ + "head": report.headers(), + "rows": report.rows() + }) }} +
+
+

{{ report.tab_name2 }}

+ {{ govukTable({ + "head": report.headers2(), + "rows": report.rows2() + }) }} +
+
+

{{ report.tab_name3 }}

+ {{ govukTable({ + "head": report.headers3(), + "rows": report.rows3() + }) }} +
+
+

{{ report.tab_name4 }}

+ {{ govukTable({ + "head": report.headers4(), + "rows": report.rows4() + }) }} +
+
+ +{% else -%} + {{ govukTable({ + "head": report.headers(), + "rows": report.rows() + }) }} +{% endif -%} \ No newline at end of file diff --git a/reports/reports/base_table.py b/reports/reports/base_table.py index 7724b191e..e735d806e 100644 --- a/reports/reports/base_table.py +++ b/reports/reports/base_table.py @@ -6,6 +6,7 @@ class ReportBaseTable(ReportBase): name = "Base Table Report" report_template = "table" + tabular_reports = False def __init__(self): pass @@ -19,9 +20,42 @@ def headers(self) -> [dict]: pass @abstractmethod - def rows(self) -> [[dict]]: + def rows(self) -> [dict]: pass @abstractmethod def row(self, row) -> [dict]: pass + + def headers2(self) -> [dict]: + return [] + + def rows2(self) -> [dict]: + return [] + + def row2(self, row) -> [dict]: + return [] + + def headers3(self) -> [dict]: + return [] + + def rows3(self) -> [dict]: + return [] + + def row3(self, row) -> [dict]: + return [] + + def headers4(self) -> [dict]: + return [] + + def rows4(self) -> [dict]: + return [] + + def row4(self, row) -> [dict]: + return [] + + def query3(self): + return [] + + def query4(self): + return [] diff --git a/reports/reports/expiring_quotas_with_no_definition_period.py b/reports/reports/expiring_quotas_with_no_definition_period.py index 8f02a8f13..58972b83d 100644 --- a/reports/reports/expiring_quotas_with_no_definition_period.py +++ b/reports/reports/expiring_quotas_with_no_definition_period.py @@ -1,21 +1,25 @@ import datetime -from django.db.models import Exists, Q +from django.db.models import Q from reports.reports.base_table import ReportBaseTable from quotas.models import ( QuotaOrderNumber, QuotaDefinition, - QuotaOrderNumberOriginExclusion, - QuotaOrderNumberOrigin, + QuotaBlocking, + QuotaSuspension, ) -from measures.models import Measure, MeasureExcludedGeographicalArea class Report(ReportBaseTable): name = "Quotas Expiring Soon" enabled = True - description = ( - "Quotas with definition periods about to expire and no future definition period" - ) + description = "Quotas with definition/sub-quota/blocking/suspension periods about to expire and no future definition period" + tabular_reports = True + tab_name = "Definitions" + tab_name2 = "Sub-quota associations" + tab_name3 = "Blocking periods" + tab_name4 = "Suspension periods" + current_time = datetime.datetime.now() + future_time = current_time + datetime.timedelta(weeks=5) def headers(self) -> [dict]: return [ @@ -24,7 +28,7 @@ def headers(self) -> [dict]: {"text": "Definition End Date"}, ] - def row(self, row: QuotaDefinition) -> [dict]: + def row(self, row) -> [dict]: return [ {"text": row.order_number}, {"text": row.valid_between.lower}, @@ -32,29 +36,29 @@ def row(self, row: QuotaDefinition) -> [dict]: ] def rows(self) -> [[dict]]: - table_rows = [] - for row in self.query(): - table_rows.append(self.row(row)) + table_rows = [self.row(row) for row in self.query()] + + if not any(table_rows): + return [ + [{"text": "There is no data for this report at present"}] + + [{"text": " "} for _ in range(len(self.headers()) - 1)] + ] return table_rows def query(self): - expiring_quotas = self.find_quotas_expiring_soon() + expiring_quotas = self.find_quota_definitions_expiring_soon() quotas_without_future_definition = self.find_quotas_without_future_definition( expiring_quotas ) return quotas_without_future_definition - def find_quotas_expiring_soon(self): - current_time = datetime.datetime.now() - future_time = current_time + datetime.timedelta(weeks=5) - - filter_query = ( - Q(valid_between__endswith__gte=current_time) - | Q(valid_between__endswith=None) - ) & Q( + def find_quota_definitions_expiring_soon(self): + filter_query = Q( valid_between__isnull=False, - valid_between__endswith__lte=future_time, + valid_between__endswith__lte=self.future_time, + ) & Q(valid_between__endswith__gte=self.current_time) | Q( + valid_between__endswith=None ) quotas_expiring_soon = QuotaDefinition.objects.latest_approved().filter( @@ -67,16 +71,173 @@ def find_quotas_without_future_definition(self, expiring_quotas): matching_data = set() for quota in expiring_quotas: - future_definitions = QuotaDefinition.objects.latest_approved().filter( + future_definitions = QuotaOrderNumber.objects.latest_approved().filter( order_number=quota.order_number, valid_between__startswith__gt=quota.valid_between.upper, ) if not future_definitions.exists(): + quota.definition_start_date = quota.valid_between.lower + quota.definition_end_date = quota.valid_between.upper matching_data.add(quota) - for quota in matching_data: - quota.definition_start_date = quota.valid_between.lower - quota.definition_end_date = quota.valid_between.upper + return list(matching_data) + + def headers2(self) -> [dict]: + return [ + {"text": "Quota Order Number"}, + {"text": "Sub-quota associations SID"}, + {"text": "Sub-quota associations Start Date"}, + {"text": "Sub-quota associations End Date"}, + {"text": "Definition Period SID"}, + ] + + def row2(self, row) -> [dict]: + sub_quotas_array = [] + + for sub_quotas in row.sub_quotas.all(): + sub_quotas_array.append( + {"text": row.order_number}, + {"text": sub_quotas.sid}, + {"text": sub_quotas.valid_between.lower}, + {"text": sub_quotas.valid_between.upper}, + {"text": row.sid}, + ) + + return sub_quotas_array + + def rows2(self) -> [[dict]]: + table_rows = [self.row2(row) for row in self.query()] + + if not any(table_rows): + return [ + [{"text": "There is no data for this report at present"}] + + [{"text": " "} for _ in range(len(self.headers2()) - 1)] + ] + + return table_rows + + def find_quota_blocking_without_future_definition(self, expiring_quotas): + matching_data = set() + + for quota_definition in expiring_quotas: + associated_blocking_definitions = ( + QuotaBlocking.objects.latest_approved().filter( + quota_definition=quota_definition, + ) + ) + + if associated_blocking_definitions.exists(): + quota_definition.definition_start_date = ( + quota_definition.valid_between.lower + ) + quota_definition.definition_end_date = ( + quota_definition.valid_between.upper + ) + matching_data.add(quota_definition) return list(matching_data) + + def headers3(self) -> [dict]: + return [ + {"text": "Quota Order Number"}, + {"text": "Blocking Period SIDs"}, + {"text": "Blocking Period Start Date"}, + {"text": "Blocking Period End Date"}, + {"text": "Definition Period SID"}, + ] + + def row3(self, row) -> [dict]: + return [ + {"text": row.order_number}, + {"text": blocking.sid for blocking in row.quotablocking_set.all()}, + { + "text": blocking.valid_between.lower + for blocking in row.quotablocking_set.all() + }, + { + "text": blocking.valid_between.upper + for blocking in row.quotablocking_set.all() + }, + {"text": row.sid}, + ] + + def rows3(self) -> [[dict]]: + table_rows = [self.row3(row) for row in self.query3()] + + if not any(table_rows): + return [ + [{"text": "There is no data for this report at present"}] + + [{"text": " "} for _ in range(len(self.headers3()) - 1)] + ] + + return table_rows + + def query3(self): + expiring_quotas = self.find_quota_definitions_expiring_soon() + quota_blocking_without_future_definition = ( + self.find_quota_blocking_without_future_definition(expiring_quotas) + ) + return quota_blocking_without_future_definition + + def find_quota_suspension_without_future_definition(self, expiring_quotas): + matching_data = set() + + for quota_definition in expiring_quotas: + future_definitions = QuotaSuspension.objects.latest_approved().filter( + quota_definition=quota_definition, + # valid_between__startswith__gt=quota_definition.valid_between.upper, + ) + + if future_definitions.exists(): + quota_definition.definition_start_date = ( + quota_definition.valid_between.lower + ) + quota_definition.definition_end_date = ( + quota_definition.valid_between.upper + ) + matching_data.add(quota_definition) + + return list(matching_data) + + def headers4(self) -> [dict]: + return [ + {"text": "Quota Order Number"}, + {"text": "Suspension Period SIDs"}, + {"text": "Suspension Period Start Date"}, + {"text": "Suspension Period End Date"}, + {"text": "Definition Period SID"}, + ] + + def row4(self, row) -> [dict]: + return [ + {"text": row.order_number}, + {"text": suspension.sid for suspension in row.quotasuspension_set.all()}, + { + "text": suspension.valid_between.lower + for suspension in row.quotasuspension_set.all() + }, + { + "text": suspension.valid_between.upper + for suspension in row.quotasuspension_set.all() + }, + {"text": row.sid}, + ] + + def rows4(self) -> [[dict]]: + table_rows = [self.row4(row) for row in self.query4()] + + if not any(table_rows): + return [ + [{"text": "There is no data for this report at present"}] + + [{"text": " "} for _ in range(len(self.headers4()) - 1)] + ] + + return table_rows + + def query4(self): + expiring_quotas = self.find_quota_definitions_expiring_soon() + quota_suspension_without_future_definition = ( + self.find_quota_suspension_without_future_definition(expiring_quotas) + ) + return quota_suspension_without_future_definition diff --git a/reports/tests/test_expiring_quotas_with_no_definition_period.py b/reports/tests/test_expiring_quotas_with_no_definition_period.py index c83e471bb..25807ff1c 100644 --- a/reports/tests/test_expiring_quotas_with_no_definition_period.py +++ b/reports/tests/test_expiring_quotas_with_no_definition_period.py @@ -1,79 +1,89 @@ import pytest import datetime -from datetime import timedelta from dateutil.relativedelta import relativedelta from common.tests import factories from common.util import TaricDateRange from reports.reports.expiring_quotas_with_no_definition_period import Report -from quotas.models import QuotaDefinition - - -@pytest.fixture -def quota_order_number(db): - return factories.QuotaOrderNumberFactory.create() - - -@pytest.fixture -def expired_quota_definition(quota_order_number): - return factories.QuotaDefinitionFactory.create( - order_number=quota_order_number, - valid_between=TaricDateRange( - datetime.datetime.today().date() + relativedelta(weeks=-2), - datetime.datetime.today().date() + relativedelta(weeks=-1), - ), - ) - - -@pytest.fixture -def expiring_soon_quota_definition(quota_order_number): - return factories.QuotaDefinitionFactory.create( - order_number=quota_order_number, - valid_between=TaricDateRange( - datetime.datetime.today().date() + relativedelta(weeks=1), - datetime.datetime.today().date() + relativedelta(weeks=2), - ), - ) - - -@pytest.fixture -def future_quota_definition(quota_order_number): - return factories.QuotaDefinitionFactory.create( - order_number=quota_order_number, - valid_between=TaricDateRange( - datetime.datetime.today().date() + relativedelta(weeks=3), - datetime.datetime.today().date() + relativedelta(weeks=4), - ), - ) @pytest.mark.django_db class TestQuotasExpiringSoonReport: - def test_quotas_expiring_soon_report_logic( - self, - quota_order_number, - expired_quota_definition, - expiring_soon_quota_definition, - future_quota_definition, - ): + def test_find_quota_definitions_expiring_soon(self, quota_order_number): report = Report() - quotas = report.query() + expiring_quota_definition = factories.QuotaDefinitionFactory.create( + order_number=quota_order_number, + valid_between=TaricDateRange( + datetime.datetime.today().date() + relativedelta(weeks=1), + datetime.datetime.today().date() + relativedelta(weeks=2), + ), + ) + + result = report.find_quota_definitions_expiring_soon() - assert len(quotas) == 1 + assert expiring_quota_definition in result - assert future_quota_definition in quotas + def test_find_quotas_without_future_definition(self, quota_order_number): + report = Report() + expiring_quota_definition = factories.QuotaDefinitionFactory.create( + order_number=quota_order_number, + valid_between=TaricDateRange( + datetime.datetime.today().date() + relativedelta(weeks=1), + datetime.datetime.today().date() + relativedelta(weeks=2), + ), + ) - assert expiring_soon_quota_definition not in quotas + result = report.find_quotas_without_future_definition( + [expiring_quota_definition] + ) - assert expired_quota_definition not in quotas + assert expiring_quota_definition in result - def test_quotas_expiring_soon_report_row( - self, quota_order_number, expiring_soon_quota_definition - ): + def test_find_quota_blocking_without_future_definition(self, quota_order_number): + report = Report() + expiring_quota_definition = factories.QuotaDefinitionFactory.create( + order_number=quota_order_number, + valid_between=TaricDateRange( + datetime.datetime.today().date() + relativedelta(weeks=1), + datetime.datetime.today().date() + relativedelta(weeks=2), + ), + ) + blocking = factories.QuotaBlockingFactory.create( + quota_definition=expiring_quota_definition + ) + + result = report.find_quota_blocking_without_future_definition( + [expiring_quota_definition] + ) + + assert expiring_quota_definition in result + assert blocking in expiring_quota_definition.quotablocking_set.all() + + def test_find_quota_suspension_without_future_definition(self, quota_order_number): + report = Report() + expiring_quota_definition = factories.QuotaDefinitionFactory.create( + order_number=quota_order_number, + valid_between=TaricDateRange( + datetime.datetime.today().date() + relativedelta(weeks=1), + datetime.datetime.today().date() + relativedelta(weeks=2), + ), + ) + suspension = factories.QuotaSuspensionFactory.create( + quota_definition=expiring_quota_definition + ) + + result = report.find_quota_suspension_without_future_definition( + [expiring_quota_definition] + ) + + assert expiring_quota_definition in result + assert suspension in expiring_quota_definition.quotasuspension_set.all() + + def test_rows2_no_data(self, quota_order_number): report = Report() - row_data = report.row(expiring_soon_quota_definition) - assert len(row_data) == 3 + # Assuming there are no expiring quota definitions + result = report.rows2() - # Check if the correct columns are present - assert {"text": expiring_soon_quota_definition.valid_between.lower} in row_data - assert {"text": expiring_soon_quota_definition.valid_between.upper} in row_data + # Check that the result contains the "no data" message + assert len(result) == 1 + assert result[0][0]["text"] == "There is no data for this report at present" From 38e4f017d1f2a9ba0977752b27861e5f5de221a8 Mon Sep 17 00:00:00 2001 From: Tash Boyse <57753415+nboyse@users.noreply.github.com> Date: Thu, 18 Jan 2024 12:26:13 +0000 Subject: [PATCH 022/118] feat: invoke UI changes to reports and create new URL path to handle reports with multiple tabs (#1134) --- common/static/common/scss/_button.scss | 7 ++- reports/jinja2/generics/table.jinja | 6 +++ .../reports/report_chart_timescale.jinja | 1 - reports/jinja2/reports/report_table.jinja | 7 +-- reports/reports/cds_approved.py | 2 +- reports/reports/cds_approved_7_day_avg.py | 2 +- reports/reports/cds_rejections.py | 4 +- ...piring_quotas_with_no_definition_period.py | 31 +++++++----- ...piring_quotas_with_no_definition_period.py | 8 ++++ reports/tests/test_report_views.py | 13 ++++- reports/urls.py | 7 ++- reports/views.py | 48 +++++++++++++++++-- 12 files changed, 110 insertions(+), 26 deletions(-) diff --git a/common/static/common/scss/_button.scss b/common/static/common/scss/_button.scss index e3be8b0bc..ca396ca3f 100644 --- a/common/static/common/scss/_button.scss +++ b/common/static/common/scss/_button.scss @@ -7,4 +7,9 @@ button.no-background { cursor: pointer; overflow: hidden; outline: none; -} \ No newline at end of file +} + +.report-button-inline { + float:right; + margin-top:-70px +} diff --git a/reports/jinja2/generics/table.jinja b/reports/jinja2/generics/table.jinja index ae72f44e6..1d5bcc2a2 100644 --- a/reports/jinja2/generics/table.jinja +++ b/reports/jinja2/generics/table.jinja @@ -27,13 +27,17 @@

{{ report.tab_name }}

+ Export to CSV {{ govukTable({ "head": report.headers(), "rows": report.rows() }) }}
+

{{ report.tab_name2 }}

+ Export to CSV +
{{ govukTable({ "head": report.headers2(), "rows": report.rows2() @@ -41,6 +45,7 @@

{{ report.tab_name3 }}

+ Export to CSV {{ govukTable({ "head": report.headers3(), "rows": report.rows3() @@ -48,6 +53,7 @@

{{ report.tab_name4 }}

+ Export to CSV {{ govukTable({ "head": report.headers4(), "rows": report.rows4() diff --git a/reports/jinja2/reports/report_chart_timescale.jinja b/reports/jinja2/reports/report_chart_timescale.jinja index f77c9b831..0fbcf7b67 100644 --- a/reports/jinja2/reports/report_chart_timescale.jinja +++ b/reports/jinja2/reports/report_chart_timescale.jinja @@ -82,7 +82,6 @@

Report: {{ report.name }}

- Timescale chart

{{ report.description|safe }}

diff --git a/reports/jinja2/reports/report_table.jinja b/reports/jinja2/reports/report_table.jinja index 16ce779a2..24e5ea9ce 100644 --- a/reports/jinja2/reports/report_table.jinja +++ b/reports/jinja2/reports/report_table.jinja @@ -12,12 +12,13 @@

Report: {{ report.name }}

- Export to CSV - Table + {% if not report.tab_name2 %} + Export to CSV + {% endif %}

{{ report.description|safe }}

-
+
{% include "generics/table.jinja" %}
{% endblock %} diff --git a/reports/reports/cds_approved.py b/reports/reports/cds_approved.py index b91bce1d1..86cad2e20 100644 --- a/reports/reports/cds_approved.py +++ b/reports/reports/cds_approved.py @@ -11,7 +11,7 @@ class Report(ReportBaseChart): name = "CDS approvals in the last 12 months" - description = "This report shows the count of approved (published) workbaskets in the last 12 months per day" + description = "This chart shows the count of approved (published) workbaskets in the last 12 months per day" chart_type = "line" report_template = "chart_timescale" days_in_past = 365 diff --git a/reports/reports/cds_approved_7_day_avg.py b/reports/reports/cds_approved_7_day_avg.py index 1b8e50a18..208bc0573 100644 --- a/reports/reports/cds_approved_7_day_avg.py +++ b/reports/reports/cds_approved_7_day_avg.py @@ -12,7 +12,7 @@ class Report(ReportBaseChart): name = "CDS approvals (7 day average) in the last 12 months" description = ( - "This report shows the 7 day average of approved (published) " + "This chart shows the 7 day average of approved (published) " "workbaskets in the last 12 months per day" ) chart_type = "line" diff --git a/reports/reports/cds_rejections.py b/reports/reports/cds_rejections.py index 5f0f4f6de..eca175d48 100644 --- a/reports/reports/cds_rejections.py +++ b/reports/reports/cds_rejections.py @@ -12,9 +12,9 @@ class Report(ReportBaseChart): name = "CDS rejections in the last 12 months" description = ( - "This report shows the count of rejected (errored) workbaskets in the last 12 months per day. " + "This chart shows the count of rejected (errored) workbaskets in the last 12 months per day. " "

" - "Note: workbaskets that are rejected, tend to be removed, so this report is for demonstration " + "Note: workbaskets that are rejected, tend to be removed, so this chart is for demonstration " "purposes only at this point. there remains some work to do to confidently track and collect " "rejection information in TAP suitable for reporting purposes." ) diff --git a/reports/reports/expiring_quotas_with_no_definition_period.py b/reports/reports/expiring_quotas_with_no_definition_period.py index 58972b83d..049fe6f8b 100644 --- a/reports/reports/expiring_quotas_with_no_definition_period.py +++ b/reports/reports/expiring_quotas_with_no_definition_period.py @@ -12,7 +12,7 @@ class Report(ReportBaseTable): name = "Quotas Expiring Soon" enabled = True - description = "Quotas with definition/sub-quota/blocking/suspension periods about to expire and no future definition period" + description = "Quotas with definition, sub-quota, blocking or suspension periods about to expire and no future definition period." tabular_reports = True tab_name = "Definitions" tab_name2 = "Sub-quota associations" @@ -54,18 +54,27 @@ def query(self): return quotas_without_future_definition def find_quota_definitions_expiring_soon(self): - filter_query = Q( - valid_between__isnull=False, - valid_between__endswith__lte=self.future_time, - ) & Q(valid_between__endswith__gte=self.current_time) | Q( - valid_between__endswith=None + expiring_quotas = QuotaDefinition.objects.latest_approved().filter( + Q( + valid_between__isnull=False, + valid_between__endswith__lte=self.future_time, + ) + & Q(valid_between__endswith__gte=self.current_time) + | Q(valid_between__endswith=None) ) - quotas_expiring_soon = QuotaDefinition.objects.latest_approved().filter( - filter_query - ) + # Filter out quota definitions with associated future definitions + filtered_quotas = [] + for quota in expiring_quotas: + future_definitions = QuotaOrderNumber.objects.latest_approved().filter( + order_number=quota.order_number, + valid_between__startswith__gt=quota.valid_between.upper, + ) - return list(quotas_expiring_soon) + if not future_definitions.exists(): + filtered_quotas.append(quota) + + return filtered_quotas def find_quotas_without_future_definition(self, expiring_quotas): matching_data = set() @@ -186,7 +195,6 @@ def find_quota_suspension_without_future_definition(self, expiring_quotas): for quota_definition in expiring_quotas: future_definitions = QuotaSuspension.objects.latest_approved().filter( quota_definition=quota_definition, - # valid_between__startswith__gt=quota_definition.valid_between.upper, ) if future_definitions.exists(): @@ -240,4 +248,5 @@ def query4(self): quota_suspension_without_future_definition = ( self.find_quota_suspension_without_future_definition(expiring_quotas) ) + return quota_suspension_without_future_definition diff --git a/reports/tests/test_expiring_quotas_with_no_definition_period.py b/reports/tests/test_expiring_quotas_with_no_definition_period.py index 25807ff1c..1965e20e1 100644 --- a/reports/tests/test_expiring_quotas_with_no_definition_period.py +++ b/reports/tests/test_expiring_quotas_with_no_definition_period.py @@ -78,6 +78,14 @@ def test_find_quota_suspension_without_future_definition(self, quota_order_numbe assert expiring_quota_definition in result assert suspension in expiring_quota_definition.quotasuspension_set.all() + def test_rows_no_data(self): + report = Report() + + result = report.rows() + + assert len(result) == 1 + assert result[0][0]["text"] == "There is no data for this report at present" + def test_rows2_no_data(self, quota_order_number): report = Report() diff --git a/reports/tests/test_report_views.py b/reports/tests/test_report_views.py index b99ba3ded..a1b03cf6c 100644 --- a/reports/tests/test_report_views.py +++ b/reports/tests/test_report_views.py @@ -5,6 +5,7 @@ from reports.utils import get_reports from reports.views import export_report_to_csv +from reports.reports.expiring_quotas_with_no_definition_period import Report pytestmark = pytest.mark.django_db @@ -41,7 +42,7 @@ def test_all_report_unauthorised(self, client_name, http_status, request): def test_export_report_to_csv(self, request): request = RequestFactory().get("/") - report_slug = "cds_rejections_in_the_last_12_months" + report_slug = "blank_goods_nomenclature_descriptions" response = export_report_to_csv(request, report_slug) @@ -51,3 +52,13 @@ def test_export_report_to_csv(self, request): response["Content-Disposition"] == f'attachment; filename="{report_slug}_report.csv"' ) + + def test_export_report_invalid_tab(self): + request = RequestFactory().get("/") + report_slug = Report.slug() + invalid_tab = "Invalid tab" + + with pytest.raises( + ValueError, match=f"Invalid current_tab value: {invalid_tab}" + ): + export_report_to_csv(request, report_slug, current_tab=invalid_tab) diff --git a/reports/urls.py b/reports/urls.py index 45e46dbb9..29f7cd9de 100644 --- a/reports/urls.py +++ b/reports/urls.py @@ -8,10 +8,15 @@ urlpatterns = [ path("reports/", views.index, name="index"), path( - "reports//export-csv/", + "reports//", views.export_report_to_csv, name="export_report_to_csv", ), + path( + "reports///export-csv/", + views.export_report_to_csv, + name="export_report_with_tabs_to_csv", + ), ] for report in utils.get_reports(): diff --git a/reports/views.py b/reports/views.py index 902efe299..e18c00733 100644 --- a/reports/views.py +++ b/reports/views.py @@ -35,20 +35,60 @@ def report(request): ) -def export_report_to_csv(request, report_slug): +def export_report_to_csv(request, report_slug, current_tab=None): report_class = utils.get_report_by_slug(report_slug) report_instance = report_class() response = HttpResponse(content_type="text/csv") - response["Content-Disposition"] = f'attachment; filename="{report_slug}_report.csv"' + + if current_tab: + response[ + "Content-Disposition" + ] = f'attachment; filename="{report_slug + "_for_" + current_tab}_report.csv"' + formatted_current_tab = current_tab.capitalize().replace("_", " ") + + # Define a dictionary to map current_tab values to methods + tab_mapping = { + report_instance.tab_name: ( + report_instance.headers(), + report_instance.rows(), + ), + report_instance.tab_name2: ( + report_instance.headers2(), + report_instance.rows2(), + ), + report_instance.tab_name3: ( + report_instance.headers3(), + report_instance.rows3(), + ), + report_instance.tab_name4: ( + report_instance.headers4(), + report_instance.rows4(), + ), + } + + # Use the dictionary to get the methods based on current_tab + methods = tab_mapping.get(formatted_current_tab) + + if methods: + headers, rows = methods + else: + # Raise an exception if current_tab doesn't match any expected values + raise ValueError(f"Invalid current_tab value: {formatted_current_tab}") + else: + response[ + "Content-Disposition" + ] = f'attachment; filename="{report_slug}_report.csv"' + headers = report_instance.headers() + rows = report_instance.rows() writer = csv.writer(response) # Check if the report is a table or a chart if hasattr(report_instance, "headers"): # For table reports - writer.writerow([header["text"] for header in report_instance.headers()]) - for row in report_instance.rows(): + writer.writerow([header["text"] for header in headers]) + for row in rows: writer.writerow([column["text"] for column in row]) else: # For chart reports From c861f24a3998a653271e23cc3a173df0f53c321b Mon Sep 17 00:00:00 2001 From: Tash Boyse <57753415+nboyse@users.noreply.github.com> Date: Fri, 19 Jan 2024 10:57:53 +0000 Subject: [PATCH 023/118] feat: Add both CSV and excel types for charts exporting (#1136) --- common/static/common/scss/_button.scss | 4 ++ .../reports/report_chart_timescale.jinja | 4 ++ ...piring_quotas_with_no_definition_period.py | 4 +- reports/tests/test_report_views.py | 23 +++++-- reports/urls.py | 9 ++- reports/views.py | 62 ++++++++++++++++--- 6 files changed, 91 insertions(+), 15 deletions(-) diff --git a/common/static/common/scss/_button.scss b/common/static/common/scss/_button.scss index ca396ca3f..fc71efcbf 100644 --- a/common/static/common/scss/_button.scss +++ b/common/static/common/scss/_button.scss @@ -13,3 +13,7 @@ button.no-background { float:right; margin-top:-70px } + +.float-elements-right { + float: right; +} \ No newline at end of file diff --git a/reports/jinja2/reports/report_chart_timescale.jinja b/reports/jinja2/reports/report_chart_timescale.jinja index 0fbcf7b67..3e2a87a0e 100644 --- a/reports/jinja2/reports/report_chart_timescale.jinja +++ b/reports/jinja2/reports/report_chart_timescale.jinja @@ -85,6 +85,10 @@

{{ report.description|safe }}

+
diff --git a/reports/reports/expiring_quotas_with_no_definition_period.py b/reports/reports/expiring_quotas_with_no_definition_period.py index 049fe6f8b..66e368727 100644 --- a/reports/reports/expiring_quotas_with_no_definition_period.py +++ b/reports/reports/expiring_quotas_with_no_definition_period.py @@ -66,8 +66,8 @@ def find_quota_definitions_expiring_soon(self): # Filter out quota definitions with associated future definitions filtered_quotas = [] for quota in expiring_quotas: - future_definitions = QuotaOrderNumber.objects.latest_approved().filter( - order_number=quota.order_number, + future_definitions = QuotaDefinition.objects.latest_approved().filter( + order_number__order_number=quota.order_number, valid_between__startswith__gt=quota.valid_between.upper, ) diff --git a/reports/tests/test_report_views.py b/reports/tests/test_report_views.py index a1b03cf6c..bbbabb3f1 100644 --- a/reports/tests/test_report_views.py +++ b/reports/tests/test_report_views.py @@ -4,8 +4,10 @@ from django.test import RequestFactory from reports.utils import get_reports -from reports.views import export_report_to_csv +from reports.views import export_report_to_csv, export_report_to_excel from reports.reports.expiring_quotas_with_no_definition_period import Report +from reports.reports.cds_approved import Report as ChartReport + pytestmark = pytest.mark.django_db @@ -41,7 +43,6 @@ def test_all_report_unauthorised(self, client_name, http_status, request): assert response.status_code == http_status def test_export_report_to_csv(self, request): - request = RequestFactory().get("/") report_slug = "blank_goods_nomenclature_descriptions" response = export_report_to_csv(request, report_slug) @@ -53,8 +54,7 @@ def test_export_report_to_csv(self, request): == f'attachment; filename="{report_slug}_report.csv"' ) - def test_export_report_invalid_tab(self): - request = RequestFactory().get("/") + def test_export_report_invalid_tab(self, request): report_slug = Report.slug() invalid_tab = "Invalid tab" @@ -62,3 +62,18 @@ def test_export_report_invalid_tab(self): ValueError, match=f"Invalid current_tab value: {invalid_tab}" ): export_report_to_csv(request, report_slug, current_tab=invalid_tab) + + def test_export_report_to_excel(self, request): + report_slug = ChartReport.slug() + + response = export_report_to_excel(request, report_slug) + + assert response.status_code == 200 + assert ( + response["Content-Type"] + == "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" + ) + assert ( + response["Content-Disposition"] + == f'attachment; filename="{report_slug}_report.xlsx"' + ) diff --git a/reports/urls.py b/reports/urls.py index 29f7cd9de..f625a0bc6 100644 --- a/reports/urls.py +++ b/reports/urls.py @@ -8,12 +8,17 @@ urlpatterns = [ path("reports/", views.index, name="index"), path( - "reports//", + "reports//export-to-csv", views.export_report_to_csv, name="export_report_to_csv", ), path( - "reports///export-csv/", + "reports//export-to-excel", + views.export_report_to_excel, + name="export_report_to_excel", + ), + path( + "reports///export-report-with-tabs-to-csv/", views.export_report_to_csv, name="export_report_with_tabs_to_csv", ), diff --git a/reports/views.py b/reports/views.py index e18c00733..608a4cd76 100644 --- a/reports/views.py +++ b/reports/views.py @@ -3,10 +3,11 @@ from django.contrib.auth.decorators import permission_required from django.http import HttpResponse from django.shortcuts import render +from openpyxl import Workbook +from openpyxl.chart import BarChart, Reference import reports.reports.index as index_model -# Create your views here. import reports.utils as utils @@ -79,8 +80,10 @@ def export_report_to_csv(request, report_slug, current_tab=None): response[ "Content-Disposition" ] = f'attachment; filename="{report_slug}_report.csv"' - headers = report_instance.headers() - rows = report_instance.rows() + headers = ( + report_instance.headers() if hasattr(report_instance, "headers") else None + ) + rows = report_instance.rows() if hasattr(report_instance, "rows") else None writer = csv.writer(response) @@ -91,9 +94,54 @@ def export_report_to_csv(request, report_slug, current_tab=None): for row in rows: writer.writerow([column["text"] for column in row]) else: - # For chart reports - writer.writerow(["Label", "Data"]) - for label, data in zip(report_instance.labels(), report_instance.data()): - writer.writerow([label, data]) + writer.writerow(["Date", "Data"]) + + for item in report_instance.data(): + writer.writerow([item["x"], item["y"]]) + + # Add an additional row with empty values because Excel needs this for data recognition + writer.writerow(["", ""]) + + return response + + +def export_report_to_excel(request, report_slug): + report_class = utils.get_report_by_slug(report_slug) + report_instance = report_class() + + response = HttpResponse( + content_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" + ) + + response[ + "Content-Disposition" + ] = f'attachment; filename="{report_slug}_report.xlsx"' + + workbook = Workbook() + sheet = workbook.active + + sheet.append(["Date", "Data"]) + + for item in report_instance.data(): + sheet.append([item["x"], item["y"]]) + + # Add an additional row with empty values because Excel needs this for data recognition + sheet.append(["", ""]) + + chart = BarChart() + data = Reference(sheet, min_col=2, min_row=1, max_col=2, max_row=sheet.max_row) + categories = Reference(sheet, min_col=1, min_row=2, max_row=sheet.max_row) + chart.add_data(data, titles_from_data=True) + chart.set_categories(categories) + chart.title = report_instance.name + chart.x_axis.title = "Date" + chart.y_axis.title = "Data" + + chart.width = 40 + chart.height = 20 + + sheet.add_chart(chart, "E5") + + workbook.save(response) return response From 062c52b7bb0c1e6df5479ace447c8c57dbb7f5bc Mon Sep 17 00:00:00 2001 From: Matthew McKenzie <97194636+mattjamc@users.noreply.github.com> Date: Mon, 22 Jan 2024 12:04:33 +0000 Subject: [PATCH 024/118] TP2000-1185 Add maintenance mode (#1137) * Add MAINTENANCE_MODE setting and middleware * Fix middleware removal and recursive redirect * Add template view and url * Add tests * Update contact us form link for other pages * Update text wording * Remove database route during maintenance * Update maintenance page template/url name --------- Co-authored-by: Dale Cannon --- common/jinja2/common/403.jinja | 2 +- common/jinja2/common/500.jinja | 2 +- common/jinja2/common/maintenance.jinja | 31 ++++++++++++++++++++++++++ common/middleware.py | 18 +++++++++++++++ common/tests/test_views.py | 20 +++++++++++++++++ common/urls.py | 1 + common/views.py | 4 ++++ sample.env | 2 ++ settings/common.py | 23 ++++++++++++++++--- urls.py | 6 ++++- 10 files changed, 103 insertions(+), 6 deletions(-) create mode 100644 common/jinja2/common/maintenance.jinja create mode 100644 common/middleware.py diff --git a/common/jinja2/common/403.jinja b/common/jinja2/common/403.jinja index f0efb1268..76038c36c 100644 --- a/common/jinja2/common/403.jinja +++ b/common/jinja2/common/403.jinja @@ -11,7 +11,7 @@

You do not have access to this part of the service

- Contact the Tariff Application Platform (TAP) team to change your access rights. + Contact the Tariff Application Platform (TAP) team to change your access rights.

You will need to describe what you were doing before seeing this error message.

The team will reply to you within two working days.

diff --git a/common/jinja2/common/500.jinja b/common/jinja2/common/500.jinja index d418a33fd..2d957ab19 100644 --- a/common/jinja2/common/500.jinja +++ b/common/jinja2/common/500.jinja @@ -12,7 +12,7 @@

Sorry, there is a problem with this service

+ href="https://forms.office.com/Pages/ResponsePage.aspx?id=7Beij6oz-0atlt_mgAa7husF7H2qg6ZMi_-_m4b1eedUMjBNRllURlk0R0dFS1FHQkVBMFhNWjROViQlQCN0PWcu"> Contact the Tariff Application Platform (TAP) team who will help to resolve this problem. diff --git a/common/jinja2/common/maintenance.jinja b/common/jinja2/common/maintenance.jinja new file mode 100644 index 000000000..a507332c4 --- /dev/null +++ b/common/jinja2/common/maintenance.jinja @@ -0,0 +1,31 @@ +{% extends "layouts/layout.jinja" %} + +{% set page_title = "Sorry, the service is unavailable" %} + +{% block header %} + +{{ govukHeader({ + "homepageUrl": "https://gov.uk/", + "serviceName": service_name, + "serviceUrl": "/", +}) }} +{% endblock %} + +{% block breadcrumb %}{% endblock %} + +{% block content %} +

+
+
+
+

Sorry, the service is unavailable

+

You will be able to use the service later.

+

+ Contact the Tariff Application Platform (TAP) team if you have any queries. {# /PS-IGNORE #} +

+

The team will reply to you within 2 working days.

+
+
+
+
+{% endblock %} diff --git a/common/middleware.py b/common/middleware.py new file mode 100644 index 000000000..25d1b8f43 --- /dev/null +++ b/common/middleware.py @@ -0,0 +1,18 @@ +from django.conf import settings +from django.shortcuts import redirect +from django.urls import reverse + + +class MaintenanceModeMiddleware: + """If MAINTENANCE_MODE env variable is True, reroute all user requests to + MaintenanceModeView.""" + + def __init__(self, get_response): + self.get_response = get_response + + def __call__(self, request): + if settings.MAINTENANCE_MODE and request.path_info != reverse("maintenance"): + return redirect(reverse("maintenance")) + + response = self.get_response(request) + return response diff --git a/common/tests/test_views.py b/common/tests/test_views.py index dda2ffede..6480201b6 100644 --- a/common/tests/test_views.py +++ b/common/tests/test_views.py @@ -4,6 +4,8 @@ from bs4 import BeautifulSoup from django.conf import settings from django.contrib.auth.models import Permission +from django.test import modify_settings +from django.test import override_settings from django.urls import reverse from common.tests import factories @@ -241,3 +243,21 @@ def test_accessibility_statement_view_returns_200(valid_user_client): "Accessibility statement for the Tariff application platform" in page.select("h1")[0].text ) + + +@override_settings(MAINTENANCE_MODE=True) +@modify_settings( + MIDDLEWARE={ + "append": "common.middleware.MaintenanceModeMiddleware", + }, +) +def test_user_redirect_during_maintenance_mode(valid_user_client): + response = valid_user_client.get(reverse("home")) + assert response.status_code == 302 + assert response.url == reverse("maintenance") + + +def test_maintenance_mode_page_content(valid_user_client): + response = valid_user_client.get(reverse("maintenance")) + assert response.status_code == 200 + assert "Sorry, the service is unavailable" in str(response.content) diff --git a/common/urls.py b/common/urls.py index 06dc8cbc7..76697e5cd 100644 --- a/common/urls.py +++ b/common/urls.py @@ -26,4 +26,5 @@ path("login", views.LoginView.as_view(), name="login"), path("logout", views.LogoutView.as_view(), name="logout"), path("api-auth/", include("rest_framework.urls")), + path("maintenance/", views.MaintenanceView.as_view(), name="maintenance"), ] diff --git a/common/views.py b/common/views.py index c8e696fdd..def5fc2da 100644 --- a/common/views.py +++ b/common/views.py @@ -465,5 +465,9 @@ def handler500(request, *args, **kwargs): return TemplateResponse(request=request, template="common/500.jinja", status=500) +class MaintenanceView(TemplateView): + template_name = "common/maintenance.jinja" + + class AccessibilityStatementView(TemplateView): template_name = "common/accessibility.jinja" diff --git a/sample.env b/sample.env index d8b634a5e..46de51fd8 100644 --- a/sample.env +++ b/sample.env @@ -9,6 +9,8 @@ LOG_LEVEL=DEBUG SENTRY_DSN= CELERY_BROKER_URL=redis://127.0.0.1:6379/1 +MAINTENANCE_MODE=False + # S3 Bucket for HMRC envelope uploads. HMRC_STORAGE_BUCKET_NAME=hmrc HMRC_STORAGE_DIRECTORY=tohmrc/staging/ diff --git a/settings/common.py b/settings/common.py index 55b73816a..4ab41b3fc 100644 --- a/settings/common.py +++ b/settings/common.py @@ -23,6 +23,8 @@ VCAP_SERVICES = json.loads(os.environ.get("VCAP_SERVICES", "{}")) VCAP_APPLICATION = json.loads(os.environ.get("VCAP_APPLICATION", "{}")) +MAINTENANCE_MODE = is_truthy(os.environ.get("MAINTENANCE_MODE", "False")) + # -- Debug # Activates debugging @@ -154,11 +156,23 @@ "common.models.utils.TransactionMiddleware", "csp.middleware.CSPMiddleware", ] + if SSO_ENABLED: MIDDLEWARE += [ "authbroker_client.middleware.ProtectAllViewsMiddleware", ] +if MAINTENANCE_MODE: + INSTALLED_APPS.remove("django.contrib.admin") + + MIDDLEWARE.remove("django.contrib.sessions.middleware.SessionMiddleware") + MIDDLEWARE.remove("django.contrib.auth.middleware.AuthenticationMiddleware") + MIDDLEWARE.remove("django.contrib.messages.middleware.MessageMiddleware") + MIDDLEWARE.remove("common.models.utils.ValidateSessionWorkBasketMiddleware") + MIDDLEWARE.remove("common.models.utils.TransactionMiddleware") + + MIDDLEWARE.append("common.middleware.MaintenanceModeMiddleware") + TEMPLATES = [ { "BACKEND": "django.template.backends.jinja2.Jinja2", @@ -295,9 +309,12 @@ else: DB_URL = os.environ.get("DATABASE_URL", "postgres://localhost:5432/tamato") -DATABASES = { - "default": dj_database_url.parse(DB_URL), -} +if not MAINTENANCE_MODE: + DATABASES = { + "default": dj_database_url.parse(DB_URL), + } +else: + DATABASES = {} SQLITE = DB_URL.startswith("sqlite") diff --git a/urls.py b/urls.py index 162a33940..8203aa32e 100644 --- a/urls.py +++ b/urls.py @@ -38,9 +38,13 @@ path("", include("reports.urls")), path("", include("taric_parsers.urls")), path("", include("workbaskets.urls", namespace="workbaskets")), - path("admin/", admin.site.urls), ] +if not settings.MAINTENANCE_MODE: + urlpatterns += { + path("admin/", admin.site.urls), + } + handler403 = "common.views.handler403" handler500 = "common.views.handler500" From ee7da3c22e81b575bb2ac3ef315a0880720c8866 Mon Sep 17 00:00:00 2001 From: A Gleeson Date: Mon, 22 Jan 2024 14:39:41 +0000 Subject: [PATCH 025/118] Increment message id & record sequence number correctly (#1083) * record seq number & message id fix * fix taricXMLRenderer, pass in value of counter --- common/renderers.py | 2 +- common/serializers.py | 14 ++- common/tests/test_serializers.py | 106 ++++++++++++++++++ .../jinja2/workbaskets/taric/transaction.xml | 2 +- 4 files changed, 119 insertions(+), 5 deletions(-) diff --git a/common/renderers.py b/common/renderers.py index 76fa7645f..5d4f46d29 100644 --- a/common/renderers.py +++ b/common/renderers.py @@ -20,5 +20,5 @@ def get_template_context(self, *args, **kwargs): context["envelope_id"] = f"{counter_generator()():06}" context["message_counter"] = counter_generator() - context["counter_generator"] = counter_generator + context["counter_generator"] = counter_generator() return context diff --git a/common/serializers.py b/common/serializers.py index 9a4d6a574..fd82896c2 100644 --- a/common/serializers.py +++ b/common/serializers.py @@ -198,7 +198,8 @@ def __init__( self, output: IO, envelope_id: int, - message_counter: Counter = counter_generator(), + sequence_counter: Counter = None, + message_counter: Counter = None, max_envelope_size: Optional[int] = None, format: str = "xml", newline: bool = False, @@ -206,13 +207,20 @@ def __init__( """ :param output: The output stream to write to. :param envelope_id: The id of the envelope. + :param sequence_counter: A counter for the record number sequence :param message_counter: A counter for the message ids. :param max_envelope_size: The maximum size of an envelope, if None then no limit. :param format: Format to serialize to, defaults to xml. :param newline: Whether to add a newline after the envelope. """ self.output = output - self.message_counter = message_counter + # Not set as default in params as the counter value persits between different instantiations of the Serilzer + self.sequence_counter = ( + sequence_counter if sequence_counter else counter_generator() + ) + self.message_counter = ( + message_counter if message_counter else counter_generator() + ) self.envelope_id = envelope_id self.envelope_size = 0 self.max_envelope_size = max_envelope_size @@ -280,7 +288,7 @@ def render_envelope_body( context={"format": self.format}, ).data, "transaction_id": transaction_id, - "counter_generator": counter_generator, + "counter_generator": self.sequence_counter, "message_counter": self.message_counter, }, ) diff --git a/common/tests/test_serializers.py b/common/tests/test_serializers.py index 3a81f201d..13ecc8d49 100644 --- a/common/tests/test_serializers.py +++ b/common/tests/test_serializers.py @@ -1,7 +1,9 @@ import io +import os import random import pytest +from bs4 import BeautifulSoup from lxml import etree from pytest_django.asserts import assertQuerysetEqual # noqa @@ -13,7 +15,9 @@ from common.tests.util import taric_xml_record_codes from exporter.serializers import MultiFileEnvelopeTransactionSerializer from exporter.serializers import RenderedTransactions +from exporter.util import dit_file_generator from taric.models import Envelope +from workbaskets.models import WorkBasket pytestmark = pytest.mark.django_db @@ -165,3 +169,105 @@ def create_output_constructor(): # TODO - it would be good to check the output more thoroughly than just the record code. # Some record codes are generated in the template, making issuperset required in this assertion. assert output_record_codes.issuperset(expected_record_codes[i]) + + +def test_transaction_envelope_serializer_counters(queued_workbasket): + """Test that the envelope serializer sets the counters in an envelope + correctly that the message id always starts from one in each envelope and + that the record sequence number increments.""" + approved_transaction = queued_workbasket.transactions.approved().last() + # add a tracked_models to the workbasket + + factories.AdditionalCodeTypeFactory(transaction=approved_transaction) + factories.AdditionalCodeDescriptionFactory(transaction=approved_transaction) + factories.RegulationFactory( + transaction=approved_transaction, + regulation_group=factories.RegulationGroupFactory( + transaction=approved_transaction, + ), + ) + factories.CertificateFactory( + transaction=approved_transaction, + certificate_type=factories.CertificateTypeFactory( + transaction=approved_transaction, + ), + description=factories.CertificateDescriptionFactory( + transaction=approved_transaction, + ), + ) + factories.FootnoteFactory( + transaction=approved_transaction, + description=factories.FootnoteDescriptionFactory( + transaction=approved_transaction, + ), + footnote_type=factories.FootnoteTypeFactory(transaction=approved_transaction), + ) + + # Make a envelope from the files + output_file_constructor = dit_file_generator("/tmp", 230001) + serializer = MultiFileEnvelopeTransactionSerializer( + output_file_constructor, + envelope_id=230001, + ) + + workbaskets = WorkBasket.objects.filter(pk=queued_workbasket.pk) + transactions = workbaskets.ordered_transactions() + + envelope = list(serializer.split_render_transactions(transactions))[0] + + assert len(envelope.transactions) > 0 + + envelope_file = envelope.output + envelope_file.seek(0, os.SEEK_SET) + soup = BeautifulSoup(envelope_file, "xml") + record_sequence_numbers = soup.find_all("oub:record.sequence.number") + message_id_numbers = soup.find_all("env:app.message") + + expected_value = 1 + for element in record_sequence_numbers: + actual_value = int(element.text) + assert actual_value == expected_value + expected_value += 1 + + expected_id = 1 + for element in message_id_numbers: + actual_value = int(element["id"]) + assert actual_value == expected_id + expected_id += 1 + + workbasket = factories.QueuedWorkBasketFactory.create() + approved_transaction2 = workbasket.transactions.approved().last() + factories.AdditionalCodeTypeFactory(transaction=approved_transaction2) + factories.AdditionalCodeDescriptionFactory(transaction=approved_transaction2) + factories.FootnoteFactory( + transaction=approved_transaction2, + description=factories.FootnoteDescriptionFactory( + transaction=approved_transaction2, + ), + footnote_type=factories.FootnoteTypeFactory(transaction=approved_transaction2), + ) + + # Make a envelope from the files + output_file_constructor = dit_file_generator("/tmp", 230002) + serializer = MultiFileEnvelopeTransactionSerializer( + output_file_constructor, + envelope_id=230002, + ) + + workbaskets = WorkBasket.objects.filter(pk=workbasket.pk) + transactions = workbaskets.ordered_transactions() + + envelope_2 = list(serializer.split_render_transactions(transactions))[0] + + assert len(envelope.transactions) > 0 + + envelope_file_2 = envelope_2.output + envelope_file_2.seek(0, os.SEEK_SET) + soup_2 = BeautifulSoup(envelope_file_2, "xml") + message_id_numbers_2 = soup_2.find_all("env:app.message") + + expected_id_2 = 1 + for element in message_id_numbers_2: + actual_value = int(element.get("id")) + assert actual_value == expected_id_2 + expected_id_2 += 1 diff --git a/workbaskets/jinja2/workbaskets/taric/transaction.xml b/workbaskets/jinja2/workbaskets/taric/transaction.xml index 0b9e1b7ac..8d0c77ee6 100644 --- a/workbaskets/jinja2/workbaskets/taric/transaction.xml +++ b/workbaskets/jinja2/workbaskets/taric/transaction.xml @@ -1,6 +1,6 @@ {%- for record in tracked_models -%} - {%- set sequence = counter_generator() -%} + {%- set sequence = counter_generator -%} {%- include record.taric_template -%} {%- endfor %} From d7f961163df6edbf983fb80eedb38c611dec5881 Mon Sep 17 00:00:00 2001 From: Tash Boyse <57753415+nboyse@users.noreply.github.com> Date: Tue, 23 Jan 2024 14:04:02 +0000 Subject: [PATCH 026/118] feat: implement URLs for quota reports to ease navigation (#1135) --- reports/reports/base_table.py | 9 ++++ ...piring_quotas_with_no_definition_period.py | 50 +++++++++++++++---- reports/reports/quotas_cannot_be_used.py | 11 ++-- reports/tests/test_report_utils.py | 26 ++++++++++ 4 files changed, 80 insertions(+), 16 deletions(-) diff --git a/reports/reports/base_table.py b/reports/reports/base_table.py index e735d806e..b638c9353 100644 --- a/reports/reports/base_table.py +++ b/reports/reports/base_table.py @@ -1,4 +1,6 @@ from abc import abstractmethod +from django.urls import reverse +from django.utils.safestring import mark_safe from reports.reports.base import ReportBase @@ -11,6 +13,13 @@ class ReportBaseTable(ReportBase): def __init__(self): pass + def link_renderer_for_quotas(self, order_number, text, fragment=None): + url = reverse("quota-ui-detail", args=[order_number.sid]) + href = url + fragment if fragment else url + return mark_safe( + f"{text}" + ) + @abstractmethod def query(self): pass diff --git a/reports/reports/expiring_quotas_with_no_definition_period.py b/reports/reports/expiring_quotas_with_no_definition_period.py index 66e368727..9d75bec07 100644 --- a/reports/reports/expiring_quotas_with_no_definition_period.py +++ b/reports/reports/expiring_quotas_with_no_definition_period.py @@ -30,7 +30,7 @@ def headers(self) -> [dict]: def row(self, row) -> [dict]: return [ - {"text": row.order_number}, + {"text": self.link_renderer_for_quotas(row.order_number, row.order_number)}, {"text": row.valid_between.lower}, {"text": row.valid_between.upper}, ] @@ -106,11 +106,23 @@ def row2(self, row) -> [dict]: for sub_quotas in row.sub_quotas.all(): sub_quotas_array.append( - {"text": row.order_number}, - {"text": sub_quotas.sid}, + { + "text": self.link_renderer_for_quotas( + row.order_number, row.order_number + ) + }, + { + "text": self.link_renderer_for_quotas( + row.order_number, sub_quotas.sid, "#sub-quotas" + ) + }, {"text": sub_quotas.valid_between.lower}, {"text": sub_quotas.valid_between.upper}, - {"text": row.sid}, + { + "text": self.link_renderer_for_quotas( + row.order_number, row.sid, "#definition-details" + ) + }, ) return sub_quotas_array @@ -158,8 +170,13 @@ def headers3(self) -> [dict]: def row3(self, row) -> [dict]: return [ - {"text": row.order_number}, - {"text": blocking.sid for blocking in row.quotablocking_set.all()}, + {"text": self.link_renderer_for_quotas(row.order_number, row.order_number)}, + { + "text": self.link_renderer_for_quotas( + row.order_number, blocking.sid, "#blocking-periods" + ) + for blocking in row.quotablocking_set.all() + }, { "text": blocking.valid_between.lower for blocking in row.quotablocking_set.all() @@ -168,7 +185,11 @@ def row3(self, row) -> [dict]: "text": blocking.valid_between.upper for blocking in row.quotablocking_set.all() }, - {"text": row.sid}, + { + "text": self.link_renderer_for_quotas( + row.order_number, row.sid, "#definition-details" + ) + }, ] def rows3(self) -> [[dict]]: @@ -219,8 +240,13 @@ def headers4(self) -> [dict]: def row4(self, row) -> [dict]: return [ - {"text": row.order_number}, - {"text": suspension.sid for suspension in row.quotasuspension_set.all()}, + {"text": self.link_renderer_for_quotas(row.order_number, row.order_number)}, + { + "text": self.link_renderer_for_quotas( + row.order_number, suspension.sid, "#blocking-periods" + ) + for suspension in row.quotasuspension_set.all() + }, { "text": suspension.valid_between.lower for suspension in row.quotasuspension_set.all() @@ -229,7 +255,11 @@ def row4(self, row) -> [dict]: "text": suspension.valid_between.upper for suspension in row.quotasuspension_set.all() }, - {"text": row.sid}, + { + "text": self.link_renderer_for_quotas( + row.order_number, row.sid, "#definition-details" + ) + }, ] def rows4(self) -> [[dict]]: diff --git a/reports/reports/quotas_cannot_be_used.py b/reports/reports/quotas_cannot_be_used.py index 5fe7f0369..b83d3694f 100644 --- a/reports/reports/quotas_cannot_be_used.py +++ b/reports/reports/quotas_cannot_be_used.py @@ -28,7 +28,7 @@ def headers(self) -> [dict]: def row(self, row: QuotaDefinition) -> [dict]: return [ - {"text": row.order_number}, + {"text": self.link_renderer_for_quotas(row, row.order_number)}, {"text": row.valid_between.lower}, {"text": row.valid_between.upper}, {"text": row.reason}, @@ -118,20 +118,19 @@ def find_quotas_that_cannot_be_used(self, quotas_with_definition_periods): break else: matching_data.add(quota.order_number) - - for quota in matching_data: + for quota_order_number in matching_data: matching_definition = next( ( quota_definition for quota_definition in quotas_with_definition_periods - if quota_definition.order_number == quota + if quota_definition.order_number == quota_order_number ), None, ) if matching_definition: - quota.reason = "Geographical area/exclusions data does not have any measures with matching data" + quota_order_number.reason = "Geographical area/exclusions data does not have any measures with matching data" else: - quota.reason = "Definition period has not been set" + quota_order_number.reason = "Definition period has not been set" return list(matching_data) diff --git a/reports/tests/test_report_utils.py b/reports/tests/test_report_utils.py index 235e4ec6a..80409a469 100644 --- a/reports/tests/test_report_utils.py +++ b/reports/tests/test_report_utils.py @@ -1,6 +1,9 @@ # Create your tests here. import pytest +from django.urls import reverse +from django.utils.safestring import SafeString +from common.tests import factories from reports.reports.base import ReportBase from reports.reports.base_table import ReportBaseTable from reports.reports.blank_goods_nomenclature_descriptions import Report @@ -45,3 +48,26 @@ def test_get_template_by_type(self): with pytest.raises(Exception) as ex: get_template_by_type("werwer") assert str(ex) == "Unknown chart type : werwer" + + @pytest.mark.django_db + def test_link_renderer_for_quotas(self, db): + order_number_obj = factories.QuotaOrderNumberFactory.create() + + report_instance = Report() + + # Test without fragment + result = report_instance.link_renderer_for_quotas(order_number_obj, "Test Text") + expected_url = reverse("quota-ui-detail", args=[order_number_obj.sid]) + expected_output = f"Test Text" + assert result == SafeString(expected_output) + + # Test with fragment + result = report_instance.link_renderer_for_quotas( + order_number_obj, "Test Text", fragment="#blocking-periods" + ) + expected_url = ( + reverse("quota-ui-detail", args=[order_number_obj.sid]) + + "#blocking-periods" + ) + expected_output = f"Test Text" + assert result == SafeString(expected_output) From bafbc7273588802af989c7d5efb59ff7609b0040 Mon Sep 17 00:00:00 2001 From: Paul Pepper <85895113+paulpepper-trade@users.noreply.github.com> Date: Fri, 26 Jan 2024 12:12:38 +0000 Subject: [PATCH 027/118] Update readme with maintenance mode instructions. (#1140) --- README.rst | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/README.rst b/README.rst index 044607d81..ee16932d0 100644 --- a/README.rst +++ b/README.rst @@ -497,6 +497,32 @@ We use a shared service accross the department for virus scanning to run locally 3. add CLAM_AV_DOMAIN without http(s):// 4. set CLAM_AV_USERNAME,CLAM_AV_PASSWORD as the username and password found in the config.py in the dit-clamav-rest project + +Application maintenance mode +---------------------------- + +The application can be put into a "maintenance mode" type of operation. By doing +so, all user web access is routed to a maintenance view and the default database +route removes the application's access to the database. This prevents +inadvertent changes by users, via the application UI, to application data while +in maintenance mode. Note, however, that this would not restrict other forms of +data update, such as active Celery tasks - Celery and other similar processes +need to be scaled down separately. + +The process for transitioning the application into and back out of maintenance +mode is as follows: + +1. Set the application’s `MAINTENANCE_MODE` environment variable to `True`. + +2. Restart the application so that it picks up the new value of `MAINTENANCE_MODE`. + +3. Complete maintenance activities. + +4. Set the value of the `MAINTENANCE_MODE` environment variable to `False`. + +5. Restart the application. + + How to contribute ----------------- From 8ecd00200bf8ed2e98f4cbc73ca272f105d1f31a Mon Sep 17 00:00:00 2001 From: Matthew McKenzie <97194636+mattjamc@users.noreply.github.com> Date: Mon, 29 Jan 2024 16:39:18 +0000 Subject: [PATCH 028/118] TP2000-1130 Move current workbasket from Session to custom User model (#1123) * Update User model references * Use custom User model * TP2000-1152-handling-invalid-workbaskets (#1113) * Update middleware to check for workbasket changing state * Update to use decorator rather than middleware, add pytest fixtures * Update tests that require a session workbasket to run * Move views and urls to workbasket app and update template * Add tests for when workbasket status changes * Tidy up following Pauls comments * Update models and templates to find workbasket in user model * Update test fixtures for workbasket being in user model * Tidy up and test updates * Update referencing to User model * Updating bdd tests for new user model * Add and update view and model unit tests * Update require_current_workbasket decorator docstring * Add docstring, move template for NoActiveWorkBasket view * Amend current workbasket id retrieval in template * Amend custom User model migration * Remake migration adding current_workbasket field to User model * Remove unused ValidateSessionWorkBasketMiddleware * Make current_workbasket optional * Add User model to admin * Use historical models to fix migration tests * Move ContentType data migration so it may be applied * Rename function to remove a users current workbasket * Amend docstrings * Remove reference to session middleware that is no longer used * Update workbaskets models following Pauls review * Bring back user workbasket middleware as extra security * Move User model from workbaskets app to common app * Add forgotten content type data migration * Remove setup_content_type fixture following patch to migrator fixture * Amend middleware util method name * Remove uneeded DoesNotExist try except block --------- Co-authored-by: Dale Cannon --- .../tests/bdd/test_edit_additional_codes.py | 36 +- additional_codes/tests/test_forms.py | 17 +- additional_codes/tests/test_views.py | 8 +- .../tests/bdd/test_edit_certificates.py | 23 +- certificates/tests/test_forms.py | 24 +- certificates/tests/test_views.py | 10 +- commodities/tests/test_migrations.py | 16 +- commodities/tests/test_views.py | 27 +- common/admin.py | 32 ++ .../common/confirm_create_description.jinja | 2 +- .../common/confirm_update_description.jinja | 2 +- common/jinja2/components/breadcrumbs.jinja | 4 +- .../includes/common/main-menu-link.jinja | 2 +- common/jinja2/layouts/layout.jinja | 4 +- common/migrations/0001_initial.py | 123 +++++++ .../0008_user_current_workbasket.py | 45 +++ common/models/__init__.py | 2 + common/models/user.py | 21 ++ common/models/utils.py | 28 +- common/serializers.py | 4 +- common/tests/bdd/test_edit_view.py | 19 +- common/tests/factories.py | 5 +- common/tests/test_filters.py | 7 +- common/tests/test_migrations.py | 3 +- conftest.py | 124 +++++-- footnotes/tests/bdd/test_edit_footnote.py | 23 +- footnotes/tests/test_forms.py | 4 +- footnotes/tests/test_views.py | 8 +- geo_areas/tests/test_forms.py | 18 +- geo_areas/tests/test_views.py | 58 +-- importer/forms.py | 4 +- .../jinja2/eu-importer/notify-success.jinja | 6 +- importer/management/commands/chunk_taric.py | 4 +- importer/management/commands/import_taric.py | 4 +- importer/management/util.py | 4 +- importer/tests/test_views.py | 4 +- measures/tests/conftest.py | 4 +- measures/tests/test_filters.py | 18 +- measures/tests/test_forms.py | 20 +- measures/tests/test_migrations.py | 217 ++++++++--- measures/tests/test_views.py | 239 +++++-------- measures/views.py | 2 +- pii-secret-exclude.txt | 1 + publishing/tests/test_migrations.py | 2 +- publishing/tests/test_views.py | 58 +-- quotas/tests/test_forms.py | 4 +- quotas/tests/test_views.py | 85 +++-- regulations/tests/test_views.py | 11 +- settings/common.py | 7 +- taric_parsers/forms.py | 4 +- taric_parsers/tasks.py | 4 +- taric_parsers/tests/test_views.py | 4 +- .../includes/workbaskets/navigation.jinja | 2 +- workbaskets/jinja2/workbaskets/checks.jinja | 2 +- workbaskets/jinja2/workbaskets/compare.jinja | 8 +- .../jinja2/workbaskets/delete_changes.jinja | 2 +- .../workbaskets/delete_changes_confirm.jinja | 4 +- .../workbaskets/delete_workbasket.jinja | 2 +- .../jinja2/workbaskets/edit-details.jinja | 2 +- .../jinja2/workbaskets/edit-workbasket.jinja | 8 +- .../workbaskets/no_active_workbasket.jinja | 37 ++ workbaskets/jinja2/workbaskets/review.jinja | 6 +- .../workbaskets/summary-workbasket.jinja | 4 +- .../jinja2/workbaskets/violation_detail.jinja | 2 +- .../jinja2/workbaskets/violations.jinja | 2 +- workbaskets/migrations/0001_initial.py | 24 +- .../0002_change_status_per_ADR008.py | 25 ++ workbaskets/models.py | 39 +- workbaskets/tests/test_models.py | 7 + workbaskets/tests/test_views.py | 338 +++++++++--------- workbaskets/urls.py | 5 + workbaskets/views/decorators.py | 20 +- workbaskets/views/ui.py | 23 +- 73 files changed, 1212 insertions(+), 755 deletions(-) create mode 100644 common/admin.py create mode 100644 common/migrations/0008_user_current_workbasket.py create mode 100644 common/models/user.py create mode 100644 workbaskets/jinja2/workbaskets/no_active_workbasket.jinja diff --git a/additional_codes/tests/bdd/test_edit_additional_codes.py b/additional_codes/tests/bdd/test_edit_additional_codes.py index 1eaab475e..1aaf049ae 100644 --- a/additional_codes/tests/bdd/test_edit_additional_codes.py +++ b/additional_codes/tests/bdd/test_edit_additional_codes.py @@ -15,13 +15,24 @@ @pytest.fixture @when("I edit additional code X000") -def model_edit_page(client, additional_code_X000): - return client.get(additional_code_X000.get_url("edit")) +def model_edit_page(client_with_current_workbasket, additional_code_X000): + return client_with_current_workbasket.get(additional_code_X000.get_url("edit")) + + +@pytest.fixture +@when("I edit additional code X000") +def model_edit_page_invalid_user( + client_with_current_workbasket_no_permissions, + additional_code_X000, +): + return client_with_current_workbasket_no_permissions.get( + additional_code_X000.get_url("edit"), + ) @then("I am not permitted to edit") -def edit_permission_denied(model_edit_page): - assert model_edit_page.status_code == 403 +def edit_permission_denied(model_edit_page_invalid_user): + assert model_edit_page_invalid_user.status_code == 403 @then("I see an edit form") @@ -31,8 +42,12 @@ def edit_permission_granted(model_edit_page): @pytest.fixture @when("I set the end date before the start date on additional code X000") -def end_date_before_start(client, response, additional_code_X000): - response["response"] = client.post( +def end_date_before_start( + client_with_current_workbasket, + response, + additional_code_X000, +): + response["response"] = client_with_current_workbasket.post( additional_code_X000.get_url("edit"), validity_period_post_data( start=date(2021, 1, 1), @@ -44,8 +59,13 @@ def end_date_before_start(client, response, additional_code_X000): @when( "I set the start date of additional code X000 to overlap the previous additional code", ) -def submit_overlapping(client, response, additional_code_X000, old_additional_code): - response["response"] = client.post( +def submit_overlapping( + client_with_current_workbasket, + response, + additional_code_X000, + old_additional_code, +): + response["response"] = client_with_current_workbasket.post( additional_code_X000.get_url("edit"), validity_period_post_data( start=old_additional_code.valid_between.lower, diff --git a/additional_codes/tests/test_forms.py b/additional_codes/tests/test_forms.py index 147da14ef..8e82bef3c 100644 --- a/additional_codes/tests/test_forms.py +++ b/additional_codes/tests/test_forms.py @@ -7,7 +7,7 @@ # https://uktrade.atlassian.net/browse/TP2000-296 -def test_additional_code_create_sid(session_with_workbasket, date_ranges): +def test_additional_code_create_sid(session_request_with_workbasket, date_ranges): """Tests that additional code type is NOT considered when generating a new sid.""" type_1 = factories.AdditionalCodeTypeFactory.create() @@ -21,7 +21,10 @@ def test_additional_code_create_sid(session_with_workbasket, date_ranges): "start_date_1": date_ranges.normal.lower.month, "start_date_2": date_ranges.normal.lower.year, } - form = forms.AdditionalCodeCreateForm(data=data, request=session_with_workbasket) + form = forms.AdditionalCodeCreateForm( + data=data, + request=session_request_with_workbasket, + ) assert form.is_valid() @@ -30,7 +33,10 @@ def test_additional_code_create_sid(session_with_workbasket, date_ranges): assert new_additional_code.sid != additional_code.sid -def test_additional_code_create_valid_data(session_with_workbasket, date_ranges): +def test_additional_code_create_valid_data( + session_request_with_workbasket, + date_ranges, +): """Tests that AdditionalCodeCreateForm.is_valid() returns True when passed required fields and additional_code_description values in cleaned data.""" code_type = factories.AdditionalCodeTypeFactory.create() @@ -42,7 +48,10 @@ def test_additional_code_create_valid_data(session_with_workbasket, date_ranges) "start_date_1": date_ranges.normal.lower.month, "start_date_2": date_ranges.normal.lower.year, } - form = forms.AdditionalCodeCreateForm(data=data, request=session_with_workbasket) + form = forms.AdditionalCodeCreateForm( + data=data, + request=session_request_with_workbasket, + ) assert form.is_valid() assert form.cleaned_data["additional_code_description"].description == "description" diff --git a/additional_codes/tests/test_views.py b/additional_codes/tests/test_views.py index 6c8e60cfe..0b68d1ff1 100644 --- a/additional_codes/tests/test_views.py +++ b/additional_codes/tests/test_views.py @@ -133,7 +133,7 @@ def test_additional_codes_detail_views( view, url_pattern, valid_user_client, - session_with_workbasket, + session_request_with_workbasket, ): """Verify that additional code detail views are under the url additional_codes/ and don't return an error.""" @@ -228,7 +228,7 @@ def test_additional_code_details_list_no_measures(valid_user_client): assert num_measures == 0 -def test_additional_code_description_create(valid_user_client): +def test_additional_code_description_create(client_with_current_workbasket): """Tests that `AdditionalCodeDescriptionCreate` view returns 200 and creates a description for the current version of an additional code.""" additional_code = factories.AdditionalCodeFactory.create() @@ -250,10 +250,10 @@ def test_additional_code_description_create(valid_user_client): } with override_current_transaction(Transaction.objects.last()): - get_response = valid_user_client.get(url) + get_response = client_with_current_workbasket.get(url) assert get_response.status_code == 200 - post_response = valid_user_client.post(url, data) + post_response = client_with_current_workbasket.post(url, data) assert post_response.status_code == 302 assert AdditionalCodeDescription.objects.filter( diff --git a/certificates/tests/bdd/test_edit_certificates.py b/certificates/tests/bdd/test_edit_certificates.py index e4054e4d3..76f1508f6 100644 --- a/certificates/tests/bdd/test_edit_certificates.py +++ b/certificates/tests/bdd/test_edit_certificates.py @@ -10,13 +10,24 @@ @pytest.fixture @when("I edit certificate X000") -def model_edit_page(client, certificate_X000): - return client.get(certificate_X000.get_url("edit")) +def model_edit_page(client_with_current_workbasket, certificate_X000): + return client_with_current_workbasket.get(certificate_X000.get_url("edit")) + + +@pytest.fixture +@when("I edit certificate X000") +def model_edit_page_invalid_user( + client_with_current_workbasket_no_permissions, + certificate_X000, +): + return client_with_current_workbasket_no_permissions.get( + certificate_X000.get_url("edit"), + ) @then("I am not permitted to edit") -def edit_permission_denied(model_edit_page): - assert model_edit_page.status_code == 403 +def edit_permission_denied(model_edit_page_invalid_user): + assert model_edit_page_invalid_user.status_code == 403 @then("I see an edit form") @@ -26,8 +37,8 @@ def edit_permission_granted(model_edit_page): @pytest.fixture @when("I set the end date before the start date on certificate X000") -def end_date_before_start(client, response, certificate_X000): - response["response"] = client.post( +def end_date_before_start(client_with_current_workbasket, response, certificate_X000): + response["response"] = client_with_current_workbasket.post( certificate_X000.get_url("edit"), { "start_date_0": "1", diff --git a/certificates/tests/test_forms.py b/certificates/tests/test_forms.py index 6d3159759..a747d5f65 100644 --- a/certificates/tests/test_forms.py +++ b/certificates/tests/test_forms.py @@ -10,7 +10,7 @@ def test_form_save_creates_new_certificate( - session_with_workbasket, + session_request_with_workbasket, ): """Tests that the certificate create form creates a new certificate, and that two certificates of the same type are created with different sid's.""" @@ -35,7 +35,7 @@ def test_form_save_creates_new_certificate( } form = forms.CertificateCreateForm( data=certificate_b_data, - request=session_with_workbasket, + request=session_request_with_workbasket, ) certificate_b = form.save(commit=False) @@ -45,7 +45,7 @@ def test_form_save_creates_new_certificate( def test_certificate_type_does_not_increment_id( - session_with_workbasket, + session_request_with_workbasket, ): """Tests that when two certificates are made with different types, the sids are not incremented.""" @@ -74,7 +74,7 @@ def test_certificate_type_does_not_increment_id( for certificate in certificates: form = forms.CertificateCreateForm( data=certificate, - request=session_with_workbasket, + request=session_request_with_workbasket, ) saved_certificate = form.save(commit=False) completed_certificates.append(saved_certificate) @@ -87,7 +87,7 @@ def test_certificate_type_does_not_increment_id( assert completed_certificates[1].sid == "001" -def test_certificate_create_form_validates_data(session_with_workbasket): +def test_certificate_create_form_validates_data(session_request_with_workbasket): """A test to check that the create form validates data and ciphers out incorrect submissions.""" @@ -101,7 +101,7 @@ def test_certificate_create_form_validates_data(session_with_workbasket): } form = forms.CertificateCreateForm( data=certificate_data, - request=session_with_workbasket, + request=session_request_with_workbasket, ) error_string = [ "Select a valid choice. That choice is not one of the available choices.", @@ -119,7 +119,7 @@ def test_certificate_create_form_validates_data(session_with_workbasket): assert not form.is_valid() -def test_certificate_create_with_custom_sid(session_with_workbasket): +def test_certificate_create_with_custom_sid(session_request_with_workbasket): """Tests that a certificate can be created with a custom sid inputted by the user.""" certificate_type = factories.CertificateTypeFactory.create() @@ -133,14 +133,14 @@ def test_certificate_create_with_custom_sid(session_with_workbasket): } form = forms.CertificateCreateForm( data=data, - request=session_with_workbasket, + request=session_request_with_workbasket, ) certificate = form.save(commit=False) assert certificate.sid == "A01" -def test_certificate_create_ignores_non_numeric_sid(session_with_workbasket): +def test_certificate_create_ignores_non_numeric_sid(session_request_with_workbasket): """Tests that a certificate is created with a numeric sid when a certificate of the same type with a non-numeric sid already exists.""" certificate_type = factories.CertificateTypeFactory.create() @@ -154,14 +154,14 @@ def test_certificate_create_ignores_non_numeric_sid(session_with_workbasket): } form = forms.CertificateCreateForm( data=data, - request=session_with_workbasket, + request=session_request_with_workbasket, ) certificate = form.save(commit=False) assert certificate.sid == "001" -def test_validation_error_raised_for_duplicate_sid(session_with_workbasket): +def test_validation_error_raised_for_duplicate_sid(session_request_with_workbasket): """Tests that a validation error is raised on create when a certificate of the same type with the same sid already exists.""" certificate_type = factories.CertificateTypeFactory.create() @@ -176,7 +176,7 @@ def test_validation_error_raised_for_duplicate_sid(session_with_workbasket): } form = forms.CertificateCreateForm( data=data, - request=session_with_workbasket, + request=session_request_with_workbasket, ) assert not form.is_valid() diff --git a/certificates/tests/test_views.py b/certificates/tests/test_views.py index cb27d9695..9ccd0849f 100644 --- a/certificates/tests/test_views.py +++ b/certificates/tests/test_views.py @@ -46,7 +46,7 @@ def test_certificate_description_delete_form(use_delete_form): def test_certificate_create_form_creates_certificate_description_object( - valid_user_api_client, + api_client_with_current_workbasket, ): # Post a form create_url = reverse("certificate-ui-create") @@ -60,7 +60,7 @@ def test_certificate_create_form_creates_certificate_description_object( "description": "A participation certificate", } - valid_user_api_client.post(create_url, form_data) + api_client_with_current_workbasket.post(create_url, form_data) # get the certificate we have made, and the certificate description matching our description on the form certificate = models.Certificate.objects.all()[0] certificate_description = models.CertificateDescription.objects.filter( @@ -84,7 +84,7 @@ def test_certificate_detail_views( view, url_pattern, valid_user_client, - session_with_workbasket, + session_request_with_workbasket, ): """Verify that certificate detail views are under the url certificates/ and don't return an error.""" @@ -128,7 +128,7 @@ def test_description_create_get_initial(): assert initial["described_certificate"] == new_version -def test_description_create_get_context_data(valid_user_api_client): +def test_description_create_get_context_data(api_client_with_current_workbasket): """Test that posting to certificate create endpoint with valid data returns a 302 and creates new description matching certificate.""" certificate = factories.CertificateFactory.create(description=None) @@ -145,7 +145,7 @@ def test_description_create_get_context_data(valid_user_api_client): "validity_start_2": 2022, } assert not models.CertificateDescription.objects.exists() - response = valid_user_api_client.post(url, post_data) + response = api_client_with_current_workbasket.post(url, post_data) assert response.status_code == 302 assert models.CertificateDescription.objects.filter( diff --git a/commodities/tests/test_migrations.py b/commodities/tests/test_migrations.py index 9f528c3ab..7ed54bcc5 100644 --- a/commodities/tests/test_migrations.py +++ b/commodities/tests/test_migrations.py @@ -2,13 +2,12 @@ import pytest -from common.tests.factories import QueuedWorkBasketFactory from common.util import TaricDateRange from common.validators import UpdateType @pytest.mark.django_db() -def test_main_migration_works(migrator, setup_content_types): +def test_main_migration_works(migrator): """Ensures that the description date fix for TOPS-745 migration works.""" # before migration @@ -16,7 +15,7 @@ def test_main_migration_works(migrator, setup_content_types): ("commodities", "0011_TOPS_745_migration_dependencies"), ) - setup_content_types(old_state.apps) + target_workbasket_id = 238 GoodsNomenclatureDescription = old_state.apps.get_model( "commodities", @@ -26,12 +25,13 @@ def test_main_migration_works(migrator, setup_content_types): Transaction = old_state.apps.get_model("common", "Transaction") Workbasket = old_state.apps.get_model("workbaskets", "WorkBasket") VersionGroup = old_state.apps.get_model("common", "VersionGroup") + User = old_state.apps.get_model("common", "User") - QueuedWorkBasketFactory.create().save() - workbasket = Workbasket.objects.create(id=238, author_id=1) + user = User.objects.create() + workbasket = Workbasket.objects.create(id=target_workbasket_id, author=user) new_transaction = Transaction.objects.create( workbasket=workbasket, - order=Transaction.objects.order_by("order").last().order + 1, + order=1, ) gn_older_version = GoodsNomenclature.objects.create( @@ -86,14 +86,12 @@ def test_main_migration_works(migrator, setup_content_types): @pytest.mark.django_db() -def test_main_migration_ignores_if_no_data(migrator, setup_content_types): +def test_main_migration_ignores_if_no_data(migrator): # before migration old_state = migrator.apply_initial_migration( ("commodities", "0011_TOPS_745_migration_dependencies"), ) - setup_content_types(old_state.apps) - GoodsNomenclatureDescription = old_state.apps.get_model( "commodities", "GoodsNomenclatureDescription", diff --git a/commodities/tests/test_views.py b/commodities/tests/test_views.py index dffc16191..0c546a35d 100644 --- a/commodities/tests/test_views.py +++ b/commodities/tests/test_views.py @@ -111,7 +111,7 @@ def test_commodities_detail_views( url_pattern, valid_user_client, requests_mock, - session_with_workbasket, + session_request_with_workbasket, ): """Verify that commodity detail views are under the url commodities/ and don't return an error.""" @@ -378,7 +378,7 @@ def test_commodity_measures_sorting_measure_type( assert measure_sids == [measure1.sid, measure2.sid, measure3.sid] -def test_add_commodity_footnote(valid_user_client, date_ranges): +def test_add_commodity_footnote(client_with_current_workbasket, date_ranges): commodity = factories.GoodsNomenclatureFactory.create( valid_between=date_ranges.big_no_end, ) @@ -396,7 +396,7 @@ def test_add_commodity_footnote(valid_user_client, date_ranges): # sanity check assert commodity.footnote_associations.count() == 0 - response = valid_user_client.post(url, data) + response = client_with_current_workbasket.post(url, data) assert response.status_code == 302 assert commodity.footnote_associations.count() == 1 @@ -411,7 +411,10 @@ def test_add_commodity_footnote(valid_user_client, date_ranges): assert new_association.goods_nomenclature == commodity -def test_add_commodity_footnote_NIG22_failure(valid_user_client, date_ranges): +def test_add_commodity_footnote_NIG22_failure( + client_with_current_workbasket, + date_ranges, +): """ Tests failure of NIG22: @@ -433,7 +436,7 @@ def test_add_commodity_footnote_NIG22_failure(valid_user_client, date_ranges): "end_date": "", } - response = valid_user_client.post(url, data) + response = client_with_current_workbasket.post(url, data) assert response.status_code == 200 @@ -443,12 +446,12 @@ def test_add_commodity_footnote_NIG22_failure(valid_user_client, date_ranges): ) -def test_add_commodity_footnote_form_page(valid_user_client, date_ranges): +def test_add_commodity_footnote_form_page(client_with_current_workbasket, date_ranges): commodity = factories.GoodsNomenclatureFactory.create( valid_between=date_ranges.big_no_end, ) url = reverse("commodity-ui-add-footnote", kwargs={"sid": commodity.sid}) - response = valid_user_client.get(url) + response = client_with_current_workbasket.get(url) assert response.status_code == 200 @@ -505,7 +508,7 @@ def test_commodity_footnotes_page(valid_user_client): assert not footnote_descriptions.difference(page_footnote_descriptions) -def test_commodity_footnote_update_success(valid_user_client, date_ranges): +def test_commodity_footnote_update_success(client_with_current_workbasket, date_ranges): commodity = factories.GoodsNomenclatureFactory.create() footnote1 = factories.FootnoteFactory.create() association1 = factories.FootnoteAssociationGoodsNomenclatureFactory.create( @@ -521,7 +524,7 @@ def test_commodity_footnote_update_success(valid_user_client, date_ranges): "start_date_2": date_ranges.later.lower.year, "end_date": "", } - response = valid_user_client.post(url, data) + response = client_with_current_workbasket.post(url, data) tx = Transaction.objects.last() updated_association = ( FootnoteAssociationGoodsNomenclature.objects.approved_up_to_transaction( @@ -532,7 +535,7 @@ def test_commodity_footnote_update_success(valid_user_client, date_ranges): assert response.url == updated_association.get_url("confirm-update") -def test_footnote_association_delete(valid_user_client): +def test_footnote_association_delete(client_with_current_workbasket): commodity = factories.GoodsNomenclatureFactory.create() footnote1 = factories.FootnoteFactory.create() association1 = factories.FootnoteAssociationGoodsNomenclatureFactory.create( @@ -540,7 +543,7 @@ def test_footnote_association_delete(valid_user_client): goods_nomenclature=commodity, ) url = association1.get_url("delete") - response = valid_user_client.post(url, {"submit": "Delete"}) + response = client_with_current_workbasket.post(url, {"submit": "Delete"}) assert response.status_code == 302 assert response.url == reverse( @@ -554,7 +557,7 @@ def test_footnote_association_delete(valid_user_client): assert tx.workbasket.tracked_models.first().goods_nomenclature == commodity assert tx.workbasket.tracked_models.first().update_type == UpdateType.DELETE - confirm_response = valid_user_client.get(response.url) + confirm_response = client_with_current_workbasket.get(response.url) soup = BeautifulSoup( confirm_response.content.decode(response.charset), "html.parser", diff --git a/common/admin.py b/common/admin.py new file mode 100644 index 000000000..2016e1c17 --- /dev/null +++ b/common/admin.py @@ -0,0 +1,32 @@ +from django.contrib import admin +from django.contrib.auth import forms as auth_forms +from django.contrib.auth import get_user_model +from django.contrib.auth.admin import UserAdmin + +User = get_user_model() + + +class UserCreationForm(auth_forms.UserCreationForm): + class Meta(auth_forms.UserCreationForm.Meta): + model = User + fields = auth_forms.UserCreationForm.Meta.fields + + +class UserChangeForm(auth_forms.UserChangeForm): + class Meta(auth_forms.UserChangeForm.Meta): + model = User + fields = auth_forms.UserCreationForm.Meta.fields + + +class UserAdmin(UserAdmin): + model = User + form = UserChangeForm + add_form = UserCreationForm + readonly_fields = ("current_workbasket",) + fieldsets = UserAdmin.fieldsets + ( + ("Workbasket", {"fields": ("current_workbasket",)}), + ) + add_fieldsets = UserAdmin.add_fieldsets + + +admin.site.register(User, UserAdmin) diff --git a/common/jinja2/common/confirm_create_description.jinja b/common/jinja2/common/confirm_create_description.jinja index 6012f63f7..2727c55e1 100644 --- a/common/jinja2/common/confirm_create_description.jinja +++ b/common/jinja2/common/confirm_create_description.jinja @@ -36,7 +36,7 @@ diff --git a/common/jinja2/common/confirm_update_description.jinja b/common/jinja2/common/confirm_update_description.jinja index 9f0fc8863..9feecdc9c 100644 --- a/common/jinja2/common/confirm_update_description.jinja +++ b/common/jinja2/common/confirm_update_description.jinja @@ -36,7 +36,7 @@ diff --git a/common/jinja2/components/breadcrumbs.jinja b/common/jinja2/components/breadcrumbs.jinja index 0f09a507a..c59eb1608 100644 --- a/common/jinja2/components/breadcrumbs.jinja +++ b/common/jinja2/components/breadcrumbs.jinja @@ -5,9 +5,9 @@
  • Home
  • - {% if request.session.workbasket %} + {% if request.user.current_workbasket %}
  • - Workbasket {{ request.session.workbasket.id }} + Workbasket {{ request.user.current_workbasket.id }}
  • {% endif %} {% for crumb in breadcrumbs_list %} diff --git a/common/jinja2/includes/common/main-menu-link.jinja b/common/jinja2/includes/common/main-menu-link.jinja index cbcce5ed3..4f3cdfe55 100644 --- a/common/jinja2/includes/common/main-menu-link.jinja +++ b/common/jinja2/includes/common/main-menu-link.jinja @@ -1,3 +1,3 @@ -{% if request.session.workbasket %} +{% if request.user.current_workbasket %}
  • Return to workbasket
  • {% endif %} diff --git a/common/jinja2/layouts/layout.jinja b/common/jinja2/layouts/layout.jinja index 331d29103..2c01a559c 100644 --- a/common/jinja2/layouts/layout.jinja +++ b/common/jinja2/layouts/layout.jinja @@ -20,11 +20,11 @@ {% block header %} {% set workbasket_html %} - {% if request.session.workbasket %} + {% if request.user.current_workbasket %} Workbasket {{ request.session.workbasket.id }} {{ workbasket_svg }} + >Workbasket {{ request.user.current_workbasket.id }} {{ workbasket_svg }} {% endif %} {% endset %} diff --git a/common/migrations/0001_initial.py b/common/migrations/0001_initial.py index b0cdcd195..4eaa62b09 100644 --- a/common/migrations/0001_initial.py +++ b/common/migrations/0001_initial.py @@ -10,9 +10,132 @@ class Migration(migrations.Migration): dependencies = [ ("contenttypes", "0002_remove_content_type_name"), ("workbaskets", "0001_initial"), + ("auth", "0012_alter_user_first_name_max_length"), ] operations = [ + # The initial migration for Django's default auth user model has been added here so that it can be substituted for a custom user model without having to truncate the django_migrations table. + migrations.CreateModel( + name="User", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("password", models.CharField(max_length=128, verbose_name="password")), + ( + "last_login", + models.DateTimeField( + blank=True, + null=True, + verbose_name="last login", + ), + ), + ( + "is_superuser", + models.BooleanField( + default=False, + help_text="Designates that this user has all permissions without explicitly assigning them.", + verbose_name="superuser status", + ), + ), + ( + "username", + models.CharField( + error_messages={ + "unique": "A user with that username already exists.", + }, + help_text="Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.", + max_length=150, + unique=True, + validators=[ + django.contrib.auth.validators.UnicodeUsernameValidator(), + ], + verbose_name="username", + ), + ), + ( + "first_name", + models.CharField( + blank=True, + max_length=150, + verbose_name="first name", + ), + ), + ( + "last_name", + models.CharField( + blank=True, + max_length=150, + verbose_name="last name", + ), + ), + ( + "email", + models.EmailField( + blank=True, + max_length=254, + verbose_name="email address", + ), + ), + ( + "is_staff", + models.BooleanField( + default=False, + help_text="Designates whether the user can log into this admin site.", + verbose_name="staff status", + ), + ), + ( + "is_active", + models.BooleanField( + default=True, + help_text="Designates whether this user should be treated as active. Unselect this instead of deleting accounts.", + verbose_name="active", + ), + ), + ( + "date_joined", + models.DateTimeField( + default=django.utils.timezone.now, + verbose_name="date joined", + ), + ), + ( + "groups", + models.ManyToManyField( + blank=True, + help_text="The groups this user belongs to. A user will get all permissions granted to each of their groups.", + related_name="user_set", + related_query_name="user", + to="auth.Group", + verbose_name="groups", + ), + ), + ( + "user_permissions", + models.ManyToManyField( + blank=True, + help_text="Specific permissions for this user.", + related_name="user_set", + related_query_name="user", + to="auth.Permission", + verbose_name="user permissions", + ), + ), + ], + options={ + "db_table": "auth_user", + }, + managers=[ + ("objects", django.contrib.auth.models.UserManager()), + ], + ), migrations.CreateModel( name="TrackedModel", fields=[ diff --git a/common/migrations/0008_user_current_workbasket.py b/common/migrations/0008_user_current_workbasket.py new file mode 100644 index 000000000..782f73d94 --- /dev/null +++ b/common/migrations/0008_user_current_workbasket.py @@ -0,0 +1,45 @@ +# Generated by Django 3.2.23 on 2024-01-24 15:41 + +import django.db.models.deletion +from django.db import migrations +from django.db import models + + +def change_user_content_type(apps, schema_editor): + """ + The addition of the new current_workbasket field marks the move to a custom + user model from this point in migration history onwards. + + As a result, the auth.User content type must be updated to reflect + common.User (custom user model) to preserve existing references. + """ + + ContentType = apps.get_model("contenttypes", "ContentType") + ct = ContentType.objects.filter( + app_label="auth", + model="user", + ).first() + if ct: + ct.app_label = "common" + ct.save() + + +class Migration(migrations.Migration): + dependencies = [ + ("workbaskets", "0008_datarow_dataupload"), + ("common", "0007_auto_20221114_1040_fix_missing_current_versions"), + ] + + operations = [ + migrations.RunPython(change_user_content_type), + migrations.AddField( + model_name="user", + name="current_workbasket", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + to="workbaskets.workbasket", + ), + ), + ] diff --git a/common/models/__init__.py b/common/models/__init__.py index a55b86f6f..e94541f1b 100644 --- a/common/models/__init__.py +++ b/common/models/__init__.py @@ -10,6 +10,7 @@ from common.models.trackedmodel import TrackedModel from common.models.trackedmodel import VersionGroup from common.models.transactions import Transaction +from common.models.user import User __all__ = [ "ApplicabilityCode", @@ -19,6 +20,7 @@ "TimestampedMixin", "TrackedModel", "Transaction", + "User", "ValidityMixin", "ValidityStartMixin", "DescriptionMixin", diff --git a/common/models/user.py b/common/models/user.py new file mode 100644 index 000000000..9fdd40c23 --- /dev/null +++ b/common/models/user.py @@ -0,0 +1,21 @@ +from django.contrib.auth.models import AbstractUser +from django.db import models + + +class User(AbstractUser): + """Custom user model.""" + + current_workbasket = models.ForeignKey( + "workbaskets.WorkBasket", + on_delete=models.SET_NULL, + null=True, + blank=True, + ) + + class Meta: + db_table = "auth_user" + + def remove_current_workbasket(self): + """Remove the user's assigned current workbasket.""" + self.current_workbasket = None + self.save() diff --git a/common/models/utils.py b/common/models/utils.py index c624159e0..dd8b7b6f7 100644 --- a/common/models/utils.py +++ b/common/models/utils.py @@ -4,6 +4,7 @@ import wrapt from django.db.models import Value +from django.shortcuts import redirect from django.urls import reverse _thread_locals = threading.local() @@ -93,9 +94,9 @@ def override_current_transaction(transaction=None): set_current_transaction(old_transaction) -def is_session_workbasket_valid(request): - """Returns True if the workbasket in the session is valid (i.e. exists and - has status EDITING.)""" +def is_current_workbasket_valid(request): + """Returns True if a user's current workbasket is valid (i.e. exists and has + status EDITING.)""" from workbaskets.models import WorkBasket from workbaskets.validators import WorkflowStatus @@ -108,15 +109,13 @@ def is_session_workbasket_valid(request): return False -class ValidateSessionWorkBasketMiddleware: +class ValidateUserWorkBasketMiddleware: """ WorkBasket middleware that: - - - Validates that any workbasket in the user's session is valid. - - Removes invalid workbaskets from the user's session. - + - Validates that a user's assigned current workbasket is valid. + - Removes invalid workbaskets from the user. This middleware should always be placed before any other middleware in - settings.MIDDLEWARE that references session workbaskets (for instance, + settings.MIDDLEWARE that references workbaskets (for instance, TransactionMiddleware). """ @@ -124,14 +123,11 @@ def __init__(self, get_response): self.get_response = get_response def __call__(self, request): - from workbaskets.models import WorkBasket + # If a user has an invalid workbasket then redirect them to the notice page + # letting them know it has been removed, otherwise continue. - # A current, editable workbasket is necessary in order to set the - # current transaction (below), so abandon this middleware's action and - # return early if there is no current editable workbasket in the - # session. - if not is_session_workbasket_valid(request): - WorkBasket.remove_current_from_session(request.session) + if not is_current_workbasket_valid(request): + redirect("workbaskets:no-active-workbasket") return self.get_response(request) diff --git a/common/serializers.py b/common/serializers.py index fd82896c2..a4c579e25 100644 --- a/common/serializers.py +++ b/common/serializers.py @@ -7,8 +7,8 @@ from typing import Optional from django.conf import settings +from django.contrib.auth import get_user_model from django.contrib.auth.models import Group -from django.contrib.auth.models import User from django.template.loader import render_to_string from drf_extra_fields.fields import DateRangeField from lxml import etree @@ -24,6 +24,8 @@ from common.util import get_taric_template from common.util import parse_xml +User = get_user_model() + logger = logging.getLogger(__name__) diff --git a/common/tests/bdd/test_edit_view.py b/common/tests/bdd/test_edit_view.py index 90ff22996..163505683 100644 --- a/common/tests/bdd/test_edit_view.py +++ b/common/tests/bdd/test_edit_view.py @@ -21,13 +21,24 @@ def tracked_model(approved_transaction): @pytest.fixture @when("I edit a model") -def model_edit_page(client, tracked_model): - return client.get(tracked_model.get_url("edit")) +def model_edit_page(client_with_current_workbasket, tracked_model): + return client_with_current_workbasket.get(tracked_model.get_url("edit")) + + +@pytest.fixture +@when("I edit a model") +def model_edit_page_invalid_user( + client_with_current_workbasket_no_permissions, + tracked_model, +): + return client_with_current_workbasket_no_permissions.get( + tracked_model.get_url("edit"), + ) @then("I am not permitted to edit") -def edit_permission_denied(model_edit_page): - assert model_edit_page.status_code == 403 +def edit_permission_denied(model_edit_page_invalid_user): + assert model_edit_page_invalid_user.status_code == 403 @then("I see an edit form") diff --git a/common/tests/factories.py b/common/tests/factories.py index a2fe14464..a32463dc2 100644 --- a/common/tests/factories.py +++ b/common/tests/factories.py @@ -5,6 +5,7 @@ from itertools import product import factory +from django.contrib.auth import get_user_model from factory.fuzzy import FuzzyChoice from factory.fuzzy import FuzzyText from faker import Faker @@ -30,6 +31,8 @@ from quotas.validators import QuotaEventType from workbaskets.validators import WorkflowStatus +User = get_user_model() + def short_description(): return factory.Faker("text", max_nb_chars=500) @@ -102,7 +105,7 @@ class UserFactory(factory.django.DjangoModelFactory): """User factory.""" class Meta: - model = "auth.User" + model = User username = factory.sequence(lambda n: f"{factory.Faker('name')}{n}") email = factory.Faker("email") diff --git a/common/tests/test_filters.py b/common/tests/test_filters.py index 4135913ff..b0d1f085e 100644 --- a/common/tests/test_filters.py +++ b/common/tests/test_filters.py @@ -48,10 +48,10 @@ def test_search_queryset_returns_case_insensitive(): def test_filter_by_current_workbasket_mixin( valid_user_client, - session_workbasket, + user_workbasket, session_request, ): - with session_workbasket.new_transaction() as transaction: + with user_workbasket.new_transaction() as transaction: commodity_in_workbasket_1 = GoodsNomenclatureFactory.create( transaction=transaction, ) @@ -61,9 +61,6 @@ def test_filter_by_current_workbasket_mixin( commodity_not_in_workbasket_1 = GoodsNomenclatureFactory.create() commodity_not_in_workbasket_2 = GoodsNomenclatureFactory.create() - session = valid_user_client.session - session["workbasket"] = {"id": session_workbasket.pk} - session.save() qs = GoodsNomenclature.objects.all() self = CommodityFilter( diff --git a/common/tests/test_migrations.py b/common/tests/test_migrations.py index a215160ae..9761c61bc 100644 --- a/common/tests/test_migrations.py +++ b/common/tests/test_migrations.py @@ -11,12 +11,11 @@ @pytest.mark.django_db() def test_missing_current_version_fix(migrator): migrator.reset() - """Ensures that the initial migration works.""" new_state = migrator.apply_initial_migration(("common", "0006_auto_20221114_1000")) # setup - user_class = new_state.apps.get_model("auth", "User") + user_class = new_state.apps.get_model("common", "User") workbasket_class = new_state.apps.get_model("workbaskets", "WorkBasket") measurement_unit_class = new_state.apps.get_model("measures", "MeasurementUnit") transaction_class = new_state.apps.get_model("common", "Transaction") diff --git a/conftest.py b/conftest.py index e2dc46965..d11a645ae 100644 --- a/conftest.py +++ b/conftest.py @@ -156,7 +156,8 @@ def tap_migrator_factory(migrator_factory): its migration unit testing, continues to cause problems in migration unit tests. A couple of examples of the reported issue: https://code.djangoproject.com/ticket/10827 - https://github.com/wemake-services/django-test-migrations/blob/93db540c00a830767eeab5f90e2eef1747c940d4/django_test_migrations/migrator.py#L73 + /PS-IGNORE---https://github.com/wemake-services/django-test-migrations/blob/93db540c00a830767eeab5f90e2eef1747c940d4/django_test_migrations/migrator.py#L73 + An initial migration must reference ContentType instances (in the DB). This can occur when inserting Permission objects during @@ -321,6 +322,29 @@ def valid_user_client(client, valid_user): return client +@pytest.fixture +def client_with_current_workbasket(client, valid_user): + client.force_login(valid_user) + workbasket = factories.WorkBasketFactory.create( + status=WorkflowStatus.EDITING, + ) + workbasket.assign_to_user(valid_user) + return client + + +@pytest.fixture +def client_with_current_workbasket_no_permissions(client): + """Returns a client with a logged in user who has a current workbasket but + no permissions.""" + user = factories.UserFactory.create() + client.force_login(user) + workbasket = factories.WorkBasketFactory.create( + status=WorkflowStatus.EDITING, + ) + workbasket.assign_to_user(user) + return client + + @pytest.fixture def superuser(): user = factories.UserFactory.create(is_superuser=True, is_staff=True) @@ -360,6 +384,16 @@ def valid_user_api_client(api_client, valid_user) -> APIClient: return api_client +@pytest.fixture +def api_client_with_current_workbasket(api_client, valid_user) -> APIClient: + api_client.force_login(valid_user) + workbasket = factories.WorkBasketFactory.create( + status=WorkflowStatus.EDITING, + ) + workbasket.assign_to_user(valid_user) + return api_client + + @pytest.fixture def taric_schema(settings) -> etree.XMLSchema: with open(settings.PATH_XSD_TARIC) as xsd_file: @@ -405,25 +439,21 @@ def published_footnote_type(queued_workbasket): @pytest.fixture @given("there is a current workbasket") -def session_workbasket(client, new_workbasket) -> WorkBasket: - # The valid_user_client.session property returns a new session instance on - # each reference, so first get a single session instance via the property. - session = client.session - new_workbasket.save_to_session(session) - session.save() +def user_workbasket(client, valid_user, new_workbasket) -> WorkBasket: + """Returns a workbasket which has been assigned to a valid logged-in + user.""" + client.force_login(valid_user) + new_workbasket.assign_to_user(valid_user) return new_workbasket @pytest.fixture -def session_empty_workbasket(valid_user_client) -> WorkBasket: +def user_empty_workbasket(client, valid_user) -> WorkBasket: + client.force_login(valid_user) workbasket = factories.WorkBasketFactory.create( status=WorkflowStatus.EDITING, ) - # The valid_user_client.session property returns a new session instance on - # each reference, so first get a single session instance via the property. - session = valid_user_client.session - workbasket.save_to_session(session) - session.save() + workbasket.assign_to_user(valid_user) return workbasket @@ -481,7 +511,16 @@ def unapproved_checked_transaction(unapproved_transaction): @pytest.fixture(scope="function") def workbasket(): - return factories.WorkBasketFactory.create() + """ + Returns existing workbasket if one already exists otherwise creates a new + one. + + This is as some tests already have a workbasket when this is called. + """ + if WorkBasket.objects.all().count() > 0: + return WorkBasket.objects.first() + else: + return factories.WorkBasketFactory.create() @pytest.fixture( @@ -522,7 +561,7 @@ def description_factory(request): @pytest.fixture -def use_create_form(valid_user_api_client: APIClient): +def use_create_form(api_client_with_current_workbasket): """ use_create_form, ported from use_update_form. @@ -551,7 +590,7 @@ def use( assert create_url, f"No create page found for {Model}" # Initial rendering of url - response = valid_user_api_client.get(create_url) + response = api_client_with_current_workbasket.get(create_url) assert response.status_code == 200 initial_form = response.context_data["form"] @@ -567,7 +606,7 @@ def use( k: data.get(k) for k in Model.identifying_fields if "__" not in k } - response = valid_user_api_client.post(create_url, data) + response = api_client_with_current_workbasket.post(create_url, data) # Check that if we expect failure that the new data was not persisted if response.status_code not in (301, 302): @@ -588,7 +627,7 @@ def use( @pytest.fixture -def use_edit_view(valid_user_api_client: APIClient): +def use_edit_view(api_client_with_current_workbasket): """ Uses the default edit form and view for a model in a workbasket with EDITING status. @@ -608,14 +647,14 @@ def use(obj: TrackedModel, data_changes: dict[str, str]): url = obj.get_url("edit") # Check initial form rendering. - get_response = valid_user_api_client.get(url) + get_response = api_client_with_current_workbasket.get(url) assert get_response.status_code == 200 # Edit and submit the data. initial_form = get_response.context_data["form"] form_data = get_form_data(initial_form) form_data.update(data_changes) - post_response = valid_user_api_client.post(url, form_data) + post_response = api_client_with_current_workbasket.post(url, form_data) # POSTing a real edits form should never create new object instances. assert Model.objects.filter(**obj.get_identifying_fields()).count() == obj_count @@ -628,7 +667,7 @@ def use(obj: TrackedModel, data_changes: dict[str, str]): @pytest.fixture -def use_update_form(valid_user_api_client: APIClient): +def use_update_form(api_client_with_current_workbasket): """ Uses the default create form and view for a model with update_type=UPDATE. @@ -653,7 +692,7 @@ def use(object: TrackedModel, new_data: Callable[[TrackedModel], dict[str, Any]] # Visit the edit page and ensure it is a success edit_url = object.get_url("edit") assert edit_url, f"No edit page found for {object}" - response = valid_user_api_client.get(edit_url) + response = api_client_with_current_workbasket.get(edit_url) assert response.status_code == 200 # Get the data out of the edit page @@ -664,7 +703,7 @@ def use(object: TrackedModel, new_data: Callable[[TrackedModel], dict[str, Any]] realised_data = new_data(object) assert set(realised_data.keys()).issubset(data.keys()) data.update(realised_data) - response = valid_user_api_client.post(edit_url, data) + response = api_client_with_current_workbasket.post(edit_url, data) # Check that if we expect failure that the new data was not persisted if response.status_code not in (301, 302): @@ -682,7 +721,7 @@ def use(object: TrackedModel, new_data: Callable[[TrackedModel], dict[str, Any]] ) # Check that what we asked to be changed has been persisted - response = valid_user_api_client.get(edit_url) + response = api_client_with_current_workbasket.get(edit_url) assert response.status_code == 200 data = get_form_data(response.context_data["form"]) for key in realised_data: @@ -704,7 +743,7 @@ def use(object: TrackedModel, new_data: Callable[[TrackedModel], dict[str, Any]] @pytest.fixture -def use_delete_form(valid_user_api_client: APIClient): +def use_delete_form(api_client_with_current_workbasket): """ Uses the default delete form and view for a model to delete an object, and returns the deleted version of the object. @@ -725,12 +764,12 @@ def use(object: TrackedModel): # Visit the delete page and ensure it is a success delete_url = object.get_url("delete") assert delete_url, f"No delete page found for {object}" - response = valid_user_api_client.get(delete_url) + response = api_client_with_current_workbasket.get(delete_url) assert response.status_code == 200 # Get the data out of the delete page data = get_form_data(response.context_data["form"]) - response = valid_user_api_client.post(delete_url, data) + response = api_client_with_current_workbasket.post(delete_url, data) # Check that if we expect failure that the new data was not persisted if response.status_code not in (301, 302): @@ -748,7 +787,7 @@ def use(object: TrackedModel): ) # Check that the delete persisted and we can't delete again - response = valid_user_api_client.get(delete_url) + response = api_client_with_current_workbasket.get(delete_url) assert response.status_code == 404 # Check that if success was expected that the new version was persisted @@ -1142,7 +1181,7 @@ def make_record( dependency.delete() record = factory_instance.create( - **{f"{reference_field_name}_id": non_existent_id} + **{f"{reference_field_name}_id": non_existent_id}, ) try: @@ -1427,19 +1466,35 @@ def unordered_transactions(): @pytest.fixture -def session_request(client): +def session_request(client, valid_user): session = client.session session.save() request = RequestFactory() request.session = session + request.user = valid_user return request @pytest.fixture -def session_with_workbasket(session_request, workbasket): - session_request.session.update({"workbasket": {"id": workbasket.pk}}) - return session_request +def session_request_with_workbasket(client, valid_user): + """ + Returns a request object which has a valid user and session associated. + + The valid user has a current workbasket. + """ + client.force_login(valid_user) + workbasket = factories.WorkBasketFactory.create( + status=WorkflowStatus.EDITING, + ) + workbasket.assign_to_user(valid_user) + + session = client.session + session.save() + request = RequestFactory() + request.session = session + request.user = valid_user + return request @pytest.fixture @@ -1891,7 +1946,8 @@ def factory_method(workbasket=None, **kwargs): return_value=MagicMock(id=factory.Faker("uuid4")), ): packaged_workbasket = factories.QueuedPackagedWorkBasketFactory( - workbasket=workbasket, **kwargs + workbasket=workbasket, + **kwargs, ) return packaged_workbasket diff --git a/footnotes/tests/bdd/test_edit_footnote.py b/footnotes/tests/bdd/test_edit_footnote.py index bee668a39..ed1dca5e6 100644 --- a/footnotes/tests/bdd/test_edit_footnote.py +++ b/footnotes/tests/bdd/test_edit_footnote.py @@ -17,8 +17,19 @@ @pytest.fixture @when("I edit footnote NC000") -def footnote_edit_screen(client, footnote_NC000): - return client.get(footnote_NC000.get_url("edit")) +def footnote_edit_screen(client_with_current_workbasket, footnote_NC000): + return client_with_current_workbasket.get(footnote_NC000.get_url("edit")) + + +@pytest.fixture +@when("I edit footnote NC000") +def footnote_edit_screen_invalid_user( + client_with_current_workbasket_no_permissions, + footnote_NC000, +): + return client_with_current_workbasket_no_permissions.get( + footnote_NC000.get_url("edit"), + ) @then("I see an edit form") @@ -28,8 +39,8 @@ def edit_permission_granted(footnote_edit_screen): @pytest.fixture @when("I set the end date before the start date on footnote NC000") -def end_date_before_start(client, response, footnote_NC000): - response["response"] = client.post( +def end_date_before_start(client_with_current_workbasket, response, footnote_NC000): + response["response"] = client_with_current_workbasket.post( footnote_NC000.get_url("edit"), validity_period_post_data( start=date(2021, 1, 1), @@ -39,8 +50,8 @@ def end_date_before_start(client, response, footnote_NC000): @when("I set the start date of footnote NC000 to predate the footnote type") -def submit_predating(client, response, footnote_NC000): - response["response"] = client.post( +def submit_predating(client_with_current_workbasket, response, footnote_NC000): + response["response"] = client_with_current_workbasket.post( footnote_NC000.get_url("edit"), validity_period_post_data( start=footnote_NC000.footnote_type.valid_between.lower - timedelta(days=1), diff --git a/footnotes/tests/test_forms.py b/footnotes/tests/test_forms.py index 2e5f4a356..34c96210b 100644 --- a/footnotes/tests/test_forms.py +++ b/footnotes/tests/test_forms.py @@ -18,7 +18,7 @@ # https://uktrade.atlassian.net/browse/TP-851 def test_form_save_creates_new_footnote_id_and_footnote_type_id_combo( - session_with_workbasket, + session_request_with_workbasket, ): """Tests that when two non-overlapping footnotes of the same type are created that these are created with a different footnote_id, to avoid @@ -41,7 +41,7 @@ def test_form_save_creates_new_footnote_id_and_footnote_type_id_combo( "start_date_2": 2022, "description": "A note on feet", } - form = forms.FootnoteCreateForm(data=data, request=session_with_workbasket) + form = forms.FootnoteCreateForm(data=data, request=session_request_with_workbasket) new_footnote = form.save(commit=False) assert earlier.footnote_id != new_footnote.footnote_id diff --git a/footnotes/tests/test_views.py b/footnotes/tests/test_views.py index 9810edac7..a432a0edf 100644 --- a/footnotes/tests/test_views.py +++ b/footnotes/tests/test_views.py @@ -129,7 +129,7 @@ def test_footnote_detail_views( view, url_pattern, valid_user_client, - session_with_workbasket, + session_request_with_workbasket, ): """Verify that measure detail views are under the url footnotes/ and don't return an error.""" @@ -228,7 +228,7 @@ def test_footnote_type_api_list_view(valid_user_client): ) -def test_footnote_description_create(valid_user_client): +def test_footnote_description_create(client_with_current_workbasket): """Tests that `FootnoteDescriptionCreate` view returns 200 and creates a description for the current version of an footnote.""" footnote = factories.FootnoteFactory.create(description=None) @@ -251,10 +251,10 @@ def test_footnote_description_create(valid_user_client): } with override_current_transaction(Transaction.objects.last()): - get_response = valid_user_client.get(url) + get_response = client_with_current_workbasket.get(url) assert get_response.status_code == 200 - post_response = valid_user_client.post(url, data) + post_response = client_with_current_workbasket.post(url, data) assert post_response.status_code == 302 assert FootnoteDescription.objects.filter(described_footnote=new_version).exists() diff --git a/geo_areas/tests/test_forms.py b/geo_areas/tests/test_forms.py index bcaaa4a86..01ec5644f 100644 --- a/geo_areas/tests/test_forms.py +++ b/geo_areas/tests/test_forms.py @@ -197,7 +197,7 @@ def test_geographical_membership_add_form_invalid_selection(date_ranges): def test_geographical_membership_edit_form_valid_deletion( date_ranges, - session_with_workbasket, + session_request_with_workbasket, ): country = factories.CountryFactory.create() area_group = factories.GeoGroupFactory.create(valid_between=date_ranges.normal) @@ -215,14 +215,14 @@ def test_geographical_membership_edit_form_valid_deletion( form = forms.GeographicalAreaEditForm( data=form_data, instance=area_group, - request=session_with_workbasket, + request=session_request_with_workbasket, ) assert form.is_valid() def test_geographical_membership_edit_form_invalid_deletion( date_ranges, - session_with_workbasket, + session_request_with_workbasket, ): country = factories.CountryFactory.create() area_group = factories.GeoGroupFactory.create(valid_between=date_ranges.normal) @@ -243,7 +243,7 @@ def test_geographical_membership_edit_form_invalid_deletion( form = forms.GeographicalAreaEditForm( data=form_data, instance=area_group, - request=session_with_workbasket, + request=session_request_with_workbasket, ) assert not form.is_valid() assert ( @@ -254,7 +254,7 @@ def test_geographical_membership_edit_form_invalid_deletion( def test_geographical_membership_edit_form_valid_end_date( date_ranges, - session_with_workbasket, + session_request_with_workbasket, ): country = factories.CountryFactory.create() area_group = factories.GeoGroupFactory.create(valid_between=date_ranges.normal) @@ -275,14 +275,14 @@ def test_geographical_membership_edit_form_valid_end_date( form = forms.GeographicalAreaEditForm( data=form_data, instance=area_group, - request=session_with_workbasket, + request=session_request_with_workbasket, ) assert form.is_valid() def test_geographical_membership_edit_form_invalid_end_date( date_ranges, - session_with_workbasket, + session_request_with_workbasket, ): country = factories.CountryFactory.create() area_group = factories.GeoGroupFactory.create(valid_between=date_ranges.normal) @@ -310,7 +310,7 @@ def test_geographical_membership_edit_form_invalid_end_date( form = forms.GeographicalAreaEditForm( data=invalid_end_date_1, instance=area_group, - request=session_with_workbasket, + request=session_request_with_workbasket, ) assert not form.is_valid() assert ( @@ -321,7 +321,7 @@ def test_geographical_membership_edit_form_invalid_end_date( form = forms.GeographicalAreaEditForm( data=invalid_end_date_2, instance=area_group, - request=session_with_workbasket, + request=session_request_with_workbasket, ) assert not form.is_valid() assert ( diff --git a/geo_areas/tests/test_views.py b/geo_areas/tests/test_views.py index f4ca956de..58f5acf17 100644 --- a/geo_areas/tests/test_views.py +++ b/geo_areas/tests/test_views.py @@ -49,7 +49,10 @@ def test_geo_area_description_delete_form(use_delete_form): ) -def test_geographical_area_description_create(valid_user_client, date_ranges): +def test_geographical_area_description_create( + client_with_current_workbasket, + date_ranges, +): """Tests that a geographical area description can be created.""" geo_area = factories.GeographicalAreaFactory.create( @@ -71,13 +74,13 @@ def test_geographical_area_description_create(valid_user_client, date_ranges): "geo_area-ui-description-create", kwargs={"sid": current_geo_area.sid}, ) - response = valid_user_client.post(url, form_data) + response = client_with_current_workbasket.post(url, form_data) assert response.status_code == 302 with override_current_transaction(Transaction.objects.last()): - new_desciption = current_geo_area.get_description() - assert new_desciption.description == form_data["description"] - assert new_desciption.validity_start == date_ranges.future.lower + new_description = current_geo_area.get_description() + assert new_description.description == form_data["description"] + assert new_description.validity_start == date_ranges.future.lower @pytest.mark.parametrize( @@ -92,7 +95,7 @@ def test_geographical_area_detail_views( view, url_pattern, valid_user_client, - session_with_workbasket, + session_request_with_workbasket, ): """Verify that geographical detail views are under the url geographical- areas and don't return an error.""" @@ -173,29 +176,29 @@ def test_geo_area_api_list_view(valid_user_client): ) -def test_geo_area_update_view_200(valid_user_client): +def test_geo_area_update_view_200(client_with_current_workbasket): geo_area = factories.GeographicalAreaFactory.create() url = reverse( "geo_area-ui-edit", kwargs={"sid": geo_area.sid}, ) - response = valid_user_client.get(url) + response = client_with_current_workbasket.get(url) assert response.status_code == 200 -def test_geo_area_edit_update_view_200(valid_user_client): +def test_geo_area_edit_update_view_200(client_with_current_workbasket): geo_area = factories.GeographicalAreaFactory.create() url = reverse( "geo_area-ui-edit-update", kwargs={"sid": geo_area.sid}, ) - response = valid_user_client.get(url) + response = client_with_current_workbasket.get(url) assert response.status_code == 200 def test_geo_area_update_view_edit_end_date( valid_user_client, - session_workbasket, + user_workbasket, date_ranges, ): """Tests that a geographical area's end date can be edited.""" @@ -224,7 +227,7 @@ def test_geo_area_update_view_edit_end_date( assert response.url == redirect_url geo_areas = GeographicalArea.objects.filter( - transaction__workbasket=session_workbasket, + transaction__workbasket=user_workbasket, ) for geo_area in geo_areas: assert geo_area.valid_between.upper == new_end_date @@ -233,7 +236,7 @@ def test_geo_area_update_view_edit_end_date( def test_geo_area_update_view_membership_add_country_or_region( valid_user_client, - session_workbasket, + user_workbasket, ): """Tests that a country or region can be added as a member of the area group being edited.""" @@ -269,7 +272,7 @@ def test_geo_area_update_view_membership_add_country_or_region( assert response.url == redirect_url workbasket = GeographicalMembership.objects.filter( - transaction__workbasket=session_workbasket, + transaction__workbasket=user_workbasket, ) for membership in workbasket: assert membership.valid_between == expected_valid_between @@ -278,7 +281,7 @@ def test_geo_area_update_view_membership_add_country_or_region( def test_geo_area_update_view_membership_add_to_group( valid_user_client, - session_workbasket, + user_workbasket, ): """Tests that the country or region being edited can be added as a member of an area group.""" @@ -313,7 +316,7 @@ def test_geo_area_update_view_membership_add_to_group( assert response.url == redirect_url workbasket = GeographicalMembership.objects.filter( - transaction__workbasket=session_workbasket, + transaction__workbasket=user_workbasket, ) for membership in workbasket: assert membership.valid_between == expected_valid_between @@ -322,7 +325,7 @@ def test_geo_area_update_view_membership_add_to_group( def test_geo_area_update_view_membership_edit_end_date( valid_user_client, - session_workbasket, + user_workbasket, date_ranges, ): """Tests that an end date for a geographical membership can be edited.""" @@ -357,7 +360,7 @@ def test_geo_area_update_view_membership_edit_end_date( assert response.url == redirect_url workbasket = GeographicalMembership.objects.filter( - transaction__workbasket=session_workbasket, + transaction__workbasket=user_workbasket, ) for membership in workbasket: assert membership.valid_between.upper == expected_end_date @@ -366,7 +369,7 @@ def test_geo_area_update_view_membership_edit_end_date( def test_geo_area_update_view_membership_deletion( valid_user_client, - session_workbasket, + user_workbasket, date_ranges, ): """Tests that a country or region can be deleted as a member of an area @@ -397,13 +400,13 @@ def test_geo_area_update_view_membership_deletion( assert response.url == redirect_url workbasket = GeographicalMembership.objects.filter( - transaction__workbasket=session_workbasket, + transaction__workbasket=user_workbasket, ) for membership in workbasket: assert membership.update_type == UpdateType.DELETE -def test_geo_area_create_view(valid_user_client, session_workbasket, date_ranges): +def test_geo_area_create_view(valid_user_client, user_workbasket, date_ranges): """Tests that a geographical area can be created.""" form_data = { "area_code": AreaCode.COUNTRY, @@ -424,7 +427,7 @@ def test_geo_area_create_view(valid_user_client, session_workbasket, date_ranges with override_current_transaction(Transaction.objects.last()): geo_area = GeographicalArea.objects.get( - transaction__workbasket=session_workbasket, + transaction__workbasket=user_workbasket, ) assert geo_area.update_type == UpdateType.CREATE assert geo_area.area_code == form_data["area_code"] @@ -434,17 +437,16 @@ def test_geo_area_create_view(valid_user_client, session_workbasket, date_ranges def test_geo_area_edit_create_view( - valid_user_client, - session_workbasket, - date_ranges, use_edit_view, + workbasket, + date_ranges, ): """Tests that geographical area CREATE instances can be edited.""" geo_area = factories.GeographicalAreaFactory.create( area_code=AreaCode.REGION, area_id="TR", valid_between=date_ranges.no_end, - transaction=session_workbasket.new_transaction(), + transaction=workbasket.new_transaction(), ) data_changes = {**date_post_data("end_date", date_ranges.normal.upper)} @@ -454,7 +456,7 @@ def test_geo_area_edit_create_view( def test_geographical_membership_create_view( valid_user_client, - session_workbasket, + user_workbasket, date_ranges, ): """Tests that multiple geographical memberships can be created.""" @@ -486,7 +488,7 @@ def test_geographical_membership_create_view( assert response.url == redirect_url memberships = GeographicalMembership.objects.filter( - transaction__workbasket=session_workbasket, + transaction__workbasket=user_workbasket, ) for i, membership in enumerate(memberships): assert membership.update_type == UpdateType.CREATE diff --git a/importer/forms.py b/importer/forms.py index 8704ce357..a148a9079 100644 --- a/importer/forms.py +++ b/importer/forms.py @@ -9,7 +9,7 @@ from defusedxml.common import DTDForbidden from django import forms from django.conf import settings -from django.contrib.auth.models import User +from django.contrib.auth import get_user_model from django.core.exceptions import ValidationError from django.core.files.base import ContentFile from django.core.files.uploadedfile import InMemoryUploadedFile @@ -27,6 +27,8 @@ from workbaskets.validators import WorkflowStatus from workbaskets.validators import tops_jira_number_validator +User = get_user_model() + class ImporterV2FormMixin: """Mixin for taric parser forms, providing common taric_file clean and diff --git a/importer/jinja2/eu-importer/notify-success.jinja b/importer/jinja2/eu-importer/notify-success.jinja index 3bba97dbd..81f2eddbb 100644 --- a/importer/jinja2/eu-importer/notify-success.jinja +++ b/importer/jinja2/eu-importer/notify-success.jinja @@ -12,8 +12,8 @@ "items": [ {"text": "Home", "href": url("home")}, {"text": "Edit an existing workbasket", "href": url("workbaskets:workbasket-ui-list")}, - {"text": "Workbasket " ~ request.session.workbasket.id ~ " - Review goods", - "href": url("workbaskets:workbasket-ui-review-goods", kwargs={"pk": request.session.workbasket.id})}, + {"text": "Workbasket " ~ request.user.current_workbasket.id ~ " - Review goods", + "href": url("workbaskets:workbasket-ui-review-goods", kwargs={"pk": request.user.current_workbasket.id})}, {"text": page_title} ] }) }} @@ -30,7 +30,7 @@ }) }} diff --git a/importer/management/commands/chunk_taric.py b/importer/management/commands/chunk_taric.py index 9d659c360..514f18485 100644 --- a/importer/management/commands/chunk_taric.py +++ b/importer/management/commands/chunk_taric.py @@ -1,6 +1,6 @@ from typing import List -from django.contrib.auth.models import User +from django.contrib.auth import get_user_model from django.core.management import BaseCommand from importer import models @@ -8,6 +8,8 @@ from importer.management.util import ImporterCommandMixin from importer.namespaces import TARIC_RECORD_GROUPS +User = get_user_model() + def setup_batch( batch_name: str, diff --git a/importer/management/commands/import_taric.py b/importer/management/commands/import_taric.py index 7c1331ed7..eea00b069 100644 --- a/importer/management/commands/import_taric.py +++ b/importer/management/commands/import_taric.py @@ -1,7 +1,7 @@ from typing import List from typing import Sequence -from django.contrib.auth.models import User +from django.contrib.auth import get_user_model from django.core.management import BaseCommand from importer.management.commands.chunk_taric import chunk_taric @@ -12,6 +12,8 @@ from workbaskets.models import TRANSACTION_PARTITION_SCHEMES from workbaskets.validators import WorkflowStatus +User = get_user_model() + def import_taric( taric3_file: str, diff --git a/importer/management/util.py b/importer/management/util.py index 7be79682c..17a26334e 100644 --- a/importer/management/util.py +++ b/importer/management/util.py @@ -1,7 +1,9 @@ -from django.contrib.auth.models import User +from django.contrib.auth import get_user_model from django.core.exceptions import MultipleObjectsReturned from django.core.exceptions import ObjectDoesNotExist +User = get_user_model() + class ImporterCommandMixin: def get_user(self, username): diff --git a/importer/tests/test_views.py b/importer/tests/test_views.py index b6cf21a5c..d3c35376c 100644 --- a/importer/tests/test_views.py +++ b/importer/tests/test_views.py @@ -4,7 +4,7 @@ import pytest from bs4 import BeautifulSoup -from django.contrib.auth.models import User +from django.contrib.auth import get_user_model from django.core.files.uploadedfile import SimpleUploadedFile from django.test import Client from django.urls import reverse @@ -18,6 +18,8 @@ pytestmark = pytest.mark.django_db +User = get_user_model() + @pytest.mark.parametrize("url_name", ["import_batch-ui-list", "import_batch-ui-create"]) def test_import_urls_requires_superuser( diff --git a/measures/tests/conftest.py b/measures/tests/conftest.py index 0db3ba8fa..107809d7c 100644 --- a/measures/tests/conftest.py +++ b/measures/tests/conftest.py @@ -274,12 +274,12 @@ def measure_edit_conditions_and_negative_action_data(measure_edit_conditions_dat @pytest.fixture -def measure_form(measure_form_data, session_with_workbasket, erga_omnes): +def measure_form(measure_form_data, session_request_with_workbasket, erga_omnes): with override_current_transaction(Transaction.objects.last()): return MeasureForm( data=measure_form_data, instance=Measure.objects.first(), - request=session_with_workbasket, + request=session_request_with_workbasket, initial={}, ) diff --git a/measures/tests/test_filters.py b/measures/tests/test_filters.py index dec411046..f7a3546fc 100644 --- a/measures/tests/test_filters.py +++ b/measures/tests/test_filters.py @@ -7,17 +7,16 @@ from common.validators import UpdateType from measures.filters import MeasureFilter from measures.models import Measure -from workbaskets.models import WorkBasket pytestmark = pytest.mark.django_db def test_filter_by_current_workbasket( valid_user_client, - session_workbasket: WorkBasket, + user_workbasket, session_request, ): - with session_workbasket.new_transaction() as transaction: + with user_workbasket.new_transaction() as transaction: measure_in_workbasket_1 = factories.MeasureFactory.create( transaction=transaction, ) @@ -27,9 +26,6 @@ def test_filter_by_current_workbasket( factories.MeasureFactory.create() factories.MeasureFactory.create() - session = valid_user_client.session - session["workbasket"] = {"id": session_workbasket.pk} - session.save() self = MeasureFilter( data={"measure_filters_modifier": "current"}, request=session_request, @@ -41,19 +37,15 @@ def test_filter_by_current_workbasket( name="measure_filters_modifier", value="current", ) - assert len(result) == len(session_workbasket.measures) - assert set(session_workbasket.measures) == set(result) + assert len(result) == len(user_workbasket.measures) + assert set(user_workbasket.measures) == set(result) def test_filter_by_certificates( valid_user_client, - session_workbasket: WorkBasket, + user_workbasket, session_request, ): - session = valid_user_client.session - session["workbasket"] = {"id": session_workbasket.pk} - session.save() - old_date_range = TaricDateRange(date(2021, 1, 1), date(2023, 1, 1)) new_date_range = TaricDateRange(date(2023, 1, 1)) diff --git a/measures/tests/test_forms.py b/measures/tests/test_forms.py index 29d26484b..3f3da2fbf 100644 --- a/measures/tests/test_forms.py +++ b/measures/tests/test_forms.py @@ -85,7 +85,7 @@ def test_measure_conditions_formset_invalid( def test_measure_form_invalid_conditions_data( measure_form_data, - session_with_workbasket, + session_request_with_workbasket, date_ranges, erga_omnes, duty_sentence_parser, @@ -109,7 +109,7 @@ def test_measure_form_invalid_conditions_data( data=form_data, initial=form_data, instance=measure, - request=session_with_workbasket, + request=session_request_with_workbasket, ) assert not measure_form.is_valid() @@ -1192,7 +1192,7 @@ def test_measure_forms_conditions_wizard_clears_unneeded_certificate(date_ranges assert form_expects_no_certificate.cleaned_data["required_certificate"] is None -def test_measure_form_valid_data(erga_omnes, session_with_workbasket): +def test_measure_form_valid_data(erga_omnes, session_request_with_workbasket): """Test that MeasureForm.is_valid returns True when passed required fields and geographical_area and sid fields in cleaned data.""" measure = factories.MeasureFactory.create() @@ -1212,7 +1212,7 @@ def test_measure_form_valid_data(erga_omnes, session_with_workbasket): data=data, initial={}, instance=Measure.objects.first(), - request=session_with_workbasket, + request=session_request_with_workbasket, ) assert form.is_valid() assert ( @@ -1226,7 +1226,7 @@ def test_measure_form_valid_data(erga_omnes, session_with_workbasket): def test_measure_form_initial_data_geo_area( initial_option, erga_omnes, - session_with_workbasket, + session_request_with_workbasket, ): group = factories.GeographicalAreaFactory.create(area_code=AreaCode.GROUP) country = factories.GeographicalAreaFactory.create() @@ -1250,14 +1250,14 @@ def test_measure_form_initial_data_geo_area( data=data, initial={}, instance=Measure.objects.first(), - request=session_with_workbasket, + request=session_request_with_workbasket, ) assert form.initial["geo_area"] == geo_area_to_choice[measure.geographical_area] def test_measure_form_cleaned_data_geo_exclusions_group( erga_omnes, - session_with_workbasket, + session_request_with_workbasket, ): """Test that MeasureForm accepts geo_area form group data and returns excluded countries in cleaned data.""" @@ -1285,7 +1285,7 @@ def test_measure_form_cleaned_data_geo_exclusions_group( data=data, initial=data, instance=Measure.objects.first(), - request=session_with_workbasket, + request=session_request_with_workbasket, ) assert form.is_valid() assert form.cleaned_data["exclusions"] == [excluded_country1, excluded_country2] @@ -1293,7 +1293,7 @@ def test_measure_form_cleaned_data_geo_exclusions_group( def test_measure_form_cleaned_data_geo_exclusions_erga_omnes( erga_omnes, - session_with_workbasket, + session_request_with_workbasket, ): """Test that MeasureForm accepts geo_area form erga omnes data and returns excluded countries in cleaned data.""" @@ -1320,7 +1320,7 @@ def test_measure_form_cleaned_data_geo_exclusions_erga_omnes( data=data, initial=data, instance=Measure.objects.first(), - request=session_with_workbasket, + request=session_request_with_workbasket, ) assert form.is_valid() assert form.cleaned_data["exclusions"] == [excluded_country1, excluded_country2] diff --git a/measures/tests/test_migrations.py b/measures/tests/test_migrations.py index d209617fb..fb9e7346c 100644 --- a/measures/tests/test_migrations.py +++ b/measures/tests/test_migrations.py @@ -4,17 +4,17 @@ import pytest from psycopg2._range import DateTimeTZRange -from common.tests.factories import DutyExpressionFactory -from common.tests.factories import GeographicalAreaFactory -from common.tests.factories import GoodsNomenclatureFactory -from common.tests.factories import MeasureTypeFactory -from common.tests.factories import QueuedWorkBasketFactory -from common.tests.factories import RegulationFactory -from common.tests.factories import RegulationGroupFactory +from common.validators import ApplicabilityCode +from common.validators import UpdateType +from measures.validators import ImportExportCode +from measures.validators import MeasureExplosionLevel +from measures.validators import MeasureTypeCombination +from measures.validators import OrderNumberCaptureCode +from workbaskets.validators import WorkflowStatus @pytest.mark.django_db() -def test_add_back_deleted_measures(migrator, setup_content_types): +def test_add_back_deleted_measures(migrator, date_ranges): from common.models.transactions import TransactionPartition """Ensures that the initial migration works.""" @@ -25,60 +25,190 @@ def test_add_back_deleted_measures(migrator, setup_content_types): ), ) - setup_content_types(old_state.apps) - # setup - target_workbasket_id = 545 - - measurement_class = old_state.apps.get_model("measures", "Measure") - - assert measurement_class.objects.filter(sid=20194965).exists() is False - assert measurement_class.objects.filter(sid=20194966).exists() is False - assert measurement_class.objects.filter(sid=20194967).exists() is False + DutyExpression = old_state.apps.get_model("measures", "DutyExpression") + ContentType = old_state.apps.get_model("contenttypes", "ContentType") + GeographicalArea = old_state.apps.get_model("geo_areas", "GeographicalArea") + GoodsNomenclature = old_state.apps.get_model("commodities", "GoodsNomenclature") + Measure = old_state.apps.get_model("measures", "Measure") + MeasureType = old_state.apps.get_model("measures", "MeasureType") + MeasureTypeSeries = old_state.apps.get_model("measures", "MeasureTypeSeries") + Regulation = old_state.apps.get_model("regulations", "Regulation") + Group = old_state.apps.get_model("regulations", "Group") + Transaction = old_state.apps.get_model("common", "Transaction") + User = old_state.apps.get_model("common", "User") + WorkBasket = old_state.apps.get_model("workbaskets", "WorkBasket") + VersionGroup = old_state.apps.get_model("common", "VersionGroup") + + goods_content_type = ContentType.objects.get(model="goodsnomenclature") + geo_area_content_type = ContentType.objects.get(model="geographicalarea") + regulation_group_content_type = ContentType.objects.get(model="group") + regulation_content_type = ContentType.objects.get(model="regulation") + measure_series_content_type = ContentType.objects.get(model="measuretypeseries") + measure_type_content_type = ContentType.objects.get(model="measuretype") + duty_expression_content_type = ContentType.objects.get(model="dutyexpression") + + assert not Measure.objects.filter(sid=20194965).exists() + assert not Measure.objects.filter(sid=20194966).exists() + assert not Measure.objects.filter(sid=20194967).exists() # mock up workbasket - new_work_basket = QueuedWorkBasketFactory.create(id=target_workbasket_id).save() + user = User.objects.create() + target_workbasket_id = 545 + workbasket = WorkBasket.objects.create( + id=target_workbasket_id, + author=user, + approver=user, + status=WorkflowStatus.QUEUED, + ) + transaction = Transaction.objects.create( + workbasket=workbasket, + order=1, + partition=TransactionPartition.REVISION, + composite_key=str(workbasket.id) + + "-" + + "1" + + "-" + + str(TransactionPartition.REVISION), + ) # create the three goods - goods_1 = GoodsNomenclatureFactory.create(item_id="0306920000").save( - force_write=True, + goods_1 = GoodsNomenclature.objects.create( + item_id="0306920000", + update_type=UpdateType.CREATE, + transaction=transaction, + version_group=VersionGroup.objects.create(), + valid_between=date_ranges.no_end, + statistical=False, + polymorphic_ctype=goods_content_type, ) - goods_2 = GoodsNomenclatureFactory.create(item_id="0307190000").save( - force_write=True, + version_group = goods_1.version_group + version_group.current_version_id = goods_1.id + version_group.save() + + goods_2 = GoodsNomenclature.objects.create( + item_id="0307190000", + update_type=UpdateType.CREATE, + transaction=transaction, + version_group=VersionGroup.objects.create(), + valid_between=date_ranges.no_end, + statistical=False, + polymorphic_ctype=goods_content_type, ) - goods_3 = GoodsNomenclatureFactory.create(item_id="0307490000").save( - force_write=True, + version_group = goods_2.version_group + version_group.current_version_id = goods_2.id + version_group.save() + + goods_3 = GoodsNomenclature.objects.create( + item_id="0307490000", + update_type=UpdateType.CREATE, + transaction=transaction, + version_group=VersionGroup.objects.create(), + valid_between=date_ranges.no_end, + statistical=False, + polymorphic_ctype=goods_content_type, ) + version_group = goods_3.version_group + version_group.current_version_id = goods_3.id + version_group.save() # create the geo area - new_geographical_area = GeographicalAreaFactory.create(sid=146).save( - force_write=True, + new_geographical_area = GeographicalArea.objects.create( + sid=146, + area_id="CA", + area_code=0, + update_type=UpdateType.CREATE, + transaction=transaction, + version_group=VersionGroup.objects.create(), + valid_between=date_ranges.no_end, + polymorphic_ctype=geo_area_content_type, ) + version_group = new_geographical_area.version_group + version_group.current_version_id = new_geographical_area.id + version_group.save() # create the regulation group - new_regulation_group = RegulationGroupFactory.create( + new_regulation_group = Group.objects.create( + update_type=UpdateType.CREATE, + transaction=transaction, + version_group=VersionGroup.objects.create(), valid_between=DateTimeTZRange( date.today() + timedelta(days=-1000), date.today() + timedelta(days=1000), ), - ).save(force_write=True) + polymorphic_ctype=regulation_group_content_type, + ) + version_group = new_regulation_group.version_group + version_group.current_version_id = new_regulation_group.id + version_group.save() # create the regulation - new_regulation = RegulationFactory.create( + new_regulation = Regulation.objects.create( + regulation_group=new_regulation_group, + regulation_id="C2100006", + approved=True, valid_between=DateTimeTZRange( date.today() + timedelta(days=-1000), date.today() + timedelta(days=1000), ), - regulation_group=new_regulation_group, - regulation_id="C2100006", - approved=True, - ).save(force_write=True) + update_type=UpdateType.CREATE, + transaction=transaction, + version_group=VersionGroup.objects.create(), + polymorphic_ctype=regulation_content_type, + ) + version_group = new_regulation.version_group + version_group.current_version_id = new_regulation.id + version_group.save() # create the measure type - new_measure_type = MeasureTypeFactory.create(sid=142).save(force_write=True) + new_measure_type_series = MeasureTypeSeries.objects.create( + id=157, + sid="C", + measure_type_combination=MeasureTypeCombination.SINGLE_MEASURE, + valid_between=date_ranges.no_end, + update_type=UpdateType.CREATE, + transaction=transaction, + version_group=VersionGroup.objects.create(), + polymorphic_ctype=measure_series_content_type, + ) + version_group = new_measure_type_series.version_group + version_group.current_version_id = new_measure_type_series.id + version_group.save() + + new_measure_type = MeasureType.objects.create( + sid=142, + trade_movement_code=ImportExportCode.IMPORT, + priority_code=1, + measure_component_applicability_code=ApplicabilityCode.MANDATORY, + origin_destination_code=ImportExportCode.IMPORT, + order_number_capture_code=OrderNumberCaptureCode.NOT_PERMITTED, + measure_explosion_level=MeasureExplosionLevel.HARMONISED_SYSTEM_CHAPTER, + measure_type_series=new_measure_type_series, + valid_between=date_ranges.no_end, + update_type=UpdateType.CREATE, + transaction=transaction, + version_group=VersionGroup.objects.create(), + polymorphic_ctype=measure_type_content_type, + ) + version_group = new_measure_type.version_group + version_group.current_version_id = new_measure_type.id + version_group.save() # create the duty expression - new_duty_expression = DutyExpressionFactory.create(sid=1).save(force_write=True) + new_duty_expression = DutyExpression.objects.create( + sid=1, + duty_amount_applicability_code=ApplicabilityCode.PERMITTED, + measurement_unit_applicability_code=ApplicabilityCode.PERMITTED, + monetary_unit_applicability_code=ApplicabilityCode.PERMITTED, + valid_between=date_ranges.no_end, + update_type=UpdateType.CREATE, + transaction=transaction, + version_group=VersionGroup.objects.create(), + polymorphic_ctype=duty_expression_content_type, + ) + version_group = new_duty_expression.version_group + version_group.current_version_id = new_duty_expression.id + version_group.save() # at this point all the appropriate elements are available within the database for the migration to create the # measures and conditions @@ -88,17 +218,15 @@ def test_add_back_deleted_measures(migrator, setup_content_types): ("measures", "0012_add_back_three_missing_measures_already_published"), ) - measurement_class = new_state.apps.get_model("measures", "Measure") + Measure = new_state.apps.get_model("measures", "Measure") measures_ids_to_check = [20194965, 20194966, 20194967] for measure_id_to_check in measures_ids_to_check: - # we should be able to get the measurements from the database now - assert ( - measurement_class.objects.filter(sid=measure_id_to_check).exists() is True - ) - assert measurement_class.objects.filter(sid=measure_id_to_check).count() == 1 - measure_to_check = measurement_class.objects.get(sid=measure_id_to_check) + # we should be able to get the measures from the database now + assert Measure.objects.filter(sid=measure_id_to_check).exists() is True + assert Measure.objects.filter(sid=measure_id_to_check).count() == 1 + measure_to_check = Measure.objects.get(sid=measure_id_to_check) # verify the transactions are on the correct partition assert measure_to_check.transaction.partition == TransactionPartition.REVISION # verify that the current version is as expected @@ -107,15 +235,12 @@ def test_add_back_deleted_measures(migrator, setup_content_types): == measure_to_check.trackedmodel_ptr_id ) - # verify that the current version is correct - migrator.reset() @pytest.mark.django_db() def test_add_back_deleted_measures_fails_silently_if_data_not_present( migrator, - setup_content_types, ): """Ensures that the initial migration works when no data to create measures are present, for local dev etc.""" @@ -127,8 +252,6 @@ def test_add_back_deleted_measures_fails_silently_if_data_not_present( ), ) - setup_content_types(old_state.apps) - measurement_class = old_state.apps.get_model("measures", "Measure") assert measurement_class.objects.filter(sid=20194965).exists() is False diff --git a/measures/tests/test_views.py b/measures/tests/test_views.py index a3f4ce8d5..619fb8798 100644 --- a/measures/tests/test_views.py +++ b/measures/tests/test_views.py @@ -135,7 +135,7 @@ def test_measure_delete(use_delete_form): use_delete_form(factories.MeasureFactory()) -def test_multiple_measure_delete_functionality(client, valid_user, session_workbasket): +def test_multiple_measure_delete_functionality(client, valid_user, user_workbasket): """Tests that MeasureMultipleDelete view's Post function takes a list of measures, and sets their update type to delete, clearing the session once completed.""" @@ -148,11 +148,6 @@ def test_multiple_measure_delete_functionality(client, valid_user, session_workb session = client.session session.update( { - "workbasket": { - "id": session_workbasket.pk, - "status": session_workbasket.status, - "title": session_workbasket.title, - }, "MULTIPLE_MEASURE_SELECTIONS": { measure_1.pk: True, measure_2.pk: True, @@ -165,7 +160,7 @@ def test_multiple_measure_delete_functionality(client, valid_user, session_workb response = client.post(url, data=post_data) workbasket_measures = Measure.objects.filter( - trackedmodel_ptr__transaction__workbasket_id=session_workbasket.id, + trackedmodel_ptr__transaction__workbasket_id=user_workbasket.id, ).order_by("sid") # on success, the page redirects to the list page @@ -176,7 +171,7 @@ def test_multiple_measure_delete_functionality(client, valid_user, session_workb assert measure.update_type == UpdateType.DELETE -def test_multiple_measure_delete_template(client, valid_user, session_workbasket): +def test_multiple_measure_delete_template(client, valid_user, user_workbasket): """Test that valid user receives a 200 on GET for MultipleMeasureDelete and correct measures display in html table.""" # Make a bunch of measures @@ -192,11 +187,6 @@ def test_multiple_measure_delete_template(client, valid_user, session_workbasket # Add a workbasket to the session, and add some selected measures to it. session.update( { - "workbasket": { - "id": session_workbasket.pk, - "status": session_workbasket.status, - "title": session_workbasket.title, - }, "MULTIPLE_MEASURE_SELECTIONS": { measure_1.pk: True, measure_2.pk: True, @@ -256,7 +246,7 @@ def test_measure_detail_views( view, url_pattern, valid_user_client, - session_with_workbasket, + session_request_with_workbasket, ): """Verify that measure detail views are under the url measures/ and don't return an error.""" @@ -514,8 +504,7 @@ def test_duties_validator( ) def test_measure_update_duty_sentence( update_data, - client, - valid_user, + client_with_current_workbasket, measure_form, duty_sentence_parser, ): @@ -533,8 +522,7 @@ def test_measure_update_duty_sentence( post_data.update(update_data) post_data["update_type"] = UpdateType.UPDATE url = reverse("measure-ui-edit", args=(measure_form.instance.sid,)) - client.force_login(valid_user) - response = client.post(url, data=post_data) + response = client_with_current_workbasket.post(url, data=post_data) assert response.status_code == 302 @@ -546,7 +534,6 @@ def test_measure_update_duty_sentence( components = measure.components.approved_up_to_transaction(tx).filter( component_measure__sid=measure_form.instance.sid, ) - assert components.exists() assert components.count() == 1 assert components.first().duty_amount == 10.000 @@ -557,8 +544,7 @@ def test_measure_update_duty_sentence( @patch("measures.forms.MeasureForm.save") def test_measure_form_save_called_on_measure_update( save, - client, - valid_user, + client_with_current_workbasket, measure_form, ): """Until work is done to make `TrackedModel` call new_version in save() we @@ -570,21 +556,20 @@ def test_measure_form_save_called_on_measure_update( post_data = {k: v for k, v in post_data.items() if v is not None} post_data["update_type"] = UpdateType.UPDATE url = reverse("measure-ui-edit", args=(measure_form.instance.sid,)) - client.force_login(valid_user) - client.post(url, data=post_data) + client_with_current_workbasket.post(url, data=post_data) save.assert_called_with(commit=False) -def test_measure_update_get_footnotes(session_with_workbasket): +def test_measure_update_get_footnotes(session_request_with_workbasket): association = factories.FootnoteAssociationMeasureFactory.create() - view = MeasureUpdate(request=session_with_workbasket) + view = MeasureUpdate(request=session_request_with_workbasket) footnotes = view.get_footnotes(association.footnoted_measure) assert len(footnotes) == 1 association.new_version( - WorkBasket.current(session_with_workbasket), + WorkBasket.current(session_request_with_workbasket), update_type=UpdateType.DELETE, ) @@ -595,7 +580,7 @@ def test_measure_update_get_footnotes(session_with_workbasket): def test_measure_update_form_creates_footnote_association( measure_form, - valid_user_client, + client_with_current_workbasket, ): """Test that editing a measure to add a new footnote doesn't require pressing "Add another footnote" button before submitting (saving) the @@ -609,7 +594,7 @@ def test_measure_update_form_creates_footnote_association( form_data["form-0-footnote"] = footnote.pk url = reverse("measure-ui-edit", kwargs={"sid": measure.sid}) - response = valid_user_client.post(url, form_data) + response = client_with_current_workbasket.post(url, form_data) assert response.status_code == 302 assert FootnoteAssociationMeasure.objects.filter( @@ -619,7 +604,10 @@ def test_measure_update_form_creates_footnote_association( # https://uktrade.atlassian.net/browse/TP2000-340 -def test_measure_update_updates_footnote_association(measure_form, client, valid_user): +def test_measure_update_updates_footnote_association( + measure_form, + client_with_current_workbasket, +): """Tests that when updating a measure with an existing footnote the MeasureFootnoteAssociation linking the measure and footnote is updated to point at the new, updated version of the measure.""" @@ -630,8 +618,7 @@ def test_measure_update_updates_footnote_association(measure_form, client, valid footnoted_measure=measure_form.instance, ) url = reverse("measure-ui-edit", args=(measure_form.instance.sid,)) - client.force_login(valid_user) - client.post(url, data=post_data) + client_with_current_workbasket.post(url, data=post_data) new_assoc = FootnoteAssociationMeasure.objects.last() ME70(new_assoc.transaction).validate(new_assoc) @@ -639,7 +626,10 @@ def test_measure_update_updates_footnote_association(measure_form, client, valid assert new_assoc.version_group == assoc.version_group -def test_measure_update_removes_footnote_association(valid_user_client, measure_form): +def test_measure_update_removes_footnote_association( + client_with_current_workbasket, + measure_form, +): """Test that when editing a measure to remove a footnote, the MeasureFootnoteAssociation, linking the measure and footnote, is updated to reflect this deletion.""" @@ -658,15 +648,15 @@ def test_measure_update_removes_footnote_association(valid_user_client, measure_ # Form stores data of footnotes on a measure in the session url = reverse("measure-ui-edit", kwargs={"sid": measure.sid}) - response = valid_user_client.get(url) + response = client_with_current_workbasket.get(url) assert response.status_code == 200 # Remove footnote2 from session to indicate its removal on form - session = valid_user_client.session + session = client_with_current_workbasket.session session[f"instance_footnotes_{measure.sid}"].remove(footnote2.pk) session.save() - response = valid_user_client.post(url, data=form_data) + response = client_with_current_workbasket.post(url, data=form_data) assert response.status_code == 302 with override_current_transaction(Transaction.objects.last()): @@ -680,7 +670,7 @@ def test_measure_update_removes_footnote_association(valid_user_client, measure_ def test_measure_update_create_conditions( - valid_user_client, + client_with_current_workbasket, measure_edit_conditions_data, duty_sentence_parser, erga_omnes, @@ -695,7 +685,10 @@ def test_measure_update_create_conditions( """ measure = Measure.objects.first() url = reverse("measure-ui-edit", args=(measure.sid,)) - response = valid_user_client.post(url, data=measure_edit_conditions_data) + response = client_with_current_workbasket.post( + url, + data=measure_edit_conditions_data, + ) assert response.status_code == 302 assert response.url == reverse("measure-ui-confirm-update", args=(measure.sid,)) @@ -737,8 +730,7 @@ def test_measure_update_create_conditions( def test_measure_update_edit_conditions( - client, - valid_user, + client_with_current_workbasket, measure_edit_conditions_data, duty_sentence_parser, erga_omnes, @@ -753,8 +745,7 @@ def test_measure_update_edit_conditions( """ measure = Measure.objects.first() url = reverse("measure-ui-edit", args=(measure.sid,)) - client.force_login(valid_user) - client.post(url, data=measure_edit_conditions_data) + client_with_current_workbasket.post(url, data=measure_edit_conditions_data) transaction_count = Transaction.objects.count() tx = Transaction.objects.last() measure_with_condition = Measure.objects.approved_up_to_transaction(tx).get( @@ -770,7 +761,7 @@ def test_measure_update_edit_conditions( measure_edit_conditions_data[ f"{MEASURE_CONDITIONS_FORMSET_PREFIX}-0-applicable_duty" ] = "10 GBP / 100 kg" - client.post(url, data=measure_edit_conditions_data) + client_with_current_workbasket.post(url, data=measure_edit_conditions_data) tx = Transaction.objects.last() updated_measure = Measure.objects.approved_up_to_transaction(tx).get( sid=measure.sid, @@ -831,8 +822,7 @@ def test_measure_update_edit_conditions( def test_measure_update_remove_conditions( - client, - valid_user, + client_with_current_workbasket, measure_edit_conditions_data, duty_sentence_parser, erga_omnes, @@ -847,11 +837,13 @@ def test_measure_update_remove_conditions( """ measure = Measure.objects.first() url = reverse("measure-ui-edit", args=(measure.sid,)) - client.force_login(valid_user) - client.post(url, data=measure_edit_conditions_data) + client_with_current_workbasket.post(url, data=measure_edit_conditions_data) measure_edit_conditions_data[f"{MEASURE_CONDITIONS_FORMSET_PREFIX}-0-DELETE"] = 1 - response = client.post(url, data=measure_edit_conditions_data) + response = client_with_current_workbasket.post( + url, + data=measure_edit_conditions_data, + ) assert response.status_code == 200 @@ -871,7 +863,10 @@ def test_measure_update_remove_conditions( ] = "" del measure_edit_conditions_data[f"{MEASURE_CONDITIONS_FORMSET_PREFIX}-0-DELETE"] transaction_count = Transaction.objects.count() - response = client.post(url, data=measure_edit_conditions_data) + response = client_with_current_workbasket.post( + url, + data=measure_edit_conditions_data, + ) assert response.status_code == 302 # We expect one transaction for the measure update and condition deletion @@ -886,8 +881,7 @@ def test_measure_update_remove_conditions( def test_measure_update_negative_condition( - client, - valid_user, + client_with_current_workbasket, measure_edit_conditions_and_negative_action_data, duty_sentence_parser, erga_omnes, @@ -902,8 +896,10 @@ def test_measure_update_negative_condition( measure = Measure.objects.first() url = reverse("measure-ui-edit", args=(measure.sid,)) - client.force_login(valid_user) - response = client.post(url, data=measure_edit_conditions_and_negative_action_data) + response = client_with_current_workbasket.post( + url, + data=measure_edit_conditions_and_negative_action_data, + ) assert response.status_code == 302 @@ -925,8 +921,7 @@ def test_measure_update_negative_condition( def test_measure_update_invalid_conditions( - client, - valid_user, + client_with_current_workbasket, measure_edit_conditions_and_negative_action_data, duty_sentence_parser, erga_omnes, @@ -946,8 +941,10 @@ def test_measure_update_invalid_conditions( measure = Measure.objects.first() url = reverse("measure-ui-edit", args=(measure.sid,)) - client.force_login(valid_user) - response = client.post(url, data=measure_edit_conditions_and_negative_action_data) + response = client_with_current_workbasket.post( + url, + data=measure_edit_conditions_and_negative_action_data, + ) assert response.status_code == 200 @@ -979,8 +976,7 @@ def test_measure_update_invalid_conditions( def test_measure_update_invalid_conditions_invalid_actions( - client, - valid_user, + client_with_current_workbasket, measure_edit_conditions_and_negative_action_data, duty_sentence_parser, erga_omnes, @@ -1010,8 +1006,10 @@ def test_measure_update_invalid_conditions_invalid_actions( measure = Measure.objects.first() url = reverse("measure-ui-edit", args=(measure.sid,)) - client.force_login(valid_user) - response = client.post(url, data=measure_edit_conditions_and_negative_action_data) + response = client_with_current_workbasket.post( + url, + data=measure_edit_conditions_and_negative_action_data, + ) assert response.status_code == 200 @@ -1027,7 +1025,7 @@ def test_measure_update_invalid_conditions_invalid_actions( ) -def test_measure_update_group_exclusion(client, valid_user, erga_omnes): +def test_measure_update_group_exclusion(client_with_current_workbasket, erga_omnes): """ Tests that measure edit view handles exclusion of one group from another group. @@ -1044,7 +1042,6 @@ def test_measure_update_group_exclusion(client, valid_user, erga_omnes): factories.GeographicalMembershipFactory.create(geo_group=erga_omnes, member=area_1) factories.GeographicalMembershipFactory.create(geo_group=erga_omnes, member=area_2) url = reverse("measure-ui-edit", args=(measure.sid,)) - client.force_login(valid_user) data = model_to_dict(measure) data = {k: v for k, v in data.items() if v is not None} start_date = data["valid_between"].lower @@ -1063,7 +1060,7 @@ def test_measure_update_group_exclusion(client, valid_user, erga_omnes): Transaction.objects.last(), ).exists() - client.post(url, data=data) + client_with_current_workbasket.post(url, data=data) measure_area_exclusions = ( MeasureExcludedGeographicalArea.objects.approved_up_to_transaction( Transaction.objects.last(), @@ -1083,16 +1080,14 @@ def test_measure_update_group_exclusion(client, valid_user, erga_omnes): assert area_2.sid in area_sids -def test_measure_edit_update_view(valid_user_client, erga_omnes): +def test_measure_edit_update_view(client_with_current_workbasket, erga_omnes): """Test that a measure UPDATE instance can be edited.""" measure = factories.MeasureFactory.create( update_type=UpdateType.UPDATE, - transaction=factories.UnapprovedTransactionFactory(), ) geo_area = factories.GeoGroupFactory.create() - url = reverse("measure-ui-edit-update", kwargs={"sid": measure.sid}) - response = valid_user_client.get(url) + response = client_with_current_workbasket.get(url) assert response.status_code == 200 data = model_to_dict(measure) @@ -1108,7 +1103,7 @@ def test_measure_edit_update_view(valid_user_client, erga_omnes): "submit": "submit", }, ) - response = valid_user_client.post(url, data=data) + response = client_with_current_workbasket.post(url, data=data) assert response.status_code == 302 with override_current_transaction(Transaction.objects.last()): @@ -1117,16 +1112,19 @@ def test_measure_edit_update_view(valid_user_client, erga_omnes): assert updated_measure.geographical_area == geo_area -def test_measure_edit_create_view(valid_user_client, duty_sentence_parser, erga_omnes): +def test_measure_edit_create_view( + client_with_current_workbasket, + duty_sentence_parser, + erga_omnes, +): """Test that a measure CREATE instance can be edited.""" measure = factories.MeasureFactory.create( update_type=UpdateType.CREATE, - transaction=factories.UnapprovedTransactionFactory(), ) geo_area = factories.CountryFactory.create() url = reverse("measure-ui-edit-create", kwargs={"sid": measure.sid}) - response = valid_user_client.get(url) + response = client_with_current_workbasket.get(url) assert response.status_code == 200 data = model_to_dict(measure) @@ -1144,7 +1142,7 @@ def test_measure_edit_create_view(valid_user_client, duty_sentence_parser, erga_ "submit": "submit", }, ) - response = valid_user_client.post(url, data=data) + response = client_with_current_workbasket.post(url, data=data) assert response.status_code == 302 with override_current_transaction(Transaction.objects.last()): @@ -1154,16 +1152,16 @@ def test_measure_edit_create_view(valid_user_client, duty_sentence_parser, erga_ @pytest.mark.django_db -def test_measure_form_wizard_start(valid_user_client): +def test_measure_form_wizard_start(client_with_current_workbasket): url = reverse("measure-ui-create", kwargs={"step": "start"}) - response = valid_user_client.get(url) + response = client_with_current_workbasket.get(url) assert response.status_code == 200 @unittest.mock.patch("measures.parsers.DutySentenceParser") def test_measure_form_wizard_finish( mock_duty_sentence_parser, - valid_user_client, + client_with_current_workbasket, regulation, duty_sentence_parser, erga_omnes, @@ -1249,10 +1247,10 @@ def test_measure_form_wizard_finish( "measure-ui-create", kwargs={"step": step_data["data"]["measure_create_wizard-current_step"]}, ) - response = valid_user_client.get(url) + response = client_with_current_workbasket.get(url) assert response.status_code == 200 - response = valid_user_client.post(url, step_data["data"]) + response = client_with_current_workbasket.post(url, step_data["data"]) assert response.status_code == 302 assert response.url == reverse( @@ -1260,7 +1258,7 @@ def test_measure_form_wizard_finish( kwargs={"step": step_data["next_step"]}, ) - complete_response = valid_user_client.get(response.url) + complete_response = client_with_current_workbasket.get(response.url) assert complete_response.status_code == 200 @@ -1725,7 +1723,7 @@ def test_measure_create_wizard_get_cleaned_data_for_step(session_request, measur def test_measure_create_wizard_quota_origins_conditional_step( - valid_user_client, + client_with_current_workbasket, quota_order_number, ): """ @@ -1766,10 +1764,10 @@ def test_measure_create_wizard_quota_origins_conditional_step( "measure-ui-create", kwargs={"step": step_data["data"]["measure_create_wizard-current_step"]}, ) - response = valid_user_client.get(url) + response = client_with_current_workbasket.get(url) assert response.status_code == 200 - response = valid_user_client.post(url, step_data["data"]) + response = client_with_current_workbasket.post(url, step_data["data"]) assert response.status_code == 302 assert response.url == reverse( @@ -1780,9 +1778,7 @@ def test_measure_create_wizard_quota_origins_conditional_step( def test_measure_form_creates_exclusions( erga_omnes, - session_with_workbasket, - valid_user, - client, + client_with_current_workbasket, ): excluded_country1 = factories.GeographicalAreaFactory.create() excluded_country2 = factories.GeographicalAreaFactory.create() @@ -1807,9 +1803,8 @@ def test_measure_form_creates_exclusions( "submit": "submit", } data.update(exclusions_data) - client.force_login(valid_user) url = reverse("measure-ui-edit", args=(measure.sid,)) - response = client.post(url, data) + response = client_with_current_workbasket.post(url, data) assert response.status_code == 302 assert measure.exclusions.all().count() == 2 assert not set( @@ -1839,7 +1834,7 @@ def test_measuretype_api_list_view(valid_user_client): def test_multiple_measure_start_and_end_date_edit_functionality( valid_user_client, - session_workbasket, + user_workbasket, mocked_diff_components, ): """Tests that MeasureEditWizard takes a list of measures, and sets their @@ -1859,11 +1854,6 @@ def test_multiple_measure_start_and_end_date_edit_functionality( session = valid_user_client.session session.update( { - "workbasket": { - "id": session_workbasket.pk, - "status": session_workbasket.status, - "title": session_workbasket.title, - }, "MULTIPLE_MEASURE_SELECTIONS": { measure_1.pk: 1, measure_2.pk: 1, @@ -1922,7 +1912,7 @@ def test_multiple_measure_start_and_end_date_edit_functionality( ) workbasket_measures = Measure.objects.filter( - trackedmodel_ptr__transaction__workbasket_id=session_workbasket.id, + trackedmodel_ptr__transaction__workbasket_id=user_workbasket.id, ).order_by("sid") complete_response = valid_user_client.get(response.url) @@ -1963,7 +1953,7 @@ def test_multiple_measure_edit_single_form_functionality( step, data, valid_user_client, - session_workbasket, + user_workbasket, mocked_diff_components, ): """Tests that MeasureEditWizard takes a list of measures, and sets their @@ -1977,11 +1967,6 @@ def test_multiple_measure_edit_single_form_functionality( session = valid_user_client.session session.update( { - "workbasket": { - "id": session_workbasket.pk, - "status": session_workbasket.status, - "title": session_workbasket.title, - }, "MULTIPLE_MEASURE_SELECTIONS": { measure_1.pk: 1, measure_2.pk: 1, @@ -2023,7 +2008,7 @@ def test_multiple_measure_edit_single_form_functionality( ) workbasket_measures = Measure.objects.filter( - trackedmodel_ptr__transaction__workbasket_id=session_workbasket.id, + trackedmodel_ptr__transaction__workbasket_id=user_workbasket.id, ).order_by("sid") complete_response = valid_user_client.get(response.url) @@ -2036,7 +2021,7 @@ def test_multiple_measure_edit_single_form_functionality( def test_multiple_measure_edit_only_regulation( valid_user_client, - session_workbasket, + user_workbasket, mocked_diff_components, ): """Tests the regulation step in MeasureEditWizard.""" @@ -2049,11 +2034,6 @@ def test_multiple_measure_edit_only_regulation( session = valid_user_client.session session.update( { - "workbasket": { - "id": session_workbasket.pk, - "status": session_workbasket.status, - "title": session_workbasket.title, - }, "MULTIPLE_MEASURE_SELECTIONS": { measure_1.pk: 1, measure_2.pk: 1, @@ -2098,7 +2078,7 @@ def test_multiple_measure_edit_only_regulation( ) workbasket_measures = Measure.objects.filter( - trackedmodel_ptr__transaction__workbasket_id=session_workbasket.id, + trackedmodel_ptr__transaction__workbasket_id=user_workbasket.id, ).order_by("sid") complete_response = valid_user_client.get(response.url) @@ -2110,7 +2090,7 @@ def test_multiple_measure_edit_only_regulation( assert measure.generating_regulation == regulation -def test_multiple_measure_edit_template(valid_user_client, session_workbasket): +def test_multiple_measure_edit_template(valid_user_client, user_workbasket): """Test that valid user receives a 200 on GET for MeasureEditWizard and correct measures display in html table.""" # Make a bunch of measures @@ -2124,11 +2104,6 @@ def test_multiple_measure_edit_template(valid_user_client, session_workbasket): # Add a workbasket to the session, and add some selected measures to it. session.update( { - "workbasket": { - "id": session_workbasket.pk, - "status": session_workbasket.status, - "title": session_workbasket.title, - }, "MULTIPLE_MEASURE_SELECTIONS": { measure_1.pk: True, measure_2.pk: True, @@ -2179,7 +2154,7 @@ def test_multiple_measure_edit_template(valid_user_client, session_workbasket): def test_measure_selection_update_view_updates_session( client, valid_user, - session_workbasket, + user_workbasket, ): # Make a bunch of measures measure_1 = factories.MeasureFactory.create() @@ -2226,7 +2201,7 @@ def test_measure_selection_update_view_updates_session( "foo", ], ) -def test_measure_list_redirect(form_action, valid_user_client, session_workbasket): +def test_measure_list_redirect(form_action, valid_user_client, user_workbasket): params = "page=2&start_date_modifier=exact&end_date_modifier=exact" url = f"{reverse('measure-ui-list')}?{params}" response = valid_user_client.post(url, {"form-action": form_action}) @@ -2271,7 +2246,7 @@ def test_measure_list_selected_measures_list(valid_user_client): def test_multiple_measure_edit_only_quota_order_number( valid_user_client, - session_workbasket, + user_workbasket, mocked_diff_components, ): """Tests the regulation step in MeasureEditWizard.""" @@ -2284,11 +2259,6 @@ def test_multiple_measure_edit_only_quota_order_number( session = valid_user_client.session session.update( { - "workbasket": { - "id": session_workbasket.pk, - "status": session_workbasket.status, - "title": session_workbasket.title, - }, "MULTIPLE_MEASURE_SELECTIONS": { measure_1.pk: 1, measure_2.pk: 1, @@ -2333,7 +2303,7 @@ def test_multiple_measure_edit_only_quota_order_number( ) workbasket_measures = Measure.objects.filter( - trackedmodel_ptr__transaction__workbasket_id=session_workbasket.id, + trackedmodel_ptr__transaction__workbasket_id=user_workbasket.id, ).order_by("sid") complete_response = valid_user_client.get(response.url) @@ -2347,7 +2317,7 @@ def test_multiple_measure_edit_only_quota_order_number( def test_multiple_measure_edit_only_duties( valid_user_client, - session_workbasket, + user_workbasket, duty_sentence_parser, ): """Tests the duties step in MeasureEditWizard.""" @@ -2360,11 +2330,6 @@ def test_multiple_measure_edit_only_duties( session = valid_user_client.session session.update( { - "workbasket": { - "id": session_workbasket.pk, - "status": session_workbasket.status, - "title": session_workbasket.title, - }, "MULTIPLE_MEASURE_SELECTIONS": { measure_1.pk: 1, measure_2.pk: 1, @@ -2409,7 +2374,7 @@ def test_multiple_measure_edit_only_duties( ) workbasket_measures = Measure.objects.filter( - trackedmodel_ptr__transaction__workbasket_id=session_workbasket.id, + trackedmodel_ptr__transaction__workbasket_id=user_workbasket.id, ).order_by("sid") complete_response = valid_user_client.get(response.url) @@ -2423,7 +2388,7 @@ def test_multiple_measure_edit_only_duties( def test_multiple_measure_edit_preserves_footnote_associations( valid_user_client, - session_workbasket, + user_workbasket, mocked_diff_components, ): """Tests that footnote associations are preserved in MeasureEditWizard.""" @@ -2439,11 +2404,6 @@ def test_multiple_measure_edit_preserves_footnote_associations( session = valid_user_client.session session.update( { - "workbasket": { - "id": session_workbasket.pk, - "status": session_workbasket.status, - "title": session_workbasket.title, - }, "MULTIPLE_MEASURE_SELECTIONS": { measure.pk: 1, }, @@ -2488,7 +2448,7 @@ def test_multiple_measure_edit_preserves_footnote_associations( ) workbasket_measures = Measure.objects.filter( - transaction__workbasket=session_workbasket, + transaction__workbasket=user_workbasket, ).order_by("sid") complete_response = valid_user_client.get(response.url) @@ -2504,7 +2464,7 @@ def test_multiple_measure_edit_preserves_footnote_associations( def test_multiple_measure_edit_geographical_area_exclusions( valid_user_client, - session_workbasket, + user_workbasket, mocked_diff_components, ): """Tests that the geographical area exclusions of multiple measures can be @@ -2517,9 +2477,6 @@ def test_multiple_measure_edit_geographical_area_exclusions( session = valid_user_client.session session.update( { - "workbasket": { - "id": session_workbasket.pk, - }, "MULTIPLE_MEASURE_SELECTIONS": { measure_1.pk: 1, measure_2.pk: 1, @@ -2567,7 +2524,7 @@ def test_multiple_measure_edit_geographical_area_exclusions( assert valid_user_client.session["MULTIPLE_MEASURE_SELECTIONS"] == {} workbasket_measures = Measure.objects.filter( - transaction__workbasket=session_workbasket, + transaction__workbasket=user_workbasket, ) assert workbasket_measures diff --git a/measures/views.py b/measures/views.py index 7290b4513..638382164 100644 --- a/measures/views.py +++ b/measures/views.py @@ -937,7 +937,7 @@ def done(self, form_list, **kwargs): cleaned_data = self.get_all_cleaned_data() created_measures = self.create_measures(cleaned_data) - created_measures[0].transaction.workbasket.save_to_session(self.request.session) + created_measures[0].transaction.workbasket.assign_to_user(self.request.user) context = self.get_context_data( form=None, diff --git a/pii-secret-exclude.txt b/pii-secret-exclude.txt index fb246ad85..80d08ba2c 100644 --- a/pii-secret-exclude.txt +++ b/pii-secret-exclude.txt @@ -25,3 +25,4 @@ common/jinja2/common/500.jinja settings/envs/docker.env Dockerfile common/jinja2/common/accessibility.jinja +common/migrations/0001_initial.py diff --git a/publishing/tests/test_migrations.py b/publishing/tests/test_migrations.py index d601cb9a1..775c2e13f 100644 --- a/publishing/tests/test_migrations.py +++ b/publishing/tests/test_migrations.py @@ -12,7 +12,7 @@ def test_add_packaged_workbasket_to_loading_report(migrator): ("publishing", "0007_crowndependenciespublishingtask_error"), ) - User = old_state.apps.get_model("auth", "User") + User = old_state.apps.get_model("common", "User") WorkBasket = old_state.apps.get_model("workbaskets", "WorkBasket") PackagedWorkBasket = old_state.apps.get_model("publishing", "PackagedWorkBasket") LoadingReport = old_state.apps.get_model("publishing", "LoadingReport") diff --git a/publishing/tests/test_views.py b/publishing/tests/test_views.py index f79cd2903..bc0dfc646 100644 --- a/publishing/tests/test_views.py +++ b/publishing/tests/test_views.py @@ -54,18 +54,10 @@ def test_packaged_workbasket_create_without_permission(client): def test_packaged_workbasket_create_form_no_rule_check( valid_user_client, - session_workbasket, + user_workbasket, ): """Tests that Packaged WorkBasket Create returns 302 and redirects work basket summary when no rule check has been executed.""" - session = valid_user_client.session - session["workbasket"] = { - "id": session_workbasket.pk, - "status": session_workbasket.status, - "title": session_workbasket.title, - "error_count": session_workbasket.tracked_model_check_errors.count(), - } - session.save() create_url = reverse("publishing:packaged-workbasket-queue-ui-create") form_data = { @@ -78,7 +70,7 @@ def test_packaged_workbasket_create_form_no_rule_check( assert ( not PackagedWorkBasket.objects.all_queued() .filter( - workbasket=session_workbasket, + workbasket=user_workbasket, ) .exists() ) @@ -89,12 +81,14 @@ def test_packaged_workbasket_create_form_no_rule_check( assert response.url[: len(response_url)] == response_url -def test_packaged_workbasket_create_form(valid_user_client): +def test_packaged_workbasket_create_form(client, valid_user): """Tests that Packaged WorkBasket Create returns 302 and redirects to confirm create page on success.""" + client.force_login(valid_user) workbasket = factories.WorkBasketFactory.create( status=WorkflowStatus.EDITING, ) + workbasket.assign_to_user(valid_user) with workbasket.new_transaction() as transaction: TransactionCheckFactory.create( transaction=transaction, @@ -102,14 +96,6 @@ def test_packaged_workbasket_create_form(valid_user_client): completed=True, ) - session = valid_user_client.session - session["workbasket"] = { - "id": workbasket.pk, - "status": workbasket.status, - "title": workbasket.title, - "error_count": workbasket.tracked_model_check_errors.count(), - } - session.save() # creating a packaged workbasket in the queue first_packaged_work_basket = factories.PackagedWorkBasketFactory() create_url = reverse("publishing:packaged-workbasket-queue-ui-create") @@ -119,7 +105,7 @@ def test_packaged_workbasket_create_form(valid_user_client): "jira_url": "www.fakejiraticket.com", } - response = valid_user_client.post(create_url, form_data) + response = client.post(create_url, form_data) assert response.status_code == 302 assert "/confirm-create/" in response.url @@ -136,19 +122,21 @@ def test_packaged_workbasket_create_form(valid_user_client): # Only compare the response URL up to the query string. assert response.url[: len(response_url)] == response_url assert second_packaged_work_basket.theme == form_data["theme"] - # Check in, form field may not contain full URL contianed within URLField object + # Check in, form field may not contain full URL contained within URLField object assert form_data["jira_url"] in second_packaged_work_basket.jira_url assert first_packaged_work_basket.position > 0 assert first_packaged_work_basket.position < second_packaged_work_basket.position -def test_packaged_workbasket_create_form_rule_check_violations(valid_user_client): +def test_packaged_workbasket_create_form_rule_check_violations(client, valid_user): """Tests that Packaged WorkBasket Create returns 302 and redirects to workbasket detail page when there are rule check violations on workbasket.""" + client.force_login(valid_user) workbasket = factories.WorkBasketFactory.create( status=WorkflowStatus.EDITING, ) + workbasket.assign_to_user(valid_user) with workbasket.new_transaction() as transaction: TransactionCheckFactory.create( transaction=transaction, @@ -156,14 +144,6 @@ def test_packaged_workbasket_create_form_rule_check_violations(valid_user_client completed=True, ) - session = valid_user_client.session - session["workbasket"] = { - "id": workbasket.pk, - "status": workbasket.status, - "title": workbasket.title, - "error_count": workbasket.tracked_model_check_errors.count(), - } - session.save() create_url = reverse("publishing:packaged-workbasket-queue-ui-create") form_data = { @@ -171,7 +151,7 @@ def test_packaged_workbasket_create_form_rule_check_violations(valid_user_client "jira_url": "www.fakejiraticket.com", } - response = valid_user_client.post(create_url, form_data) + response = client.post(create_url, form_data) # assert the packaged workbasket does not exist assert ( not PackagedWorkBasket.objects.all_queued() @@ -187,13 +167,15 @@ def test_packaged_workbasket_create_form_rule_check_violations(valid_user_client assert response.url[: len(response_url)] == response_url -def test_create_duplicate_awaiting_instances(valid_user_client, valid_user): +def test_create_duplicate_awaiting_instances(client, valid_user): """Tests that Packaged WorkBasket Create returns 302 and redirects to packaged workbasket queue page when trying to package a workbasket that is already on the queue.""" + client.force_login(valid_user) workbasket = factories.WorkBasketFactory.create( status=WorkflowStatus.EDITING, ) + workbasket.assign_to_user(valid_user) with workbasket.new_transaction() as transaction: TransactionCheckFactory.create( transaction=transaction, @@ -201,15 +183,6 @@ def test_create_duplicate_awaiting_instances(valid_user_client, valid_user): completed=True, ) - session = valid_user_client.session - session["workbasket"] = { - "id": workbasket.pk, - "status": workbasket.status, - "title": workbasket.title, - "error_count": workbasket.tracked_model_check_errors.count(), - } - session.save() - workbasket.queue(valid_user.pk, settings.TRANSACTION_SCHEMA) workbasket.save() existing_packaged = factories.PackagedWorkBasketFactory.create( @@ -218,7 +191,6 @@ def test_create_duplicate_awaiting_instances(valid_user_client, valid_user): workbasket.dequeue() workbasket.save() - """Test that a WorkBasket cannot enter the packaging queue more than once.""" create_url = reverse("publishing:packaged-workbasket-queue-ui-create") @@ -228,7 +200,7 @@ def test_create_duplicate_awaiting_instances(valid_user_client, valid_user): "jira_url": "www.fakejiraticket.com", } - response = valid_user_client.post(create_url, form_data) + response = client.post(create_url, form_data) assert response.status_code == 302 response_url = reverse("publishing:packaged-workbasket-queue-ui-list") diff --git a/quotas/tests/test_forms.py b/quotas/tests/test_forms.py index e83d06e62..18babdd09 100644 --- a/quotas/tests/test_forms.py +++ b/quotas/tests/test_forms.py @@ -29,13 +29,13 @@ def test_update_quota_form_safeguard_invalid(): assert "Please select a valid category" in form.errors["category"] -def test_update_quota_form_safeguard_disabled(valid_user_client): +def test_update_quota_form_safeguard_disabled(client_with_current_workbasket): """When a QuotaOrderNumber with the category safeguard is edited the category cannot be changed and the form field is disabled.""" quota = factories.QuotaOrderNumberFactory.create( category=validators.QuotaCategory.SAFEGUARD, ) - response = valid_user_client.get( + response = client_with_current_workbasket.get( reverse("quota-ui-edit", kwargs={"sid": quota.sid}), ) html = response.content.decode(response.charset) diff --git a/quotas/tests/test_views.py b/quotas/tests/test_views.py index 5eb69db8d..09682e4b0 100644 --- a/quotas/tests/test_views.py +++ b/quotas/tests/test_views.py @@ -743,7 +743,7 @@ def test_quota_edit_origin_new_versions(valid_user_client): def test_quota_edit_origin_exclusions( - valid_user_client, + client_with_current_workbasket, approved_transaction, geo_group1, geo_group2, @@ -770,7 +770,7 @@ def test_quota_edit_origin_exclusions( "submit": "Save", } - response = valid_user_client.post( + response = client_with_current_workbasket.post( reverse("quota_order_number_origin-ui-edit", kwargs={"sid": origin.sid}), form_data, ) @@ -799,7 +799,7 @@ def test_quota_edit_origin_exclusions( def test_quota_edit_origin_exclusions_remove( - valid_user_client, + client_with_current_workbasket, approved_transaction, geo_group1, country1, @@ -828,7 +828,7 @@ def test_quota_edit_origin_exclusions_remove( "submit": "Save", } - response = valid_user_client.post( + response = client_with_current_workbasket.post( reverse("quota_order_number_origin-ui-edit", kwargs={"sid": origin.sid}), form_data, ) @@ -859,14 +859,14 @@ def test_quota_edit_origin_exclusions_remove( ) -def test_update_quota_definition_page_200(valid_user_client): +def test_update_quota_definition_page_200(client_with_current_workbasket): quota_definition = factories.QuotaDefinitionFactory.create() url = reverse("quota_definition-ui-edit", kwargs={"sid": quota_definition.sid}) - response = valid_user_client.get(url) + response = client_with_current_workbasket.get(url) assert response.status_code == 200 -def test_update_quota_definition(valid_user_client, date_ranges): +def test_update_quota_definition(client_with_current_workbasket, date_ranges): quota_definition = factories.QuotaDefinitionFactory.create( valid_between=date_ranges.big_no_end, ) @@ -889,7 +889,7 @@ def test_update_quota_definition(valid_user_client, date_ranges): "quota_critical": "False", } - response = valid_user_client.post(url, data) + response = client_with_current_workbasket.post(url, data) assert response.status_code == 302 assert response.url == reverse( "quota_definition-ui-confirm-update", @@ -913,20 +913,20 @@ def test_update_quota_definition(valid_user_client, date_ranges): assert updated_definition.quota_critical == False -def test_delete_quota_definition_page_200(valid_user_client): +def test_delete_quota_definition_page_200(client_with_current_workbasket): quota_definition = factories.QuotaDefinitionFactory.create() url = reverse("quota_definition-ui-delete", kwargs={"sid": quota_definition.sid}) - response = valid_user_client.get(url) + response = client_with_current_workbasket.get(url) assert response.status_code == 200 -def test_delete_quota_definition(valid_user_client, date_ranges): +def test_delete_quota_definition(client_with_current_workbasket, date_ranges): quota_definition = factories.QuotaDefinitionFactory.create( valid_between=date_ranges.big_no_end, ) url = reverse("quota_definition-ui-delete", kwargs={"sid": quota_definition.sid}) - response = valid_user_client.post(url, {"submit": "Delete"}) + response = client_with_current_workbasket.post(url, {"submit": "Delete"}) assert response.status_code == 302 assert response.url == reverse( "quota_definition-ui-confirm-delete", @@ -937,7 +937,7 @@ def test_delete_quota_definition(valid_user_client, date_ranges): assert tx.workbasket.tracked_models.first().update_type == UpdateType.DELETE - confirm_response = valid_user_client.get(response.url) + confirm_response = client_with_current_workbasket.get(response.url) soup = BeautifulSoup( confirm_response.content.decode(response.charset), @@ -952,7 +952,7 @@ def test_delete_quota_definition(valid_user_client, date_ranges): def test_quota_create_origin( - valid_user_client, + client_with_current_workbasket, approved_transaction, geo_group1, date_ranges, @@ -971,7 +971,7 @@ def test_quota_create_origin( "submit": "Save", } - response = valid_user_client.post( + response = client_with_current_workbasket.post( reverse("quota_order_number_origin-ui-create", kwargs={"sid": quota.sid}), form_data, ) @@ -987,7 +987,7 @@ def test_quota_create_origin( def test_quota_create_origin_outwith_quota_period( - valid_user_client, + client_with_current_workbasket, approved_transaction, geo_group1, date_ranges, @@ -1007,7 +1007,7 @@ def test_quota_create_origin_outwith_quota_period( "submit": "Save", } - response = valid_user_client.post( + response = client_with_current_workbasket.post( reverse("quota_order_number_origin-ui-create", kwargs={"sid": quota.sid}), form_data, ) @@ -1024,7 +1024,7 @@ def test_quota_create_origin_outwith_quota_period( def test_quota_create_origin_no_overlapping_origins( - valid_user_client, + client_with_current_workbasket, approved_transaction, geo_group1, date_ranges, @@ -1050,7 +1050,7 @@ def test_quota_create_origin_no_overlapping_origins( "submit": "Save", } - response = valid_user_client.post( + response = client_with_current_workbasket.post( reverse("quota_order_number_origin-ui-create", kwargs={"sid": quota.sid}), form_data, ) @@ -1068,7 +1068,7 @@ def test_quota_create_origin_no_overlapping_origins( @pytest.mark.django_db def test_quota_order_number_and_origin_edit_create_view( - valid_user_client, + client_with_current_workbasket, date_ranges, approved_transaction, geo_group1, @@ -1090,14 +1090,14 @@ def test_quota_order_number_and_origin_edit_create_view( "submit": "Save", } - response = valid_user_client.post( + response = client_with_current_workbasket.post( reverse("quota_order_number_origin-ui-edit-create", kwargs={"sid": origin.sid}), form_data, ) assert response.status_code == 302 - response = valid_user_client.get( + response = client_with_current_workbasket.get( reverse("quota-ui-edit-create", kwargs={"sid": quota.sid}), form_data, ) @@ -1107,7 +1107,7 @@ def test_quota_order_number_and_origin_edit_create_view( @pytest.mark.django_db def test_quota_order_number_update_view( - valid_user_client, + client_with_current_workbasket, date_ranges, approved_transaction, geo_group1, @@ -1129,7 +1129,7 @@ def test_quota_order_number_update_view( "submit": "Save", } - response = valid_user_client.get( + response = client_with_current_workbasket.get( reverse("quota-ui-edit-update", kwargs={"sid": quota.sid}), form_data, ) @@ -1138,7 +1138,7 @@ def test_quota_order_number_update_view( def test_create_new_quota_definition( - valid_user_client, + client_with_current_workbasket, approved_transaction, date_ranges, mock_quota_api_no_data, @@ -1168,7 +1168,7 @@ def test_create_new_quota_definition( assert not models.QuotaDefinition.objects.all() url = reverse("quota_definition-ui-create", kwargs={"sid": quota.sid}) - response = valid_user_client.post(url, form_data) + response = client_with_current_workbasket.post(url, form_data) assert response.status_code == 302 created_definition = models.QuotaDefinition.objects.last() @@ -1179,7 +1179,7 @@ def test_create_new_quota_definition( # check definition is listed on quota order number's definition tab url = reverse("quota-ui-detail", kwargs={"sid": quota.sid}) - response = valid_user_client.get(url) + response = client_with_current_workbasket.get(url) soup = BeautifulSoup(response.content.decode(response.charset), "html.parser") definitions_tab = soup.find(id="definition-details") details = [ @@ -1197,7 +1197,7 @@ def test_create_new_quota_definition( def test_create_new_quota_definition_business_rule_violation( - valid_user_client, + client_with_current_workbasket, approved_transaction, date_ranges, ): @@ -1223,7 +1223,7 @@ def test_create_new_quota_definition_business_rule_violation( } url = reverse("quota_definition-ui-create", kwargs={"sid": quota.sid}) - response = valid_user_client.post(url, form_data) + response = client_with_current_workbasket.post(url, form_data) assert response.status_code == 200 @@ -1239,21 +1239,24 @@ def test_create_new_quota_definition_business_rule_violation( @pytest.mark.django_db def test_quota_order_number_create_200( - valid_user_client, + client_with_current_workbasket, ): - response = valid_user_client.get(reverse("quota-ui-create")) + response = client_with_current_workbasket.get(reverse("quota-ui-create")) assert response.status_code == 200 @pytest.mark.django_db def test_quota_order_number_create_errors_required( - valid_user_client, + client_with_current_workbasket, ): form_data = { "submit": "Save", } - response = valid_user_client.post(reverse("quota-ui-create"), form_data) + response = client_with_current_workbasket.post( + reverse("quota-ui-create"), + form_data, + ) assert response.status_code == 200 @@ -1298,7 +1301,7 @@ def test_quota_order_number_create_validation( mechanism, category, exp_error, - valid_user_client, + client_with_current_workbasket, date_ranges, ): form_data = { @@ -1310,7 +1313,10 @@ def test_quota_order_number_create_validation( "category": category, "submit": "Save", } - response = valid_user_client.post(reverse("quota-ui-create"), form_data) + response = client_with_current_workbasket.post( + reverse("quota-ui-create"), + form_data, + ) assert response.status_code == 200 @@ -1323,7 +1329,7 @@ def test_quota_order_number_create_validation( @pytest.mark.django_db def test_quota_order_number_create_success( - valid_user_client, + client_with_current_workbasket, date_ranges, ): form_data = { @@ -1335,7 +1341,10 @@ def test_quota_order_number_create_success( "category": validators.QuotaCategory.WTO.value, "submit": "Save", } - response = valid_user_client.post(reverse("quota-ui-create"), form_data) + response = client_with_current_workbasket.post( + reverse("quota-ui-create"), + form_data, + ) assert response.status_code == 302 @@ -1343,7 +1352,7 @@ def test_quota_order_number_create_success( assert response.url == reverse("quota-ui-confirm-create", kwargs={"sid": quota.sid}) - response2 = valid_user_client.get(response.url) + response2 = client_with_current_workbasket.get(response.url) soup = BeautifulSoup(response2.content.decode(response2.charset), "html.parser") diff --git a/regulations/tests/test_views.py b/regulations/tests/test_views.py index be4652d56..09fb45f18 100644 --- a/regulations/tests/test_views.py +++ b/regulations/tests/test_views.py @@ -60,7 +60,7 @@ def test_regulation_detail_views( view, url_pattern, valid_user_client, - session_with_workbasket, + session_request_with_workbasket, ): """Verify that regulation detail views are under the url regulations/ and don't return an error.""" @@ -284,7 +284,7 @@ def test_regulation_list_view( view, url_pattern, valid_user_client, - session_with_workbasket, + session_request_with_workbasket, ): """Verify that regulation list view is under the url regulations/ and doesn't return an error.""" @@ -353,7 +353,10 @@ def test_regulation_api_list_view(valid_user_client, date_ranges): ) -def test_regulation_update_view_new_regulation_id(date_ranges, valid_user_client): +def test_regulation_update_view_new_regulation_id( + date_ranges, + client_with_current_workbasket, +): """Test that an update to a regulation's `regulation_id` creates a new regulation, updates associated measures, and deletes old one.""" regulation = factories.UIDraftRegulationFactory.create() @@ -387,7 +390,7 @@ def test_regulation_update_view_new_regulation_id(date_ranges, valid_user_client "regulation_id": regulation.regulation_id, }, ) - response = valid_user_client.post(url, form_data) + response = client_with_current_workbasket.post(url, form_data) assert response.status_code == 302 new_regulation = Regulation.objects.get(regulation_id=new_regulation_id) diff --git a/settings/common.py b/settings/common.py index 4ab41b3fc..5b6d81f10 100644 --- a/settings/common.py +++ b/settings/common.py @@ -152,7 +152,7 @@ "django.contrib.auth.middleware.AuthenticationMiddleware", "django.contrib.messages.middleware.MessageMiddleware", "django.middleware.clickjacking.XFrameOptionsMiddleware", - "common.models.utils.ValidateSessionWorkBasketMiddleware", + "common.models.utils.ValidateUserWorkBasketMiddleware", "common.models.utils.TransactionMiddleware", "csp.middleware.CSPMiddleware", ] @@ -168,9 +168,8 @@ MIDDLEWARE.remove("django.contrib.sessions.middleware.SessionMiddleware") MIDDLEWARE.remove("django.contrib.auth.middleware.AuthenticationMiddleware") MIDDLEWARE.remove("django.contrib.messages.middleware.MessageMiddleware") - MIDDLEWARE.remove("common.models.utils.ValidateSessionWorkBasketMiddleware") MIDDLEWARE.remove("common.models.utils.TransactionMiddleware") - + MIDDLEWARE.remove("common.models.utils.ValidateUserWorkBasketMiddleware") MIDDLEWARE.append("common.middleware.MaintenanceModeMiddleware") TEMPLATES = [ @@ -258,6 +257,8 @@ "authbroker_client.backends.AuthbrokerBackend", ] +AUTH_USER_MODEL = "common.User" + # -- Security SECRET_KEY = os.environ.get("SECRET_KEY", "@@i$w*ct^hfihgh21@^8n+&ba@_l3x") diff --git a/taric_parsers/forms.py b/taric_parsers/forms.py index a48b83359..67abc9e74 100644 --- a/taric_parsers/forms.py +++ b/taric_parsers/forms.py @@ -6,7 +6,7 @@ from crispy_forms_gds.layout import Submit from django import forms from django.conf import settings -from django.contrib.auth.models import User +from django.contrib.auth import get_user_model from django.core.files.uploadedfile import InMemoryUploadedFile from django.db import transaction @@ -15,6 +15,8 @@ from taric_parsers.chunker import chunk_taric from taric_parsers.importer import run_batch +User = get_user_model() + class TaricParserFormMixin: """Mixin for taric parser forms, providing common taric_file clean and diff --git a/taric_parsers/tasks.py b/taric_parsers/tasks.py index 0b6d08870..95eb68b4d 100644 --- a/taric_parsers/tasks.py +++ b/taric_parsers/tasks.py @@ -1,7 +1,7 @@ from logging import getLogger from typing import Sequence -from django.contrib.auth.models import User +from django.contrib.auth import get_user_model import taric_parsers.importer from common.celery import app @@ -14,6 +14,8 @@ from workbaskets.models import WorkBasket from workbaskets.models import get_partition_scheme +User = get_user_model() + logger = getLogger(__name__) diff --git a/taric_parsers/tests/test_views.py b/taric_parsers/tests/test_views.py index 8397ed238..01f6f7968 100644 --- a/taric_parsers/tests/test_views.py +++ b/taric_parsers/tests/test_views.py @@ -3,7 +3,7 @@ import pytest from bs4 import BeautifulSoup -from django.contrib.auth.models import User +from django.contrib.auth import get_user_model from django.core.files.uploadedfile import SimpleUploadedFile from django.test import Client from django.urls import reverse @@ -11,6 +11,8 @@ from common.tests import factories from common.tests.factories import ImportBatchFactory +User = get_user_model() + pytestmark = pytest.mark.django_db TEST_FILES_PATH = path.join(path.dirname(__file__), "support") diff --git a/workbaskets/jinja2/includes/workbaskets/navigation.jinja b/workbaskets/jinja2/includes/workbaskets/navigation.jinja index 3a6ef141d..9025ce8b0 100644 --- a/workbaskets/jinja2/includes/workbaskets/navigation.jinja +++ b/workbaskets/jinja2/includes/workbaskets/navigation.jinja @@ -11,7 +11,7 @@ Check workbasket diff --git a/workbaskets/jinja2/workbaskets/checks.jinja b/workbaskets/jinja2/workbaskets/checks.jinja index 6c02c08e8..7271a8383 100644 --- a/workbaskets/jinja2/workbaskets/checks.jinja +++ b/workbaskets/jinja2/workbaskets/checks.jinja @@ -3,7 +3,7 @@ {% from "includes/workbaskets/navigation.jinja" import navigation %} {% set page_title %} - Workbasket {{ workbasket.id if workbasket else request.session.workbasket.id }} - Checks + Workbasket {{ workbasket.id if workbasket else request.user.current_workbasket.id }} - Checks {% endset %} {% block content %} diff --git a/workbaskets/jinja2/workbaskets/compare.jinja b/workbaskets/jinja2/workbaskets/compare.jinja index 25982b7f6..6399db269 100644 --- a/workbaskets/jinja2/workbaskets/compare.jinja +++ b/workbaskets/jinja2/workbaskets/compare.jinja @@ -9,7 +9,7 @@ {% from "components/table/macro.njk" import govukTable %} {% set page_title %} - Workbasket {{ workbasket.id if workbasket else request.session.workbasket.id }} - Compare with worksheet data + Workbasket {{ workbasket.id if workbasket else request.user.current_workbasket.id }} - Compare with worksheet data {% endset %} {% set change_workbasket_details_link = url("workbaskets:workbasket-ui-update", kwargs={"pk": workbasket.pk}) %} @@ -19,9 +19,9 @@ "items": [ {"text": "Home", "href": url("home")}, {"text": "Edit an existing workbasket", "href": url("workbaskets:workbasket-ui-list")}, - {"text": "Workbasket " ~ request.session.workbasket.id ~ " - Summary", "href": url("workbaskets:current-workbasket") }, - {"text": "Workbasket " ~ request.session.workbasket.id ~ " - Checks", "href": url("workbaskets:workbasket-checks") }, - {"text": "Workbasket " ~ request.session.workbasket.id ~ " - Compare" } + {"text": "Workbasket " ~ request.user.current_workbasket.id ~ " - Summary", "href": url("workbaskets:current-workbasket") }, + {"text": "Workbasket " ~ request.user.current_workbasket.id ~ " - Checks", "href": url("workbaskets:workbasket-checks") }, + {"text": "Workbasket " ~ request.user.current_workbasket.id ~ " - Compare" } ]}) }} {% endblock %} diff --git a/workbaskets/jinja2/workbaskets/delete_changes.jinja b/workbaskets/jinja2/workbaskets/delete_changes.jinja index 46800e442..51b9cee61 100644 --- a/workbaskets/jinja2/workbaskets/delete_changes.jinja +++ b/workbaskets/jinja2/workbaskets/delete_changes.jinja @@ -6,7 +6,7 @@ {% set page_title = "Remove tariff changes" %} {% block breadcrumb %} - {% if request.session.workbasket.id != view.workbasket.pk %} + {% if request.user.current_workbasket.id != view.workbasket.pk %} {{ breadcrumbs(request, [ {"text": "Find and view workbaskets", "href": url("workbaskets:workbasket-ui-list-all")}, { diff --git a/workbaskets/jinja2/workbaskets/delete_changes_confirm.jinja b/workbaskets/jinja2/workbaskets/delete_changes_confirm.jinja index e65bcc8ca..6c4e09e02 100644 --- a/workbaskets/jinja2/workbaskets/delete_changes_confirm.jinja +++ b/workbaskets/jinja2/workbaskets/delete_changes_confirm.jinja @@ -7,7 +7,7 @@ {% set page_title = "Remove tariff changes" %} {% block breadcrumb %} - {% if view_workbasket != session_workbasket %} + {% if view_workbasket != user_workbasket %} {{ breadcrumbs(request, [ {"text": "Find and view workbaskets", "href": url("workbaskets:workbasket-ui-list-all")}, { @@ -30,7 +30,7 @@ "classes": "govuk-!-margin-bottom-7" }) }} - {% if view_workbasket != session_workbasket %} + {% if view_workbasket != user_workbasket %} {{ govukButton({ "text": "Return to workbasket", "href": url("workbaskets:workbasket-ui-changes", kwargs={"pk": view_workbasket.pk}), diff --git a/workbaskets/jinja2/workbaskets/delete_workbasket.jinja b/workbaskets/jinja2/workbaskets/delete_workbasket.jinja index 65ab6555d..26f2ff7a8 100644 --- a/workbaskets/jinja2/workbaskets/delete_workbasket.jinja +++ b/workbaskets/jinja2/workbaskets/delete_workbasket.jinja @@ -67,7 +67,7 @@ "name": "action", "value": "delete" }) }} - {% if object.pk == request.session.workbasket["id"] %} + {% if object.pk == request.user.current_workbasket.id %} {{ govukButton({ "text": "Cancel", "href": url("workbaskets:current-workbasket"), diff --git a/workbaskets/jinja2/workbaskets/edit-details.jinja b/workbaskets/jinja2/workbaskets/edit-details.jinja index 2f684f6cf..63f52f180 100644 --- a/workbaskets/jinja2/workbaskets/edit-details.jinja +++ b/workbaskets/jinja2/workbaskets/edit-details.jinja @@ -8,7 +8,7 @@ {{ govukBreadcrumbs({ "items": [ {"text": "Home", "href": url("home")}, - {"text": "Workbasket " ~ request.session.workbasket.id ~ " - Summary", "href": url("workbaskets:current-workbasket") }, + {"text": "Workbasket " ~ request.user.current_workbasket.id ~ " - Summary", "href": url("workbaskets:current-workbasket") }, {"text": page_title} ] }) }} diff --git a/workbaskets/jinja2/workbaskets/edit-workbasket.jinja b/workbaskets/jinja2/workbaskets/edit-workbasket.jinja index fb11d19c3..785349dbe 100644 --- a/workbaskets/jinja2/workbaskets/edit-workbasket.jinja +++ b/workbaskets/jinja2/workbaskets/edit-workbasket.jinja @@ -3,8 +3,8 @@ {% from "includes/workbaskets/navigation.jinja" import navigation %} {% set page_title %} - Workbasket {{ request.session.workbasket.id }} - {% if request.session.workbasket.title %} - Add/edit items{% endif %} + Workbasket {{ request.user.current_workbasket.id }} + {% if request.user.current_workbasket.title %} - Add/edit items{% endif %} {% endset %} @@ -13,8 +13,8 @@ "items": [ {"text": "Home", "href": url("home")}, {"text": "Edit an existing workbasket", "href": url("workbaskets:workbasket-ui-list")}, - {"text": "Workbasket " ~ request.session.workbasket.id ~ " - Summary", "href": url("workbaskets:current-workbasket") }, - {"text": "Workbasket " ~ request.session.workbasket.id ~ " - Add/edit items" } + {"text": "Workbasket " ~ request.user.current_workbasket.id ~ " - Summary", "href": url("workbaskets:current-workbasket") }, + {"text": "Workbasket " ~ request.user.current_workbasket.id ~ " - Add/edit items" } ] }) }} {% endblock %} diff --git a/workbaskets/jinja2/workbaskets/no_active_workbasket.jinja b/workbaskets/jinja2/workbaskets/no_active_workbasket.jinja new file mode 100644 index 000000000..c274c34d0 --- /dev/null +++ b/workbaskets/jinja2/workbaskets/no_active_workbasket.jinja @@ -0,0 +1,37 @@ +{% extends "layouts/layout.jinja" %} + +{% from "components/button/macro.njk" import govukButton %} + +{% set page_title = "You need an active workbasket to access this page" %} + +{% block breadcrumb %}{% endblock %} + +{% block content %} +
    +
    +
    +
    +

    You need an active workbasket to access this page

    +

    + You either do not have a workbasket or your previous workbasket was no longer in the editing state. Since + you last edited it, it may have been archived, queued, published or deleted. +

    +

    Ensure you are in the correct workbasket before continuing.

    +
    +
    +
    +
    +
    +{{ govukButton({ + "text": "Select a new workbasket", + "href": url("workbaskets:workbasket-ui-list"), + "classes": "govuk-button", + }) }} + +{{ govukButton({ + "text": "Return to homepage", + "href": url("home"), + "classes": "govuk-button--secondary", + }) }} +
    +{% endblock %} \ No newline at end of file diff --git a/workbaskets/jinja2/workbaskets/review.jinja b/workbaskets/jinja2/workbaskets/review.jinja index 2fd0e00eb..427d023a3 100644 --- a/workbaskets/jinja2/workbaskets/review.jinja +++ b/workbaskets/jinja2/workbaskets/review.jinja @@ -6,7 +6,7 @@ {% set page_title %} Workbasket {{ workbasket.id }} - {{ tab_page_title }} {% endset %} -{% if workbasket == session_workbasket %} +{% if workbasket == user_workbasket %} {% set page_heading %} Workbasket {{ workbasket.id }} - Review changes {% endset %} {% else %} {% set page_heading %} Workbasket {{ workbasket.id }} - {{ workbasket.status }} {% endset %} @@ -57,7 +57,7 @@ %} {% block breadcrumb %} - {% if workbasket != session_workbasket %} + {% if workbasket != user_workbasket %} {{ breadcrumbs(request, [ {"text": "Find and view workbaskets", "href": url("workbaskets:workbasket-ui-list-all")}, { @@ -77,7 +77,7 @@ {{ page_heading }} - {% if workbasket == session_workbasket %} + {% if workbasket == user_workbasket %} {{ navigation(request, "review") }} {% else %} {{ create_workbasket_detail_navigation(active_tab="review") }} diff --git a/workbaskets/jinja2/workbaskets/summary-workbasket.jinja b/workbaskets/jinja2/workbaskets/summary-workbasket.jinja index 8854f7df1..c1792e689 100644 --- a/workbaskets/jinja2/workbaskets/summary-workbasket.jinja +++ b/workbaskets/jinja2/workbaskets/summary-workbasket.jinja @@ -5,7 +5,7 @@ {% from "includes/workbaskets/navigation.jinja" import navigation %} {% set page_title %} - Workbasket {{ workbasket.id if workbasket else request.session.workbasket.id }} - Summary + Workbasket {{ workbasket.id if workbasket else request.user.current_.workbasket.id }} - Summary {% endset %} {% set change_workbasket_details_link = url("workbaskets:workbasket-ui-update", kwargs={"pk": workbasket.pk}) %} @@ -18,7 +18,7 @@ "items": [ {"text": "Home", "href": url("home")}, {"text": "Edit an existing workbasket", "href": url("workbaskets:workbasket-ui-list")}, - {"text": "Workbasket " ~ request.session.workbasket.id ~ " - Summary" } + {"text": "Workbasket " ~ request.user.current_workbasket.id ~ " - Summary" } ]}) }} {% endblock %} diff --git a/workbaskets/jinja2/workbaskets/violation_detail.jinja b/workbaskets/jinja2/workbaskets/violation_detail.jinja index c48f687db..0c6c9e761 100644 --- a/workbaskets/jinja2/workbaskets/violation_detail.jinja +++ b/workbaskets/jinja2/workbaskets/violation_detail.jinja @@ -6,7 +6,7 @@ {% from "components/warning-text/macro.njk" import govukWarningText %} {% from "includes/workbaskets/navigation.jinja" import navigation %} -{% set page_title %}Workbasket {{ workbasket.id if workbasket else request.session.workbasket.id }} - Rule violation +{% set page_title %}Workbasket {{ workbasket.id if workbasket else request.user.current_workbasket.id }} - Rule violation details {% endset %} diff --git a/workbaskets/jinja2/workbaskets/violations.jinja b/workbaskets/jinja2/workbaskets/violations.jinja index 6cfb09dfa..242cb92d5 100644 --- a/workbaskets/jinja2/workbaskets/violations.jinja +++ b/workbaskets/jinja2/workbaskets/violations.jinja @@ -5,7 +5,7 @@ {% from "components/create_sortable_anchor.jinja" import create_sortable_anchor %} {% from "includes/workbaskets/navigation.jinja" import navigation %} -{% set page_title %}Workbasket {{ workbasket.id if workbasket else request.session.workbasket.id }} - Rule violations {% endset %} +{% set page_title %}Workbasket {{ workbasket.id if workbasket else request.user.current_workbasket.id }} - Rule violations {% endset %} {% block content %}

    {{ page_title }}

    diff --git a/workbaskets/migrations/0001_initial.py b/workbaskets/migrations/0001_initial.py index fb993f996..6b625cd5a 100644 --- a/workbaskets/migrations/0001_initial.py +++ b/workbaskets/migrations/0001_initial.py @@ -1,7 +1,5 @@ # Generated by Django 3.1 on 2021-01-06 15:33 -import django.db.models.deletion import django_fsm -from django.conf import settings from django.db import migrations from django.db import models @@ -9,9 +7,7 @@ class Migration(migrations.Migration): initial = True - dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ] + dependencies = [] operations = [ migrations.CreateModel( @@ -76,24 +72,6 @@ class Migration(migrations.Migration): max_length=50, ), ), - ( - "approver", - models.ForeignKey( - editable=False, - null=True, - on_delete=django.db.models.deletion.PROTECT, - related_name="approved_workbaskets", - to=settings.AUTH_USER_MODEL, - ), - ), - ( - "author", - models.ForeignKey( - editable=False, - on_delete=django.db.models.deletion.PROTECT, - to=settings.AUTH_USER_MODEL, - ), - ), ], options={ "abstract": False, diff --git a/workbaskets/migrations/0002_change_status_per_ADR008.py b/workbaskets/migrations/0002_change_status_per_ADR008.py index 76b66720d..08d043392 100644 --- a/workbaskets/migrations/0002_change_status_per_ADR008.py +++ b/workbaskets/migrations/0002_change_status_per_ADR008.py @@ -1,15 +1,40 @@ # Generated by Django 3.1.12 on 2021-09-21 14:10 +import django.db.models.deletion import django_fsm +from django.conf import settings from django.db import migrations +from django.db import models class Migration(migrations.Migration): dependencies = [ ("workbaskets", "0001_initial"), + ("common", "0001_initial"), ] operations = [ + # AddField operations for approver and author fields have been moved here from workbaskets.0001_initial to resolve a circular dependency issue with common.0001_initial following a change mid-project to a custom user model. + migrations.AddField( + model_name="workbasket", + name="approver", + field=models.ForeignKey( + editable=False, + null=True, + on_delete=django.db.models.deletion.PROTECT, + related_name="approved_workbaskets", + to=settings.AUTH_USER_MODEL, + ), + ), + migrations.AddField( + model_name="workbasket", + name="author", + field=models.ForeignKey( + editable=False, + on_delete=django.db.models.deletion.PROTECT, + to=settings.AUTH_USER_MODEL, + ), + ), migrations.AlterField( model_name="workbasket", name="status", diff --git a/workbaskets/models.py b/workbaskets/models.py index 97fc0933c..27b0779bd 100644 --- a/workbaskets/models.py +++ b/workbaskets/models.py @@ -8,6 +8,7 @@ from celery.result import AsyncResult from django.conf import settings +from django.contrib.auth import get_user_model from django.core.exceptions import ImproperlyConfigured from django.core.exceptions import ValidationError from django.db import models @@ -32,6 +33,8 @@ logger = logging.getLogger(__name__) +User = get_user_model() + class TransactionPartitionScheme: """ @@ -494,14 +497,10 @@ def restore(self): """WorkBasket is ready to be worked on again after being rejected by CDS.""" - def save_to_session(self, session): - session["workbasket"] = { - "id": self.pk, - "status": self.status, - "title": self.title, - "error_count": self.tracked_model_check_errors.count(), - "measure_count": self.measures.count(), - } + def assign_to_user(self, user) -> None: + """Assigns this instance as `user`'s current workbasket.""" + user.current_workbasket = self + user.save() @property def tracked_models(self) -> TrackedModelQuerySet: @@ -511,28 +510,18 @@ def tracked_models(self) -> TrackedModelQuerySet: def measures(self) -> MeasuresQuerySet: return Measure.objects.filter(transaction__workbasket=self) - @classmethod - def load_from_session(cls, session): - if "workbasket" not in session: - raise KeyError("WorkBasket not in session") - return WorkBasket.objects.get(pk=session["workbasket"]["id"]) - - @classmethod - def remove_current_from_session(cls, session): - """Remove the current workbasket from the user's session.""" - if "workbasket" in session: - del session["workbasket"] - @classmethod def current(cls, request): - """Get the current workbasket in the session.""" - if "workbasket" in request.session: - workbasket = cls.load_from_session(request.session) + """Get the user's current workbasket.""" + try: + workbasket = request.user.current_workbasket + except AttributeError: + return None + if workbasket is not None: if workbasket.status != WorkflowStatus.EDITING: - cls.remove_current_from_session(request.session) + request.user.remove_current_workbasket() return None - return workbasket else: return None diff --git a/workbaskets/tests/test_models.py b/workbaskets/tests/test_models.py index 240473454..898cd16a0 100644 --- a/workbaskets/tests/test_models.py +++ b/workbaskets/tests/test_models.py @@ -432,3 +432,10 @@ def test_invalid_workbasket_purge_transactions(workbasket_status): workbasket.purge_empty_transactions() assert workbasket.transactions.count() == 2 + + +def test_workbasket_assign_to_user(valid_user, workbasket): + """Test assign_to_user() sets the user's current workbasket.""" + assert not valid_user.current_workbasket + workbasket.assign_to_user(valid_user) + assert valid_user.current_workbasket == workbasket diff --git a/workbaskets/tests/test_views.py b/workbaskets/tests/test_views.py index 8bc4e0170..f16fc77c5 100644 --- a/workbaskets/tests/test_views.py +++ b/workbaskets/tests/test_views.py @@ -89,17 +89,13 @@ def test_workbasket_create_without_permission(client): def test_workbasket_update_view_updates_workbasket_title_and_description( valid_user_client, - session_workbasket, + user_workbasket, ): """Test that a workbasket's title and description can be updated.""" - session = valid_user_client.session - session["workbasket"] = {"id": session_workbasket.pk} - session.save() - url = reverse( "workbaskets:workbasket-ui-update", - kwargs={"pk": session_workbasket.pk}, + kwargs={"pk": user_workbasket.pk}, ) new_title = "123321" new_description = "Newly updated test description" @@ -107,8 +103,8 @@ def test_workbasket_update_view_updates_workbasket_title_and_description( "title": new_title, "reason": new_description, } - assert not session_workbasket.title == new_title - assert not session_workbasket.reason == new_description + assert not user_workbasket.title == new_title + assert not user_workbasket.reason == new_description response = valid_user_client.get(url) assert response.status_code == 200 @@ -117,12 +113,12 @@ def test_workbasket_update_view_updates_workbasket_title_and_description( assert response.status_code == 302 assert response.url == reverse( "workbaskets:workbasket-ui-confirm-update", - kwargs={"pk": session_workbasket.pk}, + kwargs={"pk": user_workbasket.pk}, ) - session_workbasket.refresh_from_db() - assert session_workbasket.title == new_title - assert session_workbasket.reason == new_description + user_workbasket.refresh_from_db() + assert user_workbasket.title == new_title + assert user_workbasket.reason == new_description def test_download( @@ -163,12 +159,12 @@ def test_download( def test_review_workbasket_displays_rule_violation_summary( valid_user_client, - session_workbasket, + user_workbasket, ): """Test that the review workbasket page includes an error summary box detailing the number of tracked model changes and business rule violations, dated to the most recent `TrackedModelCheck`.""" - with session_workbasket.new_transaction() as transaction: + with user_workbasket.new_transaction() as transaction: good = factories.GoodsNomenclatureFactory.create(transaction=transaction) check = TrackedModelCheckFactory.create( transaction_check__transaction=transaction, @@ -187,7 +183,7 @@ def test_review_workbasket_displays_rule_violation_summary( ) error_headings = page.find_all("h2", attrs={"class": "govuk-body"}) - tracked_model_count = session_workbasket.tracked_models.count() + tracked_model_count = user_workbasket.tracked_models.count() local_created_at = localtime(check.created_at) created_at = f"{local_created_at:%d %b %Y %H:%M}" @@ -196,18 +192,18 @@ def test_review_workbasket_displays_rule_violation_summary( assert f"Number of violations: 1" in error_headings[1].text -def test_edit_workbasket_page_sets_workbasket(valid_user_client, session_workbasket): +def test_edit_workbasket_page_sets_workbasket(valid_user_client, user_workbasket): response = valid_user_client.get( reverse("workbaskets:edit-workbasket"), ) assert response.status_code == 200 soup = BeautifulSoup(str(response.content), "html.parser") - assert str(session_workbasket.pk) in soup.select(".govuk-heading-xl")[0].text + assert str(user_workbasket.pk) in soup.select(".govuk-heading-xl")[0].text def test_workbasket_detail_page_url_params( valid_user_client, - session_workbasket, + user_workbasket, ): url = reverse( "workbaskets:current-workbasket", @@ -306,7 +302,7 @@ def test_select_workbasket_redirects_to_tab( "workbaskets:edit-workbasket", ), ) -def test_workbasket_views_without_permission(url_name, client, session_workbasket): +def test_workbasket_views_without_permission(url_name, client, user_workbasket): """Tests that select, list-all, delete, and edit workbasket view endpoints return 403 to users without change_workbasket permission.""" url = reverse( @@ -314,6 +310,7 @@ def test_workbasket_views_without_permission(url_name, client, session_workbaske ) user = factories.UserFactory.create() client.force_login(user) + user_workbasket.assign_to_user(user) response = client.get(url) assert response.status_code == 403 @@ -481,13 +478,13 @@ def test_workbasket_review_tabs( object_factory, num_columns, valid_user_client, - session_workbasket, + user_workbasket, ): """Tests that workbasket review tabs return 200 and display objects in table.""" - with session_workbasket.new_transaction(): + with user_workbasket.new_transaction(): object_factory() - url = reverse(url, kwargs={"pk": session_workbasket.pk}) + url = reverse(url, kwargs={"pk": user_workbasket.pk}) response = valid_user_client.get(url) assert response.status_code == 200 @@ -563,21 +560,21 @@ def test_workbasket_review_measures_filters_update_type( update_type, expected_measure_count, valid_user_client, - session_workbasket, + user_workbasket, ): """Tests that `WorkBasketReviewMeasuresView` filters measures by `update_type`.""" - with session_workbasket.new_transaction(): + with user_workbasket.new_transaction(): created_measures = factories.MeasureFactory.create_batch(2) - updated_measure = created_measures[0].new_version(workbasket=session_workbasket) + updated_measure = created_measures[0].new_version(workbasket=user_workbasket) deleted_measure = created_measures[1].new_version( update_type=UpdateType.DELETE, - workbasket=session_workbasket, + workbasket=user_workbasket, ) url = reverse( "workbaskets:workbasket-ui-review-measures", - kwargs={"pk": session_workbasket.pk}, + kwargs={"pk": user_workbasket.pk}, ) search_filter = f"?update_type={update_type}" response = valid_user_client.get(url + search_filter) @@ -649,15 +646,15 @@ def test_workbasket_review_measures_conditions(valid_user_client): @patch("workbaskets.tasks.call_check_workbasket_sync.apply_async") -def test_run_business_rules(check_workbasket, valid_user_client, session_workbasket): +def test_run_business_rules(check_workbasket, valid_user_client, user_workbasket): """Test that a GET request to the run-business-rules endpoint returns a 302, redirecting to the review workbasket page, runs the `check_workbasket` task, saves the task id on the workbasket, and deletes pre-existing `TrackedModelCheck` objects associated with the workbasket.""" check_workbasket.return_value.id = 123 - assert not session_workbasket.rule_check_task_id + assert not user_workbasket.rule_check_task_id - with session_workbasket.new_transaction() as transaction: + with user_workbasket.new_transaction() as transaction: good = factories.GoodsNomenclatureFactory.create(transaction=transaction) check = TrackedModelCheckFactory.create( transaction_check__transaction=transaction, @@ -665,13 +662,6 @@ def test_run_business_rules(check_workbasket, valid_user_client, session_workbas successful=False, ) - session = valid_user_client.session - session["workbasket"] = { - "id": session_workbasket.pk, - "status": session_workbasket.status, - "title": session_workbasket.title, - } - session.save() url = reverse( "workbaskets:workbasket-checks", ) @@ -684,21 +674,21 @@ def test_run_business_rules(check_workbasket, valid_user_client, session_workbas # Only compare the response URL up to the query string. assert response.url[: len(url)] == url - session_workbasket.refresh_from_db() + user_workbasket.refresh_from_db() check_workbasket.assert_called_once_with( - (session_workbasket.pk,), + (user_workbasket.pk,), countdown=1, ) - assert session_workbasket.rule_check_task_id - assert not session_workbasket.tracked_model_checks.exists() + assert user_workbasket.rule_check_task_id + assert not user_workbasket.tracked_model_checks.exists() -def test_workbasket_business_rule_status(valid_user_client, session_empty_workbasket): +def test_workbasket_business_rule_status(valid_user_client, user_empty_workbasket): """Testing that the live status of a workbasket resets after an item has been updated, created or deleted in the workbasket.""" - with session_empty_workbasket.new_transaction() as transaction: + with user_empty_workbasket.new_transaction() as transaction: footnote = factories.FootnoteFactory.create( transaction=transaction, footnote_type__transaction=transaction, @@ -719,7 +709,7 @@ def test_workbasket_business_rule_status(valid_user_client, session_empty_workba assert success_banner factories.FootnoteFactory.create( - transaction=session_empty_workbasket.new_transaction(), + transaction=user_empty_workbasket.new_transaction(), ) response = valid_user_client.get(url) page = BeautifulSoup(response.content.decode(response.charset)) @@ -727,9 +717,9 @@ def test_workbasket_business_rule_status(valid_user_client, session_empty_workba @pytest.fixture -def successful_business_rules_setup(session_workbasket, valid_user_client): +def successful_business_rules_setup(user_workbasket, valid_user_client): """Sets up data and runs business rules.""" - with session_workbasket.new_transaction() as transaction: + with user_workbasket.new_transaction() as transaction: good = factories.GoodsNomenclatureFactory.create(transaction=transaction) measure = factories.MeasureFactory.create(transaction=transaction) geo_area = factories.GeographicalAreaFactory.create(transaction=transaction) @@ -740,17 +730,9 @@ def successful_business_rules_setup(session_workbasket, valid_user_client): model=obj, successful=True, ) - session = valid_user_client.session - session["workbasket"] = { - "id": session_workbasket.pk, - "status": session_workbasket.status, - "title": session_workbasket.title, - "error_count": session_workbasket.tracked_model_check_errors.count(), - } - session.save() # run rule checks so unchecked_or_errored_transactions is set - check_workbasket_sync(session_workbasket) + check_workbasket_sync(user_workbasket) def import_batch_with_notification(): @@ -800,7 +782,7 @@ def import_batch_with_notification(): def test_submit_for_packaging_disabled( successful_business_rules_setup, valid_user_client, - session_workbasket, + user_workbasket, import_batch_factory, disabled, ): @@ -810,7 +792,7 @@ def test_submit_for_packaging_disabled( import_batch = import_batch_factory() if import_batch: - import_batch.workbasket_id = session_workbasket.id + import_batch.workbasket_id = user_workbasket.id if isinstance(import_batch, ImportBatch): import_batch.save() url = reverse( @@ -838,7 +820,7 @@ def test_submit_for_packaging_disabled( def test_submit_for_packaging( successful_business_rules_setup, valid_user_client, - session_workbasket, + user_workbasket, ): """Test that a link to the publishing/create url shows following a successful rule check.""" @@ -847,7 +829,7 @@ def test_submit_for_packaging( goods_import=True, ) - import_batch.workbasket_id = session_workbasket.id + import_batch.workbasket_id = user_workbasket.id if isinstance(import_batch, ImportBatch): import_batch.save() url = reverse( @@ -867,8 +849,8 @@ def test_submit_for_packaging( assert soup.find("a", href="/publishing/create/") -def test_terminate_rule_check(valid_user_client, session_workbasket): - session_workbasket.rule_check_task_id = 123 +def test_terminate_rule_check(valid_user_client, user_workbasket): + user_workbasket.rule_check_task_id = 123 url = reverse( "workbaskets:workbasket-checks", @@ -880,33 +862,25 @@ def test_terminate_rule_check(valid_user_client, session_workbasket): assert response.status_code == 302 assert response.url[: len(url)] == url - session_workbasket.refresh_from_db() + user_workbasket.refresh_from_db() - assert not session_workbasket.rule_check_task_id + assert not user_workbasket.rule_check_task_id -def test_workbasket_violations(valid_user_client, session_workbasket): +def test_workbasket_violations(valid_user_client, user_workbasket): """Test that a GET request to the violations endpoint returns a 200 and displays the correct column values for one unsuccessful `TrackedModelCheck`.""" url = reverse( "workbaskets:workbasket-ui-violations", ) - with session_workbasket.new_transaction() as transaction: + with user_workbasket.new_transaction() as transaction: good = factories.GoodsNomenclatureFactory.create(transaction=transaction) check = TrackedModelCheckFactory.create( transaction_check__transaction=transaction, model=good, successful=False, ) - session = valid_user_client.session - session["workbasket"] = { - "id": session_workbasket.pk, - "status": session_workbasket.status, - "title": session_workbasket.title, - "error_count": session_workbasket.tracked_model_check_errors.count(), - } - session.save() response = valid_user_client.get(url) assert response.status_code == 200 @@ -925,14 +899,14 @@ def test_workbasket_violations(valid_user_client, session_workbasket): def test_workbasket_violations_summary_pagination( valid_user_client, - session_workbasket, + user_workbasket, ): """Tests that the violations page paginates if there are more than 50 violations.""" url = reverse("workbaskets:workbasket-ui-violations") - with session_workbasket.new_transaction() as transaction: + with user_workbasket.new_transaction() as transaction: measures = factories.MeasureFactory.create_batch( 59, transaction=transaction, @@ -943,14 +917,6 @@ def test_workbasket_violations_summary_pagination( model=measure, successful=False, ) - session = valid_user_client.session - session["workbasket"] = { - "id": session_workbasket.pk, - "status": session_workbasket.status, - "title": session_workbasket.title, - "error_count": session_workbasket.tracked_model_check_errors.count(), - } - session.save() response = valid_user_client.get(url) assert response.status_code == 200 @@ -964,8 +930,8 @@ def test_workbasket_violations_summary_pagination( assert "Showing 50 of 59" in pagination_div_text -def test_violation_detail_page(valid_user_client, session_workbasket): - with session_workbasket.new_transaction() as transaction: +def test_violation_detail_page(valid_user_client, user_workbasket): + with user_workbasket.new_transaction() as transaction: good = factories.GoodsNomenclatureFactory.create(transaction=transaction) check = TrackedModelCheckFactory.create( transaction_check__transaction=transaction, @@ -974,16 +940,8 @@ def test_violation_detail_page(valid_user_client, session_workbasket): ) url = reverse( "workbaskets:workbasket-ui-violation-detail", - kwargs={"wb_pk": session_workbasket.pk, "pk": check.pk}, - ) - session = valid_user_client.session - session["workbasket"] = { - "id": session_workbasket.pk, - "status": session_workbasket.status, - "title": session_workbasket.title, - "error_count": session_workbasket.tracked_model_check_errors.count(), - } - session.save() + kwargs={"wb_pk": user_workbasket.pk, "pk": check.pk}, + ) response = valid_user_client.get(url) assert response.status_code == 200 @@ -997,26 +955,26 @@ def test_violation_detail_page(valid_user_client, session_workbasket): def test_violation_detail_page_superuser_override_last_violation( - superuser_client, - session_workbasket, + superuser, + client, + user_workbasket, ): """Override the last unsuccessful TrackedModelCheck on a TransactionCheck.""" - model_check = TrackedModelCheckFactory.create( successful=False, transaction_check__successful=False, ) - model_check.transaction_check.transaction.workbasket.save_to_session( - superuser_client.session, + client.force_login(superuser) + model_check.transaction_check.transaction.workbasket.assign_to_user( + superuser, ) - superuser_client.session.save() url = reverse( "workbaskets:workbasket-ui-violation-detail", - kwargs={"wb_pk": session_workbasket.pk, "pk": model_check.pk}, + kwargs={"wb_pk": user_workbasket.pk, "pk": model_check.pk}, ) - response = superuser_client.post(url, data={"action": "delete"}) + response = client.post(url, data={"action": "delete"}) assert response.status_code == 302 redirect_url = reverse("workbaskets:workbasket-ui-violations") @@ -1028,8 +986,9 @@ def test_violation_detail_page_superuser_override_last_violation( def test_violation_detail_page_superuser_override_one_of_two_violation( - superuser_client, - session_workbasket, + superuser, + client, + user_workbasket, ): """Override an unsuccessful TrackedModelCheck on a TransactionCheck that has more TrackedModelCheck.""" @@ -1038,10 +997,10 @@ def test_violation_detail_page_superuser_override_one_of_two_violation( successful=False, transaction_check__successful=False, ) - model_check.transaction_check.transaction.workbasket.save_to_session( - superuser_client.session, + client.force_login(superuser) + model_check.transaction_check.transaction.workbasket.assign_to_user( + superuser, ) - superuser_client.session.save() TrackedModelCheckFactory.create( successful=False, @@ -1058,9 +1017,9 @@ def test_violation_detail_page_superuser_override_one_of_two_violation( url = reverse( "workbaskets:workbasket-ui-violation-detail", - kwargs={"wb_pk": session_workbasket.pk, "pk": model_check.pk}, + kwargs={"wb_pk": user_workbasket.pk, "pk": model_check.pk}, ) - response = superuser_client.post(url, data={"action": "delete"}) + response = client.post(url, data={"action": "delete"}) assert response.status_code == 302 redirect_url = reverse("workbaskets:workbasket-ui-violations") @@ -1079,8 +1038,9 @@ def test_violation_detail_page_superuser_override_one_of_two_violation( def test_violation_detail_page_non_superuser_override_violation( - valid_user_client, - session_workbasket, + valid_user, + client, + user_workbasket, ): """Ensure a user without superuser status is unable to override a TrackedModelCheck.""" @@ -1089,16 +1049,16 @@ def test_violation_detail_page_non_superuser_override_violation( successful=False, transaction_check__successful=False, ) - model_check.transaction_check.transaction.workbasket.save_to_session( - valid_user_client.session, + client.force_login(valid_user) + model_check.transaction_check.transaction.workbasket.assign_to_user( + valid_user, ) - valid_user_client.session.save() url = reverse( "workbaskets:workbasket-ui-violation-detail", - kwargs={"wb_pk": session_workbasket.pk, "pk": model_check.pk}, + kwargs={"wb_pk": user_workbasket.pk, "pk": model_check.pk}, ) - response = valid_user_client.post(url, data={"action": "delete"}) + response = client.post(url, data={"action": "delete"}) assert response.status_code == 302 model_check.refresh_from_db() @@ -1107,8 +1067,8 @@ def test_violation_detail_page_non_superuser_override_violation( @pytest.fixture -def setup(session_workbasket, valid_user_client): - with session_workbasket.new_transaction() as transaction: +def setup(user_workbasket, valid_user_client): + with user_workbasket.new_transaction() as transaction: good = factories.GoodsNomenclatureFactory.create(transaction=transaction) measure = factories.MeasureFactory.create(transaction=transaction) geo_area = factories.GeographicalAreaFactory.create(transaction=transaction) @@ -1133,17 +1093,9 @@ def setup(session_workbasket, valid_user_client): model=obj, successful=False, ) - session = valid_user_client.session - session["workbasket"] = { - "id": session_workbasket.pk, - "status": session_workbasket.status, - "title": session_workbasket.title, - "error_count": session_workbasket.tracked_model_check_errors.count(), - } - session.save() -def test_violation_list_page_sorting_date(setup, valid_user_client, session_workbasket): +def test_violation_list_page_sorting_date(setup, valid_user_client, user_workbasket): """Tests the sorting of the queryset when GET params are set.""" url = reverse( "workbaskets:workbasket-ui-violations", @@ -1152,7 +1104,7 @@ def test_violation_list_page_sorting_date(setup, valid_user_client, session_work assert response.status_code == 200 - checks = session_workbasket.tracked_model_check_errors + checks = user_workbasket.tracked_model_check_errors soup = BeautifulSoup(str(response.content), "html.parser") activity_dates = [ @@ -1173,7 +1125,7 @@ def test_violation_list_page_sorting_date(setup, valid_user_client, session_work def test_violation_list_page_sorting_model_name( setup, valid_user_client, - session_workbasket, + user_workbasket, ): """Tests the sorting of the queryset when GET params are set.""" url = reverse( @@ -1183,7 +1135,7 @@ def test_violation_list_page_sorting_model_name( assert response.status_code == 200 - checks = session_workbasket.tracked_model_check_errors + checks = user_workbasket.tracked_model_check_errors soup = BeautifulSoup(str(response.content), "html.parser") activity_dates = [ @@ -1204,7 +1156,7 @@ def test_violation_list_page_sorting_model_name( def test_violation_list_page_sorting_check_name( setup, valid_user_client, - session_workbasket, + user_workbasket, ): """Tests the sorting of the queryset when GET params are set.""" url = reverse( @@ -1214,7 +1166,7 @@ def test_violation_list_page_sorting_check_name( assert response.status_code == 200 - checks = session_workbasket.tracked_model_check_errors + checks = user_workbasket.tracked_model_check_errors soup = BeautifulSoup(str(response.content), "html.parser") rule_codes = [ @@ -1232,7 +1184,7 @@ def test_violation_list_page_sorting_check_name( def test_violation_list_page_sorting_ignores_invalid_params( setup, valid_user_client, - session_workbasket, + user_workbasket, ): """Tests that the page doesn't break if invalid params are sent.""" url = reverse( @@ -1267,14 +1219,14 @@ def test_workbasket_detail_views_without_view_permission(url_name, client): def test_workbasket_detail_view_displays_workbasket_details( valid_user_client, - session_workbasket, + user_workbasket, ): """Tests that `WorkBasketDetailView` returns 200 and displays workbasket details in table.""" url = reverse( "workbaskets:workbasket-ui-detail", - kwargs={"pk": session_workbasket.pk}, + kwargs={"pk": user_workbasket.pk}, ) response = valid_user_client.get(url) assert response.status_code == 200 @@ -1283,23 +1235,23 @@ def test_workbasket_detail_view_displays_workbasket_details( table = soup.select("table")[0] row_text = [row.text for row in table.findChildren("td")] - assert session_workbasket.get_status_display().upper() in row_text[0] - assert str(session_workbasket.id) in row_text[1] - assert session_workbasket.title in row_text[2] - assert session_workbasket.reason in row_text[3] - assert str(session_workbasket.tracked_models.count()) in row_text[4] - assert session_workbasket.created_at.strftime("%d %b %y %H:%M") in row_text[5] - assert session_workbasket.updated_at.strftime("%d %b %y %H:%M") in row_text[6] + assert user_workbasket.get_status_display().upper() in row_text[0] + assert str(user_workbasket.id) in row_text[1] + assert user_workbasket.title in row_text[2] + assert user_workbasket.reason in row_text[3] + assert str(user_workbasket.tracked_models.count()) in row_text[4] + assert user_workbasket.created_at.strftime("%d %b %y %H:%M") in row_text[5] + assert user_workbasket.updated_at.strftime("%d %b %y %H:%M") in row_text[6] -def test_workbasket_changes_view_without_change_permission(client, session_workbasket): +def test_workbasket_changes_view_without_change_permission(client, user_workbasket): """Tests that `WorkBasketChangesView` displays changes in a workbasket without the ability to remove items to users without `change_workbasket` permission.""" url = reverse( "workbaskets:workbasket-ui-changes", - kwargs={"pk": session_workbasket.pk}, + kwargs={"pk": user_workbasket.pk}, ) user = factories.UserFactory.create() user.user_permissions.add(Permission.objects.get(codename="view_workbasket")) @@ -1314,21 +1266,21 @@ def test_workbasket_changes_view_without_change_permission(client, session_workb remove_button = page.find("button", value="remove-selected") assert len(columns) == 5 - assert len(rows) == session_workbasket.tracked_models.count() + assert len(rows) == user_workbasket.tracked_models.count() assert not checkboxes assert not remove_button def test_workbasket_changes_view_with_change_permission( valid_user_client, - session_workbasket, + user_workbasket, ): """Tests that `WorkBasketChangesView` displays changes in a workbasket with the ability to remove items to users with `change_workbasket` permission.""" url = reverse( "workbaskets:workbasket-ui-changes", - kwargs={"pk": session_workbasket.pk}, + kwargs={"pk": user_workbasket.pk}, ) response = valid_user_client.get(url) assert response.status_code == 200 @@ -1340,7 +1292,7 @@ def test_workbasket_changes_view_with_change_permission( remove_button = page.find("button", value="remove-selected") assert len(columns) == 6 - assert len(rows) == session_workbasket.tracked_models.count() + assert len(rows) == user_workbasket.tracked_models.count() assert checkboxes assert remove_button @@ -1698,7 +1650,7 @@ def test_workbasket_transaction_order_first_or_last_transaction_in_workbasket(): def test_successfully_delete_workbasket( valid_user_client, valid_user, - session_empty_workbasket, + user_empty_workbasket, ): """Test that deleting an empty workbasket by a user having the necessary `workbasket.can_delete` permssion.""" @@ -1706,7 +1658,7 @@ def test_successfully_delete_workbasket( valid_user.user_permissions.add( Permission.objects.get(codename="delete_workbasket"), ) - workbasket_pk = session_empty_workbasket.pk + workbasket_pk = user_empty_workbasket.pk delete_url = reverse( "workbaskets:workbasket-ui-delete", kwargs={"pk": workbasket_pk}, @@ -1739,12 +1691,12 @@ def test_successfully_delete_workbasket( def test_delete_workbasket_missing_user_permission( valid_user_client, - session_empty_workbasket, + user_empty_workbasket, ): """Test that attempts to access the delete workbasket view and delete a workbasket fails for a user without the necessary permissions.""" - workbasket_pk = session_empty_workbasket.pk + workbasket_pk = user_empty_workbasket.pk url = reverse( "workbaskets:workbasket-ui-delete", kwargs={"pk": workbasket_pk}, @@ -1764,15 +1716,15 @@ def test_delete_workbasket_missing_user_permission( def test_delete_nonempty_workbasket( valid_user_client, valid_user, - session_workbasket, + user_workbasket, ): """Test that attempts to delete a non-empty workbasket fails.""" valid_user.user_permissions.add( Permission.objects.get(codename="delete_workbasket"), ) - workbasket_pk = session_workbasket.pk - workbasket_object_count = session_workbasket.tracked_models.count() + workbasket_pk = user_workbasket.pk + workbasket_object_count = user_workbasket.tracked_models.count() delete_url = reverse( "workbaskets:workbasket-ui-delete", kwargs={"pk": workbasket_pk}, @@ -1796,7 +1748,7 @@ def test_delete_nonempty_workbasket( def test_application_access_after_workbasket_delete( valid_user_client, - session_empty_workbasket, + user_empty_workbasket, ): """ Test that after deleting a user's 'current' workbasket, the user is still @@ -1807,13 +1759,13 @@ def test_application_access_after_workbasket_delete( ensuring application avoids 500-series errors under the above conditions. """ - workbasket_pk = session_empty_workbasket.pk + workbasket_pk = user_empty_workbasket.pk url = reverse("workbaskets:workbasket-ui-list") response = valid_user_client.get(url) page = BeautifulSoup(response.content, "html.parser") # A workbasket link should be available in the header nav bar before - # session workbasket deletion. + # user workbasket deletion. assert response.status_code == 200 assert ( @@ -1821,11 +1773,11 @@ def test_application_access_after_workbasket_delete( in page.select("header nav a.workbasket-link")[0].text ) - session_empty_workbasket.delete() + user_empty_workbasket.delete() response = valid_user_client.get(url) page = BeautifulSoup(response.content, "html.parser") - # No workbasket link should exist in the header nav bar after session + # No workbasket link should exist in the header nav bar after user # workbasket deletion. assert response.status_code == 200 assert not page.select("header nav a.workbasket-link") @@ -1867,25 +1819,25 @@ def test_workbasket_delete_previously_queued_workbasket( assert workbasket.status == WorkflowStatus.ARCHIVED -def test_workbasket_compare_200(valid_user_client, session_workbasket): +def test_workbasket_compare_200(valid_user_client, user_workbasket): url = reverse("workbaskets:workbasket-check-ui-compare") response = valid_user_client.get(url) assert response.status_code == 200 -def test_workbasket_compare_prev_uploaded(valid_user_client, session_workbasket): +def test_workbasket_compare_prev_uploaded(valid_user_client, user_workbasket): factories.GoodsNomenclatureFactory() factories.GoodsNomenclatureFactory() - factories.DataUploadFactory(workbasket=session_workbasket) + factories.DataUploadFactory(workbasket=user_workbasket) url = reverse("workbaskets:workbasket-check-ui-compare") response = valid_user_client.get(url) assert "Worksheet data" in response.content.decode(response.charset) -def test_workbasket_update_prev_uploaded(valid_user_client, session_workbasket): +def test_workbasket_update_prev_uploaded(valid_user_client, user_workbasket): factories.GoodsNomenclatureFactory() factories.GoodsNomenclatureFactory() - data_upload = factories.DataUploadFactory(workbasket=session_workbasket) + data_upload = factories.DataUploadFactory(workbasket=user_workbasket) url = reverse("workbaskets:workbasket-check-ui-compare") data = { "data": ( @@ -1899,7 +1851,7 @@ def test_workbasket_update_prev_uploaded(valid_user_client, session_workbasket): assert data_upload.raw_data == data["data"] -def test_workbasket_compare_form_submit_302(valid_user_client, session_workbasket): +def test_workbasket_compare_form_submit_302(valid_user_client, user_workbasket): url = reverse("workbaskets:workbasket-check-ui-compare") data = { "data": ( @@ -1914,14 +1866,14 @@ def test_workbasket_compare_form_submit_302(valid_user_client, session_workbaske def test_workbasket_compare_found_measures( valid_user_client, - session_workbasket, + user_workbasket, date_ranges, duty_sentence_parser, percent_or_amount, ): commodity = factories.GoodsNomenclatureFactory() - with session_workbasket.new_transaction(): + with user_workbasket.new_transaction(): measure = factories.MeasureFactory( valid_between=date_ranges.normal, goods_nomenclature=commodity, @@ -2008,7 +1960,7 @@ def make_goods_import_batch(importer_storage, **kwargs): def test_review_goods_notification_button( successful_business_rules_setup, valid_user_client, - session_workbasket, + user_workbasket, import_batch_factory, visable, ): @@ -2018,7 +1970,7 @@ def test_review_goods_notification_button( import_batch = import_batch_factory() if import_batch: - import_batch.workbasket_id = session_workbasket.id + import_batch.workbasket_id = user_workbasket.id if isinstance(import_batch, ImportBatch): import_batch.save() @@ -2050,3 +2002,47 @@ def mock_xlsx_open(filename, mode): assert notify_button else: assert not notify_button + + +def test_no_active_workbasket_view(valid_user_client): + """Test that NoActiveWorkBasket view returns 200 and displays headings and + buttons.""" + response = valid_user_client.get(reverse("workbaskets:no-active-workbasket")) + + soup = BeautifulSoup(str(response.content), "html.parser") + message = soup.find("h1", text="You need an active workbasket to access this page") + select_a_new_workbasket = soup.find( + "a", + href=reverse("workbaskets:workbasket-ui-list"), + ) + return_to_homepage = soup.find("a", href=reverse("home")) + + assert response.status_code == 200 + assert message + assert select_a_new_workbasket + assert return_to_homepage + + +@pytest.mark.parametrize( + "workbasket_factory", + [ + lambda: None, + factories.ArchivedWorkBasketFactory, + factories.QueuedWorkBasketFactory, + factories.PublishedWorkBasketFactory, + ], +) +def test_require_current_workbasket_redirect(workbasket_factory, client, valid_user): + """Test that views using require_current_workbasket decorator redirect to + NoActiveWorkBasket view if the user's current workbasket is no longer in + editing state.""" + client.force_login(valid_user) + + valid_user.current_workbasket == workbasket_factory() + valid_user.save() + + # view that has require_current_workbasket decorator + response = client.get(reverse("workbaskets:current-workbasket")) + + assert response.status_code == 302 + assert response.url == reverse("workbaskets:no-active-workbasket") diff --git a/workbaskets/urls.py b/workbaskets/urls.py index 6a13b872d..bb56b2062 100644 --- a/workbaskets/urls.py +++ b/workbaskets/urls.py @@ -136,6 +136,11 @@ ui_views.WorkBasketCompare.as_view(), name="workbasket-check-ui-compare", ), + path( + "no-active-workbasket/", + ui_views.NoActiveWorkBasket.as_view(), + name="no-active-workbasket", + ), path( f"/", ui_views.WorkBasketDetailView.as_view(), diff --git a/workbaskets/views/decorators.py b/workbaskets/views/decorators.py index 4f4f7f6ac..4466be210 100644 --- a/workbaskets/views/decorators.py +++ b/workbaskets/views/decorators.py @@ -1,23 +1,21 @@ from functools import wraps +from django.shortcuts import redirect + from workbaskets.models import WorkBasket def require_current_workbasket(view_func): - """View decorator which redirects user to choose or create a workbasket + """View decorator which redirects user to select a new current workbasket before continuing.""" @wraps(view_func) def check_for_current_workbasket(request, *args, **kwargs): - if WorkBasket.current(request) is None: - workbasket = WorkBasket.objects.editable().last() - if not workbasket: - workbasket = WorkBasket.objects.create( - author=request.user, - ) - - workbasket.save_to_session(request.session) - - return view_func(request, *args, **kwargs) + try: + if WorkBasket.current(request): + return view_func(request, *args, **kwargs) + return redirect("workbaskets:no-active-workbasket") + except WorkBasket.DoesNotExist: + return redirect("workbaskets:no-active-workbasket") return check_for_current_workbasket diff --git a/workbaskets/views/ui.py b/workbaskets/views/ui.py index 6d55a709d..82ae76f73 100644 --- a/workbaskets/views/ui.py +++ b/workbaskets/views/ui.py @@ -102,7 +102,7 @@ def form_valid(self, form): self.object = form.save(commit=False) self.object.author = user self.object.save() - self.object.save_to_session(self.request.session) + self.object.assign_to_user(self.request.user) return redirect( reverse( "workbaskets:workbasket-ui-confirm-create", @@ -187,7 +187,7 @@ def post(self, request, *args, **kwargs): workbasket.restore() workbasket.save() - workbasket.save_to_session(request.session) + workbasket.assign_to_user(request.user) if workbasket_tab: view = workbasket_tab_map[workbasket_tab] @@ -275,7 +275,7 @@ class WorkBasketChangesConfirmDelete(TemplateView): def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - context["session_workbasket"] = WorkBasket.current(self.request) + context["user_workbasket"] = WorkBasket.current(self.request) context["view_workbasket"] = WorkBasket.objects.get(pk=self.kwargs["pk"]) return context @@ -437,7 +437,7 @@ def get_context_data(self, **kwargs): if result.status != "SUCCESS": context.update({"rule_check_in_progress": True}) else: - self.workbasket.save_to_session(self.request.session) + self.workbasket.assign_to_user(self.request.user) num_completed, total = self.workbasket.rule_check_progress() context.update( @@ -1221,7 +1221,7 @@ def get_context_data(self, **kwargs): if result.status != "SUCCESS": context.update({"rule_check_in_progress": True}) else: - self.workbasket.save_to_session(self.request.session) + self.workbasket.assign_to_user(self.request.user) num_completed, total = self.workbasket.rule_check_progress() context.update( @@ -1252,7 +1252,7 @@ def get_queryset(self): def get_context_data(self, *args, **kwargs): context = super().get_context_data(*args, **kwargs) - context["session_workbasket"] = WorkBasket.current(self.request) + context["user_workbasket"] = WorkBasket.current(self.request) context["workbasket"] = self.workbasket return context @@ -1300,7 +1300,7 @@ def get_context_data(self, *args, **kwargs): context = super().get_context_data(*args, **kwargs) context["tab_page_title"] = "Review commodities" context["selected_tab"] = "commodities" - context["session_workbasket"] = WorkBasket.current(self.request) + context["user_workbasket"] = WorkBasket.current(self.request) context["workbasket"] = self.workbasket context["report_lines"] = [] context["import_batch_pk"] = None @@ -1366,7 +1366,7 @@ def get_context_data(self, *args, **kwargs): context["import_batch_pk"] = import_batch.pk # notifications only relevant to a goods import - if context["workbasket"] == context["session_workbasket"]: + if context["workbasket"] == context["user_workbasket"]: context["unsent_notification"] = ( import_batch.goods_import and not Notification.objects.filter( @@ -1544,3 +1544,10 @@ def get_context_data(self, *args, **kwargs): context["selected_tab"] = "regulations" context["tab_template"] = "includes/regulations/list.jinja" return context + + +class NoActiveWorkBasket(TemplateView): + """Redirect endpoint for users without an active workbasket and views that + require one.""" + + template_name = "workbaskets/no_active_workbasket.jinja" From 0dae8b575f61782cf8637fa2d1ca0d2ed83bb4f3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 30 Jan 2024 09:35:53 +0000 Subject: [PATCH 029/118] Bump aiohttp from 3.9.1 to 3.9.2 (#1142) Bumps [aiohttp](https://github.com/aio-libs/aiohttp) from 3.9.1 to 3.9.2. - [Release notes](https://github.com/aio-libs/aiohttp/releases) - [Changelog](https://github.com/aio-libs/aiohttp/blob/master/CHANGES.rst) - [Commits](https://github.com/aio-libs/aiohttp/compare/v3.9.1...v3.9.2) --- updated-dependencies: - dependency-name: aiohttp dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index ada75aab1..ab3c422da 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -aiohttp==3.9.1 +aiohttp==3.9.2 apsw==3.43.0.0 allure-pytest-bdd==2.8.40 api-client==1.3.0 From 9fe334d2cb4ba4dcaa9617f0556c1f6c2f2a09af Mon Sep 17 00:00:00 2001 From: Doug Mills <110824173+dougmills-DIT@users.noreply.github.com> Date: Tue, 6 Feb 2024 11:32:18 +0000 Subject: [PATCH 030/118] update importer model matching to account for end dated objects. (#1146) * update importer model matching to account for end dated objects. * update importer model matching to account for end dated objects. --- taric_parsers/parsers/taric_parser.py | 29 +++++++++++++++++++++++---- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/taric_parsers/parsers/taric_parser.py b/taric_parsers/parsers/taric_parser.py index 9f0b93134..5987917c2 100644 --- a/taric_parsers/parsers/taric_parser.py +++ b/taric_parsers/parsers/taric_parser.py @@ -548,9 +548,24 @@ def get_linked_model( filtered_models = [] for model in models: if hasattr(model, "valid_between"): - # Check if this record is current - if date.today() in model.valid_between: - filtered_models.append(model) + # Check if this record is the most current + if len(filtered_models) > 0: + # if current filtered model has no end date, don't replace + if filtered_models[0].valid_between.upper_inf: + continue + # If the model does not have an end date, replace filtered models with non end dated model + elif model.valid_between.upper_inf: + filtered_models = [model] + elif ( + filtered_models[0].valid_between.upper + > model.valid_between.upper + ): + continue + else: + filtered_models = [model] + + elif len(filtered_models) == 0: + filtered_models = [model] elif hasattr(model, "validity_start"): # check for latest @@ -565,9 +580,15 @@ def get_linked_model( if len(filtered_models) == 1: return filtered_models[0] + if len(filtered_models) > 1: + raise Exception( + f"multiple models matched query for {related_model.__name__} using {fields_and_values}, please check data and query", + ) + raise Exception( - f"multiple models matched query for {related_model.__name__} using {fields_and_values}, please check data and query", + f"no models matched query for {related_model.__name__} using {fields_and_values}, please check data and query", ) + else: return None From 53cb3acb03c9646f2956bf95779dceaa6e65e044 Mon Sep 17 00:00:00 2001 From: Doug Mills <110824173+dougmills-DIT@users.noreply.github.com> Date: Wed, 7 Feb 2024 10:15:15 +0000 Subject: [PATCH 031/118] Tp2000 1211 (#1148) * update govuk dependency since its been deleted at source * update govuk dependency since its been deleted at source --- govuk_frontend_jinja/__init__.py | 0 govuk_frontend_jinja/flask_ext.py | 29 +++ govuk_frontend_jinja/templates.py | 302 ++++++++++++++++++++++++++++++ requirements.txt | 1 - 4 files changed, 331 insertions(+), 1 deletion(-) create mode 100644 govuk_frontend_jinja/__init__.py create mode 100644 govuk_frontend_jinja/flask_ext.py create mode 100644 govuk_frontend_jinja/templates.py diff --git a/govuk_frontend_jinja/__init__.py b/govuk_frontend_jinja/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/govuk_frontend_jinja/flask_ext.py b/govuk_frontend_jinja/flask_ext.py new file mode 100644 index 000000000..d84e6781b --- /dev/null +++ b/govuk_frontend_jinja/flask_ext.py @@ -0,0 +1,29 @@ +from flask.templating import Environment as FlaskEnvironment +from jinja2 import select_autoescape + +from govuk_frontend_jinja.templates import Environment as NunjucksEnvironment +from govuk_frontend_jinja.templates import NunjucksExtension +from govuk_frontend_jinja.templates import NunjucksUndefined + + +class Environment(NunjucksEnvironment, FlaskEnvironment): + pass + + +def init_govuk_frontend(app): + """ + Use the govuk_frontend_jinja Jinja environment in a Flask app. + + >>> from flask import Flask + >>> app = Flask("cheeseshop_service") + >>> init_govuk_frontend(app) + """ + app.jinja_environment = Environment + app.select_jinja_autoescape = select_autoescape( + ("html", "htm", "xml", "xhtml", "njk"), + ) + jinja_options = app.jinja_options.copy() + jinja_options["extensions"].append(NunjucksExtension) + jinja_options["undefined"] = NunjucksUndefined + app.jinja_options = jinja_options + return app diff --git a/govuk_frontend_jinja/templates.py b/govuk_frontend_jinja/templates.py new file mode 100644 index 000000000..3b109ec74 --- /dev/null +++ b/govuk_frontend_jinja/templates.py @@ -0,0 +1,302 @@ +import builtins +import os.path as path +import re +from collections.abc import Sized + +import jinja2 +import jinja2.ext +from jinja2.lexer import Token +from markupsafe import Markup + + +def njk_to_j2(template): + # Some component templates (such as radios) use `items` as the key of + # an object element. However `items` is also the name of a dictionary + # method in Python, and Jinja2 will prefer to return this attribute + # over the dict item. Handle specially. + template = re.sub(r"\.items\b", ".items__njk", template) + + # Some component templates (such as radios) append the loop index to a + # string. As the loop index is an integer this causes a TypeError in + # Python. Jinja2 has an operator `~` for string concatenation that + # converts integers to strings. + template = template.replace("+ loop.index", "~ loop.index") + + # The Character Count component in version 3 concatenates the word count + # with the hint text. As the word count is an integer this causes a + # TypeError in Python. Jinja2 has an operator `~` for string + # concatenation that converts integers to strings. + template = template.replace( + "+ (params.maxlength or params.maxwords) +", + "~ (params.maxlength or params.maxwords) ~", + ) + + # Nunjucks uses elseif, Jinja uses elif + template = template.replace("elseif", "elif") + + # Some component templates (such as input) call macros with params as + # an object which has unqoted keys. This causes Jinja to silently + # ignore the values. + template = re.sub( + r"""^([ ]*)([^ '"#\r\n:]+?)\s*:""", + r"\1'\2':", + template, + flags=re.M, + ) + + # govukFieldset can accept a call block argument, however the Jinja + # compiler does not detect this as the macro body is included from + # the template file. A workaround is to patch the declaration of the + # macro to include an explicit caller argument. + template = template.replace( + "macro govukFieldset(params)", + "macro govukFieldset(params, caller=none)", + ) + + # Many components feature an attributes field, which is supposed to be + # a dictionary. In the template for these components, the keys and values + # are iterated. In Python, the default iterator for a dict is .keys(), but + # we want .items(). + # This only works because our undefined implements .items() + # We've tested this explicitly with: govukInput, govukCheckbox, govukTable, + # govukSummaryList + template = re.sub( + r"for attribute, value in (params|item|cell|action).attributes", + r"for attribute, value in \1.attributes.items()", + template, + flags=re.M, + ) + + # Some templates try to set a variable in an outer block, which is not + # supported in Jinja. We create a namespace in those templates to get + # around this. + template = re.sub( + r"""^([ ]*)({% set describedBy =( params.*describedBy if params.*describedBy else)? "" %})""", + r"\1{%- set nonlocal = namespace() -%}\n\1\2", + template, + flags=re.M, + ) + # Change any references to describedBy to be nonlocal.describedBy, + # unless describedBy is a dictionary key (i.e. quoted or dotted). + template = re.sub(r"""(?>> foo = ChainableUndefined(name='foo') + >>> str(foo.bar['baz']) + '' + >>> foo.bar['baz'] + 42 + Traceback (most recent call last): + ... + jinja2.exceptions.UndefinedError: 'foo' is undefined + """ + return self + + __getitem__ = __getattr__ + + # Allow treating undefined as an (empty) dictionary. + # This works because Undefined is an iterable. + def items(self): + return self + + # Allow escaping with Markup. This is required when + # autoescape is enabled. Debugging this issue was + # annoying; the error messages were not clear as to + # the cause of the issue (see upstream pull request + # for info https://github.com/pallets/jinja/pull/1047) + def __html__(self): + return str(self) + + # attempt to behave a bit like js's `undefined` when concatenation is attempted + def __add__(self, other): + if isinstance(other, str): + return "undefined" + other + return super().__add__(other) + + def __radd__(self, other): + if isinstance(other, str): + return other + "undefined" + return super().__radd__(other) + + +class NunjucksCodeGenerator(jinja2.compiler.CodeGenerator): + def visit_CondExpr(self, node, frame): + if not (self.filename or "").endswith(".njk"): + return super().visit_CondExpr(node, frame) + + def write_expr2(): + if node.expr2 is not None: + return self.visit(node.expr2, frame) + # rather than complaining about a missing else + # clause we just assume it to be the empty + # string for nunjucks compatibility + return self.write('""') + + self.write("(") + self.visit(node.expr1, frame) + self.write(" if ") + self.visit(node.test, frame) + self.write(" else ") + write_expr2() + self.write(")") + + +_njk_signature = "__njk" +_builtin_function_or_method_type = type({}.keys) + + +class Environment(jinja2.Environment): + code_generator_class = NunjucksCodeGenerator + + def __init__(self, *args, **kwargs): + kwargs.setdefault("extensions", [NunjucksExtension]) + kwargs.setdefault("undefined", NunjucksUndefined) + super().__init__(*args, **kwargs) + self.filters["indent_njk"] = indent_njk + + def join_path(self, template, parent): + """Enable the use of relative paths in template import statements.""" + if template.startswith(("./", "../")): + return path.normpath(path.join(path.dirname(parent), template)) + else: + return template + + def _handle_njk(method_name): + def inner(self, obj, argument): + if isinstance(argument, str) and argument.endswith(_njk_signature): + # a njk-originated access will always be assuming a dict lookup before an attr + final_method_name = "getitem" + final_argument = argument[: -len(_njk_signature)] + else: + final_argument = argument + final_method_name = method_name + + # pleasantly surprised that super() works in this context + retval = builtins.getattr(super(), final_method_name)(obj, final_argument) + + if ( + argument == f"length{_njk_signature}" + and isinstance(retval, jinja2.runtime.Undefined) + and isinstance(obj, Sized) + ): + return len(obj) + if ( + isinstance(argument, str) + and argument.endswith(_njk_signature) + and isinstance(retval, _builtin_function_or_method_type) + ): + # the lookup has probably gone looking for attributes and found a builtin method. because + # any njk-originated lookup will have been made to prefer dict lookups over attributes, we + # can be fairly sure there isn't a dict key matching this - so we should just call this a + # failure. + return self.undefined(obj=obj, name=final_argument) + return retval + + return inner + + getitem = _handle_njk("getitem") + + getattr = _handle_njk("getattr") diff --git a/requirements.txt b/requirements.txt index ab3c422da..1558457d4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -39,7 +39,6 @@ factory-boy==3.2.0 flower==1.2.0 freezegun==1.1.0 gevent==23.9.1 -govuk-frontend-jinja @ git+https://github.com/alphagov/govuk-frontend-jinja.git@15845e4cca3a05df72c6e13ec6a7e35acc682f52 govuk-tech-docs-sphinx-theme==1.0.0 gunicorn==20.1.0 Jinja2==3.1.3 From 641a434db6cf5a926b3894ac660cc132cd7980c4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 8 Feb 2024 09:41:47 +0000 Subject: [PATCH 032/118] Bump django from 3.2.23 to 3.2.24 (#1150) Bumps [django](https://github.com/django/django) from 3.2.23 to 3.2.24. - [Commits](https://github.com/django/django/compare/3.2.23...3.2.24) --- updated-dependencies: - dependency-name: django dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 1558457d4..5db4dd0cc 100644 --- a/requirements.txt +++ b/requirements.txt @@ -11,7 +11,7 @@ coverage[toml]==5.4 crispy-forms-gds @ git+https://github.com/uktrade/crispy-forms-gds.git@b50168d0e23ffacbdd30e7819ec8f9a08e055c8e defusedxml==0.7.* dj-database-url==0.5.0 -django==3.2.23 +django==3.2.24 django-chunk-upload-handlers==0.0.13 django-crispy-forms==1.12.0 django-dotenv==1.4.2 From 4db2ff3d8205b846a2c25ef392a1144753973228 Mon Sep 17 00:00:00 2001 From: A Gleeson Date: Thu, 8 Feb 2024 13:09:53 +0000 Subject: [PATCH 033/118] Tp2000 652 force rule check after real edit (#1130) * added a check that if tracked models have been updated since the last checks business rules need run again * data migration to add timestamps to tracked models and transaction checks * tests for real edits * tests for data migrations --- checks/migrations/0006_auto_20231211_1642.py | 32 +++++++++++++ .../0007_transactioncheck_timestamps.py | 44 ++++++++++++++++++ checks/models.py | 2 +- checks/tests/test_migrations.py | 45 +++++++++++++++++++ .../0009_tracked_model_timestamp.py | 28 ++++++++++++ .../0010_set_tracked_model_datetime.py | 38 ++++++++++++++++ common/models/trackedmodel.py | 5 ++- common/tests/test_migrations.py | 32 +++++++++++++ conftest.py | 40 +++++++++++++++++ settings/common.py | 2 + workbaskets/models.py | 21 ++++++++- workbaskets/tests/test_views.py | 44 +++++++++++++++++- 12 files changed, 329 insertions(+), 4 deletions(-) create mode 100644 checks/migrations/0006_auto_20231211_1642.py create mode 100644 checks/migrations/0007_transactioncheck_timestamps.py create mode 100644 checks/tests/test_migrations.py create mode 100644 common/migrations/0009_tracked_model_timestamp.py create mode 100644 common/migrations/0010_set_tracked_model_datetime.py diff --git a/checks/migrations/0006_auto_20231211_1642.py b/checks/migrations/0006_auto_20231211_1642.py new file mode 100644 index 000000000..a417c684c --- /dev/null +++ b/checks/migrations/0006_auto_20231211_1642.py @@ -0,0 +1,32 @@ +# Generated by Django 3.2.23 on 2023-12-11 16:42 + +import django.utils.timezone +from django.db import migrations +from django.db import models + +epoch_time = 0000000000 + + +class Migration(migrations.Migration): + dependencies = [ + ("checks", "0005_trackedmodelcheck_processing_time"), + ("common", "0010_set_tracked_model_datetime"), + ("workbaskets", "0008_datarow_dataupload"), + ] + + operations = [ + migrations.AddField( + model_name="transactioncheck", + name="created_at", + field=models.DateTimeField( + auto_now_add=True, + default=django.utils.timezone.datetime.fromtimestamp(epoch_time), + ), + preserve_default=False, + ), + migrations.AddField( + model_name="transactioncheck", + name="updated_at", + field=models.DateTimeField(auto_now=True), + ), + ] diff --git a/checks/migrations/0007_transactioncheck_timestamps.py b/checks/migrations/0007_transactioncheck_timestamps.py new file mode 100644 index 000000000..f259965e3 --- /dev/null +++ b/checks/migrations/0007_transactioncheck_timestamps.py @@ -0,0 +1,44 @@ +# Generated by Django 3.2.18 on 2023-03-28 15:39 + +from django.db import migrations +from django.db.models import Max +from django.db.models import Min +from django.db.transaction import atomic + +from workbaskets.validators import WorkflowStatus + + +@atomic +def generate_timestamps(apps, schema_editor): + TransactionCheck = apps.get_model("checks", "transactioncheck") + + transaction_checks_to_update = TransactionCheck.objects.filter( + transaction__workbasket__status=WorkflowStatus.EDITING, + completed=True, + ) + + for check in transaction_checks_to_update: + if not check.model_checks.all(): + continue + aggregated_checks = check.model_checks.aggregate( + first_created_at=Min("created_at"), + last_updated_at=Max("updated_at"), + ) + check.completed = True + check.successful = False + check.created_at = aggregated_checks["first_created_at"] + check.updated_at = aggregated_checks["last_updated_at"] + check.save() + + +class Migration(migrations.Migration): + dependencies = [ + ("checks", "0006_auto_20231211_1642"), + ] + + operations = [ + migrations.RunPython( + generate_timestamps, + reverse_code=migrations.RunPython.noop, + ), + ] diff --git a/checks/models.py b/checks/models.py index 6d061c863..0bec9a901 100644 --- a/checks/models.py +++ b/checks/models.py @@ -7,7 +7,7 @@ from common.models.transactions import Transaction -class TransactionCheck(models.Model): +class TransactionCheck(TimestampedMixin): """ Represents an in-progress or completed check of a transaction for correctness. diff --git a/checks/tests/test_migrations.py b/checks/tests/test_migrations.py new file mode 100644 index 000000000..cfd96a3cb --- /dev/null +++ b/checks/tests/test_migrations.py @@ -0,0 +1,45 @@ +import pytest + +from checks.models import TransactionCheck +from checks.tests.factories import TrackedModelCheckFactory + + +@pytest.mark.django_db() +def test_timestamp_migration(migrator): + migrator.reset() + old_state = migrator.apply_initial_migration( + ( + "checks", + "0006_auto_20231211_1642", + ), + ) + migrator.apply_tested_migration( + ( + "tests", + "0003_auto_20210714_1522", + ), + ) + tracked_model_check_1 = TrackedModelCheckFactory.create( + transaction_check__completed=True, + transaction_check__successful=True, + successful=True, + ) + + transaction_check = TransactionCheck.objects.get( + pk=tracked_model_check_1.transaction_check.transaction.pk, + ) + + migrator.apply_tested_migration( + ( + "checks", + "0007_transactioncheck_timestamps", + ), + ) + + new_transaction_check = TransactionCheck.objects.get(pk=transaction_check.pk) + assert new_transaction_check.completed == True + assert new_transaction_check.successful == False + assert new_transaction_check.created_at == tracked_model_check_1.created_at + assert new_transaction_check.updated_at >= tracked_model_check_1.updated_at + + migrator.reset() diff --git a/common/migrations/0009_tracked_model_timestamp.py b/common/migrations/0009_tracked_model_timestamp.py new file mode 100644 index 000000000..e9954cdd4 --- /dev/null +++ b/common/migrations/0009_tracked_model_timestamp.py @@ -0,0 +1,28 @@ +# Generated by Django 3.2.23 on 2023-12-11 16:42 + +import django.utils.timezone +from django.db import migrations +from django.db import models + + +class Migration(migrations.Migration): + dependencies = [ + ("common", "0008_user_current_workbasket"), + ] + + operations = [ + migrations.AddField( + model_name="trackedmodel", + name="created_at", + field=models.DateTimeField( + auto_now_add=True, + default=django.utils.timezone.now, + ), + preserve_default=False, + ), + migrations.AddField( + model_name="trackedmodel", + name="updated_at", + field=models.DateTimeField(auto_now=True), + ), + ] diff --git a/common/migrations/0010_set_tracked_model_datetime.py b/common/migrations/0010_set_tracked_model_datetime.py new file mode 100644 index 000000000..17eb282d0 --- /dev/null +++ b/common/migrations/0010_set_tracked_model_datetime.py @@ -0,0 +1,38 @@ +# Generated by Django 3.2.18 on 2023-03-28 15:39 +import logging + +from django.conf import settings +from django.core.paginator import Paginator +from django.db import migrations +from django.db.transaction import atomic + +logger = logging.getLogger(__name__) + + +@atomic +def generate_timestamps(apps, schema_editor): + TrackedModel = apps.get_model("common", "trackedmodel") + all_models = TrackedModel.objects.select_related("transaction").all() + paginator = Paginator(all_models, settings.DATA_MIGRATION_BATCH_SIZE) + logger.info( + "Running Tracked Model migration in batches, total batches: %s.", + paginator.num_pages, + ) + for page_num in range(1, paginator.num_pages + 1): + logger.info("Batch number: %s", page_num) + for tracked_model in paginator.page(page_num).object_list: + tracked_model.created_at = tracked_model.transaction.created_at + tracked_model.save() + + +class Migration(migrations.Migration): + dependencies = [ + ("common", "0009_tracked_model_timestamp"), + ] + + operations = [ + migrations.RunPython( + generate_timestamps, + reverse_code=migrations.RunPython.noop, + ), + ] diff --git a/common/models/trackedmodel.py b/common/models/trackedmodel.py index d2cc3947c..1a22da4a3 100644 --- a/common/models/trackedmodel.py +++ b/common/models/trackedmodel.py @@ -27,6 +27,7 @@ from common.models import TimestampedMixin from common.models.managers import CurrentTrackedModelManager from common.models.managers import TrackedModelManager +from common.models.mixins import TimestampedMixin from common.models.tracked_qs import TrackedModelQuerySet from common.models.tracked_utils import get_deferred_set_fields from common.models.tracked_utils import get_models_linked_to @@ -55,7 +56,7 @@ class VersionGroup(TimestampedMixin): Cls = TypeVar("Cls", bound="TrackedModel") -class TrackedModel(PolymorphicModel): +class TrackedModel(PolymorphicModel, TimestampedMixin): transaction = models.ForeignKey( "common.Transaction", on_delete=models.PROTECT, @@ -391,6 +392,8 @@ def auto_value_fields(cls) -> Set[Field]: "update_type", "trackedmodel_ptr", "transaction", + "created_at", + "updated_at", } def copy( diff --git a/common/tests/test_migrations.py b/common/tests/test_migrations.py index 9761c61bc..f6be7daef 100644 --- a/common/tests/test_migrations.py +++ b/common/tests/test_migrations.py @@ -3,9 +3,12 @@ import pytest +from common.models import TrackedModel from common.models.transactions import TransactionPartition +from common.tests import factories from common.util import TaricDateRange from common.validators import UpdateType +from workbaskets.validators import WorkflowStatus @pytest.mark.django_db() @@ -74,3 +77,32 @@ def test_missing_current_version_fix(migrator): # assert assert measurement_unit.version_group.current_version_id == measurement_unit_id migrator.reset() + + +@pytest.mark.django_db() +def test_timestamp_migration(migrator): + migrator.reset() + """Ensures that the initial migration works.""" + migrator.apply_initial_migration( + ("common", "0009_tracked_model_timestamp"), + ) + + workbasket = factories.WorkBasketFactory.create( + status=WorkflowStatus.EDITING, + ) + transaction = factories.TransactionFactory.create(workbasket=workbasket) + trked1 = factories.FootnoteTypeFactory.create(transaction=transaction) + trked2 = factories.FootnoteTypeFactory.create(transaction=transaction) + + assert transaction.created_at + assert hasattr(trked1, "created_at") + assert hasattr(trked1, "updated_at") + assert trked1.created_at != transaction.created_at + + migrator.apply_tested_migration(("common", "0010_set_tracked_model_datetime")) + new_trked1 = TrackedModel.objects.get(pk=trked1.pk) + new_trked2 = TrackedModel.objects.get(pk=trked2.pk) + assert new_trked1.created_at == transaction.created_at + assert new_trked1.updated_at > transaction.updated_at + assert new_trked2.created_at == transaction.created_at + assert new_trked2.updated_at > transaction.updated_at diff --git a/conftest.py b/conftest.py index d11a645ae..581f8d5cd 100644 --- a/conftest.py +++ b/conftest.py @@ -666,6 +666,46 @@ def use(obj: TrackedModel, data_changes: dict[str, str]): return use +@pytest.fixture +def use_edit_view_no_workbasket(valid_user_api_client): + """ + Uses the default edit form and view for a model in a workbasket with EDITING + status. + + The ``object`` param is the TrackedModel instance that is to be edited and + saved, which should not create a new version. + ``data_changes`` should be a dictionary to apply to the object, effectively + applying edits. + + Will raise :class:`~django.core.exceptions.ValidationError` if the form + contains errors. + """ + + def use(obj: TrackedModel, data_changes: dict[str, str]): + Model = type(obj) + obj_count = Model.objects.filter(**obj.get_identifying_fields()).count() + url = obj.get_url("edit") + + # Check initial form rendering. + get_response = valid_user_api_client.get(url) + assert get_response.status_code == 200 + + # Edit and submit the data. + initial_form = get_response.context_data["form"] + form_data = get_form_data(initial_form) + form_data.update(data_changes) + post_response = valid_user_api_client.post(url, form_data) + + # POSTing a real edits form should never create new object instances. + assert Model.objects.filter(**obj.get_identifying_fields()).count() == obj_count + if post_response.status_code not in (301, 302): + raise ValidationError( + f"Form contained errors: {dict(post_response.context_data['form'].errors)}", + ) + + return use + + @pytest.fixture def use_update_form(api_client_with_current_workbasket): """ diff --git a/settings/common.py b/settings/common.py index 5b6d81f10..267d8f0d3 100644 --- a/settings/common.py +++ b/settings/common.py @@ -810,3 +810,5 @@ "django.core.files.uploadhandler.MemoryFileUploadHandler", # defaults "django.core.files.uploadhandler.TemporaryFileUploadHandler", # defaults ) # Order is important + +DATA_MIGRATION_BATCH_SIZE = int(os.environ.get("DATA_MIGRATION_BATCH_SIZE", "10000")) diff --git a/workbaskets/models.py b/workbaskets/models.py index 27b0779bd..61f3f19f1 100644 --- a/workbaskets/models.py +++ b/workbaskets/models.py @@ -12,6 +12,7 @@ from django.core.exceptions import ImproperlyConfigured from django.core.exceptions import ValidationError from django.db import models +from django.db.models import Max from django.db.models import QuerySet from django.db.models import Subquery from django_fsm import FSMField @@ -608,15 +609,33 @@ def delete_checks(self): @property def unchecked_or_errored_transactions(self): - return self.transactions.exclude( + """ + Returns unchecked, errored or out of date transactions from the + workbaskets. + + The query excludes transactions which have a corresponding transaction + check which has been completed, successful and was created after the + latest updated_at in the transactions tracked models. Lasted is + retrieved by annotating all the transactions for the workbasket with the + latest updated for its containing tracked models then we aggregate the + latest time from all the transactions. + """ + latest = ( + self.transactions.all() + .annotate(latest_updated_in_transaction=Max("tracked_models__updated_at")) + .aggregate(Max("latest_updated_in_transaction")) + ) + returned = self.transactions.exclude( pk__in=TransactionCheck.objects.requires_update(False) .filter( completed=True, successful=True, transaction__workbasket=self, + created_at__gt=latest["latest_updated_in_transaction__max"], ) .values("transaction__pk"), ) + return returned class Meta: verbose_name = "workbasket" diff --git a/workbaskets/tests/test_views.py b/workbaskets/tests/test_views.py index f16fc77c5..700814ce4 100644 --- a/workbaskets/tests/test_views.py +++ b/workbaskets/tests/test_views.py @@ -1,3 +1,4 @@ +import datetime import os import re from unittest.mock import MagicMock @@ -16,6 +17,7 @@ from checks.tests.factories import TrackedModelCheckFactory from common.models.utils import override_current_transaction from common.tests import factories +from common.tests.util import date_post_data from common.validators import UpdateType from exporter.tasks import upload_workbaskets from importer.models import ImportBatch @@ -716,6 +718,45 @@ def test_workbasket_business_rule_status(valid_user_client, user_empty_workbaske assert not page.find("div", attrs={"class": "govuk-notification-banner--success"}) +def test_workbasket_business_rule_status_real_edit( + valid_user_client, + use_edit_view_no_workbasket, + user_empty_workbasket, + published_footnote_type, +): + """Testing that the live status of a workbasket resets after an item has + been updated, created or deleted in the workbasket.""" + + with user_empty_workbasket.new_transaction() as transaction: + footnote = factories.FootnoteFactory.create( + update_type=UpdateType.CREATE, + transaction=transaction, + footnote_type=published_footnote_type, + ) + TrackedModelCheckFactory.create( + transaction_check__transaction=transaction, + model=footnote, + successful=True, + ) + + url = reverse("workbaskets:workbasket-checks") + response = valid_user_client.get(url) + page = BeautifulSoup(response.content.decode(response.charset)) + success_banner = page.find( + "div", + attrs={"class": "govuk-notification-banner--success"}, + ) + assert success_banner + + use_edit_view_no_workbasket( + footnote, + {**date_post_data("start_date", datetime.date.today())}, + ) + response = valid_user_client.get(url) + page = BeautifulSoup(response.content.decode(response.charset)) + assert not page.find("div", attrs={"class": "govuk-notification-banner--success"}) + + @pytest.fixture def successful_business_rules_setup(user_workbasket, valid_user_client): """Sets up data and runs business rules.""" @@ -742,9 +783,10 @@ def import_batch_with_notification(): taric_file="goods.xml", ) - return factories.GoodsSuccessfulImportNotificationFactory( + factories.GoodsSuccessfulImportNotificationFactory( notified_object_pk=import_batch.id, ) + return import_batch @pytest.mark.parametrize( From 86da168999c135cd120c386aad21161c74124348 Mon Sep 17 00:00:00 2001 From: Dale Cannon <118175145+dalecannon@users.noreply.github.com> Date: Wed, 14 Feb 2024 10:45:13 +0000 Subject: [PATCH 034/118] TP2000-1219 Prevent maintenance mode errors (#1152) * Remove authbroker middleware when in maintenance mode * Skip applying migrations in init script * Prevent maintenance mode template attempts to access user attribute on request object * Update privacy policy link --- .profile | 9 +++++++-- common/jinja2/common/maintenance.jinja | 3 ++- common/jinja2/layouts/layout.jinja | 2 +- common/tests/test_views.py | 2 +- settings/common.py | 2 +- 5 files changed, 12 insertions(+), 6 deletions(-) mode change 100644 => 100755 .profile diff --git a/.profile b/.profile old mode 100644 new mode 100755 index dcebbe256..ee12fb823 --- a/.profile +++ b/.profile @@ -3,8 +3,13 @@ # ref - https://docs.cloudfoundry.org/devguide/deploy-apps/deploy-app.html echo "---- RUNNING release tasks (.profile) ------" -echo "---- Apply Migrations ------" -python manage.py migrate + +if [[ "$MAINTENANCE_MODE" != "True" && "$MAINTENANCE_MODE" != "true" ]] ; then + echo "---- Apply Migrations ------" + python manage.py migrate +else + echo "---- Skip Applying Migrations (Maintenance Mode) ------" +fi echo "---- Collect Static Files ------" OUTPUT=$(python manage.py collectstatic --noinput --clear) diff --git a/common/jinja2/common/maintenance.jinja b/common/jinja2/common/maintenance.jinja index a507332c4..3aafad122 100644 --- a/common/jinja2/common/maintenance.jinja +++ b/common/jinja2/common/maintenance.jinja @@ -2,8 +2,9 @@ {% set page_title = "Sorry, the service is unavailable" %} -{% block header %} +{% set workbasket_html %}{% endset %} +{% block header %} {{ govukHeader({ "homepageUrl": "https://gov.uk/", "serviceName": service_name, diff --git a/common/jinja2/layouts/layout.jinja b/common/jinja2/layouts/layout.jinja index 2c01a559c..1327a955e 100644 --- a/common/jinja2/layouts/layout.jinja +++ b/common/jinja2/layouts/layout.jinja @@ -66,7 +66,7 @@ "meta": { "items": [ { - "href": "https://workspace.trade.gov.uk/working-at-dit/policies-and-guidance/policies/tariff-application-privacy-policy/", + "href": "https://workspace.trade.gov.uk/working-at-dbt/policies-and-guidance/policies/tariff-application-privacy-policy/", "text": "Privacy policy" }, { diff --git a/common/tests/test_views.py b/common/tests/test_views.py index 6480201b6..caa1617a7 100644 --- a/common/tests/test_views.py +++ b/common/tests/test_views.py @@ -195,7 +195,7 @@ def test_index_displays_footer_links(valid_user_client): assert "Privacy policy" in a_tags[0].text assert ( a_tags[0].attrs["href"] - == "https://workspace.trade.gov.uk/working-at-dit/policies-and-guidance/policies/tariff-application-privacy-policy/" + == "https://workspace.trade.gov.uk/working-at-dbt/policies-and-guidance/policies/tariff-application-privacy-policy/" ) diff --git a/settings/common.py b/settings/common.py index 267d8f0d3..7573c09af 100644 --- a/settings/common.py +++ b/settings/common.py @@ -157,7 +157,7 @@ "csp.middleware.CSPMiddleware", ] -if SSO_ENABLED: +if SSO_ENABLED and not MAINTENANCE_MODE: MIDDLEWARE += [ "authbroker_client.middleware.ProtectAllViewsMiddleware", ] From a372cbc29833a703111a0d37d1b38a7f8c927fa0 Mon Sep 17 00:00:00 2001 From: Matthew McKenzie <97194636+mattjamc@users.noreply.github.com> Date: Wed, 14 Feb 2024 13:12:01 +0000 Subject: [PATCH 035/118] Formatting updates and adding end date field to footnote create (#1154) --- common/jinja2/common/edit_description.jinja | 2 +- .../includes/common/tabs/descriptions.jinja | 1 + footnotes/filters.py | 2 +- footnotes/forms.py | 18 ++++++++++++++---- .../includes/footnotes/tabs/descriptions.jinja | 1 + 5 files changed, 18 insertions(+), 6 deletions(-) diff --git a/common/jinja2/common/edit_description.jinja b/common/jinja2/common/edit_description.jinja index 0a2eb1fe0..7a3739775 100644 --- a/common/jinja2/common/edit_description.jinja +++ b/common/jinja2/common/edit_description.jinja @@ -2,7 +2,7 @@ {% from "components/details/macro.njk" import govukDetails %} {% from "components/table/macro.njk" import govukTable %} -{% set page_title = "Edit " ~ object._meta.verbose_name %} +{% set page_title = "Edit " ~ object._meta.verbose_name ~ " details"%} {% set described_object = object.get_described_object() %} diff --git a/common/jinja2/includes/common/tabs/descriptions.jinja b/common/jinja2/includes/common/tabs/descriptions.jinja index cda4201ed..0af17abbd 100644 --- a/common/jinja2/includes/common/tabs/descriptions.jinja +++ b/common/jinja2/includes/common/tabs/descriptions.jinja @@ -24,6 +24,7 @@

    Descriptions

    Create a new description

    +

    There must be at least one description.

    {% set head = [ {"text": "Start date", "classes": "govuk-!-width-one-eighth"}, {"text": "Description"}, diff --git a/footnotes/filters.py b/footnotes/filters.py index 94206b678..5fc091c28 100644 --- a/footnotes/filters.py +++ b/footnotes/filters.py @@ -60,7 +60,7 @@ class FootnoteFilter( choices=type_choices(models.FootnoteType.objects.latest_approved()), widget=CheckboxSelectMultiple, field_name="footnote_type__footnote_type_id", - label="Footnote Type", + label="Footnote type", help_text="Select all that apply", required=False, ) diff --git a/footnotes/forms.py b/footnotes/forms.py index 5d34b7dd6..39dc026fa 100644 --- a/footnotes/forms.py +++ b/footnotes/forms.py @@ -35,11 +35,19 @@ def __init__(self, *args, **kwargs): if self.instance.pk: self.fields["code"].disabled = True - self.fields["code"].help_text = "You can't edit this" + self.fields[ + "code" + ].help_text = ( + "Footnote IDs are automatically generated and cannot be edited." + ) self.fields["code"].initial = str(self.instance) self.fields["footnote_type"].disabled = True - self.fields["footnote_type"].help_text = "You can't edit this" + self.fields[ + "footnote_type" + ].help_text = ( + "Once a footnote is published, you cannot edit the footnote type." + ) self.helper = FormHelper(self) self.helper.label_size = Size.SMALL @@ -91,8 +99,8 @@ class Meta: footnote_type = forms.ModelChoiceField( label="Footnote type", help_text=( - "Selecting the right footnote type will determine whether it can " - "be associated with measures, commodity codes, or both." + "The footnote type will determine whether it can be" + "associated with measures, commodity codes, or both." ), queryset=models.FootnoteType.objects.latest_approved(), empty_label="Select a footnote type", @@ -150,6 +158,7 @@ def __init__(self, *args, **kwargs): self.helper.layout = Layout( "footnote_type", "start_date", + "end_date", Field.textarea("description", rows=5), DescriptionHelpBox(), Submit( @@ -187,6 +196,7 @@ def __init__(self, *args, **kwargs): self.helper.layout = Layout( "footnote_type", "start_date", + "end_date", Submit( "submit", "Save", diff --git a/footnotes/jinja2/includes/footnotes/tabs/descriptions.jinja b/footnotes/jinja2/includes/footnotes/tabs/descriptions.jinja index 13be9ab35..391fce037 100644 --- a/footnotes/jinja2/includes/footnotes/tabs/descriptions.jinja +++ b/footnotes/jinja2/includes/footnotes/tabs/descriptions.jinja @@ -26,6 +26,7 @@

    Descriptions

    Create a new description

    +

    There must be at least one description.

    {% set head = [ {"text": "Start date", "classes": "govuk-!-width-one-eighth"}, {"text": "Description"}, From bd2020f25cbe1c72287aa3d62c7b5be2c00590fb Mon Sep 17 00:00:00 2001 From: Edie Pearce Date: Wed, 14 Feb 2024 15:32:35 +0000 Subject: [PATCH 036/118] TP2000-1114: React enhanced forms proof of concept (#1091) * Add react * Start to build origins form in react * Build quota origin form with initial data * Enable adding/removing of origins * Repopulate form initial in case of error on submit * Pass errors from django to react * Create origins * Add aria attribute * Reinstate geo area descriptions in form * Organise JS, code comments * Add key for react list * Simplify if statement * Add exclusions formset * Add jest for react testing * Amend gitignore * Fix error re-rendering component after submit fail * Move state management into top level component * Pass origin index to exclusions formset * Submit origin pk * Update constants.py * Test form cleaned_data * Update quota origins to use with_latest_description * Use description from annotated query * Update origins and add test * Update origin exclusions * Don't remove empty data * Fix exclusions not pre-populating * Add jest snapshot tests * Add react tests * Add jest tests to github actions * Fix query not returning origin exclusions * Fix disabled widget error * Fix origins no longer being linked to quota when order number updated * Update tests for workbasket change * Add tests for add_extra_error form method * Fix incorrect exclusion being removed * Clean up babel config * Remove unused field * Create exclusions for updated and new origins * Make sure exclusions are updated/deleted * Move current() queryset into init * Fix geographical area invalid choice error in test --- .github/workflows/jest.yml | 21 + babel.config.json | 11 + common/forms.py | 10 +- common/static/common/js/application.js | 6 +- .../QuotaOriginFormset/DeleteButton.js | 12 + .../QuotaOriginExclusionForm.js | 41 + .../QuotaOriginExclusionFormset.js | 17 + .../QuotaOriginFormset/QuotaOriginForm.js | 97 + .../js/components/QuotaOriginFormset/index.js | 106 + .../tests/__snapshots__/index.test.js.snap | 1528 ++ .../QuotaOriginFormset/tests/index.test.js | 176 + common/tests/test_forms.py | 4 +- jest-setup.js | 1 + jest.config.js | 7 + package-lock.json | 12733 +++++++++++++--- package.json | 20 +- pii-secret-exclude.txt | 1 + quotas/constants.py | 2 + quotas/forms.py | 185 +- .../includes/quotas/quota-edit-origins.jinja | 42 +- .../includes/quotas/tabs/core_data.jinja | 4 +- .../quota-origins/macros/origin_display.jinja | 2 +- quotas/models.py | 17 - quotas/tests/test_forms.py | 190 +- quotas/tests/test_views.py | 258 + quotas/views.py | 159 +- webpack.config.js | 12 + 27 files changed, 13231 insertions(+), 2431 deletions(-) create mode 100644 .github/workflows/jest.yml create mode 100644 babel.config.json create mode 100644 common/static/common/js/components/QuotaOriginFormset/DeleteButton.js create mode 100644 common/static/common/js/components/QuotaOriginFormset/QuotaOriginExclusionForm.js create mode 100644 common/static/common/js/components/QuotaOriginFormset/QuotaOriginExclusionFormset.js create mode 100644 common/static/common/js/components/QuotaOriginFormset/QuotaOriginForm.js create mode 100644 common/static/common/js/components/QuotaOriginFormset/index.js create mode 100644 common/static/common/js/components/QuotaOriginFormset/tests/__snapshots__/index.test.js.snap create mode 100644 common/static/common/js/components/QuotaOriginFormset/tests/index.test.js create mode 100644 jest-setup.js create mode 100644 jest.config.js diff --git a/.github/workflows/jest.yml b/.github/workflows/jest.yml new file mode 100644 index 000000000..514b865cf --- /dev/null +++ b/.github/workflows/jest.yml @@ -0,0 +1,21 @@ +name: CI/CD + +on: + push: + branches: [master] + pull_request: + branches: [master] + +jobs: + test: + name: "Run jest tests" + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + + - name: Set up Node + uses: ./.github/actions/setup-node + + - name: Run tests + run: npm run test diff --git a/babel.config.json b/babel.config.json new file mode 100644 index 000000000..66fae7d96 --- /dev/null +++ b/babel.config.json @@ -0,0 +1,11 @@ +{ + "presets": [ + "@babel/env", + [ + "@babel/preset-react", + { + "runtime": "automatic" // defaults to classic + } + ] + ] +} \ No newline at end of file diff --git a/common/forms.py b/common/forms.py index 7adc18fc8..8e8bd4a28 100644 --- a/common/forms.py +++ b/common/forms.py @@ -757,17 +757,15 @@ def unprefix_formset_data(prefix, data): for i in range(0, num_items): subform_initial = {} for k, v in formset_data.items(): - if v: - if k.startswith(f"{prefix}-{i}-"): - subform_initial[k.split(f"{prefix}-{i}-")[1]] = v + if k.startswith(f"{prefix}-{i}-"): + subform_initial[k.split(f"{prefix}-{i}-")[1]] = v if subform_initial: output.append(subform_initial) for k, v in formset_data.items(): subform_initial = {} - if v: - if k.startswith(f"{prefix}-__prefix__-"): - subform_initial[k.split(f"{prefix}-__prefix__-")[1]] = v + if k.startswith(f"{prefix}-__prefix__-"): + subform_initial[k.split(f"{prefix}-__prefix__-")[1]] = v if subform_initial: output.append(subform_initial) diff --git a/common/static/common/js/application.js b/common/static/common/js/application.js index 18f3a5034..82ef1e73f 100644 --- a/common/static/common/js/application.js +++ b/common/static/common/js/application.js @@ -13,6 +13,9 @@ import initConditionalMeasureConditions from './conditionalMeasureConditions'; import initFilterDisabledToggleForComCode from './conditionalDisablingFilters' import initOpenCloseAccordionSection from './openCloseAccordion'; import initTapDebounce from './buttonDebounce'; +import { setupQuotaOriginFormset } from './components/QuotaOriginFormset/index'; + + showHideCheckboxes(); // Initialise accessible-autocomplete components without a `name` attr in order // to avoid the "dummy" autocomplete field being submitted as part of the form @@ -26,4 +29,5 @@ initConditionalMeasureConditions(); initAutocompleteProgressiveEnhancement(); initFilterDisabledToggleForComCode(); initOpenCloseAccordionSection(); -initTapDebounce(); \ No newline at end of file +initTapDebounce(); +setupQuotaOriginFormset(); \ No newline at end of file diff --git a/common/static/common/js/components/QuotaOriginFormset/DeleteButton.js b/common/static/common/js/components/QuotaOriginFormset/DeleteButton.js new file mode 100644 index 000000000..12b264c47 --- /dev/null +++ b/common/static/common/js/components/QuotaOriginFormset/DeleteButton.js @@ -0,0 +1,12 @@ +import React from 'react'; + + +function DeleteButton({ renderCondition, name, func, item, parent }) { + if (renderCondition) { + return ( + + ) + } +} + +export { DeleteButton } \ No newline at end of file diff --git a/common/static/common/js/components/QuotaOriginFormset/QuotaOriginExclusionForm.js b/common/static/common/js/components/QuotaOriginFormset/QuotaOriginExclusionForm.js new file mode 100644 index 000000000..158e12147 --- /dev/null +++ b/common/static/common/js/components/QuotaOriginFormset/QuotaOriginExclusionForm.js @@ -0,0 +1,41 @@ +import React from 'react'; +import { Select } from 'govuk-react' +import { DeleteButton } from './DeleteButton' + + +function QuotaOriginExclusionForm({ exclusion, origin, options, originIndex, index, removeExclusion, errors }) { + + return ( +
    +
    Exclusion {index + 1}
    +
    + + +
    +
    + +
    +
    ) +} + +export { QuotaOriginExclusionForm } \ No newline at end of file diff --git a/common/static/common/js/components/QuotaOriginFormset/QuotaOriginExclusionFormset.js b/common/static/common/js/components/QuotaOriginFormset/QuotaOriginExclusionFormset.js new file mode 100644 index 000000000..da4b68178 --- /dev/null +++ b/common/static/common/js/components/QuotaOriginFormset/QuotaOriginExclusionFormset.js @@ -0,0 +1,17 @@ +import React from 'react'; +import { QuotaOriginExclusionForm } from './QuotaOriginExclusionForm' + + +function QuotaOriginExclusionFormset({ origin, originIndex, options, errors, addEmptyExclusion, removeExclusion }) { + + return ( +
    + {origin.exclusions.map((exclusion, i) => + + )} + +
    + ) +} + +export { QuotaOriginExclusionFormset } \ No newline at end of file diff --git a/common/static/common/js/components/QuotaOriginFormset/QuotaOriginForm.js b/common/static/common/js/components/QuotaOriginFormset/QuotaOriginForm.js new file mode 100644 index 000000000..90e726967 --- /dev/null +++ b/common/static/common/js/components/QuotaOriginFormset/QuotaOriginForm.js @@ -0,0 +1,97 @@ +import React from 'react'; +import { DateField, Fieldset, Select } from 'govuk-react' +import { QuotaOriginExclusionFormset } from './QuotaOriginExclusionFormset' +import { DeleteButton } from './DeleteButton' + + +function QuotaOriginForm({ origin, options, index, removeOrigin, addEmptyExclusion, removeExclusion, errors }) { + // If the form is submitted with no exclusions and fails validation + // the exclusions key will not exist on the origin so create it here + origin.exclusions = origin.exclusions || [] + + return ( +
    +

    Origin {index + 1}

    + +
    + + + Start date + + +
    +
    + + + End date + + +
    +
    + +
    +
    +

    Geographical exclusions

    + +
    + 0} name={"origin"} func={removeOrigin} item={origin} parent={null} /> +
    +
    ) +} + +export { QuotaOriginForm } \ No newline at end of file diff --git a/common/static/common/js/components/QuotaOriginFormset/index.js b/common/static/common/js/components/QuotaOriginFormset/index.js new file mode 100644 index 000000000..9936cf222 --- /dev/null +++ b/common/static/common/js/components/QuotaOriginFormset/index.js @@ -0,0 +1,106 @@ +import { useState } from 'react'; +import { createRoot } from 'react-dom/client'; +import React from 'react'; +import { QuotaOriginForm } from './QuotaOriginForm' + + +function QuotaOriginFormset({ data, options, errors }) { + const [origins, setOrigins] = useState([...data]); + const emptyOrigin = { + "id": "", + "pk": "", + "exclusions": [ + ], + "geo_area_name": "", + "geo_area_pk": "", + "start_date_0": "", + "start_date_1": "", + "start_date_2": "", + "end_date_0": "", + "end_date_1": "", + "end_date_2": "", + } + const emptyExclusion = { + "id": "", + "pk": "", + } + + const addEmptyOrigin = (e) => { + e.preventDefault(); + const newEmptyOrigin = { ...emptyOrigin } + newEmptyOrigin.id = Date.now() + setOrigins([...origins, { ...newEmptyOrigin }]); + } + + function removeOrigin(origin, _, e) { + e.preventDefault(); + const newOrigins = [...origins] + const index = origins.indexOf(origin) + if (index > -1) { + newOrigins.splice(index, 1) + setOrigins(newOrigins) + } + } + + function addEmptyExclusion(origin, e) { + e.preventDefault(); + // find parent origin and update exclusions + const updatedOrigin = { ...origin } + const newEmptyExclusion = { ...emptyExclusion } + newEmptyExclusion.id = Date.now() + const newExclusions = [...updatedOrigin.exclusions, newEmptyExclusion] + updatedOrigin.exclusions = newExclusions + + // update origins + const updatedOrigins = [...origins] + const index = origins.findIndex(o => o.id === origin.id) + if (index > -1) { + updatedOrigins.splice(index, 1, updatedOrigin) + setOrigins(updatedOrigins) + } + } + + function removeExclusion(exclusion, origin, e) { + e.preventDefault(); + // remove the exclusion from its parent origin + const newOrigin = { ...origin } + const exclusionIndex = newOrigin.exclusions.indexOf(exclusion) + if (exclusionIndex > -1) { + newOrigin.exclusions.splice(exclusionIndex, 1) + } + + // update the origin + const newOrigins = [...origins] + const index = newOrigins.indexOf(origin) + if (index > -1) { + newOrigins.splice(index, 1, newOrigin) + setOrigins(newOrigins) + } + } + + return ( +
    + {origins.map((origin, i) => + + )} + +
    + ) +} + +function init() { + const originsContainer = document.getElementById("quota_origins"); + const root = createRoot(originsContainer); + const origins = [...originsData]; + // originsData and geoAreasOptions come from template quotas/jinja2/includes/quotas/quota-edit-origins.jinja + // originsErrors are errors raised by django. see template quotas/jinja2/includes/quotas/quota-edit-origins.jinja + root.render( + + ); +} + +function setupQuotaOriginFormset() { + document.addEventListener('DOMContentLoaded', init()) +} + +export { setupQuotaOriginFormset, QuotaOriginFormset }; \ No newline at end of file diff --git a/common/static/common/js/components/QuotaOriginFormset/tests/__snapshots__/index.test.js.snap b/common/static/common/js/components/QuotaOriginFormset/tests/__snapshots__/index.test.js.snap new file mode 100644 index 000000000..6b1e2ba2a --- /dev/null +++ b/common/static/common/js/components/QuotaOriginFormset/tests/__snapshots__/index.test.js.snap @@ -0,0 +1,1528 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`QuotaOriginFormset renders empty formset when no initial data 1`] = ` +
    + +
    +`; + +exports[`QuotaOriginFormset renders formset with props 1`] = ` +
    +
    +

    + Origin + 1 +

    + +
    +
    + + + Start date + + +
    + + + +
    +
    +
    +
    +
    + + + End date + + + + Leave empty if a quota order number origin is needed for an unlimited time + +
    + + + +
    +
    +
    +
    + +
    +
    +

    + Geographical exclusions +

    +
    +
    +
    + Exclusion + 1 +
    +
    + + +
    +
    + +
    +
    +
    +
    + Exclusion + 2 +
    +
    + + +
    +
    + +
    +
    + +
    +
    +
    +
    +
    +

    + Origin + 2 +

    + +
    +
    + + + Start date + + +
    + + + +
    +
    +
    +
    +
    + + + End date + + + + Leave empty if a quota order number origin is needed for an unlimited time + +
    + + + +
    +
    +
    +
    + +
    +
    +

    + Geographical exclusions +

    +
    +
    +
    + Exclusion + 1 +
    +
    + + +
    +
    + +
    +
    + +
    +
    + +
    +
    + +
    +`; + +exports[`QuotaOriginFormset renders with formset errors 1`] = ` +
    +
    +

    + Origin + 1 +

    + +
    +
    + + + Start date + + +
    + + + +
    +
    +
    +
    +
    + + + End date + + + + Leave empty if a quota order number origin is needed for an unlimited time + + + The end date must be the same as or after the start date. + +
    + + + +
    +
    +
    +
    + +
    +
    +

    + Geographical exclusions +

    +
    +
    +
    + Exclusion + 1 +
    +
    + + +
    +
    + +
    +
    +
    +
    + Exclusion + 2 +
    +
    + + +
    +
    + +
    +
    + +
    +
    +
    +
    +
    +

    + Origin + 2 +

    + +
    +
    + + + Start date + + +
    + + + +
    +
    +
    +
    +
    + + + End date + + + + Leave empty if a quota order number origin is needed for an unlimited time + +
    + + + +
    +
    +
    +
    + +
    +
    +

    + Geographical exclusions +

    +
    +
    +
    + Exclusion + 1 +
    +
    + + +
    +
    + +
    +
    + +
    +
    + +
    +
    + +
    +`; diff --git a/common/static/common/js/components/QuotaOriginFormset/tests/index.test.js b/common/static/common/js/components/QuotaOriginFormset/tests/index.test.js new file mode 100644 index 000000000..c126b47ec --- /dev/null +++ b/common/static/common/js/components/QuotaOriginFormset/tests/index.test.js @@ -0,0 +1,176 @@ +import { fireEvent, render, screen } from "@testing-library/react"; +import renderer from 'react-test-renderer'; +import { QuotaOriginFormset } from '../index'; + + +const mockGeoAreaOptions = [ + { + "name": "All countries", + "value": 1 + }, + { + "name": "EU", + "value": 2 + }, + { + "name": "China", + "value": 3 + }, + { + "name": "South Korea", + "value": 4 + }, + { + "name": "Switzerland", + "value": 5 + }, +] + +const mockOrigins = [ + { + "id": 1, + "pk": 1, + "exclusions": [ + { "id": 3, "pk": 1 }, + { "id": 4, "pk": 2 }, + ], + "geographical_area": 1, + "start_date_0": 1, + "start_date_1": 1, + "start_date_2": 2000, + "end_date_0": 1, + "end_date_1": 1, + "end_date_2": 2010, + }, + { + "id": 2, + "pk": 2, + "exclusions": [ + { "id": 5, "pk": 3 }, + ], + "geographical_area": 2, + "start_date_0": 1, + "start_date_1": 1, + "start_date_2": 2000, + "end_date_0": 1, + "end_date_1": 1, + "end_date_2": 2010, + }, +] + +describe(QuotaOriginFormset, () => { + it('renders formset with props', () => { + + const mockOriginsErrors = {} + + const component = renderer.create( + , + ); + + let tree = component.toJSON(); + expect(tree).toMatchSnapshot(); + + }); + + it('renders empty formset when no initial data', () => { + + const mockOriginsErrors = {} + const mockOrigins = [] + + const component = renderer.create( + , + ); + + let tree = component.toJSON(); + expect(tree).toMatchSnapshot(); + + }); + + it('renders with formset errors', () => { + + const mockOriginsErrors = { + "origins-0-end_date": "The end date must be the same as or after the start date.", + }; + + const component = renderer.create( + , + ); + + let tree = component.toJSON(); + expect(tree).toMatchSnapshot(); + + }); + + it("should add empty origin form when add button is clicked", () => { + const mockOrigins = [] + const mockOriginsErrors = {} + + // render form with no origins + render(); + + // add an empty origin + fireEvent.click(screen.getByText("Add another origin")); + expect(screen.getByText("Origin 1")).toBeInTheDocument(); + expect(screen.queryByText("Origin 2")).not.toBeInTheDocument(); + }); + + it("should remove origin form when delete button is clicked", () => { + const mockOriginsErrors = {} + + // render form with 2 origins + render(); + + // delete the last origin + fireEvent.click(screen.getByText("Delete this origin")); + expect(screen.getByText("Origin 1")).toBeInTheDocument(); + expect(screen.queryByText("Origin 2")).not.toBeInTheDocument(); + }); + + it("should add empty exclusion form when add button is clicked", () => { + const mockOrigins = [ + { + "id": 1, + "pk": 1, + "exclusions": [ + ], + "geographical_area": 1, + "start_date_0": 1, + "start_date_1": 1, + "start_date_2": 2000, + "end_date_0": 1, + "end_date_1": 1, + "end_date_2": 2010, + }, + ] + const mockOriginsErrors = {} + + // render form with no origins + render(); + expect(screen.getByText("Origin 1")).toBeInTheDocument(); + expect(screen.queryByText("Origin 2")).not.toBeInTheDocument(); + expect(screen.queryByText("Exclusion 1")).not.toBeInTheDocument(); + expect(screen.queryByText("Delete this exclusion")).not.toBeInTheDocument(); + + // add an empty exclusion + fireEvent.click(screen.getByText("Add an exclusion")); + expect(screen.getByText("Exclusion 1")).toBeInTheDocument(); + expect(screen.getByText("Delete this exclusion")).toBeInTheDocument(); + expect(screen.queryByText("Exclusion 2")).not.toBeInTheDocument(); + }); + + it("should remove exclusion form when delete button is clicked", () => { + const mockOriginsErrors = {} + + // render form with 2 origins + // first has 2 exclusions + // second has 1 + render(); + expect(screen.getByText("Origin 1")).toBeInTheDocument(); + expect(screen.getByText("Origin 2")).toBeInTheDocument(); + expect(screen.getAllByText(/Exclusion [0-9]+/).length).toBe(3) + + // add an empty exclusion to first origin + fireEvent.click(screen.getAllByText("Add an exclusion")[0]); + expect(screen.getAllByText(/Exclusion [0-9]+/).length).toBe(4) + }); +}) diff --git a/common/tests/test_forms.py b/common/tests/test_forms.py index 75f2fed94..c342a8de1 100644 --- a/common/tests/test_forms.py +++ b/common/tests/test_forms.py @@ -183,7 +183,7 @@ def test_radio_nested_form_nested_formset_cleaned_data(): "measure-conditions-formset-0-applicable_duty": "test1", "measure-conditions-formset-1-applicable_duty": "test2", "measure-conditions-formset-2-applicable_duty": "test3", - "measure-conditions-formset-3-applicable_duty": "test4", + "measure-conditions-formset-3-applicable_duty": "", }, [ { @@ -196,7 +196,7 @@ def test_radio_nested_form_nested_formset_cleaned_data(): "applicable_duty": "test3", }, { - "applicable_duty": "test4", + "applicable_duty": "", }, ], ), diff --git a/jest-setup.js b/jest-setup.js new file mode 100644 index 000000000..02c423f5d --- /dev/null +++ b/jest-setup.js @@ -0,0 +1 @@ +import "@testing-library/jest-dom"; \ No newline at end of file diff --git a/jest.config.js b/jest.config.js new file mode 100644 index 000000000..9d486c080 --- /dev/null +++ b/jest.config.js @@ -0,0 +1,7 @@ +/** @type {import('jest').Config} */ + +module.exports = { + verbose: true, + setupFilesAfterEnv: ['/jest-setup.js'], + testEnvironment: "jest-environment-jsdom", +}; diff --git a/package-lock.json b/package-lock.json index df6db8fe8..3a72fcc25 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,30 +1,45 @@ { "name": "tamato", "version": "0.1.0", - "lockfileVersion": 2, + "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "tamato", "version": "0.1.0", "dependencies": { + "@babel/core": "^7.23.2", + "@types/styled-components": "^5.1.29", "accessible-autocomplete": "^2.0.3", "ansi-regex": "^6.0.1", + "babel-loader": "^9.1.3", "chart.js": "^3.9.1", "chartjs-adapter-moment": "^1.0.0", "css-loader": "^5.2.6", "file-loader": "^6.2.0", "govuk-frontend": "^3.13.0", + "govuk-react": "^0.10.6", "mini-css-extract-plugin": "^1.6.0", "moment": "^2.29.4", + "react": "^18.2.0", + "react-dom": "^18.2.0", "sass": "^1.38.2", "sass-loader": "^12.1.0", "style-loader": "^3.0.0", + "styled-components": "^6.1.0", "webpack": "^5.76.0", "webpack-bundle-tracker": "^1.1.0", "webpack-cli": "^4.7.2" }, "devDependencies": { + "@babel/preset-env": "^7.23.7", + "@babel/preset-react": "^7.23.3", + "@testing-library/jest-dom": "^6.2.1", + "@testing-library/react": "^14.1.2", + "babel-jest": "^29.7.0", + "jest": "^29.7.0", + "jest-environment-jsdom": "^29.7.0", + "react-test-renderer": "^18.2.0", "webpack-cli": "^4.7.2" }, "engines": { @@ -32,3116 +47,10851 @@ "npm": "^10.3.0" } }, - "node_modules/@discoveryjs/json-ext": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz", - "integrity": "sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==", - "dev": true, - "engines": { - "node": ">=10.0.0" - } + "node_modules/@adobe/css-tools": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.3.2.tgz", + "integrity": "sha512-DA5a1C0gD/pLOvhv33YMrbf2FK3oUzwNl9oOJqE4XVjuEtt6XIakRcsd7eLiOSPkp1kTRQGICTA8cKra/vFbjw==", + "dev": true }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz", - "integrity": "sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==", + "node_modules/@ampproject/remapping": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.1.tgz", + "integrity": "sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==", "dependencies": { - "@jridgewell/set-array": "^1.0.1", - "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/gen-mapping": "^0.3.0", "@jridgewell/trace-mapping": "^0.3.9" }, "engines": { "node": ">=6.0.0" } }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", - "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==", + "node_modules/@babel/code-frame": { + "version": "7.22.13", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz", + "integrity": "sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==", + "dependencies": { + "@babel/highlight": "^7.22.13", + "chalk": "^2.4.2" + }, "engines": { - "node": ">=6.0.0" + "node": ">=6.9.0" } }, - "node_modules/@jridgewell/set-array": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", - "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", + "node_modules/@babel/compat-data": { + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.23.5.tgz", + "integrity": "sha512-uU27kfDRlhfKl+w1U6vp16IuvSLtjAxdArVXPa9BvLkrr7CYIsxH5adpHObeAGY/41+syctUWOZ140a2Rvkgjw==", "engines": { - "node": ">=6.0.0" + "node": ">=6.9.0" } }, - "node_modules/@jridgewell/source-map": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.2.tgz", - "integrity": "sha512-m7O9o2uR8k2ObDysZYzdfhb08VuEml5oWGiosa1VdaPZ/A6QyPkAJuwN0Q1lhULOf6B7MtQmHENS743hWtCrgw==", + "node_modules/@babel/core": { + "version": "7.23.2", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.23.2.tgz", + "integrity": "sha512-n7s51eWdaWZ3vGT2tD4T7J6eJs3QoBXydv7vkUM06Bf1cbVD2Kc2UrkzhiQwobfV7NwOnQXYL7UBJ5VPU+RGoQ==", "dependencies": { - "@jridgewell/gen-mapping": "^0.3.0", - "@jridgewell/trace-mapping": "^0.3.9" + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.22.13", + "@babel/generator": "^7.23.0", + "@babel/helper-compilation-targets": "^7.22.15", + "@babel/helper-module-transforms": "^7.23.0", + "@babel/helpers": "^7.23.2", + "@babel/parser": "^7.23.0", + "@babel/template": "^7.22.15", + "@babel/traverse": "^7.23.2", + "@babel/types": "^7.23.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/@jridgewell/sourcemap-codec": { - "version": "1.4.14", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", - "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==" - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.17", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.17.tgz", - "integrity": "sha512-MCNzAp77qzKca9+W/+I0+sEpaUnZoeasnghNeVc41VZCEKaCH73Vq3BZZ/SzWIgrqE4H4ceI+p+b6C0mHf9T4g==", + "node_modules/@babel/generator": { + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.0.tgz", + "integrity": "sha512-lN85QRR+5IbYrMWM6Y4pE/noaQtg4pNiqeNGX60eqOfo6gtEj6uw/JagelB8vVztSd7R6M5n1+PQkDbHbBRU4g==", "dependencies": { - "@jridgewell/resolve-uri": "3.1.0", - "@jridgewell/sourcemap-codec": "1.4.14" + "@babel/types": "^7.23.0", + "@jridgewell/gen-mapping": "^0.3.2", + "@jridgewell/trace-mapping": "^0.3.17", + "jsesc": "^2.5.1" + }, + "engines": { + "node": ">=6.9.0" } }, - "node_modules/@types/eslint": { - "version": "8.4.10", - "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.4.10.tgz", - "integrity": "sha512-Sl/HOqN8NKPmhWo2VBEPm0nvHnu2LL3v9vKo8MEq0EtbJ4eVzGPl41VNPvn5E1i5poMk4/XD8UriLHpJvEP/Nw==", + "node_modules/@babel/helper-annotate-as-pure": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.22.5.tgz", + "integrity": "sha512-LvBTxu8bQSQkcyKOU+a1btnNFQ1dMAd0R6PyW3arXes06F6QLWLIrd681bxRPIXlrMGR3XYnW9JyML7dP3qgxg==", + "dev": true, "dependencies": { - "@types/estree": "*", - "@types/json-schema": "*" + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" } }, - "node_modules/@types/eslint-scope": { - "version": "3.7.4", - "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.4.tgz", - "integrity": "sha512-9K4zoImiZc3HlIp6AVUDE4CWYx22a+lhSZMYNpbjW04+YF0KWj4pJXnEMjdnFTiQibFFmElcsasJXDbdI/EPhA==", + "node_modules/@babel/helper-builder-binary-assignment-operator-visitor": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.22.15.tgz", + "integrity": "sha512-QkBXwGgaoC2GtGZRoma6kv7Szfv06khvhFav67ZExau2RaXzy8MpHSMO2PNoP2XtmQphJQRHFfg77Bq731Yizw==", + "dev": true, "dependencies": { - "@types/eslint": "*", - "@types/estree": "*" + "@babel/types": "^7.22.15" + }, + "engines": { + "node": ">=6.9.0" } }, - "node_modules/@types/estree": { - "version": "0.0.51", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.51.tgz", - "integrity": "sha512-CuPgU6f3eT/XgKKPqKd/gLZV1Xmvf1a2R5POBOGQa6uv82xpls89HU5zKeVoyR8XzHd1RGNOlQlvUe3CFkjWNQ==" - }, - "node_modules/@types/json-schema": { - "version": "7.0.11", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz", - "integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==" - }, - "node_modules/@types/node": { - "version": "18.11.9", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.9.tgz", - "integrity": "sha512-CRpX21/kGdzjOpFsZSkcrXMGIBWMGNIHXXBVFSH+ggkftxg+XYP20TESbh+zFvFj3EQOl5byk0HTRn1IL6hbqg==" - }, - "node_modules/@webassemblyjs/ast": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.1.tgz", - "integrity": "sha512-ukBh14qFLjxTQNTXocdyksN5QdM28S1CxHt2rdskFyL+xFV7VremuBLVbmCePj+URalXBENx/9Lm7lnhihtCSw==", + "node_modules/@babel/helper-compilation-targets": { + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.23.6.tgz", + "integrity": "sha512-9JB548GZoQVmzrFgp8o7KxdgkTGm6xs9DW0o/Pim72UDjzr5ObUQ6ZzYPqA+g9OTS2bBQoctLJrky0RDCAWRgQ==", "dependencies": { - "@webassemblyjs/helper-numbers": "1.11.1", - "@webassemblyjs/helper-wasm-bytecode": "1.11.1" + "@babel/compat-data": "^7.23.5", + "@babel/helper-validator-option": "^7.23.5", + "browserslist": "^4.22.2", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" } }, - "node_modules/@webassemblyjs/floating-point-hex-parser": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.1.tgz", - "integrity": "sha512-iGRfyc5Bq+NnNuX8b5hwBrRjzf0ocrJPI6GWFodBFzmFnyvrQ83SHKhmilCU/8Jv67i4GJZBMhEzltxzcNagtQ==" - }, - "node_modules/@webassemblyjs/helper-api-error": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.1.tgz", - "integrity": "sha512-RlhS8CBCXfRUR/cwo2ho9bkheSXG0+NwooXcc3PAILALf2QLdFyj7KGsKRbVc95hZnhnERon4kW/D3SZpp6Tcg==" - }, - "node_modules/@webassemblyjs/helper-buffer": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.1.tgz", - "integrity": "sha512-gwikF65aDNeeXa8JxXa2BAk+REjSyhrNC9ZwdT0f8jc4dQQeDQ7G4m0f2QCLPJiMTTO6wfDmRmj/pW0PsUvIcA==" - }, - "node_modules/@webassemblyjs/helper-numbers": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.1.tgz", - "integrity": "sha512-vDkbxiB8zfnPdNK9Rajcey5C0w+QJugEglN0of+kmO8l7lDb77AnlKYQF7aarZuCrv+l0UvqL+68gSDr3k9LPQ==", + "node_modules/@babel/helper-create-class-features-plugin": { + "version": "7.23.7", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.23.7.tgz", + "integrity": "sha512-xCoqR/8+BoNnXOY7RVSgv6X+o7pmT5q1d+gGcRlXYkI+9B31glE4jeejhKVpA04O1AtzOt7OSQ6VYKP5FcRl9g==", + "dev": true, "dependencies": { - "@webassemblyjs/floating-point-hex-parser": "1.11.1", - "@webassemblyjs/helper-api-error": "1.11.1", - "@xtuc/long": "4.2.2" + "@babel/helper-annotate-as-pure": "^7.22.5", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-function-name": "^7.23.0", + "@babel/helper-member-expression-to-functions": "^7.23.0", + "@babel/helper-optimise-call-expression": "^7.22.5", + "@babel/helper-replace-supers": "^7.22.20", + "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" } }, - "node_modules/@webassemblyjs/helper-wasm-bytecode": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.1.tgz", - "integrity": "sha512-PvpoOGiJwXeTrSf/qfudJhwlvDQxFgelbMqtq52WWiXC6Xgg1IREdngmPN3bs4RoO83PnL/nFrxucXj1+BX62Q==" - }, - "node_modules/@webassemblyjs/helper-wasm-section": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.1.tgz", - "integrity": "sha512-10P9No29rYX1j7F3EVPX3JvGPQPae+AomuSTPiF9eBQeChHI6iqjMIwR9JmOJXwpnn/oVGDk7I5IlskuMwU/pg==", + "node_modules/@babel/helper-create-regexp-features-plugin": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.22.15.tgz", + "integrity": "sha512-29FkPLFjn4TPEa3RE7GpW+qbE8tlsu3jntNYNfcGsc49LphF1PQIiD+vMZ1z1xVOKt+93khA9tc2JBs3kBjA7w==", + "dev": true, "dependencies": { - "@webassemblyjs/ast": "1.11.1", - "@webassemblyjs/helper-buffer": "1.11.1", - "@webassemblyjs/helper-wasm-bytecode": "1.11.1", - "@webassemblyjs/wasm-gen": "1.11.1" + "@babel/helper-annotate-as-pure": "^7.22.5", + "regexpu-core": "^5.3.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" } }, - "node_modules/@webassemblyjs/ieee754": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.11.1.tgz", - "integrity": "sha512-hJ87QIPtAMKbFq6CGTkZYJivEwZDbQUgYd3qKSadTNOhVY7p+gfP6Sr0lLRVTaG1JjFj+r3YchoqRYxNH3M0GQ==", + "node_modules/@babel/helper-define-polyfill-provider": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.4.4.tgz", + "integrity": "sha512-QcJMILQCu2jm5TFPGA3lCpJJTeEP+mqeXooG/NZbg/h5FTFi6V0+99ahlRsW8/kRLyb24LZVCCiclDedhLKcBA==", + "dev": true, "dependencies": { - "@xtuc/ieee754": "^1.2.0" + "@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/@webassemblyjs/leb128": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.11.1.tgz", - "integrity": "sha512-BJ2P0hNZ0u+Th1YZXJpzW6miwqQUGcIHT1G/sf72gLVD9DZ5AdYTqPNbHZh6K1M5VmKvFXwGSWZADz+qBWxeRw==", - "dependencies": { - "@xtuc/long": "4.2.2" + "node_modules/@babel/helper-environment-visitor": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", + "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==", + "engines": { + "node": ">=6.9.0" } }, - "node_modules/@webassemblyjs/utf8": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.11.1.tgz", - "integrity": "sha512-9kqcxAEdMhiwQkHpkNiorZzqpGrodQQ2IGrHHxCy+Ozng0ofyMA0lTqiLkVs1uzTRejX+/O0EOT7KxqVPuXosQ==" + "node_modules/@babel/helper-function-name": { + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz", + "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==", + "dependencies": { + "@babel/template": "^7.22.15", + "@babel/types": "^7.23.0" + }, + "engines": { + "node": ">=6.9.0" + } }, - "node_modules/@webassemblyjs/wasm-edit": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.1.tgz", - "integrity": "sha512-g+RsupUC1aTHfR8CDgnsVRVZFJqdkFHpsHMfJuWQzWU3tvnLC07UqHICfP+4XyL2tnr1amvl1Sdp06TnYCmVkA==", + "node_modules/@babel/helper-hoist-variables": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", + "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", "dependencies": { - "@webassemblyjs/ast": "1.11.1", - "@webassemblyjs/helper-buffer": "1.11.1", - "@webassemblyjs/helper-wasm-bytecode": "1.11.1", - "@webassemblyjs/helper-wasm-section": "1.11.1", - "@webassemblyjs/wasm-gen": "1.11.1", - "@webassemblyjs/wasm-opt": "1.11.1", - "@webassemblyjs/wasm-parser": "1.11.1", - "@webassemblyjs/wast-printer": "1.11.1" + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" } }, - "node_modules/@webassemblyjs/wasm-gen": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.1.tgz", - "integrity": "sha512-F7QqKXwwNlMmsulj6+O7r4mmtAlCWfO/0HdgOxSklZfQcDu0TpLiD1mRt/zF25Bk59FIjEuGAIyn5ei4yMfLhA==", + "node_modules/@babel/helper-member-expression-to-functions": { + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.23.0.tgz", + "integrity": "sha512-6gfrPwh7OuT6gZyJZvd6WbTfrqAo7vm4xCzAXOusKqq/vWdKXphTpj5klHKNmRUU6/QRGlBsyU9mAIPaWHlqJA==", + "dev": true, "dependencies": { - "@webassemblyjs/ast": "1.11.1", - "@webassemblyjs/helper-wasm-bytecode": "1.11.1", - "@webassemblyjs/ieee754": "1.11.1", - "@webassemblyjs/leb128": "1.11.1", - "@webassemblyjs/utf8": "1.11.1" + "@babel/types": "^7.23.0" + }, + "engines": { + "node": ">=6.9.0" } }, - "node_modules/@webassemblyjs/wasm-opt": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.1.tgz", - "integrity": "sha512-VqnkNqnZlU5EB64pp1l7hdm3hmQw7Vgqa0KF/KCNO9sIpI6Fk6brDEiX+iCOYrvMuBWDws0NkTOxYEb85XQHHw==", + "node_modules/@babel/helper-module-imports": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.22.15.tgz", + "integrity": "sha512-0pYVBnDKZO2fnSPCrgM/6WMc7eS20Fbok+0r88fp+YtWVLZrp4CkafFGIp+W0VKw4a22sgebPT99y+FDNMdP4w==", "dependencies": { - "@webassemblyjs/ast": "1.11.1", - "@webassemblyjs/helper-buffer": "1.11.1", - "@webassemblyjs/wasm-gen": "1.11.1", - "@webassemblyjs/wasm-parser": "1.11.1" + "@babel/types": "^7.22.15" + }, + "engines": { + "node": ">=6.9.0" } }, - "node_modules/@webassemblyjs/wasm-parser": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.1.tgz", - "integrity": "sha512-rrBujw+dJu32gYB7/Lup6UhdkPx9S9SnobZzRVL7VcBH9Bt9bCBLEuX/YXOOtBsOZ4NQrRykKhffRWHvigQvOA==", + "node_modules/@babel/helper-module-transforms": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.23.3.tgz", + "integrity": "sha512-7bBs4ED9OmswdfDzpz4MpWgSrV7FXlc3zIagvLFjS5H+Mk7Snr21vQ6QwrsoCGMfNC4e4LQPdoULEt4ykz0SRQ==", "dependencies": { - "@webassemblyjs/ast": "1.11.1", - "@webassemblyjs/helper-api-error": "1.11.1", - "@webassemblyjs/helper-wasm-bytecode": "1.11.1", - "@webassemblyjs/ieee754": "1.11.1", - "@webassemblyjs/leb128": "1.11.1", - "@webassemblyjs/utf8": "1.11.1" + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-module-imports": "^7.22.15", + "@babel/helper-simple-access": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/helper-validator-identifier": "^7.22.20" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" } }, - "node_modules/@webassemblyjs/wast-printer": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.11.1.tgz", - "integrity": "sha512-IQboUWM4eKzWW+N/jij2sRatKMh99QEelo3Eb2q0qXkvPRISAj8Qxtmw5itwqK+TTkBuUIE45AxYPToqPtL5gg==", + "node_modules/@babel/helper-optimise-call-expression": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.22.5.tgz", + "integrity": "sha512-HBwaojN0xFRx4yIvpwGqxiV2tUfl7401jlok564NgB9EHS1y6QT17FmKWm4ztqjeVdXLuC4fSvHc5ePpQjoTbw==", + "dev": true, "dependencies": { - "@webassemblyjs/ast": "1.11.1", - "@xtuc/long": "4.2.2" + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" } }, - "node_modules/@webpack-cli/configtest": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@webpack-cli/configtest/-/configtest-1.2.0.tgz", - "integrity": "sha512-4FB8Tj6xyVkyqjj1OaTqCjXYULB9FMkqQ8yGrZjRDrYh0nOE+7Lhs45WioWQQMV+ceFlE368Ukhe6xdvJM9Egg==", + "node_modules/@babel/helper-plugin-utils": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.22.5.tgz", + "integrity": "sha512-uLls06UVKgFG9QD4OeFYLEGteMIAa5kpTPcFL28yuCIIzsf6ZyKZMllKVOCZFhiZ5ptnwX4mtKdWCBE/uT4amg==", "dev": true, - "peerDependencies": { - "webpack": "4.x.x || 5.x.x", - "webpack-cli": "4.x.x" + "engines": { + "node": ">=6.9.0" } }, - "node_modules/@webpack-cli/info": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@webpack-cli/info/-/info-1.5.0.tgz", - "integrity": "sha512-e8tSXZpw2hPl2uMJY6fsMswaok5FdlGNRTktvFk2sD8RjH0hE2+XistawJx1vmKteh4NmGmNUrp+Tb2w+udPcQ==", + "node_modules/@babel/helper-remap-async-to-generator": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.22.20.tgz", + "integrity": "sha512-pBGyV4uBqOns+0UvhsTO8qgl8hO89PmiDYv+/COyp1aeMcmfrfruz+/nCMFiYyFF/Knn0yfrC85ZzNFjembFTw==", "dev": true, "dependencies": { - "envinfo": "^7.7.3" + "@babel/helper-annotate-as-pure": "^7.22.5", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-wrap-function": "^7.22.20" + }, + "engines": { + "node": ">=6.9.0" }, "peerDependencies": { - "webpack-cli": "4.x.x" + "@babel/core": "^7.0.0" } }, - "node_modules/@webpack-cli/serve": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/@webpack-cli/serve/-/serve-1.7.0.tgz", - "integrity": "sha512-oxnCNGj88fL+xzV+dacXs44HcDwf1ovs3AuEzvP7mqXw7fQntqIhQ1BRmynh4qEKQSSSRSWVyXRjmTbZIX9V2Q==", + "node_modules/@babel/helper-replace-supers": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.22.20.tgz", + "integrity": "sha512-qsW0In3dbwQUbK8kejJ4R7IHVGwHJlV6lpG6UA7a9hSa2YEiAib+N1T2kr6PEeUT+Fl7najmSOS6SmAwCHK6Tw==", "dev": true, - "peerDependencies": { - "webpack-cli": "4.x.x" + "dependencies": { + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-member-expression-to-functions": "^7.22.15", + "@babel/helper-optimise-call-expression": "^7.22.5" }, - "peerDependenciesMeta": { - "webpack-dev-server": { - "optional": true - } + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" } }, - "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==" - }, - "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==" - }, - "node_modules/accessible-autocomplete": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/accessible-autocomplete/-/accessible-autocomplete-2.0.4.tgz", - "integrity": "sha512-2p0txrSpvs5wXFUeQJHMheDPTZVSEmiUHWlEPb7vJnv2Dd1xPfoLnBQQMfNbTSit2pL/9sSQYESuD2Yyohd4Yw==", + "node_modules/@babel/helper-simple-access": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.22.5.tgz", + "integrity": "sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w==", "dependencies": { - "preact": "^8.3.1" - } - }, - "node_modules/acorn": { - "version": "8.8.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.1.tgz", - "integrity": "sha512-7zFpHzhnqYKrkYdUjF1HI1bzd0VygEGX8lFk4k5zVMqHEoES+P+7TKI+EvLO9WVMJ8eekdO0aDEK044xTXwPPA==", - "bin": { - "acorn": "bin/acorn" + "@babel/types": "^7.22.5" }, "engines": { - "node": ">=0.4.0" + "node": ">=6.9.0" } }, - "node_modules/acorn-import-assertions": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.8.0.tgz", - "integrity": "sha512-m7VZ3jwz4eK6A4Vtt8Ew1/mNbP24u0FhdyfA7fSvnJR6LMdfOYnmuIrrJAgrYfYJ10F/otaHTtrtrtmHdMNzEw==", - "peerDependencies": { - "acorn": "^8" + "node_modules/@babel/helper-skip-transparent-expression-wrappers": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.22.5.tgz", + "integrity": "sha512-tK14r66JZKiC43p8Ki33yLBVJKlQDFoA8GYN67lWCDCqoL6EMMSuM9b+Iff2jHaM/RRFYl7K+iiru7hbRqNx8Q==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" } }, - "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "node_modules/@babel/helper-split-export-declaration": { + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", + "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", "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" + "@babel/types": "^7.22.5" }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" + "engines": { + "node": ">=6.9.0" } }, - "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==", - "peerDependencies": { - "ajv": "^6.9.1" + "node_modules/@babel/helper-string-parser": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz", + "integrity": "sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==", + "engines": { + "node": ">=6.9.0" } }, - "node_modules/ansi-regex": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", - "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "node_modules/@babel/helper-validator-identifier": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", + "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", "engines": { - "node": ">=12" + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.23.5.tgz", + "integrity": "sha512-85ttAOMLsr53VgXkTbkx8oA6YTfT4q7/HzXSLEYmjcSTJPMPQtvq1BD79Byep5xMUYbGRzEpDsjUf3dyp54IKw==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-wrap-function": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.22.20.tgz", + "integrity": "sha512-pms/UwkOpnQe/PDAEdV/d7dVCoBbB+R4FvYoHGZz+4VPcg7RtYy2KP7S2lbuWM6FCSgob5wshfGESbC/hzNXZw==", + "dev": true, + "dependencies": { + "@babel/helper-function-name": "^7.22.5", + "@babel/template": "^7.22.15", + "@babel/types": "^7.22.19" }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" + "engines": { + "node": ">=6.9.0" } }, - "node_modules/anymatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz", - "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==", + "node_modules/@babel/helpers": { + "version": "7.23.2", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.23.2.tgz", + "integrity": "sha512-lzchcp8SjTSVe/fPmLwtWVBFC7+Tbn8LGHDVfDp9JGxpAY5opSaEFgt8UQvrnECWOTdji2mOWMz1rOhkHscmGQ==", "dependencies": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" + "@babel/template": "^7.22.15", + "@babel/traverse": "^7.23.2", + "@babel/types": "^7.23.0" }, "engines": { - "node": ">= 8" + "node": ">=6.9.0" } }, - "node_modules/big.js": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", - "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", + "node_modules/@babel/highlight": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.20.tgz", + "integrity": "sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==", + "dependencies": { + "@babel/helper-validator-identifier": "^7.22.20", + "chalk": "^2.4.2", + "js-tokens": "^4.0.0" + }, "engines": { - "node": "*" + "node": ">=6.9.0" } }, - "node_modules/binary-extensions": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", - "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "node_modules/@babel/parser": { + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.0.tgz", + "integrity": "sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw==", + "bin": { + "parser": "bin/babel-parser.js" + }, "engines": { - "node": ">=8" + "node": ">=6.0.0" } }, - "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/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { + "version": "7.23.3", + "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.23.3.tgz", + "integrity": "sha512-iRkKcCqb7iGnq9+3G6rZ+Ciz5VywC4XNRHe57lKM+jOeYAoR0lVqdeeDRfh0tQcTfw/+vBhHn926FmQhLtlFLQ==", + "dev": true, "dependencies": { - "fill-range": "^7.0.1" + "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { - "node": ">=8" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" } }, - "node_modules/browserslist": { - "version": "4.21.4", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.4.tgz", - "integrity": "sha512-CBHJJdDmgjl3daYjN5Cp5kbTf1mUhZoS+beLklHIvkOWscs83YAhLlF3Wsh/lciQYAcbBJgTOD44VtG31ZM4Hw==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - } - ], + "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.23.3.tgz", + "integrity": "sha512-WwlxbfMNdVEpQjZmK5mhm7oSwD3dS6eU+Iwsi4Knl9wAletWem7kaRsGOG+8UEbRyqxY4SS5zvtfXwX+jMxUwQ==", + "dev": true, "dependencies": { - "caniuse-lite": "^1.0.30001400", - "electron-to-chromium": "^1.4.251", - "node-releases": "^2.0.6", - "update-browserslist-db": "^1.0.9" - }, - "bin": { - "browserslist": "cli.js" + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", + "@babel/plugin-transform-optional-chaining": "^7.23.3" }, "engines": { - "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.13.0" } }, - "node_modules/buffer-from": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", - "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==" - }, - "node_modules/caniuse-lite": { - "version": "1.0.30001434", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001434.tgz", - "integrity": "sha512-aOBHrLmTQw//WFa2rcF1If9fa3ypkC1wzqqiKHgfdrXTWcU8C4gKVZT77eQAPWN1APys3+uQ0Df07rKauXGEYA==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/caniuse-lite" - } - ] - }, - "node_modules/chart.js": { - "version": "3.9.1", - "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-3.9.1.tgz", - "integrity": "sha512-Ro2JbLmvg83gXF5F4sniaQ+lTbSv18E+TIf2cOeiH1Iqd2PGFOtem+DUufMZsCJwFE7ywPOpfXFBwRTGq7dh6w==" - }, - "node_modules/chartjs-adapter-moment": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/chartjs-adapter-moment/-/chartjs-adapter-moment-1.0.0.tgz", - "integrity": "sha512-PqlerEvQcc5hZLQ/NQWgBxgVQ4TRdvkW3c/t+SUEQSj78ia3hgLkf2VZ2yGJtltNbEEFyYGm+cA6XXevodYvWA==", - "peerDependencies": { - "chart.js": "^3.0.0", - "moment": "^2.10.2" - } - }, - "node_modules/chokidar": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", - "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", - "funding": [ - { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - ], + "node_modules/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": { + "version": "7.23.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.23.7.tgz", + "integrity": "sha512-LlRT7HgaifEpQA1ZgLVOIJZZFVPWN5iReq/7/JixwBtwcoeVGDBD53ZV28rrsLYOZs1Y/EHhA8N/Z6aazHR8cw==", + "dev": true, "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" + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { - "node": ">= 8.10.0" + "node": ">=6.9.0" }, - "optionalDependencies": { - "fsevents": "~2.3.2" + "peerDependencies": { + "@babel/core": "^7.0.0" } }, - "node_modules/chrome-trace-event": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz", - "integrity": "sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg==", + "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.0" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/clone-deep": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", - "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", + "node_modules/@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", "dev": true, "dependencies": { - "is-plain-object": "^2.0.4", - "kind-of": "^6.0.2", - "shallow-clone": "^3.0.0" + "@babel/helper-plugin-utils": "^7.8.0" }, - "engines": { - "node": ">=6" + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/colorette": { - "version": "2.0.19", - "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.19.tgz", - "integrity": "sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ==", - "dev": true - }, - "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==" + "node_modules/@babel/plugin-syntax-bigint": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", + "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } }, - "node_modules/cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", "dev": true, "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" + "@babel/helper-plugin-utils": "^7.12.13" }, - "engines": { - "node": ">= 8" + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/css-loader": { - "version": "5.2.7", - "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-5.2.7.tgz", - "integrity": "sha512-Q7mOvpBNBG7YrVGMxRxcBJZFL75o+cH2abNASdibkj/fffYD8qWbInZrD0S9ccI6vZclF3DsHE7njGlLtaHbhg==", + "node_modules/@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", + "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", + "dev": true, "dependencies": { - "icss-utils": "^5.1.0", - "loader-utils": "^2.0.0", - "postcss": "^8.2.15", - "postcss-modules-extract-imports": "^3.0.0", - "postcss-modules-local-by-default": "^4.0.0", - "postcss-modules-scope": "^3.0.0", - "postcss-modules-values": "^4.0.0", - "postcss-value-parser": "^4.1.0", - "schema-utils": "^3.0.0", - "semver": "^7.3.5" + "@babel/helper-plugin-utils": "^7.14.5" }, "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" + "node": ">=6.9.0" }, "peerDependencies": { - "webpack": "^4.27.0 || ^5.0.0" + "@babel/core": "^7.0.0-0" } }, - "node_modules/cssesc": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", - "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", - "bin": { - "cssesc": "bin/cssesc" + "node_modules/@babel/plugin-syntax-dynamic-import": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz", + "integrity": "sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" }, - "engines": { - "node": ">=4" + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/electron-to-chromium": { - "version": "1.4.284", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.284.tgz", - "integrity": "sha512-M8WEXFuKXMYMVr45fo8mq0wUrrJHheiKZf6BArTKk9ZBYCKJEOU5H8cdWgDT+qCVZf7Na4lVUaZsA+h6uA9+PA==" - }, - "node_modules/emojis-list": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", - "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==", - "engines": { - "node": ">= 4" + "node_modules/@babel/plugin-syntax-export-namespace-from": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-export-namespace-from/-/plugin-syntax-export-namespace-from-7.8.3.tgz", + "integrity": "sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.3" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/enhanced-resolve": { - "version": "5.10.0", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.10.0.tgz", - "integrity": "sha512-T0yTFjdpldGY8PmuXXR0PyQ1ufZpEGiHVrp7zHKB7jdR4qlmZHhONVM5AQOAWXuF/w3dnHbEQVrNptJgt7F+cQ==", + "node_modules/@babel/plugin-syntax-import-assertions": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.23.3.tgz", + "integrity": "sha512-lPgDSU+SJLK3xmFDTV2ZRQAiM7UuUjGidwBywFavObCiZc1BeAAcMtHJKUya92hPHO+at63JJPLygilZard8jw==", + "dev": true, "dependencies": { - "graceful-fs": "^4.2.4", - "tapable": "^2.2.0" + "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { - "node": ">=10.13.0" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/envinfo": { - "version": "7.8.1", - "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.8.1.tgz", - "integrity": "sha512-/o+BXHmB7ocbHEAs6F2EnG0ogybVVUdkRunTT2glZU9XAaGmhqskrvKwqXuDfNjEO0LZKWdejEEpnq8aM0tOaw==", + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.23.3.tgz", + "integrity": "sha512-pawnE0P9g10xgoP7yKr6CK63K2FMsTE+FZidZO/1PwRdzmAPVs+HS1mAURUsgaoxammTJvULUdIkEK0gOcU2tA==", "dev": true, - "bin": { - "envinfo": "dist/cli.js" + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { - "node": ">=4" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/es-module-lexer": { - "version": "0.9.3", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-0.9.3.tgz", - "integrity": "sha512-1HQ2M2sPtxwnvOvT1ZClHyQDiggdNjURWpY2we6aMKCQiUVxTmVs2UYPLIrD84sS+kMdUwfBSylbJPwNnBrnHQ==" - }, - "node_modules/escalade": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", - "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", - "engines": { - "node": ">=6" + "node_modules/@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-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==", + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^4.1.1" + "@babel/helper-plugin-utils": "^7.8.0" }, - "engines": { - "node": ">=8.0.0" + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "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==", + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.22.5.tgz", + "integrity": "sha512-gvyP4hZrgrs/wWMaocvxZ44Hw0b3W8Pe+cMxc8V1ULQ07oh8VNbIRaoD1LRZVTvD+0nieDKjfgKg89sD7rrKrg==", + "dev": true, "dependencies": { - "estraverse": "^5.2.0" + "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { - "node": ">=4.0" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-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==", - "engines": { - "node": ">=4.0" + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/estraverse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", - "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", - "engines": { - "node": ">=4.0" + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/events": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", - "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", - "engines": { - "node": ">=0.8.x" + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "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==" + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } }, - "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==" + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } }, - "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==", + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", "dev": true, - "engines": { - "node": ">= 4.9.1" + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/file-loader": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/file-loader/-/file-loader-6.2.0.tgz", - "integrity": "sha512-qo3glqyTa61Ytg4u73GultjHGjdRyig3tG6lPtyX/jOEJvHif9uB0/OCI2Kif6ctF3caQTW2G5gym21oAsI4pw==", + "node_modules/@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", + "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", + "dev": true, "dependencies": { - "loader-utils": "^2.0.0", - "schema-utils": "^3.0.0" + "@babel/helper-plugin-utils": "^7.14.5" }, "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" + "node": ">=6.9.0" }, "peerDependencies": { - "webpack": "^4.0.0 || ^5.0.0" + "@babel/core": "^7.0.0-0" } }, - "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/@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "dev": true, "dependencies": { - "to-regex-range": "^5.0.1" + "@babel/helper-plugin-utils": "^7.14.5" }, "engines": { - "node": ">=8" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.23.3.tgz", + "integrity": "sha512-9EiNjVJOMwCO+43TqoTrgQ8jMwcAd0sWyXi9RPfIsLTj4R2MADDDQXELhffaUx/uJv2AYcxBgPwH6j4TIA4ytQ==", "dev": true, "dependencies": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" + "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { - "node": ">=8" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "hasInstallScript": true, - "optional": true, - "os": [ - "darwin" - ], + "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": "^8.16.0 || ^10.6.0 || >=11.0.0" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" } }, - "node_modules/function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", - "dev": true - }, - "node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "node_modules/@babel/plugin-transform-arrow-functions": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.23.3.tgz", + "integrity": "sha512-NzQcQrzaQPkaEwoTm4Mhyl8jI1huEL/WWIEvudjTCMJ9aBZNpsJbMASx7EQECtQQPS/DcnFpo0FIh3LvEO9cxQ==", + "dev": true, "dependencies": { - "is-glob": "^4.0.1" + "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { - "node": ">= 6" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-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==" - }, - "node_modules/govuk-frontend": { - "version": "3.14.0", - "resolved": "https://registry.npmjs.org/govuk-frontend/-/govuk-frontend-3.14.0.tgz", - "integrity": "sha512-y7FTuihCSA8Hty+e9h0uPhCoNanCAN+CLioNFlPmlbeHXpbi09VMyxTcH+XfnMPY4Cp++7096v0rLwwdapTXnA==", + "node_modules/@babel/plugin-transform-async-generator-functions": { + "version": "7.23.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.23.7.tgz", + "integrity": "sha512-PdxEpL71bJp1byMG0va5gwQcXHxuEYC/BgI/e88mGTtohbZN28O5Yit0Plkkm/dBzCF/BxmbNcses1RH1T+urA==", + "dev": true, + "dependencies": { + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-remap-async-to-generator": "^7.22.20", + "@babel/plugin-syntax-async-generators": "^7.8.4" + }, "engines": { - "node": ">= 4.2.0" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/graceful-fs": { - "version": "4.2.10", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", - "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==" - }, - "node_modules/has": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", - "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "node_modules/@babel/plugin-transform-async-to-generator": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.23.3.tgz", + "integrity": "sha512-A7LFsKi4U4fomjqXJlZg/u0ft/n8/7n7lpffUP/ZULx/DtV9SGlNKZolHH6PE8Xl1ngCc0M11OaeZptXVkfKSw==", "dev": true, "dependencies": { - "function-bind": "^1.1.1" + "@babel/helper-module-imports": "^7.22.15", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-remap-async-to-generator": "^7.22.20" }, "engines": { - "node": ">= 0.4.0" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "node_modules/@babel/plugin-transform-block-scoped-functions": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.23.3.tgz", + "integrity": "sha512-vI+0sIaPIO6CNuM9Kk5VmXcMVRiOpDh7w2zZt9GXzmE/9KD70CUEVhvPR/etAeNK/FAEkhxQtXOzVF3EuRL41A==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, "engines": { - "node": ">=8" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/icss-utils": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz", - "integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==", + "node_modules/@babel/plugin-transform-block-scoping": { + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.23.4.tgz", + "integrity": "sha512-0QqbP6B6HOh7/8iNR4CQU2Th/bbRtBp4KS9vcaZd1fZ0wSh5Fyssg0UCIHwxh+ka+pNDREbVLQnHCMHKZfPwfw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, "engines": { - "node": "^10 || ^12 || >= 14" + "node": ">=6.9.0" }, "peerDependencies": { - "postcss": "^8.1.0" + "@babel/core": "^7.0.0-0" } }, - "node_modules/immutable": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.1.0.tgz", - "integrity": "sha512-oNkuqVTA8jqG1Q6c+UglTOD1xhC1BtjKI7XkCXRkZHrN5m18/XsnUp8Q89GkQO/z+0WjonSvl0FLhDYftp46nQ==" - }, - "node_modules/import-local": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.1.0.tgz", - "integrity": "sha512-ASB07uLtnDs1o6EHjKpX34BKYDSqnFerfTOJL2HvMqF70LnxpjkzDB8J44oT9pu4AMPkQwf8jl6szgvNd2tRIg==", + "node_modules/@babel/plugin-transform-class-properties": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.23.3.tgz", + "integrity": "sha512-uM+AN8yCIjDPccsKGlw271xjJtGii+xQIF/uMPS8H15L12jZTsLfF4o5vNO7d/oUguOyfdikHGc/yi9ge4SGIg==", "dev": true, "dependencies": { - "pkg-dir": "^4.2.0", - "resolve-cwd": "^3.0.0" - }, - "bin": { - "import-local-fixture": "fixtures/cli.js" + "@babel/helper-create-class-features-plugin": "^7.22.15", + "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { - "node": ">=8" + "node": ">=6.9.0" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/interpret": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/interpret/-/interpret-2.2.0.tgz", - "integrity": "sha512-Ju0Bz/cEia55xDwUWEa8+olFpCiQoypjnQySseKtmjNrnps3P+xfpUmGr90T7yjlVJmOtybRvPXhKMbHr+fWnw==", + "node_modules/@babel/plugin-transform-class-static-block": { + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.23.4.tgz", + "integrity": "sha512-nsWu/1M+ggti1SOALj3hfx5FXzAY06fwPJsUZD4/A5e1bWi46VUIWtD+kOX6/IdhXGsXBWllLFDSnqSCdUNydQ==", "dev": true, + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.22.15", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-class-static-block": "^7.14.5" + }, "engines": { - "node": ">= 0.10" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.12.0" } }, - "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==", + "node_modules/@babel/plugin-transform-classes": { + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.23.5.tgz", + "integrity": "sha512-jvOTR4nicqYC9yzOHIhXG5emiFEOpappSJAl73SDSEDcybD+Puuze8Tnpb9p9qEyYup24tq891gkaygIFvWDqg==", + "dev": true, "dependencies": { - "binary-extensions": "^2.0.0" + "@babel/helper-annotate-as-pure": "^7.22.5", + "@babel/helper-compilation-targets": "^7.22.15", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-function-name": "^7.23.0", + "@babel/helper-optimise-call-expression": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-replace-supers": "^7.22.20", + "@babel/helper-split-export-declaration": "^7.22.6", + "globals": "^11.1.0" }, "engines": { - "node": ">=8" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/is-core-module": { - "version": "2.11.0", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.11.0.tgz", - "integrity": "sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw==", + "node_modules/@babel/plugin-transform-computed-properties": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.23.3.tgz", + "integrity": "sha512-dTj83UVTLw/+nbiHqQSFdwO9CbTtwq1DsDqm3CUEtDrZNET5rT5E6bIdTlOftDTDLMYxvxHNEYO4B9SLl8SLZw==", "dev": true, "dependencies": { - "has": "^1.0.3" + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/template": "^7.22.15" }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "node_modules/@babel/plugin-transform-destructuring": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.23.3.tgz", + "integrity": "sha512-n225npDqjDIr967cMScVKHXJs7rout1q+tt50inyBCPkyZ8KxeI6d+GIbSBTT/w/9WdlWDOej3V9HE5Lgk57gw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, "engines": { - "node": ">=0.10.0" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "node_modules/@babel/plugin-transform-dotall-regex": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.23.3.tgz", + "integrity": "sha512-vgnFYDHAKzFaTVp+mneDsIEbnJ2Np/9ng9iviHw3P/KVcgONxpNULEW/51Z/BaFojG2GI2GwwXck5uV1+1NOYQ==", + "dev": true, "dependencies": { - "is-extglob": "^2.1.1" + "@babel/helper-create-regexp-features-plugin": "^7.22.15", + "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { - "node": ">=0.10.0" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "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==", + "node_modules/@babel/plugin-transform-duplicate-keys": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.23.3.tgz", + "integrity": "sha512-RrqQ+BQmU3Oyav3J+7/myfvRCq7Tbz+kKLLshUmMwNlDHExbGL7ARhajvoBJEvc+fCguPPu887N+3RRXBVKZUA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, "engines": { - "node": ">=0.12.0" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "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==", + "node_modules/@babel/plugin-transform-dynamic-import": { + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.23.4.tgz", + "integrity": "sha512-V6jIbLhdJK86MaLh4Jpghi8ho5fGzt3imHOBu/x0jlBaPYqDoWz4RDXjmMOfnh+JWNaQleEAByZLV0QzBT4YQQ==", "dev": true, "dependencies": { - "isobject": "^3.0.1" + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-dynamic-import": "^7.8.3" }, "engines": { - "node": ">=0.10.0" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true - }, - "node_modules/isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", + "node_modules/@babel/plugin-transform-exponentiation-operator": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.23.3.tgz", + "integrity": "sha512-5fhCsl1odX96u7ILKHBj4/Y8vipoqwsJMh4csSA8qFfxrZDEA4Ssku2DyNvMJSmZNOEBT750LfFPbtrnTP90BQ==", "dev": true, + "dependencies": { + "@babel/helper-builder-binary-assignment-operator-visitor": "^7.22.15", + "@babel/helper-plugin-utils": "^7.22.5" + }, "engines": { - "node": ">=0.10.0" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "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==", + "node_modules/@babel/plugin-transform-export-namespace-from": { + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.23.4.tgz", + "integrity": "sha512-GzuSBcKkx62dGzZI1WVgTWvkkz84FZO5TC5T8dl/Tht/rAla6Dg/Mz9Yhypg+ezVACf/rgDuQt3kbWEv7LdUDQ==", + "dev": true, "dependencies": { - "@types/node": "*", - "merge-stream": "^2.0.0", - "supports-color": "^8.0.0" + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-export-namespace-from": "^7.8.3" }, "engines": { - "node": ">= 10.13.0" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "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==" + "node_modules/@babel/plugin-transform-for-of": { + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.23.6.tgz", + "integrity": "sha512-aYH4ytZ0qSuBbpfhuofbg/e96oQ7U2w1Aw/UQmKT+1l39uEhUPoFS3fHevDc1G0OvewyDudfMKY1OulczHzWIw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } }, - "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==" + "node_modules/@babel/plugin-transform-function-name": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.23.3.tgz", + "integrity": "sha512-I1QXp1LxIvt8yLaib49dRW5Okt7Q4oaxao6tFVKS/anCdEOMtYwWVKoiOA1p34GOWIZjUK0E+zCp7+l1pfQyiw==", + "dev": true, + "dependencies": { + "@babel/helper-compilation-targets": "^7.22.15", + "@babel/helper-function-name": "^7.23.0", + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } }, - "node_modules/json5": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", - "bin": { - "json5": "lib/cli.js" + "node_modules/@babel/plugin-transform-json-strings": { + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.23.4.tgz", + "integrity": "sha512-81nTOqM1dMwZ/aRXQ59zVubN9wHGqk6UtqRK+/q+ciXmRy8fSolhGVvG09HHRGo4l6fr/c4ZhXUQH0uFW7PZbg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-json-strings": "^7.8.3" }, "engines": { - "node": ">=6" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "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==", + "node_modules/@babel/plugin-transform-literals": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.23.3.tgz", + "integrity": "sha512-wZ0PIXRxnwZvl9AYpqNUxpZ5BiTGrYt7kueGQ+N5FiQ7RCOD4cm8iShd6S6ggfVIWaJf2EMk8eRzAh52RfP4rQ==", "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, "engines": { - "node": ">=0.10.0" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/klona": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/klona/-/klona-2.0.5.tgz", - "integrity": "sha512-pJiBpiXMbt7dkzXe8Ghj/u4FfXOOa98fPW+bihOJ4SjnoijweJrNThJfd3ifXpXhREjpoF2mZVH1GfS9LV3kHQ==", + "node_modules/@babel/plugin-transform-logical-assignment-operators": { + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.23.4.tgz", + "integrity": "sha512-Mc/ALf1rmZTP4JKKEhUwiORU+vcfarFVLfcFiolKUo6sewoxSEgl36ak5t+4WamRsNr6nzjZXQjM35WsU+9vbg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4" + }, "engines": { - "node": ">= 8" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-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==", + "node_modules/@babel/plugin-transform-member-expression-literals": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.23.3.tgz", + "integrity": "sha512-sC3LdDBDi5x96LA+Ytekz2ZPk8i/Ck+DEuDbRAll5rknJ5XRTSaPKEYwomLcs1AA8wg9b3KjIQRsnApj+q51Ag==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, "engines": { - "node": ">=6.11.5" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/loader-utils": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", - "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", + "node_modules/@babel/plugin-transform-modules-amd": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.23.3.tgz", + "integrity": "sha512-vJYQGxeKM4t8hYCKVBlZX/gtIY2I7mRGFNcm85sgXGMTBcoV3QdVtdpbcWEbzbfUIUZKwvgFT82mRvaQIebZzw==", + "dev": true, "dependencies": { - "big.js": "^5.2.2", - "emojis-list": "^3.0.0", - "json5": "^2.1.2" + "@babel/helper-module-transforms": "^7.23.3", + "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { - "node": ">=8.9.0" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "node_modules/@babel/plugin-transform-modules-commonjs": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.23.3.tgz", + "integrity": "sha512-aVS0F65LKsdNOtcz6FRCpE4OgsP2OFnW46qNxNIX9h3wuzaNcSQsJysuMwqSibC98HPrf2vCgtxKNwS0DAlgcA==", "dev": true, "dependencies": { - "p-locate": "^4.1.0" + "@babel/helper-module-transforms": "^7.23.3", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-simple-access": "^7.22.5" }, "engines": { - "node": ">=8" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/lodash.assign": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/lodash.assign/-/lodash.assign-4.2.0.tgz", - "integrity": "sha512-hFuH8TY+Yji7Eja3mGiuAxBqLagejScbG8GbG0j6o9vzn0YL14My+ktnqtZgFTosKymC9/44wP6s7xyuLfnClw==" - }, - "node_modules/lodash.defaults": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", - "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==" - }, - "node_modules/lodash.foreach": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.foreach/-/lodash.foreach-4.5.0.tgz", - "integrity": "sha512-aEXTF4d+m05rVOAUG3z4vZZ4xVexLKZGF0lIxuHZ1Hplpk/3B6Z1+/ICICYRLm7c41Z2xiejbkCkJoTlypoXhQ==" - }, - "node_modules/lodash.frompairs": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/lodash.frompairs/-/lodash.frompairs-4.0.1.tgz", - "integrity": "sha512-dvqe2I+cO5MzXCMhUnfYFa9MD+/760yx2aTAN1lqEcEkf896TxgrX373igVdqSJj6tQd0jnSLE1UMuKufqqxFw==" - }, - "node_modules/lodash.get": { - "version": "4.4.2", - "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", - "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==" - }, - "node_modules/lodash.topairs": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/lodash.topairs/-/lodash.topairs-4.3.0.tgz", - "integrity": "sha512-qrRMbykBSEGdOgQLJJqVSdPWMD7Q+GJJ5jMRfQYb+LTLsw3tYVIabnCzRqTJb2WTo17PG5gNzXuFaZgYH/9SAQ==" - }, - "node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "node_modules/@babel/plugin-transform-modules-systemjs": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.23.3.tgz", + "integrity": "sha512-ZxyKGTkF9xT9YJuKQRo19ewf3pXpopuYQd8cDXqNzc3mUNbOME0RKMoZxviQk74hwzfQsEe66dE92MaZbdHKNQ==", + "dev": true, "dependencies": { - "yallist": "^4.0.0" + "@babel/helper-hoist-variables": "^7.22.5", + "@babel/helper-module-transforms": "^7.23.3", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-validator-identifier": "^7.22.20" }, "engines": { - "node": ">=10" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "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==" - }, - "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==", + "node_modules/@babel/plugin-transform-modules-umd": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.23.3.tgz", + "integrity": "sha512-zHsy9iXX2nIsCBFPud3jKn1IRPWg3Ing1qOZgeKV39m1ZgIdpJqvlWVeiHBZC6ITRG0MfskhYe9cLgntfSFPIg==", + "dev": true, + "dependencies": { + "@babel/helper-module-transforms": "^7.23.3", + "@babel/helper-plugin-utils": "^7.22.5" + }, "engines": { - "node": ">= 0.6" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "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==", + "node_modules/@babel/plugin-transform-named-capturing-groups-regex": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.22.5.tgz", + "integrity": "sha512-YgLLKmS3aUBhHaxp5hi1WJTgOUb/NCuDHzGT9z9WTt3YG+CPRhJs6nprbStx6DnWM4dh6gt7SU3sZodbZ08adQ==", + "dev": true, "dependencies": { - "mime-db": "1.52.0" + "@babel/helper-create-regexp-features-plugin": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { - "node": ">= 0.6" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" } }, - "node_modules/mini-css-extract-plugin": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-1.6.2.tgz", - "integrity": "sha512-WhDvO3SjGm40oV5y26GjMJYjd2UMqrLAGKy5YS2/3QKJy2F7jgynuHTir/tgUUOiNQu5saXHdc8reo7YuhhT4Q==", + "node_modules/@babel/plugin-transform-new-target": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.23.3.tgz", + "integrity": "sha512-YJ3xKqtJMAT5/TIZnpAR3I+K+WaDowYbN3xyxI8zxx/Gsypwf9B9h0VB+1Nh6ACAAPRS5NSRje0uVv5i79HYGQ==", + "dev": true, "dependencies": { - "loader-utils": "^2.0.0", - "schema-utils": "^3.0.0", - "webpack-sources": "^1.1.0" + "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" + "node": ">=6.9.0" }, "peerDependencies": { - "webpack": "^4.4.0 || ^5.0.0" + "@babel/core": "^7.0.0-0" } }, - "node_modules/moment": { - "version": "2.29.4", - "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz", - "integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==", + "node_modules/@babel/plugin-transform-nullish-coalescing-operator": { + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.23.4.tgz", + "integrity": "sha512-jHE9EVVqHKAQx+VePv5LLGHjmHSJR76vawFPTdlxR/LVJPfOEGxREQwQfjuZEOPTwG92X3LINSh3M40Rv4zpVA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3" + }, "engines": { - "node": "*" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/nanoid": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz", - "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "bin": { - "nanoid": "bin/nanoid.cjs" + "node_modules/@babel/plugin-transform-numeric-separator": { + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.23.4.tgz", + "integrity": "sha512-mps6auzgwjRrwKEZA05cOwuDc9FAzoyFS4ZsG/8F43bTLf/TgkJg7QXOrPO1JO599iA3qgK9MXdMGOEC8O1h6Q==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-numeric-separator": "^7.10.4" }, "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-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==" - }, - "node_modules/node-releases": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.6.tgz", - "integrity": "sha512-PiVXnNuFm5+iYkLBNeq5211hvO38y63T0i2KKh2KnUs3RpzJ+JtODFjkD8yjLwnDkTYF1eKXheUwdssR+NRZdg==" - }, - "node_modules/normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "node_modules/@babel/plugin-transform-object-rest-spread": { + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.23.4.tgz", + "integrity": "sha512-9x9K1YyeQVw0iOXJlIzwm8ltobIIv7j2iLyP2jIhEbqPRQ7ScNgwQufU2I0Gq11VjyG4gI4yMXt2VFags+1N3g==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.23.3", + "@babel/helper-compilation-targets": "^7.22.15", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-transform-parameters": "^7.23.3" + }, "engines": { - "node": ">=0.10.0" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "node_modules/@babel/plugin-transform-object-super": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.23.3.tgz", + "integrity": "sha512-BwQ8q0x2JG+3lxCVFohg+KbQM7plfpBwThdW9A6TMtWwLsbDA01Ek2Zb/AgDN39BiZsExm4qrXxjk+P1/fzGrA==", "dev": true, "dependencies": { - "p-try": "^2.0.0" + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-replace-supers": "^7.22.20" }, "engines": { - "node": ">=6" + "node": ">=6.9.0" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "node_modules/@babel/plugin-transform-optional-catch-binding": { + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.23.4.tgz", + "integrity": "sha512-XIq8t0rJPHf6Wvmbn9nFxU6ao4c7WhghTR5WyV8SrJfUFzyxhCm4nhC+iAp3HFhbAKLfYpgzhJ6t4XCtVwqO5A==", "dev": true, "dependencies": { - "p-limit": "^2.2.0" + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3" }, "engines": { - "node": ">=8" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "node_modules/@babel/plugin-transform-optional-chaining": { + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.23.4.tgz", + "integrity": "sha512-ZU8y5zWOfjM5vZ+asjgAPwDaBjJzgufjES89Rs4Lpq63O300R/kOz30WCLo6BxxX6QVEilwSlpClnG5cZaikTA==", "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", + "@babel/plugin-syntax-optional-chaining": "^7.8.3" + }, "engines": { - "node": ">=6" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "node_modules/@babel/plugin-transform-parameters": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.23.3.tgz", + "integrity": "sha512-09lMt6UsUb3/34BbECKVbVwrT9bO6lILWln237z7sLaWnMsTi7Yc9fhX5DLpkJzAGfaReXI22wP41SZmnAA3Vw==", "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, "engines": { - "node": ">=8" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "node_modules/@babel/plugin-transform-private-methods": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.23.3.tgz", + "integrity": "sha512-UzqRcRtWsDMTLrRWFvUBDwmw06tCQH9Rl1uAjfh6ijMSmGYQ+fpdB+cnqRC8EMh5tuuxSv0/TejGL+7vyj+50g==", "dev": true, + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.22.15", + "@babel/helper-plugin-utils": "^7.22.5" + }, "engines": { - "node": ">=8" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/path-parse": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true - }, - "node_modules/picocolors": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", - "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==" - }, - "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "node_modules/@babel/plugin-transform-private-property-in-object": { + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.23.4.tgz", + "integrity": "sha512-9G3K1YqTq3F4Vt88Djx1UZ79PDyj+yKRnUy7cZGSMe+a7jkwD259uKKuUzQlPkGam7R+8RJwh5z4xO27fA1o2A==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.22.5", + "@babel/helper-create-class-features-plugin": "^7.22.15", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5" + }, "engines": { - "node": ">=8.6" + "node": ">=6.9.0" }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/pkg-dir": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", - "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "node_modules/@babel/plugin-transform-property-literals": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.23.3.tgz", + "integrity": "sha512-jR3Jn3y7cZp4oEWPFAlRsSWjxKe4PZILGBSd4nis1TsC5qeSpb+nrtihJuDhNI7QHiVbUaiXa0X2RZY3/TI6Nw==", "dev": true, "dependencies": { - "find-up": "^4.0.0" + "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { - "node": ">=8" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/postcss": { - "version": "8.4.31", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", - "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], + "node_modules/@babel/plugin-transform-react-display-name": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.23.3.tgz", + "integrity": "sha512-GnvhtVfA2OAtzdX58FJxU19rhoGeQzyVndw3GgtdECQvQFXPEZIOVULHVZGAYmOgmqjXpVpfocAbSjh99V/Fqw==", + "dev": true, "dependencies": { - "nanoid": "^3.3.6", - "picocolors": "^1.0.0", - "source-map-js": "^1.0.2" + "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { - "node": "^10 || ^12 || >=14" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/postcss-modules-extract-imports": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.0.0.tgz", - "integrity": "sha512-bdHleFnP3kZ4NYDhuGlVK+CMrQ/pqUm8bx/oGL93K6gVwiclvX5x0n76fYMKuIGKzlABOy13zsvqjb0f92TEXw==", + "node_modules/@babel/plugin-transform-react-jsx": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.22.15.tgz", + "integrity": "sha512-oKckg2eZFa8771O/5vi7XeTvmM6+O9cxZu+kanTU7tD4sin5nO/G8jGJhq8Hvt2Z0kUoEDRayuZLaUlYl8QuGA==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.22.5", + "@babel/helper-module-imports": "^7.22.15", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-jsx": "^7.22.5", + "@babel/types": "^7.22.15" + }, "engines": { - "node": "^10 || ^12 || >= 14" + "node": ">=6.9.0" }, "peerDependencies": { - "postcss": "^8.1.0" + "@babel/core": "^7.0.0-0" } }, - "node_modules/postcss-modules-local-by-default": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.0.tgz", - "integrity": "sha512-sT7ihtmGSF9yhm6ggikHdV0hlziDTX7oFoXtuVWeDd3hHObNkcHRo9V3yg7vCAY7cONyxJC/XXCmmiHHcvX7bQ==", + "node_modules/@babel/plugin-transform-react-jsx-development": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.22.5.tgz", + "integrity": "sha512-bDhuzwWMuInwCYeDeMzyi7TaBgRQei6DqxhbyniL7/VG4RSS7HtSL2QbY4eESy1KJqlWt8g3xeEBGPuo+XqC8A==", + "dev": true, "dependencies": { - "icss-utils": "^5.0.0", - "postcss-selector-parser": "^6.0.2", - "postcss-value-parser": "^4.1.0" + "@babel/plugin-transform-react-jsx": "^7.22.5" }, "engines": { - "node": "^10 || ^12 || >= 14" + "node": ">=6.9.0" }, "peerDependencies": { - "postcss": "^8.1.0" + "@babel/core": "^7.0.0-0" } }, - "node_modules/postcss-modules-scope": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.0.0.tgz", - "integrity": "sha512-hncihwFA2yPath8oZ15PZqvWGkWf+XUfQgUGamS4LqoP1anQLOsOJw0vr7J7IwLpoY9fatA2qiGUGmuZL0Iqlg==", + "node_modules/@babel/plugin-transform-react-pure-annotations": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.23.3.tgz", + "integrity": "sha512-qMFdSS+TUhB7Q/3HVPnEdYJDQIk57jkntAwSuz9xfSE4n+3I+vHYCli3HoHawN1Z3RfCz/y1zXA/JXjG6cVImQ==", + "dev": true, "dependencies": { - "postcss-selector-parser": "^6.0.4" + "@babel/helper-annotate-as-pure": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { - "node": "^10 || ^12 || >= 14" + "node": ">=6.9.0" }, "peerDependencies": { - "postcss": "^8.1.0" + "@babel/core": "^7.0.0-0" } }, - "node_modules/postcss-modules-values": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz", - "integrity": "sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==", + "node_modules/@babel/plugin-transform-regenerator": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.23.3.tgz", + "integrity": "sha512-KP+75h0KghBMcVpuKisx3XTu9Ncut8Q8TuvGO4IhY+9D5DFEckQefOuIsB/gQ2tG71lCke4NMrtIPS8pOj18BQ==", + "dev": true, "dependencies": { - "icss-utils": "^5.0.0" + "@babel/helper-plugin-utils": "^7.22.5", + "regenerator-transform": "^0.15.2" }, "engines": { - "node": "^10 || ^12 || >= 14" + "node": ">=6.9.0" }, "peerDependencies": { - "postcss": "^8.1.0" + "@babel/core": "^7.0.0-0" } }, - "node_modules/postcss-selector-parser": { - "version": "6.0.11", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.11.tgz", - "integrity": "sha512-zbARubNdogI9j7WY4nQJBiNqQf3sLS3wCP4WfOidu+p28LofJqDH1tcXypGrcmMHhDk2t9wGhCsYe/+szLTy1g==", + "node_modules/@babel/plugin-transform-reserved-words": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.23.3.tgz", + "integrity": "sha512-QnNTazY54YqgGxwIexMZva9gqbPa15t/x9VS+0fsEFWplwVpXYZivtgl43Z1vMpc1bdPP2PP8siFeVcnFvA3Cg==", + "dev": true, "dependencies": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" + "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { - "node": ">=4" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/postcss-value-parser": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" - }, - "node_modules/preact": { - "version": "8.5.3", - "resolved": "https://registry.npmjs.org/preact/-/preact-8.5.3.tgz", - "integrity": "sha512-O3kKP+1YdgqHOFsZF2a9JVdtqD+RPzCQc3rP+Ualf7V6rmRDchZ9MJbiGTT7LuyqFKZqlHSOyO/oMFmI2lVTsw==", - "hasInstallScript": true - }, - "node_modules/punycode": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", - "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", + "node_modules/@babel/plugin-transform-shorthand-properties": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.23.3.tgz", + "integrity": "sha512-ED2fgqZLmexWiN+YNFX26fx4gh5qHDhn1O2gvEhreLW2iI63Sqm4llRLCXALKrCnbN4Jy0VcMQZl/SAzqug/jg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, "engines": { - "node": ">=6" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/randombytes": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", - "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "node_modules/@babel/plugin-transform-spread": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.23.3.tgz", + "integrity": "sha512-VvfVYlrlBVu+77xVTOAoxQ6mZbnIq5FM0aGBSFEcIh03qHf+zNqA4DC/3XMUozTg7bZV3e3mZQ0i13VB6v5yUg==", + "dev": true, "dependencies": { - "safe-buffer": "^5.1.0" + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "node_modules/@babel/plugin-transform-sticky-regex": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.23.3.tgz", + "integrity": "sha512-HZOyN9g+rtvnOU3Yh7kSxXrKbzgrm5X4GncPY1QOquu7epga5MxKHVpYu2hvQnry/H+JjckSYRb93iNfsioAGg==", + "dev": true, "dependencies": { - "picomatch": "^2.2.1" + "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { - "node": ">=8.10.0" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/rechoir": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.7.1.tgz", - "integrity": "sha512-/njmZ8s1wVeR6pjTZ+0nCnv8SpZNRMT2D1RLOJQESlYFDBvwpTA4KWJpZ+sBJ4+vhjILRcK7JIFdGCdxEAAitg==", + "node_modules/@babel/plugin-transform-template-literals": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.23.3.tgz", + "integrity": "sha512-Flok06AYNp7GV2oJPZZcP9vZdszev6vPBkHLwxwSpaIqx75wn6mUd3UFWsSsA0l8nXAKkyCmL/sR02m8RYGeHg==", "dev": true, "dependencies": { - "resolve": "^1.9.0" + "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { - "node": ">= 0.10" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/resolve": { - "version": "1.22.1", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz", - "integrity": "sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==", + "node_modules/@babel/plugin-transform-typeof-symbol": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.23.3.tgz", + "integrity": "sha512-4t15ViVnaFdrPC74be1gXBSMzXk3B4Us9lP7uLRQHTFpV5Dvt33pn+2MyyNxmN3VTTm3oTrZVMUmuw3oBnQ2oQ==", "dev": true, "dependencies": { - "is-core-module": "^2.9.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" + "@babel/helper-plugin-utils": "^7.22.5" }, - "bin": { - "resolve": "bin/resolve" + "engines": { + "node": ">=6.9.0" }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/resolve-cwd": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", - "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "node_modules/@babel/plugin-transform-unicode-escapes": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.23.3.tgz", + "integrity": "sha512-OMCUx/bU6ChE3r4+ZdylEqAjaQgHAgipgW8nsCfu5pGqDcFytVd91AwRvUJSBZDz0exPGgnjoqhgRYLRjFZc9Q==", "dev": true, "dependencies": { - "resolve-from": "^5.0.0" + "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { - "node": ">=8" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/resolve-from": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", - "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "node_modules/@babel/plugin-transform-unicode-property-regex": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.23.3.tgz", + "integrity": "sha512-KcLIm+pDZkWZQAFJ9pdfmh89EwVfmNovFBcXko8szpBeF8z68kWIPeKlmSOkT9BXJxs2C0uk+5LxoxIv62MROA==", "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.22.15", + "@babel/helper-plugin-utils": "^7.22.5" + }, "engines": { - "node": ">=8" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, - "node_modules/sass": { - "version": "1.56.1", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.56.1.tgz", - "integrity": "sha512-VpEyKpyBPCxE7qGDtOcdJ6fFbcpOM+Emu7uZLxVrkX8KVU/Dp5UF7WLvzqRuUhB6mqqQt1xffLoG+AndxTZrCQ==", + "node_modules/@babel/plugin-transform-unicode-regex": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.23.3.tgz", + "integrity": "sha512-wMHpNA4x2cIA32b/ci3AfwNgheiva2W0WUKWTK7vBHBhDKfPsc5cFGNWm69WBqpwd86u1qwZ9PWevKqm1A3yAw==", + "dev": true, "dependencies": { - "chokidar": ">=3.0.0 <4.0.0", - "immutable": "^4.0.0", - "source-map-js": ">=0.6.2 <2.0.0" - }, - "bin": { - "sass": "sass.js" + "@babel/helper-create-regexp-features-plugin": "^7.22.15", + "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { - "node": ">=12.0.0" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/sass-loader": { - "version": "12.6.0", - "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-12.6.0.tgz", - "integrity": "sha512-oLTaH0YCtX4cfnJZxKSLAyglED0naiYfNG1iXfU5w1LNZ+ukoA5DtyDIN5zmKVZwYNJP4KRc5Y3hkWga+7tYfA==", + "node_modules/@babel/plugin-transform-unicode-sets-regex": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.23.3.tgz", + "integrity": "sha512-W7lliA/v9bNR83Qc3q1ip9CQMZ09CcHDbHfbLRDNuAhn1Mvkr1ZNF7hPmztMQvtTGVLJ9m8IZqWsTkXOml8dbw==", + "dev": true, "dependencies": { - "klona": "^2.0.4", - "neo-async": "^2.6.2" + "@babel/helper-create-regexp-features-plugin": "^7.22.15", + "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { - "node": ">= 12.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" + "node": ">=6.9.0" }, "peerDependencies": { - "fibers": ">= 3.1.0", - "node-sass": "^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0", - "sass": "^1.3.0", - "sass-embedded": "*", - "webpack": "^5.0.0" - }, - "peerDependenciesMeta": { - "fibers": { - "optional": true - }, - "node-sass": { - "optional": true - }, - "sass": { - "optional": true - }, - "sass-embedded": { - "optional": true - } + "@babel/core": "^7.0.0" } }, - "node_modules/schema-utils": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.1.tgz", - "integrity": "sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw==", + "node_modules/@babel/preset-env": { + "version": "7.23.7", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.23.7.tgz", + "integrity": "sha512-SY27X/GtTz/L4UryMNJ6p4fH4nsgWbz84y9FE0bQeWJP6O5BhgVCt53CotQKHCOeXJel8VyhlhujhlltKms/CA==", + "dev": true, "dependencies": { - "@types/json-schema": "^7.0.8", - "ajv": "^6.12.5", - "ajv-keywords": "^3.5.2" + "@babel/compat-data": "^7.23.5", + "@babel/helper-compilation-targets": "^7.23.6", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-validator-option": "^7.23.5", + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.23.3", + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.23.3", + "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.23.7", + "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-dynamic-import": "^7.8.3", + "@babel/plugin-syntax-export-namespace-from": "^7.8.3", + "@babel/plugin-syntax-import-assertions": "^7.23.3", + "@babel/plugin-syntax-import-attributes": "^7.23.3", + "@babel/plugin-syntax-import-meta": "^7.10.4", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5", + "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", + "@babel/plugin-transform-arrow-functions": "^7.23.3", + "@babel/plugin-transform-async-generator-functions": "^7.23.7", + "@babel/plugin-transform-async-to-generator": "^7.23.3", + "@babel/plugin-transform-block-scoped-functions": "^7.23.3", + "@babel/plugin-transform-block-scoping": "^7.23.4", + "@babel/plugin-transform-class-properties": "^7.23.3", + "@babel/plugin-transform-class-static-block": "^7.23.4", + "@babel/plugin-transform-classes": "^7.23.5", + "@babel/plugin-transform-computed-properties": "^7.23.3", + "@babel/plugin-transform-destructuring": "^7.23.3", + "@babel/plugin-transform-dotall-regex": "^7.23.3", + "@babel/plugin-transform-duplicate-keys": "^7.23.3", + "@babel/plugin-transform-dynamic-import": "^7.23.4", + "@babel/plugin-transform-exponentiation-operator": "^7.23.3", + "@babel/plugin-transform-export-namespace-from": "^7.23.4", + "@babel/plugin-transform-for-of": "^7.23.6", + "@babel/plugin-transform-function-name": "^7.23.3", + "@babel/plugin-transform-json-strings": "^7.23.4", + "@babel/plugin-transform-literals": "^7.23.3", + "@babel/plugin-transform-logical-assignment-operators": "^7.23.4", + "@babel/plugin-transform-member-expression-literals": "^7.23.3", + "@babel/plugin-transform-modules-amd": "^7.23.3", + "@babel/plugin-transform-modules-commonjs": "^7.23.3", + "@babel/plugin-transform-modules-systemjs": "^7.23.3", + "@babel/plugin-transform-modules-umd": "^7.23.3", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.22.5", + "@babel/plugin-transform-new-target": "^7.23.3", + "@babel/plugin-transform-nullish-coalescing-operator": "^7.23.4", + "@babel/plugin-transform-numeric-separator": "^7.23.4", + "@babel/plugin-transform-object-rest-spread": "^7.23.4", + "@babel/plugin-transform-object-super": "^7.23.3", + "@babel/plugin-transform-optional-catch-binding": "^7.23.4", + "@babel/plugin-transform-optional-chaining": "^7.23.4", + "@babel/plugin-transform-parameters": "^7.23.3", + "@babel/plugin-transform-private-methods": "^7.23.3", + "@babel/plugin-transform-private-property-in-object": "^7.23.4", + "@babel/plugin-transform-property-literals": "^7.23.3", + "@babel/plugin-transform-regenerator": "^7.23.3", + "@babel/plugin-transform-reserved-words": "^7.23.3", + "@babel/plugin-transform-shorthand-properties": "^7.23.3", + "@babel/plugin-transform-spread": "^7.23.3", + "@babel/plugin-transform-sticky-regex": "^7.23.3", + "@babel/plugin-transform-template-literals": "^7.23.3", + "@babel/plugin-transform-typeof-symbol": "^7.23.3", + "@babel/plugin-transform-unicode-escapes": "^7.23.3", + "@babel/plugin-transform-unicode-property-regex": "^7.23.3", + "@babel/plugin-transform-unicode-regex": "^7.23.3", + "@babel/plugin-transform-unicode-sets-regex": "^7.23.3", + "@babel/preset-modules": "0.1.6-no-external-plugins", + "babel-plugin-polyfill-corejs2": "^0.4.7", + "babel-plugin-polyfill-corejs3": "^0.8.7", + "babel-plugin-polyfill-regenerator": "^0.5.4", + "core-js-compat": "^3.31.0", + "semver": "^6.3.1" }, "engines": { - "node": ">= 10.13.0" + "node": ">=6.9.0" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "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": { - "lru-cache": "^6.0.0" + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/types": "^7.4.4", + "esutils": "^2.0.2" }, - "bin": { - "semver": "bin/semver.js" + "peerDependencies": { + "@babel/core": "^7.0.0-0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/@babel/preset-react": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/preset-react/-/preset-react-7.23.3.tgz", + "integrity": "sha512-tbkHOS9axH6Ysf2OUEqoSZ6T3Fa2SrNH6WTWSPBboxKzdxNc9qOICeLXkNG0ZEwbQ1HY8liwOce4aN/Ceyuq6w==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-validator-option": "^7.22.15", + "@babel/plugin-transform-react-display-name": "^7.23.3", + "@babel/plugin-transform-react-jsx": "^7.22.15", + "@babel/plugin-transform-react-jsx-development": "^7.22.5", + "@babel/plugin-transform-react-pure-annotations": "^7.23.3" }, "engines": { - "node": ">=10" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/serialize-javascript": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz", - "integrity": "sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==", + "node_modules/@babel/regjsgen": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@babel/regjsgen/-/regjsgen-0.8.0.tgz", + "integrity": "sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA==", + "dev": true + }, + "node_modules/@babel/runtime": { + "version": "7.23.2", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.2.tgz", + "integrity": "sha512-mM8eg4yl5D6i3lu2QKPuPH4FArvJ8KhTofbE7jwMUv9KX5mBvwPAqnV3MlyBNqdp9RyRKP6Yck8TrfYrPvX3bg==", "dependencies": { - "randombytes": "^2.1.0" + "regenerator-runtime": "^0.14.0" + }, + "engines": { + "node": ">=6.9.0" } }, - "node_modules/shallow-clone": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", - "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==", - "dev": true, + "node_modules/@babel/template": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz", + "integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==", "dependencies": { - "kind-of": "^6.0.2" + "@babel/code-frame": "^7.22.13", + "@babel/parser": "^7.22.15", + "@babel/types": "^7.22.15" }, "engines": { - "node": ">=8" + "node": ">=6.9.0" } }, - "node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, + "node_modules/@babel/traverse": { + "version": "7.23.2", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.2.tgz", + "integrity": "sha512-azpe59SQ48qG6nu2CzcMLbxUudtN+dOM9kDbUqGq3HXUJRlo7i8fvPoxQUzYgLZ4cMVmuZgm8vvBpNeRhd6XSw==", "dependencies": { - "shebang-regex": "^3.0.0" + "@babel/code-frame": "^7.22.13", + "@babel/generator": "^7.23.0", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-function-name": "^7.23.0", + "@babel/helper-hoist-variables": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/parser": "^7.23.0", + "@babel/types": "^7.23.0", + "debug": "^4.1.0", + "globals": "^11.1.0" }, "engines": { - "node": ">=8" + "node": ">=6.9.0" } }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, + "node_modules/@babel/types": { + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.0.tgz", + "integrity": "sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg==", + "dependencies": { + "@babel/helper-string-parser": "^7.22.5", + "@babel/helper-validator-identifier": "^7.22.20", + "to-fast-properties": "^2.0.0" + }, "engines": { - "node": ">=8" + "node": ">=6.9.0" } }, - "node_modules/source-list-map": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.1.tgz", - "integrity": "sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw==" + "node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true }, - "node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "node_modules/@discoveryjs/json-ext": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz", + "integrity": "sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==", + "dev": true, "engines": { - "node": ">=0.10.0" + "node": ">=10.0.0" } }, - "node_modules/source-map-js": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", - "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", - "engines": { - "node": ">=0.10.0" + "node_modules/@emotion/is-prop-valid": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.2.1.tgz", + "integrity": "sha512-61Mf7Ufx4aDxx1xlDeOm8aFFigGHE4z+0sKCa+IHCeZKiyP9RLD0Mmx7m8b9/Cf37f7NAvQOOJAbQQGVr5uERw==", + "dependencies": { + "@emotion/memoize": "^0.8.1" } }, - "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==", + "node_modules/@emotion/memoize": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.8.1.tgz", + "integrity": "sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA==" + }, + "node_modules/@emotion/unitless": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.8.1.tgz", + "integrity": "sha512-KOEGMu6dmJZtpadb476IsZBclKvILjopjUii3V+7MnXIQCYh8W3NgNcgwo21n9LXZX6EDIKvqfjYxXebDwxKmQ==" + }, + "node_modules/@govuk-react/back-link": { + "version": "0.10.6", + "resolved": "https://registry.npmjs.org/@govuk-react/back-link/-/back-link-0.10.6.tgz", + "integrity": "sha512-68ZAp3jw4f57E8sle6ReHCXlQkjwkcvNF7Ai8ONbZ/Wlj5bCzywPWwKxj4BTr6Yr/KAyxXeSpEvAjY9AvyVoEQ==", "dependencies": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" + "@govuk-react/constants": "^0.10.6", + "@govuk-react/lib": "^0.10.6", + "govuk-colours": "^1.1.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "styled-components": ">=5.1" } }, - "node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "node_modules/@govuk-react/breadcrumbs": { + "version": "0.10.6", + "resolved": "https://registry.npmjs.org/@govuk-react/breadcrumbs/-/breadcrumbs-0.10.6.tgz", + "integrity": "sha512-XqidFXw4oeqpuTfQVfz9z1Uw4RaX10yg6cfAdhd8KNjqoxyQKf+SsrKrERKl3/x633m9R8JuHSRcoZrTnna1vg==", "dependencies": { - "ansi-regex": "^5.0.1" + "@govuk-react/constants": "^0.10.6", + "@govuk-react/lib": "^0.10.6", + "govuk-colours": "^1.1.0" }, - "engines": { - "node": ">=8" + "peerDependencies": { + "react": ">=16.8.0", + "styled-components": ">=5.1" } }, - "node_modules/strip-ansi/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "engines": { - "node": ">=8" + "node_modules/@govuk-react/button": { + "version": "0.10.6", + "resolved": "https://registry.npmjs.org/@govuk-react/button/-/button-0.10.6.tgz", + "integrity": "sha512-UB+wCBiz4Z/00HsSFzB5VzyhK9WASKvXKg0nmt1D9W1ovo9GI2JlX0Zy/+m3pS55ZVHmmrHa5Bi+eGMlUAvZAw==", + "dependencies": { + "@govuk-react/constants": "^0.10.6", + "@govuk-react/icons": "^0.10.6", + "@govuk-react/lib": "^0.10.6", + "govuk-colours": "^1.1.0", + "polished": "^4.1.2" + }, + "peerDependencies": { + "react": ">=16.8.0", + "styled-components": ">=5.1" } }, - "node_modules/style-loader": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-3.3.1.tgz", - "integrity": "sha512-GPcQ+LDJbrcxHORTRes6Jy2sfvK2kS6hpSfI/fXhPt+spVzxF6LJ1dHLN9zIGmVaaP044YKaIatFaufENRiDoQ==", - "engines": { - "node": ">= 12.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" + "node_modules/@govuk-react/caption": { + "version": "0.10.6", + "resolved": "https://registry.npmjs.org/@govuk-react/caption/-/caption-0.10.6.tgz", + "integrity": "sha512-RkWZ6SSs73iL5iUK5MmRZQ/lX3buIsYSM3q20Rvq5I5alfkutQUHMcpIPRvK2WpFZ0rjXjxVbyqMMovezXVdHQ==", + "dependencies": { + "@govuk-react/constants": "^0.10.6", + "@govuk-react/lib": "^0.10.6", + "govuk-colours": "^1.1.0" }, "peerDependencies": { - "webpack": "^5.0.0" + "react": ">=16.8.0", + "styled-components": ">=5.1" } }, - "node_modules/supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "node_modules/@govuk-react/checkbox": { + "version": "0.10.6", + "resolved": "https://registry.npmjs.org/@govuk-react/checkbox/-/checkbox-0.10.6.tgz", + "integrity": "sha512-j0IaysLp8HHIZoza/Ys/7fx32CtX1AqBiyVa8qyprAHzig6iR1pd8MvXefPN1H9U6J6r7HVYrdMxJfk/eMd+hQ==", "dependencies": { - "has-flag": "^4.0.0" + "@govuk-react/constants": "^0.10.6", + "@govuk-react/hint-text": "^0.10.6", + "@govuk-react/lib": "^0.10.6", + "govuk-colours": "^1.1.0" }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" + "peerDependencies": { + "react": ">=16.8.0", + "styled-components": ">=5.1" } }, - "node_modules/supports-preserve-symlinks-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "dev": true, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node_modules/@govuk-react/constants": { + "version": "0.10.6", + "resolved": "https://registry.npmjs.org/@govuk-react/constants/-/constants-0.10.6.tgz", + "integrity": "sha512-gXwMnkoWVihOecqzFLSsmov1imQvhsip2hTN49Jnh8mSPd86ltGrndFREQP8WluC/MnOt8dcuaHsuDbYAdtpuA==", + "dependencies": { + "govuk-colours": "^1.1.0" } }, - "node_modules/tapable": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", - "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", - "engines": { - "node": ">=6" + "node_modules/@govuk-react/date-field": { + "version": "0.10.6", + "resolved": "https://registry.npmjs.org/@govuk-react/date-field/-/date-field-0.10.6.tgz", + "integrity": "sha512-8sotTaYdlPT/Dj1in/eDb2g1GMdnrxaBo5XPo8vt2HpS1eBy3Fxts1gO5qjcmkgnXYppbQl0zkcxhHDEAIyAdw==", + "dependencies": { + "@govuk-react/constants": "^0.10.6", + "@govuk-react/error-text": "^0.10.6", + "@govuk-react/hint-text": "^0.10.6", + "@govuk-react/input": "^0.10.6", + "@govuk-react/label": "^0.10.6", + "@govuk-react/label-text": "^0.10.6", + "@govuk-react/lib": "^0.10.6", + "govuk-colours": "^1.1.0", + "multi-input-input": "0.0.3" + }, + "peerDependencies": { + "react": ">=16.8.0", + "styled-components": ">=5.1" } }, - "node_modules/terser": { - "version": "5.15.1", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.15.1.tgz", - "integrity": "sha512-K1faMUvpm/FBxjBXud0LWVAGxmvoPbZbfTCYbSgaaYQaIXI3/TdI7a7ZGA73Zrou6Q8Zmz3oeUTsp/dj+ag2Xw==", + "node_modules/@govuk-react/details": { + "version": "0.10.6", + "resolved": "https://registry.npmjs.org/@govuk-react/details/-/details-0.10.6.tgz", + "integrity": "sha512-p5D48HLGSMwmscaT7giQHujd2yQ+JFs6f/6Za7lbcAnLn6DMtaav8Tdc2cYXInC/qt7aeEcbKGWtg50GC6fZJw==", "dependencies": { - "@jridgewell/source-map": "^0.3.2", - "acorn": "^8.5.0", - "commander": "^2.20.0", - "source-map-support": "~0.5.20" + "@govuk-react/constants": "^0.10.6", + "@govuk-react/lib": "^0.10.6", + "govuk-colours": "^1.1.0", + "polished": "^4.1.2" }, - "bin": { - "terser": "bin/terser" - }, - "engines": { - "node": ">=10" + "peerDependencies": { + "react": ">=15", + "styled-components": ">=5.1" } }, - "node_modules/terser-webpack-plugin": { - "version": "5.3.6", - "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.6.tgz", - "integrity": "sha512-kfLFk+PoLUQIbLmB1+PZDMRSZS99Mp+/MHqDNmMA6tOItzRt+Npe3E+fsMs5mfcM0wCtrrdU387UnV+vnSffXQ==", + "node_modules/@govuk-react/document-footer-metadata": { + "version": "0.10.6", + "resolved": "https://registry.npmjs.org/@govuk-react/document-footer-metadata/-/document-footer-metadata-0.10.6.tgz", + "integrity": "sha512-tc0EMcdaWBmZFqHzxg/BEYBMqhBX3ulN46JAv/o6oqXrwoaaqdU6IlrwIULR0jHGxSroZ/muFuao4omp1YpopQ==", "dependencies": { - "@jridgewell/trace-mapping": "^0.3.14", - "jest-worker": "^27.4.5", - "schema-utils": "^3.1.1", - "serialize-javascript": "^6.0.0", - "terser": "^5.14.1" - }, - "engines": { - "node": ">= 10.13.0" + "@govuk-react/constants": "^0.10.6", + "@govuk-react/lib": "^0.10.6", + "@govuk-react/unordered-list": "^0.10.6" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" + "peerDependencies": { + "react": ">=16.8.0", + "styled-components": ">=5.1" + } + }, + "node_modules/@govuk-react/error-summary": { + "version": "0.10.6", + "resolved": "https://registry.npmjs.org/@govuk-react/error-summary/-/error-summary-0.10.6.tgz", + "integrity": "sha512-wBVYgdzGH0Bvq4ddFiKCo6I4+48H0igw1vEkDfj5Yyw8skvKXrCGbELkFAPYgE7A7vD9Rbkcs12YO3zcy+U6cQ==", + "dependencies": { + "@govuk-react/constants": "^0.10.6", + "@govuk-react/heading": "^0.10.6", + "@govuk-react/input-field": "^0.10.6", + "@govuk-react/lib": "^0.10.6", + "@govuk-react/link": "^0.10.6", + "@govuk-react/list-item": "^0.10.6", + "@govuk-react/paragraph": "^0.10.6", + "@govuk-react/text-area": "^0.10.6", + "@govuk-react/unordered-list": "^0.10.6", + "govuk-colours": "^1.1.0" }, "peerDependencies": { - "webpack": "^5.1.0" + "react": ">=16.8.0", + "styled-components": ">=5.1" + } + }, + "node_modules/@govuk-react/error-text": { + "version": "0.10.6", + "resolved": "https://registry.npmjs.org/@govuk-react/error-text/-/error-text-0.10.6.tgz", + "integrity": "sha512-nGA09zO4Km024uK2U0qemXJ0fWI0DNPGTiS07SXmXBjBW3hUJetO5jy7bgTA8Hz8EDgvPFA7s7fZ3xe6CaPFng==", + "dependencies": { + "@govuk-react/constants": "^0.10.6", + "@govuk-react/lib": "^0.10.6", + "govuk-colours": "^1.1.0" }, - "peerDependenciesMeta": { - "@swc/core": { - "optional": true - }, - "esbuild": { - "optional": true - }, - "uglify-js": { - "optional": true - } + "peerDependencies": { + "react": ">=16.8.0", + "styled-components": ">=5.1" } }, - "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==", + "node_modules/@govuk-react/fieldset": { + "version": "0.10.6", + "resolved": "https://registry.npmjs.org/@govuk-react/fieldset/-/fieldset-0.10.6.tgz", + "integrity": "sha512-RcALeKkwSKXSTE0UGKpAVT3Y/wgayozeqcznjMYqhj1NTf8mEtZEv3Nxa6PG9uNsaqiNxTLqPK8KuUyESK4naA==", "dependencies": { - "is-number": "^7.0.0" + "@govuk-react/constants": "^0.10.6", + "@govuk-react/lib": "^0.10.6", + "govuk-colours": "^1.1.0" }, - "engines": { - "node": ">=8.0" + "peerDependencies": { + "react": ">=16.8.0", + "styled-components": ">=5.1" } }, - "node_modules/update-browserslist-db": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.10.tgz", - "integrity": "sha512-OztqDenkfFkbSG+tRxBeAnCVPckDBcvibKd35yDONx6OU8N7sqgwc7rCbkJ/WcYtVRZ4ba68d6byhC21GFh7sQ==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - } - ], + "node_modules/@govuk-react/file-upload": { + "version": "0.10.6", + "resolved": "https://registry.npmjs.org/@govuk-react/file-upload/-/file-upload-0.10.6.tgz", + "integrity": "sha512-woyPfoG8JS4NInlLld3WIUO47QTJKX0i/hz6LNK5bSNH92D4JvATbA8SNm8rSqaGunjUZD/8UFO8WBT/MtZxRw==", "dependencies": { - "escalade": "^3.1.1", - "picocolors": "^1.0.0" + "@govuk-react/constants": "^0.10.6", + "@govuk-react/error-text": "^0.10.6", + "@govuk-react/hint-text": "^0.10.6", + "@govuk-react/label": "^0.10.6", + "@govuk-react/label-text": "^0.10.6" }, - "bin": { - "browserslist-lint": "cli.js" + "peerDependencies": { + "react": ">=16.8.0", + "styled-components": ">=5.1" + } + }, + "node_modules/@govuk-react/footer": { + "version": "0.10.6", + "resolved": "https://registry.npmjs.org/@govuk-react/footer/-/footer-0.10.6.tgz", + "integrity": "sha512-EB6qaJvaYKogilPADlF7/KLMQPLxonEVbOKEJCqboyavyClKt8FV+/2ZTi42nsDa6ObE2/+UxWpEe/MlgPTrfw==", + "dependencies": { + "@govuk-react/constants": "^0.10.6", + "@govuk-react/heading": "^0.10.6", + "@govuk-react/icons": "^0.10.6", + "@govuk-react/lib": "^0.10.6", + "@govuk-react/link": "^0.10.6", + "@govuk-react/visually-hidden": "^0.10.6", + "govuk-colours": "^1.1.0" }, "peerDependencies": { - "browserslist": ">= 4.21.0" + "react": ">=16.8.0", + "styled-components": ">=5.1" } }, - "node_modules/uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "node_modules/@govuk-react/form-group": { + "version": "0.10.6", + "resolved": "https://registry.npmjs.org/@govuk-react/form-group/-/form-group-0.10.6.tgz", + "integrity": "sha512-cx536tpKdxIIkGXIc0NcQRqxKSvhT0Ygy2F3/6JRE22tQ3J18JUlb48a2IqCLsY19Hu7cijcEfX5P1+23knJ0g==", "dependencies": { - "punycode": "^2.1.0" + "@govuk-react/constants": "^0.10.6", + "@govuk-react/lib": "^0.10.6", + "govuk-colours": "^1.1.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "styled-components": ">=5.1" } }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" + "node_modules/@govuk-react/global-style": { + "version": "0.10.6", + "resolved": "https://registry.npmjs.org/@govuk-react/global-style/-/global-style-0.10.6.tgz", + "integrity": "sha512-KvGtA58kms+poH6vc5JJDiNFWAabm8SkxpAIemI2QfjxKPJEroih0jQg3Yi+BqvOCrfvSFyd9fHSYFiiCX5e2Q==", + "peerDependencies": { + "react": ">=16.8.0", + "styled-components": ">=5.1" + } }, - "node_modules/watchpack": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz", - "integrity": "sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==", + "node_modules/@govuk-react/grid-col": { + "version": "0.10.6", + "resolved": "https://registry.npmjs.org/@govuk-react/grid-col/-/grid-col-0.10.6.tgz", + "integrity": "sha512-9hrpjxSG2Zda5vZumeX/lZpIMYM1Q5Xf1/GmhCU+Cforfc4F733f5w9hOpzr4S+FGOaXmri7dwcDLTjclyrOlw==", "dependencies": { - "glob-to-regexp": "^0.4.1", - "graceful-fs": "^4.1.2" + "@govuk-react/constants": "^0.10.6", + "@govuk-react/lib": "^0.10.6" }, - "engines": { - "node": ">=10.13.0" + "peerDependencies": { + "react": ">=16.8.0", + "styled-components": ">=5.1" } }, - "node_modules/webpack": { - "version": "5.76.0", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.76.0.tgz", - "integrity": "sha512-l5sOdYBDunyf72HW8dF23rFtWq/7Zgvt/9ftMof71E/yUb1YLOBmTgA2K4vQthB3kotMrSj609txVE0dnr2fjA==", + "node_modules/@govuk-react/grid-row": { + "version": "0.10.6", + "resolved": "https://registry.npmjs.org/@govuk-react/grid-row/-/grid-row-0.10.6.tgz", + "integrity": "sha512-2mn/Xunz7q443rfwwyScKI4GcqafpebhSKRD03QcAij6Ys2aIuJ9MU85OPSF3qG5t2IZoN3sUNIx0l0AXUoAOQ==", "dependencies": { - "@types/eslint-scope": "^3.7.3", - "@types/estree": "^0.0.51", - "@webassemblyjs/ast": "1.11.1", - "@webassemblyjs/wasm-edit": "1.11.1", - "@webassemblyjs/wasm-parser": "1.11.1", - "acorn": "^8.7.1", - "acorn-import-assertions": "^1.7.6", - "browserslist": "^4.14.5", - "chrome-trace-event": "^1.0.2", - "enhanced-resolve": "^5.10.0", - "es-module-lexer": "^0.9.0", - "eslint-scope": "5.1.1", - "events": "^3.2.0", - "glob-to-regexp": "^0.4.1", - "graceful-fs": "^4.2.9", - "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.1.0", - "tapable": "^2.1.1", - "terser-webpack-plugin": "^5.1.3", - "watchpack": "^2.4.0", - "webpack-sources": "^3.2.3" + "@govuk-react/constants": "^0.10.6", + "@govuk-react/lib": "^0.10.6" }, - "bin": { - "webpack": "bin/webpack.js" - }, - "engines": { - "node": ">=10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependenciesMeta": { - "webpack-cli": { - "optional": true - } + "peerDependencies": { + "react": ">=16.8.0", + "styled-components": ">=5.1" } }, - "node_modules/webpack-bundle-tracker": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/webpack-bundle-tracker/-/webpack-bundle-tracker-1.7.0.tgz", - "integrity": "sha512-CwdFpeLcc4uBurgmtszCHW6ISJ5RN70jvGWnvUG/7LQS1gmv2g6IdYw9A8DvT4rydHzWnRFwqVsx1hN1IebkQA==", + "node_modules/@govuk-react/heading": { + "version": "0.10.6", + "resolved": "https://registry.npmjs.org/@govuk-react/heading/-/heading-0.10.6.tgz", + "integrity": "sha512-x/79I3b9xyicmIGWOMO5jatWxhAIqOGpdYPEAzwPWZgpZQ/AtmQMM4/09o3voQNzvc4j8UwX24pYTX/C1/yPXg==", "dependencies": { - "lodash.assign": "^4.2.0", - "lodash.defaults": "^4.2.0", - "lodash.foreach": "^4.5.0", - "lodash.frompairs": "^4.0.1", - "lodash.get": "^4.4.2", - "lodash.topairs": "^4.3.0", - "strip-ansi": "^6.0.0" + "@govuk-react/constants": "^0.10.6", + "@govuk-react/lib": "^0.10.6" + }, + "peerDependencies": { + "react": ">=16.8.0", + "styled-components": ">=5.1" } }, - "node_modules/webpack-cli": { - "version": "4.10.0", - "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-4.10.0.tgz", - "integrity": "sha512-NLhDfH/h4O6UOy+0LSso42xvYypClINuMNBVVzX4vX98TmTaTUxwRbXdhucbFMd2qLaCTcLq/PdYrvi8onw90w==", - "dev": true, + "node_modules/@govuk-react/hint-text": { + "version": "0.10.6", + "resolved": "https://registry.npmjs.org/@govuk-react/hint-text/-/hint-text-0.10.6.tgz", + "integrity": "sha512-IXXDsqp13x8sdT4b9A1RcZu2gI0Lej8IshUEnOSXY1oV7aUNUlqcFkIfbvgQg9P6BFJbdVC4Ure4SlFxXrnbjg==", "dependencies": { - "@discoveryjs/json-ext": "^0.5.0", - "@webpack-cli/configtest": "^1.2.0", - "@webpack-cli/info": "^1.5.0", - "@webpack-cli/serve": "^1.7.0", - "colorette": "^2.0.14", - "commander": "^7.0.0", - "cross-spawn": "^7.0.3", - "fastest-levenshtein": "^1.0.12", - "import-local": "^3.0.2", - "interpret": "^2.2.0", - "rechoir": "^0.7.0", - "webpack-merge": "^5.7.3" + "@govuk-react/constants": "^0.10.6", + "@govuk-react/lib": "^0.10.6", + "govuk-colours": "^1.1.0" }, - "bin": { - "webpack-cli": "bin/cli.js" + "peerDependencies": { + "react": ">=16.8.0", + "styled-components": ">=5.1" + } + }, + "node_modules/@govuk-react/icon-crown": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/@govuk-react/icon-crown/-/icon-crown-0.0.8.tgz", + "integrity": "sha512-Jz/KHDwPfEj2t8owdtEigjRSzaBxjNTUPXtljAdoaACRJ/RUXTsudAZOGcvpndOiT1zd7Z/l+B5vHsvqPNfHMQ==", + "peerDependencies": { + "react": ">=16.2.0" + } + }, + "node_modules/@govuk-react/icons": { + "version": "0.10.6", + "resolved": "https://registry.npmjs.org/@govuk-react/icons/-/icons-0.10.6.tgz", + "integrity": "sha512-7pY5EG0ot/3wttHQaJjbji7k44woNsa9vI1bvorDulEpv9DzXs2O4fx6EnVQHoDFf40CLoP1yamQwVbsKdhz+A==", + "dependencies": { + "@govuk-react/constants": "^0.10.6", + "govuk-colours": "^1.1.0" }, - "engines": { - "node": ">=10.13.0" + "peerDependencies": { + "react": ">=16.8.0", + "styled-components": ">=5.1" + } + }, + "node_modules/@govuk-react/input": { + "version": "0.10.6", + "resolved": "https://registry.npmjs.org/@govuk-react/input/-/input-0.10.6.tgz", + "integrity": "sha512-VqHSv2zaeYQRyn7H25ySr2Wy1OhdprHwPA2rCnIi1pxa6qCXMnTbMGjibgJosI6XXLhHxx9gXovoKhg0jMi2WA==", + "dependencies": { + "@govuk-react/constants": "^0.10.6", + "@govuk-react/lib": "^0.10.6", + "govuk-colours": "^1.1.0" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" + "peerDependencies": { + "react": ">=16.8.0", + "styled-components": ">=5.1" + } + }, + "node_modules/@govuk-react/input-field": { + "version": "0.10.6", + "resolved": "https://registry.npmjs.org/@govuk-react/input-field/-/input-field-0.10.6.tgz", + "integrity": "sha512-FUXEbLhYCbUMHs9A8BFbEt/knl1wDxAw7t1D2LwJO8dL5IPoxac8PRCWq14T9r5XyGnPemEgJgVdW0Hc5mulAQ==", + "dependencies": { + "@govuk-react/constants": "^0.10.6", + "@govuk-react/error-text": "^0.10.6", + "@govuk-react/hint-text": "^0.10.6", + "@govuk-react/input": "^0.10.6", + "@govuk-react/label": "^0.10.6", + "@govuk-react/label-text": "^0.10.6" }, "peerDependencies": { - "webpack": "4.x.x || 5.x.x" + "react": ">=16.8.0", + "styled-components": ">=5.1" + } + }, + "node_modules/@govuk-react/inset-text": { + "version": "0.10.6", + "resolved": "https://registry.npmjs.org/@govuk-react/inset-text/-/inset-text-0.10.6.tgz", + "integrity": "sha512-rdcrcW+qpwbYv6tTvQOlFSRir2CODZJ8q13zVeZYzroUik8czZsuhgmU3JhaAVnfxPrlVmsX3DuFvnvnMUy3Sg==", + "dependencies": { + "@govuk-react/constants": "^0.10.6", + "@govuk-react/lib": "^0.10.6", + "govuk-colours": "^1.1.0" }, - "peerDependenciesMeta": { - "@webpack-cli/generators": { - "optional": true - }, - "@webpack-cli/migrate": { - "optional": true - }, - "webpack-bundle-analyzer": { - "optional": true - }, - "webpack-dev-server": { - "optional": true - } + "peerDependencies": { + "react": ">=16.8.0", + "styled-components": ">=5.1" } }, - "node_modules/webpack-cli/node_modules/commander": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", - "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", - "dev": true, - "engines": { - "node": ">= 10" + "node_modules/@govuk-react/label": { + "version": "0.10.6", + "resolved": "https://registry.npmjs.org/@govuk-react/label/-/label-0.10.6.tgz", + "integrity": "sha512-ia9qjQ0XLiB8BRNTsUN+v7XVrICA6e4+wNVcD0903B/SIn9uC26CMQADQ4LtTfUNZBEqxckrADdgg10nf/0roQ==", + "dependencies": { + "@govuk-react/constants": "^0.10.6", + "@govuk-react/lib": "^0.10.6", + "govuk-colours": "^1.1.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "styled-components": ">=5.1" } }, - "node_modules/webpack-merge": { - "version": "5.8.0", - "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.8.0.tgz", - "integrity": "sha512-/SaI7xY0831XwP6kzuwhKWVKDP9t1QY1h65lAFLbZqMPIuYcD9QAW4u9STIbU9kaJbPBB/geU/gLr1wDjOhQ+Q==", - "dev": true, + "node_modules/@govuk-react/label-text": { + "version": "0.10.6", + "resolved": "https://registry.npmjs.org/@govuk-react/label-text/-/label-text-0.10.6.tgz", + "integrity": "sha512-EWHTgmsOKY8aYgU3/2O+kRKRMvQEAiuot8jFDnVdgLw9J19S7Pi75owluCFimtc4bz70c7oDlc9Lp+gNhWsP3Q==", "dependencies": { - "clone-deep": "^4.0.1", - "wildcard": "^2.0.0" + "@govuk-react/lib": "^0.10.6" }, - "engines": { - "node": ">=10.0.0" + "peerDependencies": { + "react": ">=16.8.0", + "styled-components": ">=5.1" } }, - "node_modules/webpack-sources": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-1.4.3.tgz", - "integrity": "sha512-lgTS3Xhv1lCOKo7SA5TjKXMjpSM4sBjNV5+q2bqesbSPs5FjGmU6jjtBSkX9b4qW87vDIsCIlUPOEhbZrMdjeQ==", + "node_modules/@govuk-react/lead-paragraph": { + "version": "0.10.6", + "resolved": "https://registry.npmjs.org/@govuk-react/lead-paragraph/-/lead-paragraph-0.10.6.tgz", + "integrity": "sha512-+r8DIc3pBpIRMM4Ms6DnO/CdmPdhskenjogC/RsTYJsXr0PjfWnF0mLU5kBOVQhsv82Ycx12b9stmbKPui65rA==", "dependencies": { - "source-list-map": "^2.0.0", - "source-map": "~0.6.1" + "@govuk-react/lib": "^0.10.6" + }, + "peerDependencies": { + "react": ">=16.8.0", + "styled-components": ">=5.1" } }, - "node_modules/webpack/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==", - "engines": { - "node": ">=10.13.0" + "node_modules/@govuk-react/lib": { + "version": "0.10.6", + "resolved": "https://registry.npmjs.org/@govuk-react/lib/-/lib-0.10.6.tgz", + "integrity": "sha512-nC0dBnVpcEccZUnNaWt3DkLz4Q/tU7NypAZi3A4f6QJYrBHTpOZEDj7DCSi7ZXPMOiMuR+CcWhBqUtYNkvfcFw==", + "dependencies": { + "@govuk-react/constants": "^0.10.6", + "govuk-colours": "^1.1.0" } }, - "node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, + "node_modules/@govuk-react/link": { + "version": "0.10.6", + "resolved": "https://registry.npmjs.org/@govuk-react/link/-/link-0.10.6.tgz", + "integrity": "sha512-c6K3/surYuIdxjkVmnw/47hRkpuhzIgzXlzG36XsnhzXpdrOn5mK8HmZag7Pehib6HrEXzrMFSzQ5hntRwZ75Q==", "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" + "@govuk-react/lib": "^0.10.6", + "govuk-colours": "^1.1.0" }, - "engines": { - "node": ">= 8" + "peerDependencies": { + "react": ">=16.8.0", + "styled-components": ">=5.1" } }, - "node_modules/wildcard": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.0.tgz", - "integrity": "sha512-JcKqAHLPxcdb9KM49dufGXn2x3ssnfjbcaQdLlfZsL9rH9wgDQjUtDxbo8NE0F6SFvydeu1VhZe7hZuHsB2/pw==", - "dev": true - }, - "node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" - } - }, - "dependencies": { - "@discoveryjs/json-ext": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz", - "integrity": "sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==", - "dev": true - }, - "@jridgewell/gen-mapping": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz", - "integrity": "sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==", - "requires": { - "@jridgewell/set-array": "^1.0.1", - "@jridgewell/sourcemap-codec": "^1.4.10", - "@jridgewell/trace-mapping": "^0.3.9" + "node_modules/@govuk-react/list-item": { + "version": "0.10.6", + "resolved": "https://registry.npmjs.org/@govuk-react/list-item/-/list-item-0.10.6.tgz", + "integrity": "sha512-fvqHv6Z2HGwoQAMdBePThi0M4sL95PfZn6y2PQ1wn6t02uDD2XW4aUon/NAof9YkO8nD6U9+9wvb73vAT54vmA==", + "dependencies": { + "@govuk-react/constants": "^0.10.6", + "@govuk-react/lib": "^0.10.6" + }, + "peerDependencies": { + "react": ">=16.8.0", + "styled-components": ">=5.1" } }, - "@jridgewell/resolve-uri": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", - "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==" - }, - "@jridgewell/set-array": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", - "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==" - }, - "@jridgewell/source-map": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.2.tgz", - "integrity": "sha512-m7O9o2uR8k2ObDysZYzdfhb08VuEml5oWGiosa1VdaPZ/A6QyPkAJuwN0Q1lhULOf6B7MtQmHENS743hWtCrgw==", - "requires": { - "@jridgewell/gen-mapping": "^0.3.0", - "@jridgewell/trace-mapping": "^0.3.9" + "node_modules/@govuk-react/loading-box": { + "version": "0.10.6", + "resolved": "https://registry.npmjs.org/@govuk-react/loading-box/-/loading-box-0.10.6.tgz", + "integrity": "sha512-I1iqTINfgc2VZxdtdTvPruPgEr7mN+iZEb6qxeOjSSIXoLWQjGkYE0EV4DNF24kLAeuAneY9uszIURKqDcALnw==", + "dependencies": { + "@govuk-react/constants": "^0.10.6", + "@govuk-react/icons": "^0.10.6", + "govuk-colours": "^1.1.0", + "hex-rgb": "^4.0.0", + "react-transition-group": "^4.4.5" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0", + "styled-components": ">=5.1" } }, - "@jridgewell/sourcemap-codec": { - "version": "1.4.14", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", - "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==" - }, - "@jridgewell/trace-mapping": { - "version": "0.3.17", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.17.tgz", - "integrity": "sha512-MCNzAp77qzKca9+W/+I0+sEpaUnZoeasnghNeVc41VZCEKaCH73Vq3BZZ/SzWIgrqE4H4ceI+p+b6C0mHf9T4g==", - "requires": { - "@jridgewell/resolve-uri": "3.1.0", - "@jridgewell/sourcemap-codec": "1.4.14" + "node_modules/@govuk-react/main": { + "version": "0.10.6", + "resolved": "https://registry.npmjs.org/@govuk-react/main/-/main-0.10.6.tgz", + "integrity": "sha512-ZgH7SwdddE7gGZ9QhPBxRcWtow5YDb2HQu5XibH6FKZWaQTaSxbHDnwUvTRe+1LFLp9xGhv70jnyyzW5b6NoAA==", + "dependencies": { + "@govuk-react/constants": "^0.10.6" + }, + "peerDependencies": { + "react": ">=16.8.0", + "styled-components": ">=5.1" } }, - "@types/eslint": { - "version": "8.4.10", - "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.4.10.tgz", - "integrity": "sha512-Sl/HOqN8NKPmhWo2VBEPm0nvHnu2LL3v9vKo8MEq0EtbJ4eVzGPl41VNPvn5E1i5poMk4/XD8UriLHpJvEP/Nw==", - "requires": { - "@types/estree": "*", - "@types/json-schema": "*" + "node_modules/@govuk-react/multi-choice": { + "version": "0.10.6", + "resolved": "https://registry.npmjs.org/@govuk-react/multi-choice/-/multi-choice-0.10.6.tgz", + "integrity": "sha512-y3whM8MSmuyIqIAPRjkWyGuM17RUlwaY1s9xbi1uGYxZ+L/LcPKOqEc3D2GusIRS63gf1WAa84LoIuVYSg9CjA==", + "dependencies": { + "@govuk-react/constants": "^0.10.6", + "@govuk-react/error-text": "^0.10.6", + "@govuk-react/hint-text": "^0.10.6", + "@govuk-react/label-text": "^0.10.6", + "@govuk-react/lib": "^0.10.6", + "govuk-colours": "^1.1.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "styled-components": ">=5.1" } }, - "@types/eslint-scope": { - "version": "3.7.4", - "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.4.tgz", - "integrity": "sha512-9K4zoImiZc3HlIp6AVUDE4CWYx22a+lhSZMYNpbjW04+YF0KWj4pJXnEMjdnFTiQibFFmElcsasJXDbdI/EPhA==", - "requires": { - "@types/eslint": "*", - "@types/estree": "*" + "node_modules/@govuk-react/ordered-list": { + "version": "0.10.6", + "resolved": "https://registry.npmjs.org/@govuk-react/ordered-list/-/ordered-list-0.10.6.tgz", + "integrity": "sha512-+1f2ZoJJhKKZ/4viQoXku0DGJyN96REFbmSBZjclOhsrExTJ2NPVBwtT4mLicjlquND7MDV3t8gyKYQ5OHmBiw==", + "dependencies": { + "@govuk-react/constants": "^0.10.6", + "@govuk-react/lib": "^0.10.6", + "@govuk-react/list-item": "^0.10.6" + }, + "peerDependencies": { + "react": ">=16.8.0", + "styled-components": ">=5.1" } }, - "@types/estree": { - "version": "0.0.51", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.51.tgz", - "integrity": "sha512-CuPgU6f3eT/XgKKPqKd/gLZV1Xmvf1a2R5POBOGQa6uv82xpls89HU5zKeVoyR8XzHd1RGNOlQlvUe3CFkjWNQ==" - }, - "@types/json-schema": { - "version": "7.0.11", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz", - "integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==" - }, - "@types/node": { - "version": "18.11.9", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.9.tgz", - "integrity": "sha512-CRpX21/kGdzjOpFsZSkcrXMGIBWMGNIHXXBVFSH+ggkftxg+XYP20TESbh+zFvFj3EQOl5byk0HTRn1IL6hbqg==" - }, - "@webassemblyjs/ast": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.1.tgz", - "integrity": "sha512-ukBh14qFLjxTQNTXocdyksN5QdM28S1CxHt2rdskFyL+xFV7VremuBLVbmCePj+URalXBENx/9Lm7lnhihtCSw==", - "requires": { - "@webassemblyjs/helper-numbers": "1.11.1", - "@webassemblyjs/helper-wasm-bytecode": "1.11.1" - } - }, - "@webassemblyjs/floating-point-hex-parser": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.1.tgz", - "integrity": "sha512-iGRfyc5Bq+NnNuX8b5hwBrRjzf0ocrJPI6GWFodBFzmFnyvrQ83SHKhmilCU/8Jv67i4GJZBMhEzltxzcNagtQ==" - }, - "@webassemblyjs/helper-api-error": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.1.tgz", - "integrity": "sha512-RlhS8CBCXfRUR/cwo2ho9bkheSXG0+NwooXcc3PAILALf2QLdFyj7KGsKRbVc95hZnhnERon4kW/D3SZpp6Tcg==" - }, - "@webassemblyjs/helper-buffer": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.1.tgz", - "integrity": "sha512-gwikF65aDNeeXa8JxXa2BAk+REjSyhrNC9ZwdT0f8jc4dQQeDQ7G4m0f2QCLPJiMTTO6wfDmRmj/pW0PsUvIcA==" - }, - "@webassemblyjs/helper-numbers": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.1.tgz", - "integrity": "sha512-vDkbxiB8zfnPdNK9Rajcey5C0w+QJugEglN0of+kmO8l7lDb77AnlKYQF7aarZuCrv+l0UvqL+68gSDr3k9LPQ==", - "requires": { - "@webassemblyjs/floating-point-hex-parser": "1.11.1", - "@webassemblyjs/helper-api-error": "1.11.1", - "@xtuc/long": "4.2.2" + "node_modules/@govuk-react/page": { + "version": "0.10.6", + "resolved": "https://registry.npmjs.org/@govuk-react/page/-/page-0.10.6.tgz", + "integrity": "sha512-tgLbbNuAzNniAUqfD4Iy7oQYuI3BgMCcsXld1ClOYCnJSZfLsQx+qwxfFhKaQaVX1I9oDeEGLrv7FYAkM+MH/A==", + "dependencies": { + "@govuk-react/constants": "^0.10.6", + "@govuk-react/lib": "^0.10.6", + "@govuk-react/skip-link": "^0.10.6", + "@govuk-react/top-nav": "^0.10.6" + }, + "peerDependencies": { + "react": ">=16.8.0", + "styled-components": ">=5.1" } }, - "@webassemblyjs/helper-wasm-bytecode": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.1.tgz", - "integrity": "sha512-PvpoOGiJwXeTrSf/qfudJhwlvDQxFgelbMqtq52WWiXC6Xgg1IREdngmPN3bs4RoO83PnL/nFrxucXj1+BX62Q==" - }, - "@webassemblyjs/helper-wasm-section": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.1.tgz", - "integrity": "sha512-10P9No29rYX1j7F3EVPX3JvGPQPae+AomuSTPiF9eBQeChHI6iqjMIwR9JmOJXwpnn/oVGDk7I5IlskuMwU/pg==", - "requires": { - "@webassemblyjs/ast": "1.11.1", - "@webassemblyjs/helper-buffer": "1.11.1", - "@webassemblyjs/helper-wasm-bytecode": "1.11.1", - "@webassemblyjs/wasm-gen": "1.11.1" + "node_modules/@govuk-react/pagination": { + "version": "0.10.6", + "resolved": "https://registry.npmjs.org/@govuk-react/pagination/-/pagination-0.10.6.tgz", + "integrity": "sha512-zynjmXGXbGLxD5/XtyJy6FTuWhDAFtMKXYdtZOHoMvnflZnQEy6ce5u7vSj3uKxGL2k/xPGgkXMRQQCU/k4S8A==", + "dependencies": { + "@govuk-react/constants": "^0.10.6", + "@govuk-react/icons": "^0.10.6", + "@govuk-react/lib": "^0.10.6", + "govuk-colours": "^1.1.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "styled-components": ">=5.1" } }, - "@webassemblyjs/ieee754": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.11.1.tgz", - "integrity": "sha512-hJ87QIPtAMKbFq6CGTkZYJivEwZDbQUgYd3qKSadTNOhVY7p+gfP6Sr0lLRVTaG1JjFj+r3YchoqRYxNH3M0GQ==", - "requires": { - "@xtuc/ieee754": "^1.2.0" + "node_modules/@govuk-react/panel": { + "version": "0.10.6", + "resolved": "https://registry.npmjs.org/@govuk-react/panel/-/panel-0.10.6.tgz", + "integrity": "sha512-IzN4WhU+QI1MrS3vF0p0sVQ/sBxkBsnbO3IoCONjDKlfg5/LYWW60LpR6VyAN9UVUXKo3MaOm0iK+Kz0vXOSGQ==", + "dependencies": { + "@govuk-react/constants": "^0.10.6", + "@govuk-react/lib": "^0.10.6", + "govuk-colours": "^1.1.0", + "polished": "^4.1.2" + }, + "peerDependencies": { + "react": ">=16.8.0", + "styled-components": ">=5.1" } }, - "@webassemblyjs/leb128": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.11.1.tgz", - "integrity": "sha512-BJ2P0hNZ0u+Th1YZXJpzW6miwqQUGcIHT1G/sf72gLVD9DZ5AdYTqPNbHZh6K1M5VmKvFXwGSWZADz+qBWxeRw==", - "requires": { - "@xtuc/long": "4.2.2" + "node_modules/@govuk-react/paragraph": { + "version": "0.10.6", + "resolved": "https://registry.npmjs.org/@govuk-react/paragraph/-/paragraph-0.10.6.tgz", + "integrity": "sha512-kvoNWsAqIg1VvysVpO1jZbZMelTLYEja7qtnXdCXpswtZQwqyEmccnkmmDU8Cc4+t5chDRyYOU/FOCBy449CfQ==", + "dependencies": { + "@govuk-react/lib": "^0.10.6", + "@govuk-react/link": "^0.10.6", + "react-markdown": "^5.0.3" + }, + "peerDependencies": { + "react": ">=16.8.0", + "styled-components": ">=5.1" } }, - "@webassemblyjs/utf8": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.11.1.tgz", - "integrity": "sha512-9kqcxAEdMhiwQkHpkNiorZzqpGrodQQ2IGrHHxCy+Ozng0ofyMA0lTqiLkVs1uzTRejX+/O0EOT7KxqVPuXosQ==" - }, - "@webassemblyjs/wasm-edit": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.1.tgz", - "integrity": "sha512-g+RsupUC1aTHfR8CDgnsVRVZFJqdkFHpsHMfJuWQzWU3tvnLC07UqHICfP+4XyL2tnr1amvl1Sdp06TnYCmVkA==", - "requires": { - "@webassemblyjs/ast": "1.11.1", - "@webassemblyjs/helper-buffer": "1.11.1", - "@webassemblyjs/helper-wasm-bytecode": "1.11.1", - "@webassemblyjs/helper-wasm-section": "1.11.1", - "@webassemblyjs/wasm-gen": "1.11.1", - "@webassemblyjs/wasm-opt": "1.11.1", - "@webassemblyjs/wasm-parser": "1.11.1", - "@webassemblyjs/wast-printer": "1.11.1" - } - }, - "@webassemblyjs/wasm-gen": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.1.tgz", - "integrity": "sha512-F7QqKXwwNlMmsulj6+O7r4mmtAlCWfO/0HdgOxSklZfQcDu0TpLiD1mRt/zF25Bk59FIjEuGAIyn5ei4yMfLhA==", - "requires": { - "@webassemblyjs/ast": "1.11.1", - "@webassemblyjs/helper-wasm-bytecode": "1.11.1", - "@webassemblyjs/ieee754": "1.11.1", - "@webassemblyjs/leb128": "1.11.1", - "@webassemblyjs/utf8": "1.11.1" - } - }, - "@webassemblyjs/wasm-opt": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.1.tgz", - "integrity": "sha512-VqnkNqnZlU5EB64pp1l7hdm3hmQw7Vgqa0KF/KCNO9sIpI6Fk6brDEiX+iCOYrvMuBWDws0NkTOxYEb85XQHHw==", - "requires": { - "@webassemblyjs/ast": "1.11.1", - "@webassemblyjs/helper-buffer": "1.11.1", - "@webassemblyjs/wasm-gen": "1.11.1", - "@webassemblyjs/wasm-parser": "1.11.1" - } - }, - "@webassemblyjs/wasm-parser": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.1.tgz", - "integrity": "sha512-rrBujw+dJu32gYB7/Lup6UhdkPx9S9SnobZzRVL7VcBH9Bt9bCBLEuX/YXOOtBsOZ4NQrRykKhffRWHvigQvOA==", - "requires": { - "@webassemblyjs/ast": "1.11.1", - "@webassemblyjs/helper-api-error": "1.11.1", - "@webassemblyjs/helper-wasm-bytecode": "1.11.1", - "@webassemblyjs/ieee754": "1.11.1", - "@webassemblyjs/leb128": "1.11.1", - "@webassemblyjs/utf8": "1.11.1" - } - }, - "@webassemblyjs/wast-printer": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.11.1.tgz", - "integrity": "sha512-IQboUWM4eKzWW+N/jij2sRatKMh99QEelo3Eb2q0qXkvPRISAj8Qxtmw5itwqK+TTkBuUIE45AxYPToqPtL5gg==", - "requires": { - "@webassemblyjs/ast": "1.11.1", - "@xtuc/long": "4.2.2" + "node_modules/@govuk-react/phase-banner": { + "version": "0.10.6", + "resolved": "https://registry.npmjs.org/@govuk-react/phase-banner/-/phase-banner-0.10.6.tgz", + "integrity": "sha512-TX5OMb1ORlpzQZHwoP+diuLutH3orpmlxRjuGxLrp5psVDLu/ET9EHz6rqPLETCiml1DbRE0+XB2/mVDET4GCA==", + "dependencies": { + "@govuk-react/constants": "^0.10.6", + "@govuk-react/lib": "^0.10.6", + "@govuk-react/tag": "^0.10.6", + "govuk-colours": "^1.1.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "styled-components": ">=5.1" } }, - "@webpack-cli/configtest": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@webpack-cli/configtest/-/configtest-1.2.0.tgz", - "integrity": "sha512-4FB8Tj6xyVkyqjj1OaTqCjXYULB9FMkqQ8yGrZjRDrYh0nOE+7Lhs45WioWQQMV+ceFlE368Ukhe6xdvJM9Egg==", - "dev": true, - "requires": {} - }, - "@webpack-cli/info": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@webpack-cli/info/-/info-1.5.0.tgz", - "integrity": "sha512-e8tSXZpw2hPl2uMJY6fsMswaok5FdlGNRTktvFk2sD8RjH0hE2+XistawJx1vmKteh4NmGmNUrp+Tb2w+udPcQ==", - "dev": true, - "requires": { - "envinfo": "^7.7.3" + "node_modules/@govuk-react/radio": { + "version": "0.10.6", + "resolved": "https://registry.npmjs.org/@govuk-react/radio/-/radio-0.10.6.tgz", + "integrity": "sha512-oPkZSNNzK/T9veD9Rk0yVN5XMmMfLzGOMrzGtugszt6pyXHHgEtaeQZJM/nSR0ZzAMXyk6YM8wlGrqXfT18Kug==", + "dependencies": { + "@govuk-react/constants": "^0.10.6", + "@govuk-react/hint-text": "^0.10.6", + "@govuk-react/lib": "^0.10.6", + "govuk-colours": "^1.1.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "styled-components": ">=5.1" } }, - "@webpack-cli/serve": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/@webpack-cli/serve/-/serve-1.7.0.tgz", - "integrity": "sha512-oxnCNGj88fL+xzV+dacXs44HcDwf1ovs3AuEzvP7mqXw7fQntqIhQ1BRmynh4qEKQSSSRSWVyXRjmTbZIX9V2Q==", - "dev": true, - "requires": {} - }, - "@xtuc/ieee754": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", - "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==" - }, - "@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==" + "node_modules/@govuk-react/related-items": { + "version": "0.10.6", + "resolved": "https://registry.npmjs.org/@govuk-react/related-items/-/related-items-0.10.6.tgz", + "integrity": "sha512-ORfFWgFqiIBxmDAxXOWSNE/L5ij8sUJwysyQR4/vxYCOKsMpBg6VOU8sl8BaKa2RJTWqgvC/e6m4MSJsm9Jkag==", + "dependencies": { + "@govuk-react/constants": "^0.10.6", + "@govuk-react/lib": "^0.10.6", + "govuk-colours": "^1.1.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "styled-components": ">=5.1" + } }, - "accessible-autocomplete": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/accessible-autocomplete/-/accessible-autocomplete-2.0.4.tgz", - "integrity": "sha512-2p0txrSpvs5wXFUeQJHMheDPTZVSEmiUHWlEPb7vJnv2Dd1xPfoLnBQQMfNbTSit2pL/9sSQYESuD2Yyohd4Yw==", - "requires": { - "preact": "^8.3.1" + "node_modules/@govuk-react/search-box": { + "version": "0.10.6", + "resolved": "https://registry.npmjs.org/@govuk-react/search-box/-/search-box-0.10.6.tgz", + "integrity": "sha512-L6C18OMRANfWx6mtrU8m3ycU0UFe3kENd2LD2n4lapHC9IlS8N89drc7dXCdKoyRG/s9LUjBxzETbHUasaUMLQ==", + "dependencies": { + "@govuk-react/constants": "^0.10.6", + "@govuk-react/icons": "^0.10.6", + "@govuk-react/lib": "^0.10.6", + "govuk-colours": "^1.1.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "styled-components": ">=5.1" } }, - "acorn": { - "version": "8.8.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.1.tgz", - "integrity": "sha512-7zFpHzhnqYKrkYdUjF1HI1bzd0VygEGX8lFk4k5zVMqHEoES+P+7TKI+EvLO9WVMJ8eekdO0aDEK044xTXwPPA==" + "node_modules/@govuk-react/section-break": { + "version": "0.10.6", + "resolved": "https://registry.npmjs.org/@govuk-react/section-break/-/section-break-0.10.6.tgz", + "integrity": "sha512-zq8S6lkqwIocfb+l/3WTesLGH3oM8EFI6srTJ5GwwIrBiVWOpeTvNXI0ckjM1C/QI1sah3JP9K2XvKdeCWbi4w==", + "dependencies": { + "@govuk-react/lib": "^0.10.6", + "govuk-colours": "^1.1.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "styled-components": ">=5.1" + } }, - "acorn-import-assertions": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.8.0.tgz", - "integrity": "sha512-m7VZ3jwz4eK6A4Vtt8Ew1/mNbP24u0FhdyfA7fSvnJR6LMdfOYnmuIrrJAgrYfYJ10F/otaHTtrtrtmHdMNzEw==", - "requires": {} + "node_modules/@govuk-react/select": { + "version": "0.10.6", + "resolved": "https://registry.npmjs.org/@govuk-react/select/-/select-0.10.6.tgz", + "integrity": "sha512-k+1DQzyAX5FxyTAggoCplqYfI/bcDtCIDMFLZO01Yreo/fRGq7aPv7y1RHDL0744MZix0BFA9loyuJDAe8dbUw==", + "dependencies": { + "@govuk-react/constants": "^0.10.6", + "@govuk-react/error-text": "^0.10.6", + "@govuk-react/hint-text": "^0.10.6", + "@govuk-react/label": "^0.10.6", + "@govuk-react/label-text": "^0.10.6", + "@govuk-react/lib": "^0.10.6", + "govuk-colours": "^1.1.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "styled-components": ">=5.1" + } }, - "ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "requires": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" + "node_modules/@govuk-react/skip-link": { + "version": "0.10.6", + "resolved": "https://registry.npmjs.org/@govuk-react/skip-link/-/skip-link-0.10.6.tgz", + "integrity": "sha512-K+upaClQMiB8BD7ZOBqB8FXAP+epoSjnImRcEus62S2pxCU4MGHhWUBvMJ7nlj8Czap0S454jOs1TMzJa41Eyw==", + "dependencies": { + "@govuk-react/constants": "^0.10.6", + "@govuk-react/lib": "^0.10.6", + "govuk-colours": "^1.1.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "styled-components": ">=5.1" } }, - "ajv-keywords": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", - "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", - "requires": {} + "node_modules/@govuk-react/table": { + "version": "0.10.6", + "resolved": "https://registry.npmjs.org/@govuk-react/table/-/table-0.10.6.tgz", + "integrity": "sha512-mQ/5WA3/S+mI7Buu2JPGZK6pbuyx4XX2jcm/6q+1drQDf0DLCn5QkNKFKPE1lqkt66uFxby7cw8JZnClFXsVJw==", + "dependencies": { + "@govuk-react/constants": "^0.10.6", + "@govuk-react/lib": "^0.10.6", + "govuk-colours": "^1.1.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "styled-components": ">=5.1" + } }, - "ansi-regex": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", - "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==" + "node_modules/@govuk-react/tabs": { + "version": "0.10.6", + "resolved": "https://registry.npmjs.org/@govuk-react/tabs/-/tabs-0.10.6.tgz", + "integrity": "sha512-cdtWN6dk3q+iGpRZthLzBkMGc0ysh7R9JTrsR3jU3l55/DqgK+z4wy4stmqjJsaT9C6yj0lYydEDofSelHtqIg==", + "dependencies": { + "@govuk-react/constants": "^0.10.6", + "@govuk-react/lib": "^0.10.6", + "govuk-colours": "^1.1.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "styled-components": ">=5.1" + } }, - "anymatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz", - "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==", - "requires": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" + "node_modules/@govuk-react/tag": { + "version": "0.10.6", + "resolved": "https://registry.npmjs.org/@govuk-react/tag/-/tag-0.10.6.tgz", + "integrity": "sha512-OMT31Tlc01IfLZOI+1qHlaNyatUN5ytEUrrTTeFbBdIYFKKOtAV9qvT7gEpr0eTmFyJ79hmcqtsgtlf3hJEgJA==", + "dependencies": { + "@govuk-react/lib": "^0.10.6", + "govuk-colours": "^1.1.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "styled-components": ">=5.1" } }, - "big.js": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", - "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==" + "node_modules/@govuk-react/text-area": { + "version": "0.10.6", + "resolved": "https://registry.npmjs.org/@govuk-react/text-area/-/text-area-0.10.6.tgz", + "integrity": "sha512-Z0S/w7A3WRqZ3LmCLxV6UXyysJoec4PkdNl6G5bGpIPRS1/7LA8ZDmeUq/OD3vKsEJQI59oYHbh8yylX8zMuzg==", + "dependencies": { + "@govuk-react/constants": "^0.10.6", + "@govuk-react/error-text": "^0.10.6", + "@govuk-react/hint-text": "^0.10.6", + "@govuk-react/label": "^0.10.6", + "@govuk-react/label-text": "^0.10.6", + "govuk-colours": "^1.1.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "styled-components": ">=5.1" + } }, - "binary-extensions": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", - "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==" + "node_modules/@govuk-react/top-nav": { + "version": "0.10.6", + "resolved": "https://registry.npmjs.org/@govuk-react/top-nav/-/top-nav-0.10.6.tgz", + "integrity": "sha512-mjzf9NZbB/6x4hoKrwuhk9KplpktGq3q3wrFGWxGBXPk9s6tT/OIiT/Bxz0vy0Om03xz/7F7GgMx3mzFUTjG2Q==", + "dependencies": { + "@govuk-react/button": "^0.10.6", + "@govuk-react/constants": "^0.10.6", + "@govuk-react/icon-crown": "0.0.8", + "@govuk-react/icons": "^0.10.6", + "@govuk-react/lib": "^0.10.6", + "@govuk-react/search-box": "^0.10.6", + "govuk-colours": "^1.1.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "styled-components": ">=5.1" + } }, - "braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", - "requires": { - "fill-range": "^7.0.1" + "node_modules/@govuk-react/unordered-list": { + "version": "0.10.6", + "resolved": "https://registry.npmjs.org/@govuk-react/unordered-list/-/unordered-list-0.10.6.tgz", + "integrity": "sha512-1saN2SBnpXGrWYEtdjwbxtJtJdKT9iidiiFmWlK3BWtii2gF91LCU47TCoTbNRLjKabXTLbxOd/Gu6UDCk1h3Q==", + "dependencies": { + "@govuk-react/ordered-list": "^0.10.6" + }, + "peerDependencies": { + "react": ">=16.8.0", + "styled-components": ">=5.1" } }, - "browserslist": { - "version": "4.21.4", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.4.tgz", - "integrity": "sha512-CBHJJdDmgjl3daYjN5Cp5kbTf1mUhZoS+beLklHIvkOWscs83YAhLlF3Wsh/lciQYAcbBJgTOD44VtG31ZM4Hw==", - "requires": { - "caniuse-lite": "^1.0.30001400", - "electron-to-chromium": "^1.4.251", - "node-releases": "^2.0.6", - "update-browserslist-db": "^1.0.9" + "node_modules/@govuk-react/visually-hidden": { + "version": "0.10.6", + "resolved": "https://registry.npmjs.org/@govuk-react/visually-hidden/-/visually-hidden-0.10.6.tgz", + "integrity": "sha512-fsI/ZaNzTbKy8CYiBbGEoYpU9F0/Vj2+qf4+pohjwprfmfbPwp239PHanqmIjVYpJ/5vjEmBF2yajt9Rg8p4hw==", + "dependencies": { + "@govuk-react/lib": "^0.10.6", + "govuk-colours": "^1.1.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "styled-components": ">=5.1" } }, - "buffer-from": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", - "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==" + "node_modules/@govuk-react/warning-text": { + "version": "0.10.6", + "resolved": "https://registry.npmjs.org/@govuk-react/warning-text/-/warning-text-0.10.6.tgz", + "integrity": "sha512-j17041WKRtN9JmPNo91D3T6sn0a7esJ6R/aNN7GlhJIP6lC3IwDvzyNlahGXY7SugNZDUueLw2+6tOzcPyr9dA==", + "dependencies": { + "@govuk-react/constants": "^0.10.6", + "@govuk-react/icons": "^0.10.6", + "@govuk-react/lib": "^0.10.6", + "govuk-colours": "^1.1.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "styled-components": ">=5.1" + } }, - "caniuse-lite": { - "version": "1.0.30001434", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001434.tgz", - "integrity": "sha512-aOBHrLmTQw//WFa2rcF1If9fa3ypkC1wzqqiKHgfdrXTWcU8C4gKVZT77eQAPWN1APys3+uQ0Df07rKauXGEYA==" + "node_modules/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dev": true, + "dependencies": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } }, - "chart.js": { - "version": "3.9.1", - "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-3.9.1.tgz", - "integrity": "sha512-Ro2JbLmvg83gXF5F4sniaQ+lTbSv18E+TIf2cOeiH1Iqd2PGFOtem+DUufMZsCJwFE7ywPOpfXFBwRTGq7dh6w==" + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "engines": { + "node": ">=8" + } }, - "chartjs-adapter-moment": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/chartjs-adapter-moment/-/chartjs-adapter-moment-1.0.0.tgz", - "integrity": "sha512-PqlerEvQcc5hZLQ/NQWgBxgVQ4TRdvkW3c/t+SUEQSj78ia3hgLkf2VZ2yGJtltNbEEFyYGm+cA6XXevodYvWA==", - "requires": {} + "node_modules/@jest/console": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.7.0.tgz", + "integrity": "sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } }, - "chokidar": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", - "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", - "requires": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "fsevents": "~2.3.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" + "node_modules/@jest/console/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" } }, - "chrome-trace-event": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz", - "integrity": "sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg==" + "node_modules/@jest/console/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" + } }, - "clone-deep": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", - "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", + "node_modules/@jest/console/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, - "requires": { - "is-plain-object": "^2.0.4", - "kind-of": "^6.0.2", - "shallow-clone": "^3.0.0" + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" } }, - "colorette": { - "version": "2.0.19", - "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.19.tgz", - "integrity": "sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ==", + "node_modules/@jest/console/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 }, - "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==" + "node_modules/@jest/console/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } }, - "cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "node_modules/@jest/console/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, - "requires": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" } }, - "css-loader": { - "version": "5.2.7", - "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-5.2.7.tgz", - "integrity": "sha512-Q7mOvpBNBG7YrVGMxRxcBJZFL75o+cH2abNASdibkj/fffYD8qWbInZrD0S9ccI6vZclF3DsHE7njGlLtaHbhg==", - "requires": { - "icss-utils": "^5.1.0", - "loader-utils": "^2.0.0", - "postcss": "^8.2.15", - "postcss-modules-extract-imports": "^3.0.0", - "postcss-modules-local-by-default": "^4.0.0", - "postcss-modules-scope": "^3.0.0", - "postcss-modules-values": "^4.0.0", - "postcss-value-parser": "^4.1.0", - "schema-utils": "^3.0.0", - "semver": "^7.3.5" + "node_modules/@jest/core": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.7.0.tgz", + "integrity": "sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==", + "dev": true, + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/reporters": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-changed-files": "^29.7.0", + "jest-config": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-resolve-dependencies": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "jest-watcher": "^29.7.0", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } } }, - "cssesc": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", - "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==" - }, - "electron-to-chromium": { - "version": "1.4.284", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.284.tgz", - "integrity": "sha512-M8WEXFuKXMYMVr45fo8mq0wUrrJHheiKZf6BArTKk9ZBYCKJEOU5H8cdWgDT+qCVZf7Na4lVUaZsA+h6uA9+PA==" + "node_modules/@jest/core/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" + } }, - "emojis-list": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", - "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==" + "node_modules/@jest/core/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" + } }, - "enhanced-resolve": { - "version": "5.10.0", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.10.0.tgz", - "integrity": "sha512-T0yTFjdpldGY8PmuXXR0PyQ1ufZpEGiHVrp7zHKB7jdR4qlmZHhONVM5AQOAWXuF/w3dnHbEQVrNptJgt7F+cQ==", - "requires": { - "graceful-fs": "^4.2.4", - "tapable": "^2.2.0" + "node_modules/@jest/core/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" } }, - "envinfo": { - "version": "7.8.1", - "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.8.1.tgz", - "integrity": "sha512-/o+BXHmB7ocbHEAs6F2EnG0ogybVVUdkRunTT2glZU9XAaGmhqskrvKwqXuDfNjEO0LZKWdejEEpnq8aM0tOaw==", + "node_modules/@jest/core/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 }, - "es-module-lexer": { - "version": "0.9.3", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-0.9.3.tgz", - "integrity": "sha512-1HQ2M2sPtxwnvOvT1ZClHyQDiggdNjURWpY2we6aMKCQiUVxTmVs2UYPLIrD84sS+kMdUwfBSylbJPwNnBrnHQ==" - }, - "escalade": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", - "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==" - }, - "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==", - "requires": { - "esrecurse": "^4.3.0", - "estraverse": "^4.1.1" + "node_modules/@jest/core/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" } }, - "esrecurse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", - "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", - "requires": { - "estraverse": "^5.2.0" - }, + "node_modules/@jest/core/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": { - "estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==" - } + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" } }, - "estraverse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", - "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==" - }, - "events": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", - "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==" - }, - "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==" - }, - "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==" - }, - "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 - }, - "file-loader": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/file-loader/-/file-loader-6.2.0.tgz", - "integrity": "sha512-qo3glqyTa61Ytg4u73GultjHGjdRyig3tG6lPtyX/jOEJvHif9uB0/OCI2Kif6ctF3caQTW2G5gym21oAsI4pw==", - "requires": { - "loader-utils": "^2.0.0", - "schema-utils": "^3.0.0" + "node_modules/@jest/environment": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz", + "integrity": "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==", + "dev": true, + "dependencies": { + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "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==", - "requires": { - "to-regex-range": "^5.0.1" + "node_modules/@jest/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==", + "dev": true, + "dependencies": { + "expect": "^29.7.0", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "node_modules/@jest/expect-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz", + "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", "dev": true, - "requires": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" + "dependencies": { + "jest-get-type": "^29.6.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "optional": true - }, - "function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", - "dev": true + "node_modules/@jest/fake-timers": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.7.0.tgz", + "integrity": "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@sinonjs/fake-timers": "^10.0.2", + "@types/node": "*", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } }, - "glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "requires": { - "is-glob": "^4.0.1" + "node_modules/@jest/globals": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.7.0.tgz", + "integrity": "sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==", + "dev": true, + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/types": "^29.6.3", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "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==" + "node_modules/@jest/reporters": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.7.0.tgz", + "integrity": "sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==", + "dev": true, + "dependencies": { + "@bcoe/v8-coverage": "^0.2.3", + "@jest/console": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "@types/node": "*", + "chalk": "^4.0.0", + "collect-v8-coverage": "^1.0.0", + "exit": "^0.1.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^6.0.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.0", + "istanbul-reports": "^3.1.3", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "slash": "^3.0.0", + "string-length": "^4.0.1", + "strip-ansi": "^6.0.0", + "v8-to-istanbul": "^9.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } }, - "govuk-frontend": { - "version": "3.14.0", - "resolved": "https://registry.npmjs.org/govuk-frontend/-/govuk-frontend-3.14.0.tgz", - "integrity": "sha512-y7FTuihCSA8Hty+e9h0uPhCoNanCAN+CLioNFlPmlbeHXpbi09VMyxTcH+XfnMPY4Cp++7096v0rLwwdapTXnA==" + "node_modules/@jest/reporters/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" + } }, - "graceful-fs": { - "version": "4.2.10", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", - "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==" + "node_modules/@jest/reporters/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" + } }, - "has": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", - "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "node_modules/@jest/reporters/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, - "requires": { - "function-bind": "^1.1.1" + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" } }, - "has-flag": { + "node_modules/@jest/reporters/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/@jest/reporters/node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" - }, - "icss-utils": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz", - "integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==", - "requires": {} - }, - "immutable": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.1.0.tgz", - "integrity": "sha512-oNkuqVTA8jqG1Q6c+UglTOD1xhC1BtjKI7XkCXRkZHrN5m18/XsnUp8Q89GkQO/z+0WjonSvl0FLhDYftp46nQ==" - }, - "import-local": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.1.0.tgz", - "integrity": "sha512-ASB07uLtnDs1o6EHjKpX34BKYDSqnFerfTOJL2HvMqF70LnxpjkzDB8J44oT9pu4AMPkQwf8jl6szgvNd2tRIg==", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true, - "requires": { - "pkg-dir": "^4.2.0", - "resolve-cwd": "^3.0.0" + "engines": { + "node": ">=8" } }, - "interpret": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/interpret/-/interpret-2.2.0.tgz", - "integrity": "sha512-Ju0Bz/cEia55xDwUWEa8+olFpCiQoypjnQySseKtmjNrnps3P+xfpUmGr90T7yjlVJmOtybRvPXhKMbHr+fWnw==", - "dev": true + "node_modules/@jest/reporters/node_modules/jest-worker": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", + "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", + "dev": true, + "dependencies": { + "@types/node": "*", + "jest-util": "^29.7.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } }, - "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==", - "requires": { - "binary-extensions": "^2.0.0" + "node_modules/@jest/reporters/node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" } }, - "is-core-module": { - "version": "2.11.0", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.11.0.tgz", - "integrity": "sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw==", + "node_modules/@jest/reporters/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, - "requires": { - "has": "^1.0.3" + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" } }, - "is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==" + "node_modules/@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } }, - "is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "requires": { - "is-extglob": "^2.1.1" + "node_modules/@jest/source-map": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.6.3.tgz", + "integrity": "sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.18", + "callsites": "^3.0.0", + "graceful-fs": "^4.2.9" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==" + "node_modules/@jest/test-result": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.7.0.tgz", + "integrity": "sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==", + "dev": true, + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "collect-v8-coverage": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } }, - "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==", + "node_modules/@jest/test-sequencer": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.7.0.tgz", + "integrity": "sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==", "dev": true, - "requires": { - "isobject": "^3.0.1" + "dependencies": { + "@jest/test-result": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true + "node_modules/@jest/transform": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", + "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", + "dev": true, + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "babel-plugin-istanbul": "^6.1.1", + "chalk": "^4.0.0", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "micromatch": "^4.0.4", + "pirates": "^4.0.4", + "slash": "^3.0.0", + "write-file-atomic": "^4.0.2" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } }, - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", + "node_modules/@jest/transform/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/@jest/transform/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/@jest/transform/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/@jest/transform/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 }, - "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==", - "requires": { + "node_modules/@jest/transform/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/transform/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/@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", "@types/node": "*", - "merge-stream": "^2.0.0", - "supports-color": "^8.0.0" + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "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==" + "node_modules/@jest/types/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" + } }, - "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==" + "node_modules/@jest/types/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" + } }, - "json5": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==" + "node_modules/@jest/types/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" + } }, - "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==", + "node_modules/@jest/types/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 }, - "klona": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/klona/-/klona-2.0.5.tgz", - "integrity": "sha512-pJiBpiXMbt7dkzXe8Ghj/u4FfXOOa98fPW+bihOJ4SjnoijweJrNThJfd3ifXpXhREjpoF2mZVH1GfS9LV3kHQ==" + "node_modules/@jest/types/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/types/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/@jridgewell/gen-mapping": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", + "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", + "dependencies": { + "@jridgewell/set-array": "^1.0.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.9" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz", + "integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", + "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.5.tgz", + "integrity": "sha512-UTYAUj/wviwdsMfzoSJspJxbkH5o1snzwX0//0ENX1u/55kkZZkcTZP6u9bwKGkv+dkk9at4m1Cpt0uY80kcpQ==", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.0", + "@jridgewell/trace-mapping": "^0.3.9" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.15", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.20", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.20.tgz", + "integrity": "sha512-R8LcPeWZol2zR8mmH3JeKQ6QRCFb7XgUhV9ZlGhHLGyg4wpPiPZNQOOWhFZhxKw8u//yTbNGI42Bx/3paXEQ+Q==", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@sinclair/typebox": { + "version": "0.27.8", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", + "dev": true + }, + "node_modules/@sinonjs/commons": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.0.tgz", + "integrity": "sha512-jXBtWAF4vmdNmZgD5FoKsVLv3rPgDnLgPbU84LIJ3otV44vJlDRokVng5v8NFJdCf/da9legHcKaRuZs4L7faA==", + "dev": true, + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", + "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^3.0.0" + } + }, + "node_modules/@testing-library/dom": { + "version": "9.3.4", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-9.3.4.tgz", + "integrity": "sha512-FlS4ZWlp97iiNWig0Muq8p+3rVDjRiYE+YKGbAqXOu9nwJFFOdL00kFpz42M+4huzYi86vAK1sOOfyOG45muIQ==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.1.3", + "chalk": "^4.1.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "pretty-format": "^27.0.2" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@testing-library/dom/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } }, - "loader-runner": { + "node_modules/@testing-library/dom/node_modules/ansi-styles": { "version": "4.3.0", - "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz", - "integrity": "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==" + "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/@testing-library/dom/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/@testing-library/dom/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/@testing-library/dom/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/@testing-library/dom/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@testing-library/dom/node_modules/pretty-format": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/@testing-library/dom/node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@testing-library/dom/node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "dev": true + }, + "node_modules/@testing-library/dom/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/@testing-library/jest-dom": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.2.1.tgz", + "integrity": "sha512-Nuy/uFFDe9h/2jwoUuMKgoxvgkUv4S9jI9bARj6dGUKJ3euRhg8JFi5sciYbrayoxkadEOZednRT9+vo6LvvxQ==", + "dev": true, + "dependencies": { + "@adobe/css-tools": "^4.3.2", + "@babel/runtime": "^7.9.2", + "aria-query": "^5.0.0", + "chalk": "^3.0.0", + "css.escape": "^1.5.1", + "dom-accessibility-api": "^0.6.3", + "lodash": "^4.17.15", + "redent": "^3.0.0" + }, + "engines": { + "node": ">=14", + "npm": ">=6", + "yarn": ">=1" + }, + "peerDependencies": { + "@jest/globals": ">= 28", + "@types/bun": "latest", + "@types/jest": ">= 28", + "jest": ">= 28", + "vitest": ">= 0.32" + }, + "peerDependenciesMeta": { + "@jest/globals": { + "optional": true + }, + "@types/bun": { + "optional": true + }, + "@types/jest": { + "optional": true + }, + "jest": { + "optional": true + }, + "vitest": { + "optional": true + } + } + }, + "node_modules/@testing-library/jest-dom/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/@testing-library/jest-dom/node_modules/chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@testing-library/jest-dom/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/@testing-library/jest-dom/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/@testing-library/jest-dom/node_modules/dom-accessibility-api": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", + "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==", + "dev": true + }, + "node_modules/@testing-library/jest-dom/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@testing-library/jest-dom/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/@testing-library/react": { + "version": "14.1.2", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-14.1.2.tgz", + "integrity": "sha512-z4p7DVBTPjKM5qDZ0t5ZjzkpSNb+fZy1u6bzO7kk8oeGagpPCAtgh4cx1syrfp7a+QWkM021jGqjJaxJJnXAZg==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.12.5", + "@testing-library/dom": "^9.0.0", + "@types/react-dom": "^18.0.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "react": "^18.0.0", + "react-dom": "^18.0.0" + } + }, + "node_modules/@tootallnate/once": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", + "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==", + "dev": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/@types/aria-query": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", + "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", + "dev": true + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.6.8", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.8.tgz", + "integrity": "sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==", + "dev": true, + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.5.tgz", + "integrity": "sha512-WXCyOcRtH37HAUkpXhUduaxdm82b4GSlyTqajXviN4EfiuPgNYR109xMCKvpl6zPIpua0DGlMEDCq+g8EdoheQ==", + "dev": true, + "dependencies": { + "@babel/types": "^7.20.7" + } + }, + "node_modules/@types/eslint": { + "version": "8.44.6", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.44.6.tgz", + "integrity": "sha512-P6bY56TVmX8y9J87jHNgQh43h6VVU+6H7oN7hgvivV81K2XY8qJZ5vqPy/HdUoVIelii2kChYVzQanlswPWVFw==", + "dependencies": { + "@types/estree": "*", + "@types/json-schema": "*" + } + }, + "node_modules/@types/eslint-scope": { + "version": "3.7.6", + "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.6.tgz", + "integrity": "sha512-zfM4ipmxVKWdxtDaJ3MP3pBurDXOCoyjvlpE3u6Qzrmw4BPbfm4/ambIeTk/r/J0iq/+2/xp0Fmt+gFvXJY2PQ==", + "dependencies": { + "@types/eslint": "*", + "@types/estree": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.4.tgz", + "integrity": "sha512-2JwWnHK9H+wUZNorf2Zr6ves96WHoWDJIftkcxPKsS7Djta6Zu519LarhRNljPXkpsZR2ZMwNCPeW7omW07BJw==" + }, + "node_modules/@types/graceful-fs": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", + "integrity": "sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/hoist-non-react-statics": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.4.tgz", + "integrity": "sha512-ZchYkbieA+7tnxwX/SCBySx9WwvWR8TaP5tb2jRAzwvLb/rWchGw3v0w3pqUbUvj0GCwW2Xz/AVPSk6kUGctXQ==", + "dependencies": { + "@types/react": "*", + "hoist-non-react-statics": "^3.3.0" + } + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "dev": true + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", + "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", + "dev": true, + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", + "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", + "dev": true, + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/jsdom": { + "version": "20.0.1", + "resolved": "https://registry.npmjs.org/@types/jsdom/-/jsdom-20.0.1.tgz", + "integrity": "sha512-d0r18sZPmMQr1eG35u12FZfhIXNrnsPU/g5wvRKCUf/tOGilKKwYMYGqh33BNR6ba+2gkHw1EUiHoN3mn7E5IQ==", + "dev": true, + "dependencies": { + "@types/node": "*", + "@types/tough-cookie": "*", + "parse5": "^7.0.0" + } + }, + "node_modules/@types/json-schema": { + "version": "7.0.14", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.14.tgz", + "integrity": "sha512-U3PUjAudAdJBeC2pgN8uTIKgxrb4nlDF3SF0++EldXQvQBGkpFZMSnwQiIoDU77tv45VgNkl/L4ouD+rEomujw==" + }, + "node_modules/@types/mdast": { + "version": "3.0.14", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.14.tgz", + "integrity": "sha512-gVZ04PGgw1qLZKsnWnyFv4ORnaJ+DXLdHTVSFbU8yX6xZ34Bjg4Q32yPkmveUP1yItXReKfB0Aknlh/3zxTKAw==", + "dependencies": { + "@types/unist": "^2" + } + }, + "node_modules/@types/node": { + "version": "20.8.10", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.8.10.tgz", + "integrity": "sha512-TlgT8JntpcbmKUFzjhsyhGfP2fsiz1Mv56im6enJ905xG1DAYesxJaeSbGqQmAw8OWPdhyJGhGSQGKRNJ45u9w==", + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/@types/prop-types": { + "version": "15.7.9", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.9.tgz", + "integrity": "sha512-n1yyPsugYNSmHgxDFjicaI2+gCNjsBck8UX9kuofAKlc0h1bL+20oSF72KeNaW2DUlesbEVCFgyV2dPGTiY42g==" + }, + "node_modules/@types/react": { + "version": "18.2.33", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.33.tgz", + "integrity": "sha512-v+I7S+hu3PIBoVkKGpSYYpiBT1ijqEzWpzQD62/jm4K74hPpSP7FF9BnKG6+fg2+62weJYkkBWDJlZt5JO/9hg==", + "dependencies": { + "@types/prop-types": "*", + "@types/scheduler": "*", + "csstype": "^3.0.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.2.18", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.2.18.tgz", + "integrity": "sha512-TJxDm6OfAX2KJWJdMEVTwWke5Sc/E/RlnPGvGfS0W7+6ocy2xhDVQVh/KvC2Uf7kACs+gDytdusDSdWfWkaNzw==", + "dev": true, + "dependencies": { + "@types/react": "*" + } + }, + "node_modules/@types/scheduler": { + "version": "0.16.5", + "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.5.tgz", + "integrity": "sha512-s/FPdYRmZR8SjLWGMCuax7r3qCWQw9QKHzXVukAuuIJkXkDRwp+Pu5LMIVFi0Fxbav35WURicYr8u1QsoybnQw==" + }, + "node_modules/@types/stack-utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", + "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", + "dev": true + }, + "node_modules/@types/styled-components": { + "version": "5.1.29", + "resolved": "https://registry.npmjs.org/@types/styled-components/-/styled-components-5.1.29.tgz", + "integrity": "sha512-5h/ah9PAblggQ6Laa4peplT4iY5ddA8qM1LMD4HzwToUWs3hftfy0fayeRgbtH1JZUdw5CCaowmz7Lnb8SjIxQ==", + "dependencies": { + "@types/hoist-non-react-statics": "*", + "@types/react": "*", + "csstype": "^3.0.2" + } + }, + "node_modules/@types/stylis": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@types/stylis/-/stylis-4.2.2.tgz", + "integrity": "sha512-Rm17MsTpQQP5Jq4BF7CdrxJsDufoiL/q5IbJZYZmOZAJALyijgF7BzLgobXUqraNcQdqFYLYGeglDp6QzaxPpg==" + }, + "node_modules/@types/tough-cookie": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz", + "integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==", + "dev": true + }, + "node_modules/@types/unist": { + "version": "2.0.9", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.9.tgz", + "integrity": "sha512-zC0iXxAv1C1ERURduJueYzkzZ2zaGyc+P2c95hgkikHPr3z8EdUZOlgEQ5X0DRmwDZn+hekycQnoeiiRVrmilQ==" + }, + "node_modules/@types/yargs": { + "version": "17.0.32", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz", + "integrity": "sha512-xQ67Yc/laOG5uMfX/093MRlGGCIBzZMarVa+gfNKJxWAIgykYpVGkBdbqEzGDDfCrVUj6Hiff4mTZ5BA6TmAog==", + "dev": true, + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "dev": true + }, + "node_modules/@webassemblyjs/ast": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.6.tgz", + "integrity": "sha512-IN1xI7PwOvLPgjcf180gC1bqn3q/QaOCwYUahIOhbYUu8KA/3tw2RT/T0Gidi1l7Hhj5D/INhJxiICObqpMu4Q==", + "dependencies": { + "@webassemblyjs/helper-numbers": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6" + } + }, + "node_modules/@webassemblyjs/floating-point-hex-parser": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.6.tgz", + "integrity": "sha512-ejAj9hfRJ2XMsNHk/v6Fu2dGS+i4UaXBXGemOfQ/JfQ6mdQg/WXtwleQRLLS4OvfDhv8rYnVwH27YJLMyYsxhw==" + }, + "node_modules/@webassemblyjs/helper-api-error": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.6.tgz", + "integrity": "sha512-o0YkoP4pVu4rN8aTJgAyj9hC2Sv5UlkzCHhxqWj8butaLvnpdc2jOwh4ewE6CX0txSfLn/UYaV/pheS2Txg//Q==" + }, + "node_modules/@webassemblyjs/helper-buffer": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.6.tgz", + "integrity": "sha512-z3nFzdcp1mb8nEOFFk8DrYLpHvhKC3grJD2ardfKOzmbmJvEf/tPIqCY+sNcwZIY8ZD7IkB2l7/pqhUhqm7hLA==" + }, + "node_modules/@webassemblyjs/helper-numbers": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.6.tgz", + "integrity": "sha512-vUIhZ8LZoIWHBohiEObxVm6hwP034jwmc9kuq5GdHZH0wiLVLIPcMCdpJzG4C11cHoQ25TFIQj9kaVADVX7N3g==", + "dependencies": { + "@webassemblyjs/floating-point-hex-parser": "1.11.6", + "@webassemblyjs/helper-api-error": "1.11.6", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/helper-wasm-bytecode": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.6.tgz", + "integrity": "sha512-sFFHKwcmBprO9e7Icf0+gddyWYDViL8bpPjJJl0WHxCdETktXdmtWLGVzoHbqUcY4Be1LkNfwTmXOJUFZYSJdA==" + }, + "node_modules/@webassemblyjs/helper-wasm-section": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.6.tgz", + "integrity": "sha512-LPpZbSOwTpEC2cgn4hTydySy1Ke+XEu+ETXuoyvuyezHO3Kjdu90KK95Sh9xTbmjrCsUwvWwCOQQNta37VrS9g==", + "dependencies": { + "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/helper-buffer": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6", + "@webassemblyjs/wasm-gen": "1.11.6" + } + }, + "node_modules/@webassemblyjs/ieee754": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.11.6.tgz", + "integrity": "sha512-LM4p2csPNvbij6U1f19v6WR56QZ8JcHg3QIJTlSwzFcmx6WSORicYj6I63f9yU1kEUtrpG+kjkiIAkevHpDXrg==", + "dependencies": { + "@xtuc/ieee754": "^1.2.0" + } + }, + "node_modules/@webassemblyjs/leb128": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.11.6.tgz", + "integrity": "sha512-m7a0FhE67DQXgouf1tbN5XQcdWoNgaAuoULHIfGFIEVKA6tu/edls6XnIlkmS6FrXAquJRPni3ZZKjw6FSPjPQ==", + "dependencies": { + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/utf8": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.11.6.tgz", + "integrity": "sha512-vtXf2wTQ3+up9Zsg8sa2yWiQpzSsMyXj0qViVP6xKGCUT8p8YJ6HqI7l5eCnWx1T/FYdsv07HQs2wTFbbof/RA==" + }, + "node_modules/@webassemblyjs/wasm-edit": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.6.tgz", + "integrity": "sha512-Ybn2I6fnfIGuCR+Faaz7YcvtBKxvoLV3Lebn1tM4o/IAJzmi9AWYIPWpyBfU8cC+JxAO57bk4+zdsTjJR+VTOw==", + "dependencies": { + "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/helper-buffer": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6", + "@webassemblyjs/helper-wasm-section": "1.11.6", + "@webassemblyjs/wasm-gen": "1.11.6", + "@webassemblyjs/wasm-opt": "1.11.6", + "@webassemblyjs/wasm-parser": "1.11.6", + "@webassemblyjs/wast-printer": "1.11.6" + } + }, + "node_modules/@webassemblyjs/wasm-gen": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.6.tgz", + "integrity": "sha512-3XOqkZP/y6B4F0PBAXvI1/bky7GryoogUtfwExeP/v7Nzwo1QLcq5oQmpKlftZLbT+ERUOAZVQjuNVak6UXjPA==", + "dependencies": { + "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6", + "@webassemblyjs/ieee754": "1.11.6", + "@webassemblyjs/leb128": "1.11.6", + "@webassemblyjs/utf8": "1.11.6" + } + }, + "node_modules/@webassemblyjs/wasm-opt": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.6.tgz", + "integrity": "sha512-cOrKuLRE7PCe6AsOVl7WasYf3wbSo4CeOk6PkrjS7g57MFfVUF9u6ysQBBODX0LdgSvQqRiGz3CXvIDKcPNy4g==", + "dependencies": { + "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/helper-buffer": "1.11.6", + "@webassemblyjs/wasm-gen": "1.11.6", + "@webassemblyjs/wasm-parser": "1.11.6" + } + }, + "node_modules/@webassemblyjs/wasm-parser": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.6.tgz", + "integrity": "sha512-6ZwPeGzMJM3Dqp3hCsLgESxBGtT/OeCvCZ4TA1JUPYgmhAx38tTPR9JaKy0S5H3evQpO/h2uWs2j6Yc/fjkpTQ==", + "dependencies": { + "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/helper-api-error": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6", + "@webassemblyjs/ieee754": "1.11.6", + "@webassemblyjs/leb128": "1.11.6", + "@webassemblyjs/utf8": "1.11.6" + } + }, + "node_modules/@webassemblyjs/wast-printer": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.11.6.tgz", + "integrity": "sha512-JM7AhRcE+yW2GWYaKeHL5vt4xqee5N2WcezptmgyhNS+ScggqcT1OtXykhAb13Sn5Yas0j2uv9tHgrjwvzAP4A==", + "dependencies": { + "@webassemblyjs/ast": "1.11.6", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webpack-cli/configtest": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@webpack-cli/configtest/-/configtest-1.2.0.tgz", + "integrity": "sha512-4FB8Tj6xyVkyqjj1OaTqCjXYULB9FMkqQ8yGrZjRDrYh0nOE+7Lhs45WioWQQMV+ceFlE368Ukhe6xdvJM9Egg==", + "dev": true, + "peerDependencies": { + "webpack": "4.x.x || 5.x.x", + "webpack-cli": "4.x.x" + } + }, + "node_modules/@webpack-cli/info": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@webpack-cli/info/-/info-1.5.0.tgz", + "integrity": "sha512-e8tSXZpw2hPl2uMJY6fsMswaok5FdlGNRTktvFk2sD8RjH0hE2+XistawJx1vmKteh4NmGmNUrp+Tb2w+udPcQ==", + "dev": true, + "dependencies": { + "envinfo": "^7.7.3" + }, + "peerDependencies": { + "webpack-cli": "4.x.x" + } + }, + "node_modules/@webpack-cli/serve": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@webpack-cli/serve/-/serve-1.7.0.tgz", + "integrity": "sha512-oxnCNGj88fL+xzV+dacXs44HcDwf1ovs3AuEzvP7mqXw7fQntqIhQ1BRmynh4qEKQSSSRSWVyXRjmTbZIX9V2Q==", + "dev": true, + "peerDependencies": { + "webpack-cli": "4.x.x" + }, + "peerDependenciesMeta": { + "webpack-dev-server": { + "optional": true + } + } + }, + "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==" + }, + "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==" + }, + "node_modules/abab": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz", + "integrity": "sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==", + "deprecated": "Use your platform's native atob() and btoa() methods instead", + "dev": true + }, + "node_modules/accessible-autocomplete": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/accessible-autocomplete/-/accessible-autocomplete-2.0.4.tgz", + "integrity": "sha512-2p0txrSpvs5wXFUeQJHMheDPTZVSEmiUHWlEPb7vJnv2Dd1xPfoLnBQQMfNbTSit2pL/9sSQYESuD2Yyohd4Yw==", + "dependencies": { + "preact": "^8.3.1" + } + }, + "node_modules/acorn": { + "version": "8.11.2", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.2.tgz", + "integrity": "sha512-nc0Axzp/0FILLEVsm4fNwLCwMttvhEI263QtVPQcbpfZZ3ts0hLsZGOpE6czNlid7CJ9MlyH8reXkpsf3YUY4w==", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-globals": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-7.0.1.tgz", + "integrity": "sha512-umOSDSDrfHbTNPuNpC2NSnnA3LUrqpevPb4T9jRx4MagXNS0rs+gwiTcAvqCRmsD6utzsrzNt+ebm00SNWiC3Q==", + "dev": true, + "dependencies": { + "acorn": "^8.1.0", + "acorn-walk": "^8.0.2" + } + }, + "node_modules/acorn-import-assertions": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.9.0.tgz", + "integrity": "sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA==", + "peerDependencies": { + "acorn": "^8" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.2.tgz", + "integrity": "sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "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/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ajv-formats/node_modules/ajv": { + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", + "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats/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==" + }, + "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==", + "peerDependencies": { + "ajv": "^6.9.1" + } + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "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==", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/aria-query": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.1.3.tgz", + "integrity": "sha512-R5iJ5lkuHybztUfuOAznmboyjWq8O6sqNqtK7CLOqdydi54VNbORp49mb14KbWgG1QD3JFO9hJdZ+y4KutfdOQ==", + "dev": true, + "dependencies": { + "deep-equal": "^2.0.5" + } + }, + "node_modules/array-buffer-byte-length": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.0.tgz", + "integrity": "sha512-LPuwb2P+NrQw3XhxGc36+XSvuBPopovXYTR9Ew++Du9Yb/bx5AzBfrIsBoj0EZUifjQU+sHL21sseZ3jerWO/A==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "is-array-buffer": "^3.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true + }, + "node_modules/available-typed-arrays": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz", + "integrity": "sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/babel-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", + "integrity": "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==", + "dev": true, + "dependencies": { + "@jest/transform": "^29.7.0", + "@types/babel__core": "^7.1.14", + "babel-plugin-istanbul": "^6.1.1", + "babel-preset-jest": "^29.6.3", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.8.0" + } + }, + "node_modules/babel-jest/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/babel-jest/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/babel-jest/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/babel-jest/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/babel-jest/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-jest/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/babel-loader": { + "version": "9.1.3", + "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-9.1.3.tgz", + "integrity": "sha512-xG3ST4DglodGf8qSwv0MdeWLhrDsw/32QMdTO5T1ZIp9gQur0HkCyFs7Awskr10JKXFXwpAhiCuYX5oGXnRGbw==", + "dependencies": { + "find-cache-dir": "^4.0.0", + "schema-utils": "^4.0.0" + }, + "engines": { + "node": ">= 14.15.0" + }, + "peerDependencies": { + "@babel/core": "^7.12.0", + "webpack": ">=5" + } + }, + "node_modules/babel-loader/node_modules/ajv": { + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", + "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/babel-loader/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==", + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/babel-loader/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==" + }, + "node_modules/babel-loader/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==", + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/babel-plugin-istanbul": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", + "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-instrument": "^5.0.4", + "test-exclude": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-istanbul/node_modules/istanbul-lib-instrument": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", + "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", + "dev": true, + "dependencies": { + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-jest-hoist": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz", + "integrity": "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==", + "dev": true, + "dependencies": { + "@babel/template": "^7.3.3", + "@babel/types": "^7.3.3", + "@types/babel__core": "^7.1.14", + "@types/babel__traverse": "^7.0.6" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/babel-plugin-polyfill-corejs2": { + "version": "0.4.7", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.7.tgz", + "integrity": "sha512-LidDk/tEGDfuHW2DWh/Hgo4rmnw3cduK6ZkOI1NPFceSK3n/yAGeOsNT7FLnSGHkXj3RHGSEVkN3FsCTY6w2CQ==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.22.6", + "@babel/helper-define-polyfill-provider": "^0.4.4", + "semver": "^6.3.1" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/babel-plugin-polyfill-corejs3": { + "version": "0.8.7", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.8.7.tgz", + "integrity": "sha512-KyDvZYxAzkC0Aj2dAPyDzi2Ym15e5JKZSK+maI7NAwSqofvuFglbSsxE7wUOvTg9oFVnHMzVzBKcqEb4PJgtOA==", + "dev": true, + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.4.4", + "core-js-compat": "^3.33.1" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/babel-plugin-polyfill-regenerator": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.5.4.tgz", + "integrity": "sha512-S/x2iOCvDaCASLYsOOgWOq4bCfKYVqvO/uxjkaYyZ3rVsVE3CeAI/c84NpyuBBymEgNvHgjEot3a9/Z/kXvqsg==", + "dev": true, + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.4.4" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/babel-preset-current-node-syntax": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.0.1.tgz", + "integrity": "sha512-M7LQ0bxarkxQoN+vz5aJPsLBn77n8QgTFmo8WK0/44auK2xlCXrYcUxHFxgU7qW5Yzw/CjmLRK2uJzaCd7LvqQ==", + "dev": true, + "dependencies": { + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.8.3", + "@babel/plugin-syntax-import-meta": "^7.8.3", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.8.3", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.8.3", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-top-level-await": "^7.8.3" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/babel-preset-jest": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz", + "integrity": "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==", + "dev": true, + "dependencies": { + "babel-plugin-jest-hoist": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/bail": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/bail/-/bail-1.0.5.tgz", + "integrity": "sha512-xFbRxM1tahm08yHBP16MMjVUAvDaBMD38zsM9EMAUN61omwLmKlOpB/Zku5QkjZ8TZ4vn53pj+t518cH0S03RQ==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "node_modules/big.js": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", + "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", + "engines": { + "node": "*" + } + }, + "node_modules/binary-extensions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "engines": { + "node": ">=8" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "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/browserslist": { + "version": "4.22.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.22.2.tgz", + "integrity": "sha512-0UgcrvQmBDvZHFGdYUehrCNIazki7/lUP3kkoi/r3YB2amZbFM9J43ZRkJTXBUZK4gmx56+Sqk9+Vs9mwZx9+A==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "caniuse-lite": "^1.0.30001565", + "electron-to-chromium": "^1.4.601", + "node-releases": "^2.0.14", + "update-browserslist-db": "^1.0.13" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/bser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", + "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", + "dev": true, + "dependencies": { + "node-int64": "^0.4.0" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==" + }, + "node_modules/call-bind": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz", + "integrity": "sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.1", + "set-function-length": "^1.1.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/camelize": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/camelize/-/camelize-1.0.1.tgz", + "integrity": "sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001576", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001576.tgz", + "integrity": "sha512-ff5BdakGe2P3SQsMsiqmt1Lc8221NR1VzHj5jXN5vBny9A6fpze94HiVV/n7XRosOlsShJcvMv5mdnpjOGCEgg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ] + }, + "node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/char-regex": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", + "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/character-entities": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-1.2.4.tgz", + "integrity": "sha512-iBMyeEHxfVnIakwOuDXpVkc54HijNgCyQB2w0VfGQThle6NXn50zU6V/u+LDhxHcDUPojn6Kpga3PTAD8W1bQw==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-legacy": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-1.1.4.tgz", + "integrity": "sha512-3Xnr+7ZFS1uxeiUDvV02wQ+QDbc55o97tIV5zHScSPJpcLm/r0DFPcoY3tYRp+VZukxuMeKgXYmsXQHO05zQeA==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-reference-invalid": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-1.1.4.tgz", + "integrity": "sha512-mKKUkUbhPpQlCOfIuZkvSEgktjPFIsZKRRbC6KWVEMvlzblj3i3asQv5ODsrwt0N3pHAEvjP8KTQPHkp0+6jOg==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/chart.js": { + "version": "3.9.1", + "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-3.9.1.tgz", + "integrity": "sha512-Ro2JbLmvg83gXF5F4sniaQ+lTbSv18E+TIf2cOeiH1Iqd2PGFOtem+DUufMZsCJwFE7ywPOpfXFBwRTGq7dh6w==" + }, + "node_modules/chartjs-adapter-moment": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/chartjs-adapter-moment/-/chartjs-adapter-moment-1.0.1.tgz", + "integrity": "sha512-Uz+nTX/GxocuqXpGylxK19YG4R3OSVf8326D+HwSTsNw1LgzyIGRo+Qujwro1wy6X+soNSnfj5t2vZ+r6EaDmA==", + "peerDependencies": { + "chart.js": ">=3.0.0", + "moment": "^2.10.2" + } + }, + "node_modules/chokidar": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", + "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ], + "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" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chrome-trace-event": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz", + "integrity": "sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg==", + "engines": { + "node": ">=6.0" + } + }, + "node_modules/ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "engines": { + "node": ">=8" + } + }, + "node_modules/cjs-module-lexer": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.2.3.tgz", + "integrity": "sha512-0TNiGstbQmCFwt4akjjBg5pLRTSyj/PkWQ1ZoO2zntmg9yLqSRxwEa4iCfQLGjqhiqBfOJa7W/E8wfGrTDmlZQ==", + "dev": true + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/clone-deep": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", + "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", + "dev": true, + "dependencies": { + "is-plain-object": "^2.0.4", + "kind-of": "^6.0.2", + "shallow-clone": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", + "dev": true, + "engines": { + "iojs": ">= 1.0.0", + "node": ">= 0.12.0" + } + }, + "node_modules/collect-v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.2.tgz", + "integrity": "sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==", + "dev": true + }, + "node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" + }, + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "dev": true + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "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==" + }, + "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==" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "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==" + }, + "node_modules/core-js-compat": { + "version": "3.35.0", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.35.0.tgz", + "integrity": "sha512-5blwFAddknKeNgsjBzilkdQ0+YK8L1PfqPYq40NOYMYFSS38qj+hpTcLLWwpIwA2A5bje/x5jmVn2tzUMg9IVw==", + "dev": true, + "dependencies": { + "browserslist": "^4.22.2" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, + "node_modules/create-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", + "integrity": "sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "prompts": "^2.0.1" + }, + "bin": { + "create-jest": "bin/create-jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/create-jest/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/create-jest/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/create-jest/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/create-jest/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/create-jest/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/create-jest/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/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/css-color-keywords": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz", + "integrity": "sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg==", + "engines": { + "node": ">=4" + } + }, + "node_modules/css-loader": { + "version": "5.2.7", + "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-5.2.7.tgz", + "integrity": "sha512-Q7mOvpBNBG7YrVGMxRxcBJZFL75o+cH2abNASdibkj/fffYD8qWbInZrD0S9ccI6vZclF3DsHE7njGlLtaHbhg==", + "dependencies": { + "icss-utils": "^5.1.0", + "loader-utils": "^2.0.0", + "postcss": "^8.2.15", + "postcss-modules-extract-imports": "^3.0.0", + "postcss-modules-local-by-default": "^4.0.0", + "postcss-modules-scope": "^3.0.0", + "postcss-modules-values": "^4.0.0", + "postcss-value-parser": "^4.1.0", + "schema-utils": "^3.0.0", + "semver": "^7.3.5" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^4.27.0 || ^5.0.0" + } + }, + "node_modules/css-loader/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/css-loader/node_modules/semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/css-loader/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + }, + "node_modules/css-to-react-native": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/css-to-react-native/-/css-to-react-native-3.2.0.tgz", + "integrity": "sha512-e8RKaLXMOFii+02mOlqwjbD00KSEKqblnpO9e++1aXS1fPQOpS1YoqdVHBqPjHNoxeF2mimzVqawm2KCbEdtHQ==", + "dependencies": { + "camelize": "^1.0.0", + "css-color-keywords": "^1.0.0", + "postcss-value-parser": "^4.0.2" + } + }, + "node_modules/css.escape": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", + "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", + "dev": true + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/cssom": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.5.0.tgz", + "integrity": "sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw==", + "dev": true + }, + "node_modules/csstype": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz", + "integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==" + }, + "node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decimal.js": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.4.3.tgz", + "integrity": "sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==", + "dev": true + }, + "node_modules/dedent": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.5.1.tgz", + "integrity": "sha512-+LxW+KLWxu3HW3M2w2ympwtqPrqYRzU8fqi6Fhd18fBALe15blJPI/I4+UHveMVG6lJqB4JNd4UG0S5cnVHwIg==", + "dev": true, + "peerDependencies": { + "babel-plugin-macros": "^3.1.0" + }, + "peerDependenciesMeta": { + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/deep-equal": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-2.2.3.tgz", + "integrity": "sha512-ZIwpnevOurS8bpT4192sqAowWM76JDKSHYzMLty3BZGSswgq6pBaH3DhCSW5xVAZICZyKdOBPjwww5wfgT/6PA==", + "dev": true, + "dependencies": { + "array-buffer-byte-length": "^1.0.0", + "call-bind": "^1.0.5", + "es-get-iterator": "^1.1.3", + "get-intrinsic": "^1.2.2", + "is-arguments": "^1.1.1", + "is-array-buffer": "^3.0.2", + "is-date-object": "^1.0.5", + "is-regex": "^1.1.4", + "is-shared-array-buffer": "^1.0.2", + "isarray": "^2.0.5", + "object-is": "^1.1.5", + "object-keys": "^1.1.1", + "object.assign": "^4.1.4", + "regexp.prototype.flags": "^1.5.1", + "side-channel": "^1.0.4", + "which-boxed-primitive": "^1.0.2", + "which-collection": "^1.0.1", + "which-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/define-data-property": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.1.tgz", + "integrity": "sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.2.1", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/detect-newline": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", + "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "dev": true, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/dom-accessibility-api": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", + "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", + "dev": true + }, + "node_modules/dom-helpers": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", + "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", + "dependencies": { + "@babel/runtime": "^7.8.7", + "csstype": "^3.0.2" + } + }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ] + }, + "node_modules/domexception": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/domexception/-/domexception-4.0.0.tgz", + "integrity": "sha512-A2is4PLG+eeSfoTMA95/s4pvAoSo2mKtiM5jlHkAVewmiO8ISFTFKZjH7UAM1Atli/OT/7JHOrJRJiMKUZKYBw==", + "deprecated": "Use your platform's native DOMException instead", + "dev": true, + "dependencies": { + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz", + "integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.4.623", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.623.tgz", + "integrity": "sha512-lKoz10iCYlP1WtRYdh5MvocQPWVRoI7ysp6qf18bmeBgR8abE6+I2CsfyNKztRDZvhdWc+krKT6wS7Neg8sw3A==" + }, + "node_modules/emittery": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", + "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sindresorhus/emittery?sponsor=1" + } + }, + "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/emojis-list": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", + "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==", + "engines": { + "node": ">= 4" + } + }, + "node_modules/enhanced-resolve": { + "version": "5.15.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.15.0.tgz", + "integrity": "sha512-LXYT42KJ7lpIKECr2mAXIaMldcNCh/7E0KBKOu4KSfkHmP+mZmSs+8V5gBAqisWBy0OO4W5Oyys0GO1Y8KtdKg==", + "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", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/envinfo": { + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.11.0.tgz", + "integrity": "sha512-G9/6xF1FPbIw0TtalAMaVPpiq2aDEuKLXM314jPVAO9r2fo2a4BLqMNkmRS7O/xPPZ+COAhGIz3ETvHEV3eUcg==", + "dev": true, + "bin": { + "envinfo": "dist/cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/es-get-iterator": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/es-get-iterator/-/es-get-iterator-1.1.3.tgz", + "integrity": "sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.3", + "has-symbols": "^1.0.3", + "is-arguments": "^1.1.1", + "is-map": "^2.0.2", + "is-set": "^2.0.2", + "is-string": "^1.0.7", + "isarray": "^2.0.5", + "stop-iteration-iterator": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-module-lexer": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.3.1.tgz", + "integrity": "sha512-JUFAyicQV9mXc3YRxPnDlrfBKpqt6hUYzz9/boprUJHs4e4KVr3XwOF70doO6gwXUor6EWZJAyWAfKki84t20Q==" + }, + "node_modules/escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/escodegen": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", + "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", + "dev": true, + "dependencies": { + "esprima": "^4.0.1", + "estraverse": "^5.2.0", + "esutils": "^2.0.2" + }, + "bin": { + "escodegen": "bin/escodegen.js", + "esgenerate": "bin/esgenerate.js" + }, + "engines": { + "node": ">=6.0" + }, + "optionalDependencies": { + "source-map": "~0.6.1" + } + }, + "node_modules/escodegen/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/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==", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "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==", + "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==", + "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==", + "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/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/exit": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", + "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", + "dev": true, + "dependencies": { + "@jest/expect-utils": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" + }, + "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==" + }, + "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==" + }, + "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/fb-watchman": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", + "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", + "dev": true, + "dependencies": { + "bser": "2.1.1" + } + }, + "node_modules/file-loader": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/file-loader/-/file-loader-6.2.0.tgz", + "integrity": "sha512-qo3glqyTa61Ytg4u73GultjHGjdRyig3tG6lPtyX/jOEJvHif9uB0/OCI2Kif6ctF3caQTW2G5gym21oAsI4pw==", + "dependencies": { + "loader-utils": "^2.0.0", + "schema-utils": "^3.0.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^4.0.0 || ^5.0.0" + } + }, + "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/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==", + "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==", + "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==", + "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==", + "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==", + "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==", + "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==", + "dependencies": { + "find-up": "^6.3.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/flat": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", + "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", + "dev": true, + "bin": { + "flat": "cli.js" + } + }, + "node_modules/for-each": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", + "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", + "dev": true, + "dependencies": { + "is-callable": "^1.1.3" + } + }, + "node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dev": true, + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true + }, + "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/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "dev": true, + "funding": { + "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==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "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.2", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.2.tgz", + "integrity": "sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.2", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "dev": true, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "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==" + }, + "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==", + "engines": { + "node": ">=4" + } + }, + "node_modules/gopd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.1.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/govuk-colours": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/govuk-colours/-/govuk-colours-1.1.0.tgz", + "integrity": "sha512-EcwnP9PsWubmTcsJtLF8w0D5b69j43LwYtSqGyPtO3903J0bbwB2t5ujWZ9UhsbLamuUmigBkNpLItU3/KuFqw==" + }, + "node_modules/govuk-frontend": { + "version": "3.14.0", + "resolved": "https://registry.npmjs.org/govuk-frontend/-/govuk-frontend-3.14.0.tgz", + "integrity": "sha512-y7FTuihCSA8Hty+e9h0uPhCoNanCAN+CLioNFlPmlbeHXpbi09VMyxTcH+XfnMPY4Cp++7096v0rLwwdapTXnA==", + "engines": { + "node": ">= 4.2.0" + } + }, + "node_modules/govuk-react": { + "version": "0.10.6", + "resolved": "https://registry.npmjs.org/govuk-react/-/govuk-react-0.10.6.tgz", + "integrity": "sha512-a10BEPsp5qXtwEfqeWUzljGtNdExODU3KFkZg5BQ0uR5d31lKCmowIaLZ/+OvPw3xJ+KzNhJxAZZme4Cp/+eeQ==", + "dependencies": { + "@govuk-react/back-link": "^0.10.6", + "@govuk-react/breadcrumbs": "^0.10.6", + "@govuk-react/button": "^0.10.6", + "@govuk-react/caption": "^0.10.6", + "@govuk-react/checkbox": "^0.10.6", + "@govuk-react/constants": "^0.10.6", + "@govuk-react/date-field": "^0.10.6", + "@govuk-react/details": "^0.10.6", + "@govuk-react/document-footer-metadata": "^0.10.6", + "@govuk-react/error-summary": "^0.10.6", + "@govuk-react/error-text": "^0.10.6", + "@govuk-react/fieldset": "^0.10.6", + "@govuk-react/file-upload": "^0.10.6", + "@govuk-react/footer": "^0.10.6", + "@govuk-react/form-group": "^0.10.6", + "@govuk-react/global-style": "^0.10.6", + "@govuk-react/grid-col": "^0.10.6", + "@govuk-react/grid-row": "^0.10.6", + "@govuk-react/heading": "^0.10.6", + "@govuk-react/hint-text": "^0.10.6", + "@govuk-react/icons": "^0.10.6", + "@govuk-react/input": "^0.10.6", + "@govuk-react/input-field": "^0.10.6", + "@govuk-react/inset-text": "^0.10.6", + "@govuk-react/label": "^0.10.6", + "@govuk-react/label-text": "^0.10.6", + "@govuk-react/lead-paragraph": "^0.10.6", + "@govuk-react/link": "^0.10.6", + "@govuk-react/list-item": "^0.10.6", + "@govuk-react/loading-box": "^0.10.6", + "@govuk-react/main": "^0.10.6", + "@govuk-react/multi-choice": "^0.10.6", + "@govuk-react/ordered-list": "^0.10.6", + "@govuk-react/page": "^0.10.6", + "@govuk-react/pagination": "^0.10.6", + "@govuk-react/panel": "^0.10.6", + "@govuk-react/paragraph": "^0.10.6", + "@govuk-react/phase-banner": "^0.10.6", + "@govuk-react/radio": "^0.10.6", + "@govuk-react/related-items": "^0.10.6", + "@govuk-react/search-box": "^0.10.6", + "@govuk-react/section-break": "^0.10.6", + "@govuk-react/select": "^0.10.6", + "@govuk-react/skip-link": "^0.10.6", + "@govuk-react/table": "^0.10.6", + "@govuk-react/tabs": "^0.10.6", + "@govuk-react/tag": "^0.10.6", + "@govuk-react/text-area": "^0.10.6", + "@govuk-react/top-nav": "^0.10.6", + "@govuk-react/unordered-list": "^0.10.6", + "@govuk-react/visually-hidden": "^0.10.6", + "@govuk-react/warning-text": "^0.10.6", + "govuk-colours": "^1.1.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0", + "styled-components": ">=5.1" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" + }, + "node_modules/has-bigints": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", + "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "engines": { + "node": ">=4" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.1.tgz", + "integrity": "sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.2.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", + "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", + "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", + "dev": true, + "dependencies": { + "has-symbols": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz", + "integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hex-rgb": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/hex-rgb/-/hex-rgb-4.3.0.tgz", + "integrity": "sha512-Ox1pJVrDCyGHMG9CFg1tmrRUMRPRsAWYc/PinY0XzJU4K7y7vjNoLKIQ7BR5UJMCxNN8EM1MNDmHWA/B3aZUuw==", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/hoist-non-react-statics": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", + "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "dependencies": { + "react-is": "^16.7.0" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true + }, + "node_modules/html-to-react": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/html-to-react/-/html-to-react-1.7.0.tgz", + "integrity": "sha512-b5HTNaTGyOj5GGIMiWVr1k57egAZ/vGy0GGefnCQ1VW5hu9+eku8AXHtf2/DeD95cj/FKBKYa1J7SWBOX41yUQ==", + "dependencies": { + "domhandler": "^5.0", + "htmlparser2": "^9.0", + "lodash.camelcase": "^4.3.0" + }, + "peerDependencies": { + "react": "^0.13.0 || ^0.14.0 || >=15" + } + }, + "node_modules/htmlparser2": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-9.0.0.tgz", + "integrity": "sha512-uxbSI98wmFT/G4P2zXx4OVx04qWUmyFPrD2/CNepa2Zo3GPNaCaaxElDgwUrwYWkK1nr9fft0Ya8dws8coDLLQ==", + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.1.0", + "entities": "^4.5.0" + } + }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "engines": { + "node": ">=10.17.0" + } + }, + "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/icss-utils": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz", + "integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==", + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/immutable": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.4.tgz", + "integrity": "sha512-fsXeu4J4i6WNWSikpI88v/PcVflZz+6kMhUfIwc5SY+poQRPnaf5V7qds6SUyUN3cVxEzuCab7QIoLOQ+DQ1wA==" + }, + "node_modules/import-local": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.1.0.tgz", + "integrity": "sha512-ASB07uLtnDs1o6EHjKpX34BKYDSqnFerfTOJL2HvMqF70LnxpjkzDB8J44oT9pu4AMPkQwf8jl6szgvNd2tRIg==", + "dev": true, + "dependencies": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "dev": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "node_modules/internal-slot": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.6.tgz", + "integrity": "sha512-Xj6dv+PsbtwyPpEflsejS+oIZxmMlV44zAhG479uYu89MsjcYOhCFnNyKrkJrihbsiasQyY0afoCl/9BLR65bg==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.2.2", + "hasown": "^2.0.0", + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/interpret": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-2.2.0.tgz", + "integrity": "sha512-Ju0Bz/cEia55xDwUWEa8+olFpCiQoypjnQySseKtmjNrnps3P+xfpUmGr90T7yjlVJmOtybRvPXhKMbHr+fWnw==", + "dev": true, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-alphabetical": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-1.0.4.tgz", + "integrity": "sha512-DwzsA04LQ10FHTZuL0/grVDk4rFoVH1pjAToYwBrHSxcrBIGQuXrQMtD5U1b0U2XVgKZCTLLP8u2Qxqhy3l2Vg==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-alphanumerical": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-1.0.4.tgz", + "integrity": "sha512-UzoZUr+XfVz3t3v4KyGEniVL9BDRoQtY7tOyrRybkVNjDFWyo1yhXNGrrBTQxp3ib9BLAWs7k2YKBQsFRkZG9A==", + "dependencies": { + "is-alphabetical": "^1.0.0", + "is-decimal": "^1.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-arguments": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz", + "integrity": "sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-array-buffer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.2.tgz", + "integrity": "sha512-y+FyyR/w8vfIRq4eQcM1EYgSTnmHXPqaF+IgzgraytCFq5Xh8lllDVmAZolPJiZttZLeFSINPYMaEJ7/vWUa1w==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.2.0", + "is-typed-array": "^1.1.10" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true + }, + "node_modules/is-bigint": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", + "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==", + "dev": true, + "dependencies": { + "has-bigints": "^1.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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/is-boolean-object": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", + "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-buffer": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz", + "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "engines": { + "node": ">=4" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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==", + "dev": true, + "dependencies": { + "hasown": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", + "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", + "dev": true, + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-decimal": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-1.0.4.tgz", + "integrity": "sha512-RGdriMmQQvZ2aqaQq3awNA6dCGtKpiDFcOzrTWrDAT2MiWrKQVPmxLGHl7Y2nNu6led0kEyoX0enY0qXYsv9zw==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "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, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-generator-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", + "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-hexadecimal": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-1.0.4.tgz", + "integrity": "sha512-gyPJuv83bHMpocVYoqof5VDiZveEoGoFL8m3BXNb2VW8Xs+rz9kqO8LOQ5DH6EsuvilT1ApazU0pyl+ytbPtlw==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-map": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.2.tgz", + "integrity": "sha512-cOZFQQozTha1f4MxLFzlgKYPTyj26picdZTx82hbc/Xf4K/tZOOXSCkMvU4pKioRXGDLJRn0GM7Upe7kR721yg==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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/is-number-object": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz", + "integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==", + "dev": true, + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-plain-obj": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", + "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", + "engines": { + "node": ">=8" + } + }, + "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/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true + }, + "node_modules/is-regex": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", + "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-set": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.2.tgz", + "integrity": "sha512-+2cnTEZeY5z/iXGbLhPrOAaK/Mau5k5eXq9j14CpRTftq0pAJu2MwVRSZhyZWBzx3o6X795Lz6Bpb6R0GKf37g==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-shared-array-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz", + "integrity": "sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-string": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", + "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", + "dev": true, + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-symbol": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", + "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", + "dev": true, + "dependencies": { + "has-symbols": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.12.tgz", + "integrity": "sha512-Z14TF2JNG8Lss5/HMqt0//T9JeHXttXy5pH/DBU4vi98ozO2btxzq9MwYDZYnKwU8nRsz/+GVFVRDq3DkVuSPg==", + "dev": true, + "dependencies": { + "which-typed-array": "^1.1.11" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakmap": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.1.tgz", + "integrity": "sha512-NSBR4kH5oVj1Uwvv970ruUkCV7O1mzgVFO4/rev2cLRda9Tm9HrL70ZPut4rOHgY0FNrUu9BCbXA2sdQ+x0chA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakset": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.2.tgz", + "integrity": "sha512-t2yVvttHkQktwnNNmBQ98AhENLdPUTDTE21uPqAQ0ARwQfGeQKRVS0NNurH7bTf7RrvcVn1OOge45CnBeHCSmg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "node_modules/isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.1.tgz", + "integrity": "sha512-EAMEJBsYuyyztxMxW3g7ugGPkrZsV57v0Hmv3mm1uQsmB+QnZuepg731CRaIgeUVSdmsTngOkSnauNF8p7FIhA==", + "dev": true, + "dependencies": { + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-instrument/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-instrument/node_modules/semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-instrument/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report/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/istanbul-lib-source-maps": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", + "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", + "dev": true, + "dependencies": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.6.tgz", + "integrity": "sha512-TLgnMkKg3iTDsQ9PbPTdpfAK2DzjF9mqUG7RMgcQl8oFjad8ob4laGxv5XV5U9MAfx8D6tSJiUyuAwzLicaxlg==", + "dev": true, + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", + "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", + "dev": true, + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/types": "^29.6.3", + "import-local": "^3.0.2", + "jest-cli": "^29.7.0" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-changed-files": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.7.0.tgz", + "integrity": "sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==", + "dev": true, + "dependencies": { + "execa": "^5.0.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-changed-files/node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-changed-files/node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-circus": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.7.0.tgz", + "integrity": "sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==", + "dev": true, + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "co": "^4.6.0", + "dedent": "^1.0.0", + "is-generator-fn": "^2.0.0", + "jest-each": "^29.7.0", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0", + "pretty-format": "^29.7.0", + "pure-rand": "^6.0.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-circus/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/jest-circus/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/jest-circus/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/jest-circus/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/jest-circus/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-circus/node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-circus/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/jest-circus/node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-cli": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.7.0.tgz", + "integrity": "sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==", + "dev": true, + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "create-jest": "^29.7.0", + "exit": "^0.1.2", + "import-local": "^3.0.2", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "yargs": "^17.3.1" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-cli/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/jest-cli/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/jest-cli/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/jest-cli/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/jest-cli/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-cli/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/jest-config": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.7.0.tgz", + "integrity": "sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==", + "dev": true, + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/test-sequencer": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-jest": "^29.7.0", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "deepmerge": "^4.2.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-circus": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "micromatch": "^4.0.4", + "parse-json": "^5.2.0", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@types/node": "*", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/jest-config/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/jest-config/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/jest-config/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/jest-config/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/jest-config/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-config/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/jest-diff": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", + "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", + "dev": true, + "dependencies": { + "chalk": "^4.0.0", + "diff-sequences": "^29.6.3", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-diff/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/jest-diff/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/jest-diff/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/jest-diff/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/jest-diff/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-diff/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/jest-docblock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.7.0.tgz", + "integrity": "sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==", + "dev": true, + "dependencies": { + "detect-newline": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-each": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.7.0.tgz", + "integrity": "sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "jest-util": "^29.7.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-each/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/jest-each/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/jest-each/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/jest-each/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/jest-each/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-each/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/jest-environment-jsdom": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-29.7.0.tgz", + "integrity": "sha512-k9iQbsf9OyOfdzWH8HDmrRT0gSIcX+FLNW7IQq94tFX0gynPwqDTW0Ho6iMVNjGz/nb+l/vW3dWM2bbLLpkbXA==", + "dev": true, + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/jsdom": "^20.0.0", + "@types/node": "*", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0", + "jsdom": "^20.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "canvas": "^2.5.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jest-environment-jsdom/node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dev": true, + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/jest-environment-jsdom/node_modules/cssstyle": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-2.3.0.tgz", + "integrity": "sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A==", + "dev": true, + "dependencies": { + "cssom": "~0.3.6" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-environment-jsdom/node_modules/cssstyle/node_modules/cssom": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz", + "integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==", + "dev": true + }, + "node_modules/jest-environment-jsdom/node_modules/data-urls": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-3.0.2.tgz", + "integrity": "sha512-Jy/tj3ldjZJo63sVAvg6LHt2mHvl4V6AgRAmNDtLdm7faqtsx+aJG42rsyCo9JCoRVKwPFzKlIPx3DIibwSIaQ==", + "dev": true, + "dependencies": { + "abab": "^2.0.6", + "whatwg-mimetype": "^3.0.0", + "whatwg-url": "^11.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/jest-environment-jsdom/node_modules/html-encoding-sniffer": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz", + "integrity": "sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==", + "dev": true, + "dependencies": { + "whatwg-encoding": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/jest-environment-jsdom/node_modules/http-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", + "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", + "dev": true, + "dependencies": { + "@tootallnate/once": "2", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/jest-environment-jsdom/node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "dev": true, + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/jest-environment-jsdom/node_modules/jsdom": { + "version": "20.0.3", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-20.0.3.tgz", + "integrity": "sha512-SYhBvTh89tTfCD/CRdSOm13mOBa42iTaTyfyEWBdKcGdPxPtLFBXuHR8XHb33YNYaP+lLbmSvBTsnoesCNJEsQ==", + "dev": true, + "dependencies": { + "abab": "^2.0.6", + "acorn": "^8.8.1", + "acorn-globals": "^7.0.0", + "cssom": "^0.5.0", + "cssstyle": "^2.3.0", + "data-urls": "^3.0.2", + "decimal.js": "^10.4.2", + "domexception": "^4.0.0", + "escodegen": "^2.0.0", + "form-data": "^4.0.0", + "html-encoding-sniffer": "^3.0.0", + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.1", + "is-potential-custom-element-name": "^1.0.1", + "nwsapi": "^2.2.2", + "parse5": "^7.1.1", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^4.1.2", + "w3c-xmlserializer": "^4.0.0", + "webidl-conversions": "^7.0.0", + "whatwg-encoding": "^2.0.0", + "whatwg-mimetype": "^3.0.0", + "whatwg-url": "^11.0.0", + "ws": "^8.11.0", + "xml-name-validator": "^4.0.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "canvas": "^2.5.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jest-environment-jsdom/node_modules/tr46": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-3.0.0.tgz", + "integrity": "sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==", + "dev": true, + "dependencies": { + "punycode": "^2.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/jest-environment-jsdom/node_modules/w3c-xmlserializer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-4.0.0.tgz", + "integrity": "sha512-d+BFHzbiCx6zGfz0HyQ6Rg69w9k19nviJspaj4yNscGjrHu94sVP+aRm75yEbCh+r2/yR+7q6hux9LVtbuTGBw==", + "dev": true, + "dependencies": { + "xml-name-validator": "^4.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/jest-environment-jsdom/node_modules/whatwg-encoding": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz", + "integrity": "sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==", + "dev": true, + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/jest-environment-jsdom/node_modules/whatwg-mimetype": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz", + "integrity": "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/jest-environment-jsdom/node_modules/whatwg-url": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-11.0.0.tgz", + "integrity": "sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==", + "dev": true, + "dependencies": { + "tr46": "^3.0.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/jest-environment-jsdom/node_modules/xml-name-validator": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz", + "integrity": "sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/jest-environment-node": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz", + "integrity": "sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==", + "dev": true, + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "dev": true, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-haste-map": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz", + "integrity": "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@types/graceful-fs": "^4.1.3", + "@types/node": "*", + "anymatch": "^3.0.3", + "fb-watchman": "^2.0.0", + "graceful-fs": "^4.2.9", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "micromatch": "^4.0.4", + "walker": "^1.0.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "optionalDependencies": { + "fsevents": "^2.3.2" + } + }, + "node_modules/jest-haste-map/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-haste-map/node_modules/jest-worker": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", + "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", + "dev": true, + "dependencies": { + "@types/node": "*", + "jest-util": "^29.7.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-haste-map/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/jest-leak-detector": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz", + "integrity": "sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==", + "dev": true, + "dependencies": { + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-matcher-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", + "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", + "dev": true, + "dependencies": { + "chalk": "^4.0.0", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-matcher-utils/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/jest-matcher-utils/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/jest-matcher-utils/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/jest-matcher-utils/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/jest-matcher-utils/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-matcher-utils/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/jest-message-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", + "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.6.3", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-message-util/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/jest-message-util/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/jest-message-util/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/jest-message-util/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/jest-message-util/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-message-util/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/jest-mock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz", + "integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-pnp-resolver": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", + "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", + "dev": true, + "engines": { + "node": ">=6" + }, + "peerDependencies": { + "jest-resolve": "*" + }, + "peerDependenciesMeta": { + "jest-resolve": { + "optional": true + } + } + }, + "node_modules/jest-regex-util": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", + "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", + "dev": true, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.7.0.tgz", + "integrity": "sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==", + "dev": true, + "dependencies": { + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-pnp-resolver": "^1.2.2", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "resolve": "^1.20.0", + "resolve.exports": "^2.0.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve-dependencies": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.7.0.tgz", + "integrity": "sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==", + "dev": true, + "dependencies": { + "jest-regex-util": "^29.6.3", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve/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/jest-resolve/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/jest-resolve/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/jest-resolve/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/jest-resolve/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-resolve/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/jest-runner": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.7.0.tgz", + "integrity": "sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==", + "dev": true, + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/environment": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "graceful-fs": "^4.2.9", + "jest-docblock": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-leak-detector": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-resolve": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-util": "^29.7.0", + "jest-watcher": "^29.7.0", + "jest-worker": "^29.7.0", + "p-limit": "^3.1.0", + "source-map-support": "0.5.13" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runner/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/jest-runner/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/jest-runner/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/jest-runner/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/jest-runner/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-runner/node_modules/jest-worker": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", + "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", + "dev": true, + "dependencies": { + "@types/node": "*", + "jest-util": "^29.7.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runner/node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/jest-runner/node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-runner/node_modules/source-map-support": { + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", + "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", + "dev": true, + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/jest-runner/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/jest-runner/node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-runtime": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.7.0.tgz", + "integrity": "sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==", + "dev": true, + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/globals": "^29.7.0", + "@jest/source-map": "^29.6.3", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "cjs-module-lexer": "^1.0.0", + "collect-v8-coverage": "^1.0.0", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0", + "strip-bom": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runtime/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/jest-runtime/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/jest-runtime/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/jest-runtime/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/jest-runtime/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-runtime/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/jest-snapshot": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.7.0.tgz", + "integrity": "sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==", + "dev": true, + "dependencies": { + "@babel/core": "^7.11.6", + "@babel/generator": "^7.7.2", + "@babel/plugin-syntax-jsx": "^7.7.2", + "@babel/plugin-syntax-typescript": "^7.7.2", + "@babel/types": "^7.3.3", + "@jest/expect-utils": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0", + "chalk": "^4.0.0", + "expect": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "natural-compare": "^1.4.0", + "pretty-format": "^29.7.0", + "semver": "^7.5.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot/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/jest-snapshot/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/jest-snapshot/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/jest-snapshot/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/jest-snapshot/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-snapshot/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jest-snapshot/node_modules/semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jest-snapshot/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/jest-snapshot/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-util/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/jest-util/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/jest-util/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/jest-util/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/jest-util/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-util/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/jest-validate": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.7.0.tgz", + "integrity": "sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "camelcase": "^6.2.0", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "leven": "^3.1.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate/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/jest-validate/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-validate/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/jest-validate/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/jest-validate/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/jest-validate/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-validate/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/jest-watcher": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.7.0.tgz", + "integrity": "sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==", + "dev": true, + "dependencies": { + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "jest-util": "^29.7.0", + "string-length": "^4.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-watcher/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/jest-watcher/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/jest-watcher/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/jest-watcher/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/jest-watcher/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-watcher/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/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==", + "dependencies": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/jest-worker/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" + }, + "node_modules/js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dev": true, + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=4" + } + }, + "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==" + }, + "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==" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "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/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/klona": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/klona/-/klona-2.0.6.tgz", + "integrity": "sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA==", + "engines": { + "node": ">= 8" + } + }, + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true + }, + "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==", + "engines": { + "node": ">=6.11.5" + } + }, + "node_modules/loader-utils": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", + "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", + "dependencies": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^2.1.2" + }, + "engines": { + "node": ">=8.9.0" + } + }, + "node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true + }, + "node_modules/lodash.assign": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.assign/-/lodash.assign-4.2.0.tgz", + "integrity": "sha512-hFuH8TY+Yji7Eja3mGiuAxBqLagejScbG8GbG0j6o9vzn0YL14My+ktnqtZgFTosKymC9/44wP6s7xyuLfnClw==" + }, + "node_modules/lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==" + }, + "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.defaults": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", + "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==" + }, + "node_modules/lodash.foreach": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.foreach/-/lodash.foreach-4.5.0.tgz", + "integrity": "sha512-aEXTF4d+m05rVOAUG3z4vZZ4xVexLKZGF0lIxuHZ1Hplpk/3B6Z1+/ICICYRLm7c41Z2xiejbkCkJoTlypoXhQ==" + }, + "node_modules/lodash.frompairs": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.frompairs/-/lodash.frompairs-4.0.1.tgz", + "integrity": "sha512-dvqe2I+cO5MzXCMhUnfYFa9MD+/760yx2aTAN1lqEcEkf896TxgrX373igVdqSJj6tQd0jnSLE1UMuKufqqxFw==" + }, + "node_modules/lodash.get": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", + "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==" + }, + "node_modules/lodash.topairs": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.topairs/-/lodash.topairs-4.3.0.tgz", + "integrity": "sha512-qrRMbykBSEGdOgQLJJqVSdPWMD7Q+GJJ5jMRfQYb+LTLsw3tYVIabnCzRqTJb2WTo17PG5gNzXuFaZgYH/9SAQ==" + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "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==", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/lz-string": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", + "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", + "dev": true, + "bin": { + "lz-string": "bin/bin.js" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/make-dir/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/makeerror": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", + "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", + "dev": true, + "dependencies": { + "tmpl": "1.0.5" + } + }, + "node_modules/mdast-add-list-metadata": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mdast-add-list-metadata/-/mdast-add-list-metadata-1.0.1.tgz", + "integrity": "sha512-fB/VP4MJ0LaRsog7hGPxgOrSL3gE/2uEdZyDuSEnKCv/8IkYHiDkIQSbChiJoHyxZZXZ9bzckyRk+vNxFzh8rA==", + "dependencies": { + "unist-util-visit-parents": "1.1.2" + } + }, + "node_modules/mdast-util-from-markdown": { + "version": "0.8.5", + "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-0.8.5.tgz", + "integrity": "sha512-2hkTXtYYnr+NubD/g6KGBS/0mFmBcifAsI0yIWRiRo0PjVs6SSOSOdtzbp6kSGnShDN6G5aWZpKQ2lWRy27mWQ==", + "dependencies": { + "@types/mdast": "^3.0.0", + "mdast-util-to-string": "^2.0.0", + "micromark": "~2.11.0", + "parse-entities": "^2.0.0", + "unist-util-stringify-position": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-string": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-2.0.0.tgz", + "integrity": "sha512-AW4DRS3QbBayY/jJmD8437V1Gombjf8RSOUCMFBuo5iHi58AGEgVCKQ+ezHkZZDpAQS75hcBMpLqjpJTjtUL7w==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "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==" + }, + "node_modules/micromark": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-2.11.4.tgz", + "integrity": "sha512-+WoovN/ppKolQOFIAajxi7Lu9kInbPxFuTBVEavFcL8eAfVstoc5MocPmqBeAdBOJV00uaVjegzH4+MA0DN/uA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "debug": "^4.0.0", + "parse-entities": "^2.0.0" + } + }, + "node_modules/micromatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", + "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "dev": true, + "dependencies": { + "braces": "^3.0.2", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "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==", + "engines": { + "node": ">= 0.6" + } + }, + "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==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/mini-css-extract-plugin": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-1.6.2.tgz", + "integrity": "sha512-WhDvO3SjGm40oV5y26GjMJYjd2UMqrLAGKy5YS2/3QKJy2F7jgynuHTir/tgUUOiNQu5saXHdc8reo7YuhhT4Q==", + "dependencies": { + "loader-utils": "^2.0.0", + "schema-utils": "^3.0.0", + "webpack-sources": "^1.1.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^4.4.0 || ^5.0.0" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/moment": { + "version": "2.29.4", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz", + "integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==", + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/multi-input-input": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/multi-input-input/-/multi-input-input-0.0.3.tgz", + "integrity": "sha512-jzpCxDqvyi7eqdgPykDlv11y1CWOS02T+oaSrLlzVeG3mSrQbNi75/QWsWkrgwxvWWvAkq+e0Pk59uN1YaYGLg==" + }, + "node_modules/nanoid": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz", + "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true + }, + "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==" + }, + "node_modules/node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", + "dev": true + }, + "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==" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/nwsapi": { + "version": "2.2.7", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.7.tgz", + "integrity": "sha512-ub5E4+FBPKwAZx0UwIQOjYWGHTEq5sPqHQNRN8Z9e4A7u3Tj1weLJsL59yH9vmvqEtBHaOmT6cYQKIZOxp35FQ==", + "dev": true + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", + "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-is": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.5.tgz", + "integrity": "sha512-3cyDsyHgtmi7I7DfSSI2LDp6SK2lwvtbg0p0R1e0RvTqF5ceGx+K2dfSjm1bKDMVCFEDAQvy+o8c6a7VujOddw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.5.tgz", + "integrity": "sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.5", + "define-properties": "^1.2.1", + "has-symbols": "^1.0.3", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-entities": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-2.0.0.tgz", + "integrity": "sha512-kkywGpCcRYhqQIchaWqZ875wzpS/bMKhz5HnN3p7wveJTkTtyAB/AlnS0f8DFSqYW1T82t6yEAkEcB+A1I3MbQ==", + "dependencies": { + "character-entities": "^1.0.0", + "character-entities-legacy": "^1.0.0", + "character-reference-invalid": "^1.0.0", + "is-alphanumerical": "^1.0.0", + "is-decimal": "^1.0.0", + "is-hexadecimal": "^1.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parse5": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz", + "integrity": "sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==", + "dev": true, + "dependencies": { + "entities": "^4.4.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "node_modules/picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pirates": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", + "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/polished": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/polished/-/polished-4.2.2.tgz", + "integrity": "sha512-Sz2Lkdxz6F2Pgnpi9U5Ng/WdWAUZxmHrNPoVlm3aAemxoy2Qy7LGjQg4uf8qKelDAUW94F4np3iH2YPf2qefcQ==", + "dependencies": { + "@babel/runtime": "^7.17.8" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/postcss": { + "version": "8.4.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "nanoid": "^3.3.6", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-modules-extract-imports": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.0.0.tgz", + "integrity": "sha512-bdHleFnP3kZ4NYDhuGlVK+CMrQ/pqUm8bx/oGL93K6gVwiclvX5x0n76fYMKuIGKzlABOy13zsvqjb0f92TEXw==", + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-local-by-default": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.3.tgz", + "integrity": "sha512-2/u2zraspoACtrbFRnTijMiQtb4GW4BvatjaG/bCjYQo8kLTdevCUlwuBHx2sCnSyrI3x3qj4ZK1j5LQBgzmwA==", + "dependencies": { + "icss-utils": "^5.0.0", + "postcss-selector-parser": "^6.0.2", + "postcss-value-parser": "^4.1.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-scope": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.0.0.tgz", + "integrity": "sha512-hncihwFA2yPath8oZ15PZqvWGkWf+XUfQgUGamS4LqoP1anQLOsOJw0vr7J7IwLpoY9fatA2qiGUGmuZL0Iqlg==", + "dependencies": { + "postcss-selector-parser": "^6.0.4" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-values": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz", + "integrity": "sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==", + "dependencies": { + "icss-utils": "^5.0.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.0.13", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.13.tgz", + "integrity": "sha512-EaV1Gl4mUEV4ddhDnv/xtj7sxwrwxdetHdWUGnT4VJQf+4d05v6lHYZr8N573k5Z0BViss7BDhfWtKS3+sfAqQ==", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" + }, + "node_modules/preact": { + "version": "8.5.3", + "resolved": "https://registry.npmjs.org/preact/-/preact-8.5.3.tgz", + "integrity": "sha512-O3kKP+1YdgqHOFsZF2a9JVdtqD+RPzCQc3rP+Ualf7V6rmRDchZ9MJbiGTT7LuyqFKZqlHSOyO/oMFmI2lVTsw==", + "hasInstallScript": true + }, + "node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/pretty-format/node_modules/react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", + "dev": true + }, + "node_modules/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "dev": true, + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/psl": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", + "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==", + "dev": true + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "engines": { + "node": ">=6" + } + }, + "node_modules/pure-rand": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.0.4.tgz", + "integrity": "sha512-LA0Y9kxMYv47GIPJy6MI84fqTd2HmYZI83W/kM/SkKfDlajnZYfmXFTxkbY+xSBPkLJxltMa9hIkmdc29eguMA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ] + }, + "node_modules/querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", + "dev": true + }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/react": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", + "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", + "integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.0" + }, + "peerDependencies": { + "react": "^18.2.0" + } + }, + "node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" + }, + "node_modules/react-markdown": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-5.0.3.tgz", + "integrity": "sha512-jDWOc1AvWn0WahpjW6NK64mtx6cwjM4iSsLHJPNBqoAgGOVoIdJMqaKX4++plhOtdd4JksdqzlDibgPx6B/M2w==", + "dependencies": { + "@types/mdast": "^3.0.3", + "@types/unist": "^2.0.3", + "html-to-react": "^1.3.4", + "mdast-add-list-metadata": "1.0.1", + "prop-types": "^15.7.2", + "react-is": "^16.8.6", + "remark-parse": "^9.0.0", + "unified": "^9.0.0", + "unist-util-visit": "^2.0.0", + "xtend": "^4.0.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + }, + "peerDependencies": { + "@types/react": ">=16", + "react": ">=16" + } + }, + "node_modules/react-shallow-renderer": { + "version": "16.15.0", + "resolved": "https://registry.npmjs.org/react-shallow-renderer/-/react-shallow-renderer-16.15.0.tgz", + "integrity": "sha512-oScf2FqQ9LFVQgA73vr86xl2NaOIX73rh+YFqcOp68CWj56tSfgtGKrEbyhCj0rSijyG9M1CYprTh39fBi5hzA==", + "dev": true, + "dependencies": { + "object-assign": "^4.1.1", + "react-is": "^16.12.0 || ^17.0.0 || ^18.0.0" + }, + "peerDependencies": { + "react": "^16.0.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/react-test-renderer": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-test-renderer/-/react-test-renderer-18.2.0.tgz", + "integrity": "sha512-JWD+aQ0lh2gvh4NM3bBM42Kx+XybOxCpgYK7F8ugAlpaTSnWsX+39Z4XkOykGZAHrjwwTZT3x3KxswVWxHPUqA==", + "dev": true, + "dependencies": { + "react-is": "^18.2.0", + "react-shallow-renderer": "^16.15.0", + "scheduler": "^0.23.0" + }, + "peerDependencies": { + "react": "^18.2.0" + } + }, + "node_modules/react-test-renderer/node_modules/react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", + "dev": true + }, + "node_modules/react-transition-group": { + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", + "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==", + "dependencies": { + "@babel/runtime": "^7.5.5", + "dom-helpers": "^5.0.1", + "loose-envify": "^1.4.0", + "prop-types": "^15.6.2" + }, + "peerDependencies": { + "react": ">=16.6.0", + "react-dom": ">=16.6.0" + } + }, + "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/rechoir": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.7.1.tgz", + "integrity": "sha512-/njmZ8s1wVeR6pjTZ+0nCnv8SpZNRMT2D1RLOJQESlYFDBvwpTA4KWJpZ+sBJ4+vhjILRcK7JIFdGCdxEAAitg==", + "dev": true, + "dependencies": { + "resolve": "^1.9.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/redent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "dev": true, + "dependencies": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "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.1.1", + "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.1.1.tgz", + "integrity": "sha512-X007RyZLsCJVVrjgEFVpLUTZwyOZk3oiL75ZcuYjlIWd6rNJtOjkBwQc5AsRrpbKVkxN6sklw/k/9m2jJYOf8Q==", + "dev": true, + "dependencies": { + "regenerate": "^1.4.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/regenerator-runtime": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz", + "integrity": "sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA==" + }, + "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/regexp.prototype.flags": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.1.tgz", + "integrity": "sha512-sy6TXMN+hnP/wMy+ISxg3krXx7BAtWVO4UouuCN/ziM9UEne0euamVNafDfvC83bRNr95y0V5iijeDQFUNpvrg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "set-function-name": "^2.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/regexpu-core": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-5.3.2.tgz", + "integrity": "sha512-RAM5FlZz+Lhmo7db9L298p2vHP5ZywrVXmVXpmAD9GuL5MPH6t9ROw1iA/wfHkQ76Qe7AaPF0nGuim96/IrQMQ==", + "dev": true, + "dependencies": { + "@babel/regjsgen": "^0.8.0", + "regenerate": "^1.4.2", + "regenerate-unicode-properties": "^10.1.0", + "regjsparser": "^0.9.1", + "unicode-match-property-ecmascript": "^2.0.0", + "unicode-match-property-value-ecmascript": "^2.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/regjsparser": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.9.1.tgz", + "integrity": "sha512-dQUtn90WanSNl+7mQKcXAgZxvUe7Z0SqXlgzv0za4LwiUhyzBC58yQO3liFoUgu8GiJVInAhJjkj1N0EtQ5nkQ==", + "dev": true, + "dependencies": { + "jsesc": "~0.5.0" + }, + "bin": { + "regjsparser": "bin/parser" + } + }, + "node_modules/regjsparser/node_modules/jsesc": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", + "integrity": "sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA==", + "dev": true, + "bin": { + "jsesc": "bin/jsesc" + } + }, + "node_modules/remark-parse": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-9.0.0.tgz", + "integrity": "sha512-geKatMwSzEXKHuzBNU1z676sGcDcFoChMK38TgdHJNAYfFtsfHDQG7MoJAjs6sgYMqyLduCYWDIWZIxiPeafEw==", + "dependencies": { + "mdast-util-from-markdown": "^0.8.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "engines": { + "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==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "dev": true + }, + "node_modules/resolve": { + "version": "1.22.8", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", + "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "dev": true, + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve.exports": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.2.tgz", + "integrity": "sha512-X2UW6Nw3n/aMgDVy+0rSqgHlv39WZAlZrXCdnbyEiKm17DSqHX4MmQMaST3FbeWR5FTuRcUwYAziZajji0Y7mg==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "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.69.5", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.69.5.tgz", + "integrity": "sha512-qg2+UCJibLr2LCVOt3OlPhr/dqVHWOa9XtZf2OjbLs/T4VPSJ00udtgJxH3neXZm+QqX8B+3cU7RaLqp1iVfcQ==", + "dependencies": { + "chokidar": ">=3.0.0 <4.0.0", + "immutable": "^4.0.0", + "source-map-js": ">=0.6.2 <2.0.0" + }, + "bin": { + "sass": "sass.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-loader": { + "version": "12.6.0", + "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-12.6.0.tgz", + "integrity": "sha512-oLTaH0YCtX4cfnJZxKSLAyglED0naiYfNG1iXfU5w1LNZ+ukoA5DtyDIN5zmKVZwYNJP4KRc5Y3hkWga+7tYfA==", + "dependencies": { + "klona": "^2.0.4", + "neo-async": "^2.6.2" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "fibers": ">= 3.1.0", + "node-sass": "^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0", + "sass": "^1.3.0", + "sass-embedded": "*", + "webpack": "^5.0.0" + }, + "peerDependenciesMeta": { + "fibers": { + "optional": true + }, + "node-sass": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + } + } + }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, + "node_modules/scheduler": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz", + "integrity": "sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "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==", + "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/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/serialize-javascript": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.1.tgz", + "integrity": "sha512-owoXEFjWRllis8/M1Q+Cw5k8ZH40e3zhp/ovX+Xr/vi1qj6QesbyXXViFbpNvWvPNAD62SutwEXavefrLJWj7w==", + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "node_modules/set-function-length": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.0.tgz", + "integrity": "sha512-4DBHDoyHlM1IRPGYcoxexgh67y4ueR53FKV1yyxwFMY7aCqcN/38M1+SwZ/qJQ8iLv7+ck385ot4CcisOAPT9w==", + "dev": true, + "dependencies": { + "define-data-property": "^1.1.1", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.2", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + } }, - "loader-utils": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", - "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", - "requires": { - "big.js": "^5.2.2", - "emojis-list": "^3.0.0", - "json5": "^2.1.2" + "node_modules/set-function-name": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.1.tgz", + "integrity": "sha512-tMNCiqYVkXIZgc2Hnoy2IvC/f8ezc5koaRFkCjrpWzGpCd3qbZXPzVy9MAZzK1ch/X0jvSkojys3oqJN0qCmdA==", + "dev": true, + "dependencies": { + "define-data-property": "^1.0.1", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" } }, - "locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "node_modules/shallow-clone": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", + "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==", "dev": true, - "requires": { - "p-locate": "^4.1.0" + "dependencies": { + "kind-of": "^6.0.2" + }, + "engines": { + "node": ">=8" } }, - "lodash.assign": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/lodash.assign/-/lodash.assign-4.2.0.tgz", - "integrity": "sha512-hFuH8TY+Yji7Eja3mGiuAxBqLagejScbG8GbG0j6o9vzn0YL14My+ktnqtZgFTosKymC9/44wP6s7xyuLfnClw==" + "node_modules/shallowequal": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz", + "integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==" }, - "lodash.defaults": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", - "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==" + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } }, - "lodash.foreach": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.foreach/-/lodash.foreach-4.5.0.tgz", - "integrity": "sha512-aEXTF4d+m05rVOAUG3z4vZZ4xVexLKZGF0lIxuHZ1Hplpk/3B6Z1+/ICICYRLm7c41Z2xiejbkCkJoTlypoXhQ==" + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "engines": { + "node": ">=8" + } }, - "lodash.frompairs": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/lodash.frompairs/-/lodash.frompairs-4.0.1.tgz", - "integrity": "sha512-dvqe2I+cO5MzXCMhUnfYFa9MD+/760yx2aTAN1lqEcEkf896TxgrX373igVdqSJj6tQd0jnSLE1UMuKufqqxFw==" + "node_modules/side-channel": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", + "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.0", + "get-intrinsic": "^1.0.2", + "object-inspect": "^1.9.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, - "lodash.get": { - "version": "4.4.2", - "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", - "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==" + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true }, - "lodash.topairs": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/lodash.topairs/-/lodash.topairs-4.3.0.tgz", - "integrity": "sha512-qrRMbykBSEGdOgQLJJqVSdPWMD7Q+GJJ5jMRfQYb+LTLsw3tYVIabnCzRqTJb2WTo17PG5gNzXuFaZgYH/9SAQ==" + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "dev": true }, - "lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "requires": { - "yallist": "^4.0.0" + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "engines": { + "node": ">=8" } }, - "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==" - }, - "mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==" + "node_modules/source-list-map": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.1.tgz", + "integrity": "sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw==" }, - "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==", - "requires": { - "mime-db": "1.52.0" + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "engines": { + "node": ">=0.10.0" } }, - "mini-css-extract-plugin": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-1.6.2.tgz", - "integrity": "sha512-WhDvO3SjGm40oV5y26GjMJYjd2UMqrLAGKy5YS2/3QKJy2F7jgynuHTir/tgUUOiNQu5saXHdc8reo7YuhhT4Q==", - "requires": { - "loader-utils": "^2.0.0", - "schema-utils": "^3.0.0", - "webpack-sources": "^1.1.0" + "node_modules/source-map-js": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", + "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", + "engines": { + "node": ">=0.10.0" } }, - "moment": { - "version": "2.29.4", - "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz", - "integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==" - }, - "nanoid": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz", - "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==" + "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==", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } }, - "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==" + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true }, - "node-releases": { + "node_modules/stack-utils": { "version": "2.0.6", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.6.tgz", - "integrity": "sha512-PiVXnNuFm5+iYkLBNeq5211hvO38y63T0i2KKh2KnUs3RpzJ+JtODFjkD8yjLwnDkTYF1eKXheUwdssR+NRZdg==" - }, - "normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==" - }, - "p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", "dev": true, - "requires": { - "p-try": "^2.0.0" + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" } }, - "p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "node_modules/stack-utils/node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", "dev": true, - "requires": { - "p-limit": "^2.2.0" + "engines": { + "node": ">=8" } }, - "p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "dev": true - }, - "path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true - }, - "path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true - }, - "path-parse": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true - }, - "picocolors": { + "node_modules/stop-iteration-iterator": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", - "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==" + "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.0.0.tgz", + "integrity": "sha512-iCGQj+0l0HOdZ2AEeBADlsRC+vsnDsZsbdSiH1yNSjcfKM7fdpCMfqAL/dwF5BLiw/XhRft/Wax6zQbhq2BcjQ==", + "dev": true, + "dependencies": { + "internal-slot": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + } }, - "picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==" + "node_modules/string-length": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", + "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", + "dev": true, + "dependencies": { + "char-regex": "^1.0.2", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + } }, - "pkg-dir": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", - "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "node_modules/string-width": { + "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, - "requires": { - "find-up": "^4.0.0" + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" } }, - "postcss": { - "version": "8.4.31", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", - "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", - "requires": { - "nanoid": "^3.3.6", - "picocolors": "^1.0.0", - "source-map-js": "^1.0.2" + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" } }, - "postcss-modules-extract-imports": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.0.0.tgz", - "integrity": "sha512-bdHleFnP3kZ4NYDhuGlVK+CMrQ/pqUm8bx/oGL93K6gVwiclvX5x0n76fYMKuIGKzlABOy13zsvqjb0f92TEXw==", - "requires": {} + "node_modules/strip-ansi/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "engines": { + "node": ">=8" + } }, - "postcss-modules-local-by-default": { + "node_modules/strip-bom": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.0.tgz", - "integrity": "sha512-sT7ihtmGSF9yhm6ggikHdV0hlziDTX7oFoXtuVWeDd3hHObNkcHRo9V3yg7vCAY7cONyxJC/XXCmmiHHcvX7bQ==", - "requires": { - "icss-utils": "^5.0.0", - "postcss-selector-parser": "^6.0.2", - "postcss-value-parser": "^4.1.0" + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "dev": true, + "engines": { + "node": ">=8" } }, - "postcss-modules-scope": { + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/strip-indent": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.0.0.tgz", - "integrity": "sha512-hncihwFA2yPath8oZ15PZqvWGkWf+XUfQgUGamS4LqoP1anQLOsOJw0vr7J7IwLpoY9fatA2qiGUGmuZL0Iqlg==", - "requires": { - "postcss-selector-parser": "^6.0.4" + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, + "dependencies": { + "min-indent": "^1.0.0" + }, + "engines": { + "node": ">=8" } }, - "postcss-modules-values": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz", - "integrity": "sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==", - "requires": { - "icss-utils": "^5.0.0" + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/style-loader": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-3.3.3.tgz", + "integrity": "sha512-53BiGLXAcll9maCYtZi2RCQZKa8NQQai5C4horqKyRmHj9H7QmcUyucrH+4KW/gBQbXM2AsB0axoEcFZPlfPcw==", + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.0.0" } }, - "postcss-selector-parser": { - "version": "6.0.11", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.11.tgz", - "integrity": "sha512-zbARubNdogI9j7WY4nQJBiNqQf3sLS3wCP4WfOidu+p28LofJqDH1tcXypGrcmMHhDk2t9wGhCsYe/+szLTy1g==", - "requires": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" + "node_modules/styled-components": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/styled-components/-/styled-components-6.1.0.tgz", + "integrity": "sha512-VWNfYYBuXzuLS/QYEeoPgMErP26WL+dX9//rEh80B2mmlS1yRxRxuL5eax4m6ybYEUoHWlTy2XOU32767mlMkg==", + "dependencies": { + "@emotion/is-prop-valid": "^1.2.1", + "@emotion/unitless": "^0.8.0", + "@types/stylis": "^4.0.2", + "css-to-react-native": "^3.2.0", + "csstype": "^3.1.2", + "postcss": "^8.4.31", + "shallowequal": "^1.1.0", + "stylis": "^4.3.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/styled-components" + }, + "peerDependencies": { + "react": ">= 16.8.0", + "react-dom": ">= 16.8.0" } }, - "postcss-value-parser": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" - }, - "preact": { - "version": "8.5.3", - "resolved": "https://registry.npmjs.org/preact/-/preact-8.5.3.tgz", - "integrity": "sha512-O3kKP+1YdgqHOFsZF2a9JVdtqD+RPzCQc3rP+Ualf7V6rmRDchZ9MJbiGTT7LuyqFKZqlHSOyO/oMFmI2lVTsw==" + "node_modules/stylis": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.0.tgz", + "integrity": "sha512-E87pIogpwUsUwXw7dNyU4QDjdgVMy52m+XEOPEKUn161cCzWjjhPSQhByfd1CcNvrOLnXQ6OnnZDwnJrz/Z4YQ==" }, - "punycode": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", - "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" + "node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } }, - "randombytes": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", - "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", - "requires": { - "safe-buffer": "^5.1.0" + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "requires": { - "picomatch": "^2.2.1" + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true + }, + "node_modules/tapable": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", + "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", + "engines": { + "node": ">=6" } }, - "rechoir": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.7.1.tgz", - "integrity": "sha512-/njmZ8s1wVeR6pjTZ+0nCnv8SpZNRMT2D1RLOJQESlYFDBvwpTA4KWJpZ+sBJ4+vhjILRcK7JIFdGCdxEAAitg==", - "dev": true, - "requires": { - "resolve": "^1.9.0" + "node_modules/terser": { + "version": "5.24.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.24.0.tgz", + "integrity": "sha512-ZpGR4Hy3+wBEzVEnHvstMvqpD/nABNelQn/z2r0fjVWGQsN3bpOLzQlqDxmb4CDZnXq5lpjnQ+mHQLAOpfM5iw==", + "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" } }, - "resolve": { - "version": "1.22.1", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz", - "integrity": "sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==", - "dev": true, - "requires": { - "is-core-module": "^2.9.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" + "node_modules/terser-webpack-plugin": { + "version": "5.3.9", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.9.tgz", + "integrity": "sha512-ZuXsqE07EcggTWQjXUj+Aot/OMcD0bMKGgF63f7UxYcu5/AJF53aIpK1YoP5xR9l6s/Hy2b+t1AM0bLNPRuhwA==", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.17", + "jest-worker": "^27.4.5", + "schema-utils": "^3.1.1", + "serialize-javascript": "^6.0.1", + "terser": "^5.16.8" + }, + "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 + } } }, - "resolve-cwd": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", - "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", "dev": true, - "requires": { - "resolve-from": "^5.0.0" + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" } }, - "resolve-from": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", - "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "node_modules/tmpl": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", + "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", "dev": true }, - "safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" - }, - "sass": { - "version": "1.56.1", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.56.1.tgz", - "integrity": "sha512-VpEyKpyBPCxE7qGDtOcdJ6fFbcpOM+Emu7uZLxVrkX8KVU/Dp5UF7WLvzqRuUhB6mqqQt1xffLoG+AndxTZrCQ==", - "requires": { - "chokidar": ">=3.0.0 <4.0.0", - "immutable": "^4.0.0", - "source-map-js": ">=0.6.2 <2.0.0" + "node_modules/to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", + "engines": { + "node": ">=4" } }, - "sass-loader": { - "version": "12.6.0", - "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-12.6.0.tgz", - "integrity": "sha512-oLTaH0YCtX4cfnJZxKSLAyglED0naiYfNG1iXfU5w1LNZ+ukoA5DtyDIN5zmKVZwYNJP4KRc5Y3hkWga+7tYfA==", - "requires": { - "klona": "^2.0.4", - "neo-async": "^2.6.2" + "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" } }, - "schema-utils": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.1.tgz", - "integrity": "sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw==", - "requires": { - "@types/json-schema": "^7.0.8", - "ajv": "^6.12.5", - "ajv-keywords": "^3.5.2" + "node_modules/tough-cookie": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.3.tgz", + "integrity": "sha512-aX/y5pVRkfRnfmuX+OdbSdXvPe6ieKX/G2s7e98f4poJHnqH3281gDPm/metm6E/WRamfx7WC4HUqkWHfQHprw==", + "dev": true, + "dependencies": { + "psl": "^1.1.33", + "punycode": "^2.1.1", + "universalify": "^0.2.0", + "url-parse": "^1.5.3" + }, + "engines": { + "node": ">=6" } }, - "semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", - "requires": { - "lru-cache": "^6.0.0" + "node_modules/trough": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/trough/-/trough-1.0.5.tgz", + "integrity": "sha512-rvuRbTarPXmMb79SmzEp8aqXNKcK+y0XaB298IXueQ8I2PsrATcPBCSPyK/dDNa2iWOhKlfNnOjdAOTBU/nkFA==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" } }, - "serialize-javascript": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz", - "integrity": "sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==", - "requires": { - "randombytes": "^2.1.0" - } + "node_modules/tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" }, - "shallow-clone": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", - "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==", + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", "dev": true, - "requires": { - "kind-of": "^6.0.2" + "engines": { + "node": ">=4" } }, - "shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", "dev": true, - "requires": { - "shebang-regex": "^3.0.0" + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true + "node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" }, - "source-list-map": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.1.tgz", - "integrity": "sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw==" + "node_modules/unicode-canonical-property-names-ecmascript": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz", + "integrity": "sha512-yY5PpDlfVIU5+y/BSCxAJRBIS1Zc2dDG3Ujq+sR0U+JjUevW2JhocOF+soROYDSaAezOzOKuyyixhD6mBknSmQ==", + "dev": true, + "engines": { + "node": ">=4" + } }, - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" + "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" + } }, - "source-map-js": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", - "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==" + "node_modules/unicode-match-property-value-ecmascript": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.1.0.tgz", + "integrity": "sha512-qxkjQt6qjg/mYscYMC0XKRn3Rh0wFPlfxB0xkt9CfyTvpX1Ra0+rAmdX2QyAobptSEvuy4RtpPRui6XkV+8wjA==", + "dev": true, + "engines": { + "node": ">=4" + } }, - "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==", - "requires": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" + "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" } }, - "strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "requires": { - "ansi-regex": "^5.0.1" - }, + "node_modules/unified": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/unified/-/unified-9.2.2.tgz", + "integrity": "sha512-Sg7j110mtefBD+qunSLO1lqOEKdrwBFBrR6Qd8f4uwkhWNlbkaqwHse6e7QvD3AP/MNoJdEDLaf8OxYyoWgorQ==", "dependencies": { - "ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==" - } + "bail": "^1.0.0", + "extend": "^3.0.0", + "is-buffer": "^2.0.0", + "is-plain-obj": "^2.0.0", + "trough": "^1.0.0", + "vfile": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "style-loader": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-3.3.1.tgz", - "integrity": "sha512-GPcQ+LDJbrcxHORTRes6Jy2sfvK2kS6hpSfI/fXhPt+spVzxF6LJ1dHLN9zIGmVaaP044YKaIatFaufENRiDoQ==", - "requires": {} - }, - "supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", - "requires": { - "has-flag": "^4.0.0" + "node_modules/unist-util-is": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-4.1.0.tgz", + "integrity": "sha512-ZOQSsnce92GrxSqlnEEseX0gi7GH9zTJZ0p9dtu87WRb/37mMPO2Ilx1s/t9vBHrFhbgweUwb+t7cIn5dxPhZg==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "supports-preserve-symlinks-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "dev": true + "node_modules/unist-util-stringify-position": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-2.0.3.tgz", + "integrity": "sha512-3faScn5I+hy9VleOq/qNbAd6pAx7iH5jYBMS9I1HgQVijz/4mv5Bvw5iw1sC/90CODiKo81G/ps8AJrISn687g==", + "dependencies": { + "@types/unist": "^2.0.2" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } }, - "tapable": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", - "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==" - }, - "terser": { - "version": "5.15.1", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.15.1.tgz", - "integrity": "sha512-K1faMUvpm/FBxjBXud0LWVAGxmvoPbZbfTCYbSgaaYQaIXI3/TdI7a7ZGA73Zrou6Q8Zmz3oeUTsp/dj+ag2Xw==", - "requires": { - "@jridgewell/source-map": "^0.3.2", - "acorn": "^8.5.0", - "commander": "^2.20.0", - "source-map-support": "~0.5.20" + "node_modules/unist-util-visit": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-2.0.3.tgz", + "integrity": "sha512-iJ4/RczbJMkD0712mGktuGpm/U4By4FfDonL7N/9tATGIF4imikjOuagyMY53tnZq3NP6BcmlrHhEKAfGWjh7Q==", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-is": "^4.0.0", + "unist-util-visit-parents": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "terser-webpack-plugin": { - "version": "5.3.6", - "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.6.tgz", - "integrity": "sha512-kfLFk+PoLUQIbLmB1+PZDMRSZS99Mp+/MHqDNmMA6tOItzRt+Npe3E+fsMs5mfcM0wCtrrdU387UnV+vnSffXQ==", - "requires": { - "@jridgewell/trace-mapping": "^0.3.14", - "jest-worker": "^27.4.5", - "schema-utils": "^3.1.1", - "serialize-javascript": "^6.0.0", - "terser": "^5.14.1" + "node_modules/unist-util-visit-parents": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-1.1.2.tgz", + "integrity": "sha512-yvo+MMLjEwdc3RhhPYSximset7rwjMrdt9E41Smmvg25UQIenzrN83cRnF1JMzoMi9zZOQeYXHSDf7p+IQkW3Q==" + }, + "node_modules/unist-util-visit/node_modules/unist-util-visit-parents": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-3.1.1.tgz", + "integrity": "sha512-1KROIZWo6bcMrZEwiH2UrXDyalAa0uqzWCxCJj6lPOvTve2WkfgCytoDTPaMnodXh1WrXOq0haVYHj99ynJlsg==", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-is": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "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==", - "requires": { - "is-number": "^7.0.0" + "node_modules/universalify": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", + "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", + "dev": true, + "engines": { + "node": ">= 4.0.0" } }, - "update-browserslist-db": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.10.tgz", - "integrity": "sha512-OztqDenkfFkbSG+tRxBeAnCVPckDBcvibKd35yDONx6OU8N7sqgwc7rCbkJ/WcYtVRZ4ba68d6byhC21GFh7sQ==", - "requires": { + "node_modules/update-browserslist-db": { + "version": "1.0.13", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz", + "integrity": "sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { "escalade": "^3.1.1", "picocolors": "^1.0.0" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" } }, - "uri-js": { + "node_modules/uri-js": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "requires": { + "dependencies": { "punycode": "^2.1.0" } }, - "util-deprecate": { + "node_modules/url-parse": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", + "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", + "dev": true, + "dependencies": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, + "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" }, - "watchpack": { + "node_modules/v8-to-istanbul": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.2.0.tgz", + "integrity": "sha512-/EH/sDgxU2eGxajKdwLCDmQ4FWq+kpi3uCmBGpw1xJtnAxEjlD8j8PEiGWpCIMIs3ciNAgH0d3TTJiUkYzyZjA==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^2.0.0" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/vfile": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-4.2.1.tgz", + "integrity": "sha512-O6AE4OskCG5S1emQ/4gl8zK586RqA3srz3nfK/Viy0UPToBc5Trp9BVFb1u0CjsKrAWwnpr4ifM/KBXPWwJbCA==", + "dependencies": { + "@types/unist": "^2.0.0", + "is-buffer": "^2.0.0", + "unist-util-stringify-position": "^2.0.0", + "vfile-message": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-message": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-2.0.4.tgz", + "integrity": "sha512-DjssxRGkMvifUOJre00juHoP9DPWuzjxKuMDrhNbk2TdaYYBNMStsNhEOt3idrtI12VQYM/1+iM0KOzXi4pxwQ==", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-stringify-position": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/walker": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", + "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", + "dev": true, + "dependencies": { + "makeerror": "1.0.12" + } + }, + "node_modules/watchpack": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz", "integrity": "sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==", - "requires": { + "dependencies": { "glob-to-regexp": "^0.4.1", "graceful-fs": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "dev": true, + "engines": { + "node": ">=12" } }, - "webpack": { - "version": "5.76.0", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.76.0.tgz", - "integrity": "sha512-l5sOdYBDunyf72HW8dF23rFtWq/7Zgvt/9ftMof71E/yUb1YLOBmTgA2K4vQthB3kotMrSj609txVE0dnr2fjA==", - "requires": { + "node_modules/webpack": { + "version": "5.89.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.89.0.tgz", + "integrity": "sha512-qyfIC10pOr70V+jkmud8tMfajraGCZMBWJtrmuBymQKCrLTRejBI8STDp1MCyZu/QTdZSeacCQYpYNQVOzX5kw==", + "dependencies": { "@types/eslint-scope": "^3.7.3", - "@types/estree": "^0.0.51", - "@webassemblyjs/ast": "1.11.1", - "@webassemblyjs/wasm-edit": "1.11.1", - "@webassemblyjs/wasm-parser": "1.11.1", + "@types/estree": "^1.0.0", + "@webassemblyjs/ast": "^1.11.5", + "@webassemblyjs/wasm-edit": "^1.11.5", + "@webassemblyjs/wasm-parser": "^1.11.5", "acorn": "^8.7.1", - "acorn-import-assertions": "^1.7.6", + "acorn-import-assertions": "^1.9.0", "browserslist": "^4.14.5", "chrome-trace-event": "^1.0.2", - "enhanced-resolve": "^5.10.0", - "es-module-lexer": "^0.9.0", + "enhanced-resolve": "^5.15.0", + "es-module-lexer": "^1.2.1", "eslint-scope": "5.1.1", "events": "^3.2.0", "glob-to-regexp": "^0.4.1", @@ -3150,25 +10900,33 @@ "loader-runner": "^4.2.0", "mime-types": "^2.1.27", "neo-async": "^2.6.2", - "schema-utils": "^3.1.0", + "schema-utils": "^3.2.0", "tapable": "^2.1.1", - "terser-webpack-plugin": "^5.1.3", + "terser-webpack-plugin": "^5.3.7", "watchpack": "^2.4.0", "webpack-sources": "^3.2.3" }, - "dependencies": { - "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==" + "bin": { + "webpack": "bin/webpack.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependenciesMeta": { + "webpack-cli": { + "optional": true } } }, - "webpack-bundle-tracker": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/webpack-bundle-tracker/-/webpack-bundle-tracker-1.7.0.tgz", - "integrity": "sha512-CwdFpeLcc4uBurgmtszCHW6ISJ5RN70jvGWnvUG/7LQS1gmv2g6IdYw9A8DvT4rydHzWnRFwqVsx1hN1IebkQA==", - "requires": { + "node_modules/webpack-bundle-tracker": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/webpack-bundle-tracker/-/webpack-bundle-tracker-1.8.1.tgz", + "integrity": "sha512-X1qtXG4ue92gjWQO2VhLVq8HDEf9GzUWE0OQyAQObVEZsFB1SUtSQ7o47agF5WZIaHfJUTKak4jEErU0gzoPcQ==", + "dependencies": { "lodash.assign": "^4.2.0", "lodash.defaults": "^4.2.0", "lodash.foreach": "^4.5.0", @@ -3178,12 +10936,12 @@ "strip-ansi": "^6.0.0" } }, - "webpack-cli": { + "node_modules/webpack-cli": { "version": "4.10.0", "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-4.10.0.tgz", "integrity": "sha512-NLhDfH/h4O6UOy+0LSso42xvYypClINuMNBVVzX4vX98TmTaTUxwRbXdhucbFMd2qLaCTcLq/PdYrvi8onw90w==", "dev": true, - "requires": { + "dependencies": { "@discoveryjs/json-ext": "^0.5.0", "@webpack-cli/configtest": "^1.2.0", "@webpack-cli/info": "^1.5.0", @@ -3197,53 +10955,300 @@ "rechoir": "^0.7.0", "webpack-merge": "^5.7.3" }, - "dependencies": { - "commander": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", - "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", - "dev": true + "bin": { + "webpack-cli": "bin/cli.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "4.x.x || 5.x.x" + }, + "peerDependenciesMeta": { + "@webpack-cli/generators": { + "optional": true + }, + "@webpack-cli/migrate": { + "optional": true + }, + "webpack-bundle-analyzer": { + "optional": true + }, + "webpack-dev-server": { + "optional": true } } }, - "webpack-merge": { - "version": "5.8.0", - "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.8.0.tgz", - "integrity": "sha512-/SaI7xY0831XwP6kzuwhKWVKDP9t1QY1h65lAFLbZqMPIuYcD9QAW4u9STIbU9kaJbPBB/geU/gLr1wDjOhQ+Q==", + "node_modules/webpack-cli/node_modules/commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "dev": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/webpack-merge": { + "version": "5.10.0", + "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.10.0.tgz", + "integrity": "sha512-+4zXKdx7UnO+1jaN4l2lHVD+mFvnlZQP/6ljaJVb4SZiwIKeUnrT5l0gkT8z+n4hKpC+jpOv6O9R+gLtag7pSA==", "dev": true, - "requires": { + "dependencies": { "clone-deep": "^4.0.1", + "flat": "^5.0.2", "wildcard": "^2.0.0" + }, + "engines": { + "node": ">=10.0.0" } }, - "webpack-sources": { + "node_modules/webpack-sources": { "version": "1.4.3", "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-1.4.3.tgz", "integrity": "sha512-lgTS3Xhv1lCOKo7SA5TjKXMjpSM4sBjNV5+q2bqesbSPs5FjGmU6jjtBSkX9b4qW87vDIsCIlUPOEhbZrMdjeQ==", - "requires": { + "dependencies": { "source-list-map": "^2.0.0", "source-map": "~0.6.1" } }, - "which": { + "node_modules/webpack/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==", + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", "dev": true, - "requires": { + "dependencies": { "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" } }, - "wildcard": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.0.tgz", - "integrity": "sha512-JcKqAHLPxcdb9KM49dufGXn2x3ssnfjbcaQdLlfZsL9rH9wgDQjUtDxbo8NE0F6SFvydeu1VhZe7hZuHsB2/pw==", + "node_modules/which-boxed-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", + "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", + "dev": true, + "dependencies": { + "is-bigint": "^1.0.1", + "is-boolean-object": "^1.1.0", + "is-number-object": "^1.0.4", + "is-string": "^1.0.5", + "is-symbol": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-collection": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.1.tgz", + "integrity": "sha512-W8xeTUwaln8i3K/cY1nGXzdnVZlidBcagyNFtBdD5kxnb4TvGKR7FfSIS3mYpwWS1QUCutfKz8IY8RjftB0+1A==", + "dev": true, + "dependencies": { + "is-map": "^2.0.1", + "is-set": "^2.0.1", + "is-weakmap": "^2.0.1", + "is-weakset": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.13.tgz", + "integrity": "sha512-P5Nra0qjSncduVPEAr7xhoF5guty49ArDTwzJ/yNuPIbZppyRxFQsRCWrocxIY+CnMVG+qfbU2FmDKyvSGClow==", + "dev": true, + "dependencies": { + "available-typed-arrays": "^1.0.5", + "call-bind": "^1.0.4", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/wildcard": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.1.tgz", + "integrity": "sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==", "dev": true }, - "yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + "node_modules/wrap-ansi": { + "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": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi/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/wrap-ansi/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/wrap-ansi/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/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true + }, + "node_modules/write-file-atomic": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", + "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", + "dev": true, + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.7" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/ws": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.16.0.tgz", + "integrity": "sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ==", + "dev": true, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "engines": { + "node": ">=0.4" + } + }, + "node_modules/y18n": { + "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": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==" + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/yocto-queue": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.0.0.tgz", + "integrity": "sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==", + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } } } } diff --git a/package.json b/package.json index 55c4647b2..d71a1ad96 100644 --- a/package.json +++ b/package.json @@ -11,18 +11,25 @@ "npm": "^10.3.0" }, "dependencies": { + "@babel/core": "^7.23.2", + "@types/styled-components": "^5.1.29", "accessible-autocomplete": "^2.0.3", "ansi-regex": "^6.0.1", + "babel-loader": "^9.1.3", "chart.js": "^3.9.1", "chartjs-adapter-moment": "^1.0.0", "css-loader": "^5.2.6", "file-loader": "^6.2.0", "govuk-frontend": "^3.13.0", + "govuk-react": "^0.10.6", "mini-css-extract-plugin": "^1.6.0", "moment": "^2.29.4", + "react": "^18.2.0", + "react-dom": "^18.2.0", "sass": "^1.38.2", "sass-loader": "^12.1.0", "style-loader": "^3.0.0", + "styled-components": "^6.1.0", "webpack": "^5.76.0", "webpack-bundle-tracker": "^1.1.0", "webpack-cli": "^4.7.2" @@ -32,9 +39,18 @@ "build": "npx webpack-cli --config webpack.config.js --stats-children", "clean": "rm -f ./run/static/webpack_bundles/*", "heroku-prebuild": "", - "heroku-postbuild": "npm run build" + "heroku-postbuild": "npm run build", + "test": "jest" }, "devDependencies": { + "@babel/preset-env": "^7.23.7", + "@babel/preset-react": "^7.23.3", + "@testing-library/jest-dom": "^6.2.1", + "@testing-library/react": "^14.1.2", + "babel-jest": "^29.7.0", + "jest": "^29.7.0", + "jest-environment-jsdom": "^29.7.0", + "react-test-renderer": "^18.2.0", "webpack-cli": "^4.7.2" } -} +} \ No newline at end of file diff --git a/pii-secret-exclude.txt b/pii-secret-exclude.txt index 80d08ba2c..4fdcdec14 100644 --- a/pii-secret-exclude.txt +++ b/pii-secret-exclude.txt @@ -25,4 +25,5 @@ common/jinja2/common/500.jinja settings/envs/docker.env Dockerfile common/jinja2/common/accessibility.jinja +common/static/common/js/components/QuotaOriginFormset/tests/__snapshots__/index.test.js.snap common/migrations/0001_initial.py diff --git a/quotas/constants.py b/quotas/constants.py index 6a3621f56..95463a779 100644 --- a/quotas/constants.py +++ b/quotas/constants.py @@ -1 +1,3 @@ QUOTA_ORIGIN_EXCLUSIONS_FORMSET_PREFIX = "quota-origin-exclusions-formset" +QUOTA_ORIGINS_FORMSET_PREFIX = "origins" +QUOTA_EXCLUSIONS_FORMSET_PREFIX = "exclusions" diff --git a/quotas/forms.py b/quotas/forms.py index 74dbbc316..ccb296efc 100644 --- a/quotas/forms.py +++ b/quotas/forms.py @@ -9,6 +9,7 @@ from crispy_forms_gds.layout import Size from crispy_forms_gds.layout import Submit from django import forms +from django.core.exceptions import NON_FIELD_ERRORS from django.core.exceptions import ValidationError from django.template.loader import render_to_string from django.urls import reverse_lazy @@ -25,7 +26,9 @@ from measures.models import MeasurementUnit from quotas import models from quotas import validators +from quotas.constants import QUOTA_EXCLUSIONS_FORMSET_PREFIX from quotas.constants import QUOTA_ORIGIN_EXCLUSIONS_FORMSET_PREFIX +from quotas.constants import QUOTA_ORIGINS_FORMSET_PREFIX CATEGORY_HELP_TEXT = "Categories are required for the TAP database but will not appear as a TARIC3 object in your workbasket" SAFEGUARD_HELP_TEXT = ( @@ -132,24 +135,45 @@ class Meta: category = forms.ChoiceField( label="", - choices=[], # set in __init__ + choices=validators.QuotaCategory.choices, error_messages={"invalid_choice": "Please select a valid category"}, ) + def clean_category(self): + value = self.cleaned_data.get("category") + # the widget is disabled and data is not submitted. fall back to instance value + if not value: + return self.instance.category + if ( + self.instance.category == validators.QuotaCategory.SAFEGUARD + and value != validators.QuotaCategory.SAFEGUARD + ): + raise ValidationError(SAFEGUARD_HELP_TEXT) + return value + def __init__(self, *args, **kwargs): + self.request = kwargs.pop("request") + self.geo_area_options = kwargs.pop("geo_area_options") + self.existing_origins = kwargs.pop("existing_origins") super().__init__(*args, **kwargs) self.init_fields() self.set_initial_data(*args, **kwargs) - self.init_layout() + self.init_layout(self.request) def set_initial_data(self, *args, **kwargs): self.fields["category"].initial = self.instance.category def init_fields(self): if self.instance.category == validators.QuotaCategory.SAFEGUARD: + self.fields["category"].required = False self.fields["category"].widget = forms.Select( + choices=[ + ( + validators.QuotaCategory.SAFEGUARD.value, + validators.QuotaCategory.SAFEGUARD.label, + ), + ], attrs={"disabled": True}, - choices=validators.QuotaCategory.choices, ) self.fields["category"].help_text = SAFEGUARD_HELP_TEXT else: @@ -158,7 +182,123 @@ def init_fields(self): self.fields["start_date"].help_text = START_DATE_HELP_TEXT - def init_layout(self): + def get_origins_initial(self): + initial = [ + { + "id": o.pk, # unique identifier used by react + "pk": o.pk, + "exclusions": [ + {"pk": e.pk, "id": e.excluded_geographical_area.pk} + for e in o.quotaordernumberoriginexclusion_set.current() + ], + "geographical_area": o.geographical_area.pk, + "start_date_0": o.valid_between.lower.day, + "start_date_1": o.valid_between.lower.month, + "start_date_2": o.valid_between.lower.year, + "end_date_0": o.valid_between.upper.day + if o.valid_between.upper + else "", + "end_date_1": o.valid_between.upper.month + if o.valid_between.upper + else "", + "end_date_2": o.valid_between.upper.year + if o.valid_between.upper + else "", + } + for o in self.existing_origins + ] + # if we just submitted the form, overwrite initial with submitted data + # this prevents newly added origin data being cleared if the form does not pass validation + if self.data.get("submit"): + new_data = unprefix_formset_data( + QUOTA_ORIGINS_FORMSET_PREFIX, + self.data.copy(), + ) + initial = new_data + + return initial + + def add_extra_error(self, field, error): + """ + A modification of Django's add_error method that allows us to add data + to self._errors under custom keys that are not field names or + NON_FIELD_ERRORS. + + Used to pass errors to the React form. + """ + if not isinstance(error, ValidationError): + error = ValidationError(error) + + if hasattr(error, "error_dict"): + if field is not None: + raise TypeError( + "The argument `field` must be `None` when the `error` " + "argument contains errors for multiple fields.", + ) + else: + error = error.error_dict + else: + error = {field or NON_FIELD_ERRORS: error.error_list} + + for field, error_list in error.items(): + if field not in self.errors: + self._errors[field] = self.error_class() + self._errors[field].extend(error_list) + if field in self.cleaned_data: + del self.cleaned_data[field] + + def clean(self): + # unprefix origins formset + submitted_data = unprefix_formset_data( + QUOTA_ORIGINS_FORMSET_PREFIX, + self.data.copy(), + ) + # for each origin, unprefix exclusions formset + for i, origin_data in enumerate(submitted_data): + exclusions = unprefix_formset_data( + QUOTA_EXCLUSIONS_FORMSET_PREFIX, + origin_data.copy(), + ) + submitted_data[i]["exclusions"] = exclusions + + self.cleaned_data["origins"] = [] + + for i, origin_data in enumerate(submitted_data): + # instantiate a form per origin data to do validation + origin_form = QuotaOrderNumberOriginUpdateReactForm( + data=origin_data, + initial=origin_data, + ) + + cleaned_exclusions = [] + + for exclusion in origin_data["exclusions"]: + exclusion_form = QuotaOriginExclusionsReactForm( + data=exclusion, + initial=exclusion, + ) + if not exclusion_form.is_valid(): + for field, e in exclusion_form.errors.as_data().items(): + self.add_extra_error( + f"{QUOTA_ORIGINS_FORMSET_PREFIX}-{i}-{field}", + e, + ) + else: + cleaned_exclusions.append(exclusion_form.cleaned_data) + + if not origin_form.is_valid(): + for field, e in origin_form.errors.as_data().items(): + self.add_extra_error( + f"{QUOTA_ORIGINS_FORMSET_PREFIX}-{i}-{field}", + e, + ) + else: + origin_form.cleaned_data["exclusions"] = cleaned_exclusions + self.cleaned_data["origins"].append(origin_form.cleaned_data) + + return super().clean() + + def init_layout(self, request): self.helper = FormHelper(self) self.helper.label_size = Size.SMALL self.helper.legend_size = Size.SMALL @@ -167,6 +307,10 @@ def init_layout(self): "includes/quotas/quota-edit-origins.jinja", { "object": self.instance, + "request": request, + "geo_area_options": self.geo_area_options, + "origins_initial": self.get_origins_initial(), + "errors": self.errors, }, ) @@ -188,9 +332,8 @@ def init_layout(self): HTML(origins_html), ), ), - css_class="govuk-grid-column-two-thirds", ), - css_class="govuk-grid-row", + css_class="govuk-width-!-two-thirds", ), Submit( "submit", @@ -619,7 +762,7 @@ def init_layout(self): AccordionSection( "Volume", HTML.p( - "The initial volume is the legal balance applied to the definition period.

    The current volume is the starting balance for the quota." + "The initial volume is the legal balance applied to the definition period.

    The current volume is the starting balance for the quota.", ), "initial_volume", "volume", @@ -673,3 +816,31 @@ def get_geo_area_initial(self): initial[QUOTA_ORIGIN_EXCLUSIONS_FORMSET_PREFIX] = initial_exclusions return initial + + +class QuotaOrderNumberOriginUpdateReactForm(QuotaOrderNumberOriginUpdateForm): + """Used only to validate data sent from the quota edit React form.""" + + pk = forms.IntegerField(required=False) + + +class QuotaOriginExclusionsReactForm(forms.Form): + """Used only to validate data sent from the quota edit React form.""" + + pk = forms.IntegerField(required=False) + # field name is different to match the react form + geographical_area = forms.ModelChoiceField( + label="", + queryset=GeographicalArea.objects.all(), + help_text="Select a country to be excluded:", + required=False, + ) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.fields["geographical_area"].queryset = ( + GeographicalArea.objects.current() + .with_latest_description() + .as_at_today_and_beyond() + .order_by("description") + ) diff --git a/quotas/jinja2/includes/quotas/quota-edit-origins.jinja b/quotas/jinja2/includes/quotas/quota-edit-origins.jinja index ac47709c0..4ac7063b8 100644 --- a/quotas/jinja2/includes/quotas/quota-edit-origins.jinja +++ b/quotas/jinja2/includes/quotas/quota-edit-origins.jinja @@ -1,16 +1,36 @@ {% from "quota-origins/macros/origin_display.jinja" import origin_display %} -
    - {% for origin in object.quotaordernumberorigin_set.current().with_latest_geo_area_description()%} -
    - {{ origin_display(origin) }} +
    +
    + {% for origin in object.quotaordernumberorigin_set.current().with_latest_geo_area_description() %} +
    + {{ origin_display(origin) }} +
    + + {% endfor %}
    - - {% endfor %} + + Create new origin +
    - - Create new origin - \ No newline at end of file + diff --git a/quotas/jinja2/includes/quotas/tabs/core_data.jinja b/quotas/jinja2/includes/quotas/tabs/core_data.jinja index ba9ddac0d..4df963503 100644 --- a/quotas/jinja2/includes/quotas/tabs/core_data.jinja +++ b/quotas/jinja2/includes/quotas/tabs/core_data.jinja @@ -1,7 +1,7 @@ {% from "quota-origins/macros/origin_display.jinja" import origin_display %} {% set origins %} - {% for origin in object.quotaordernumberorigin_set.current() %} + {% for origin in object.quotaordernumberorigin_set.current().with_latest_geo_area_description() %} {{ origin_display(origin) }} {% endfor %} {% endset %} @@ -18,7 +18,7 @@ }, { "key": {"text": "Origins"}, - "value": {"text": origins if object.origins.all() else "-"}, + "value": {"text": origins if object.quotaordernumberorigin_set.current().with_latest_geo_area_description() else "-"}, "actions": {"items": []} }, { diff --git a/quotas/jinja2/quota-origins/macros/origin_display.jinja b/quotas/jinja2/quota-origins/macros/origin_display.jinja index 6f94a83fb..26e17200a 100644 --- a/quotas/jinja2/quota-origins/macros/origin_display.jinja +++ b/quotas/jinja2/quota-origins/macros/origin_display.jinja @@ -13,7 +13,7 @@ {% endset %} {% set geo_area_summary -%} {{ geo_area_name }} - {% if object.geographical_exclusions %} (with exclusions){% endif %} + {% if object.quotaordernumberoriginexclusion_set.current() %} (with exclusions){% endif %} {% endset %} {% set origin_link -%} {{ geo_area_name }} diff --git a/quotas/models.py b/quotas/models.py index 1ce43855e..90b709796 100644 --- a/quotas/models.py +++ b/quotas/models.py @@ -112,23 +112,6 @@ def geographical_exclusion_descriptions(self): return sorted(descriptions) - @property - def geographical_exclusions(self): - origin_ids = list( - self.quotaordernumberorigin_set.current().values_list( - "pk", - flat=True, - ), - ) - exclusions = [ - e.excluded_geographical_area - for e in QuotaOrderNumberOriginExclusion.objects.current().filter( - origin_id__in=origin_ids, - ) - ] - - return exclusions - class Meta: verbose_name = "quota" diff --git a/quotas/tests/test_forms.py b/quotas/tests/test_forms.py index 18babdd09..0472e4958 100644 --- a/quotas/tests/test_forms.py +++ b/quotas/tests/test_forms.py @@ -1,17 +1,22 @@ +import datetime + import pytest from bs4 import BeautifulSoup +from django.core.exceptions import ValidationError from django.urls import reverse from common.models.transactions import Transaction from common.models.utils import override_current_transaction from common.tests import factories +from common.util import TaricDateRange +from geo_areas.models import GeographicalArea from quotas import forms from quotas import validators pytestmark = pytest.mark.django_db -def test_update_quota_form_safeguard_invalid(): +def test_update_quota_form_safeguard_invalid(session_request_with_workbasket): """When a QuotaOrderNumber with the category safeguard is edited the category cannot be changed.""" quota = factories.QuotaOrderNumberFactory.create( @@ -24,9 +29,41 @@ def test_update_quota_form_safeguard_invalid(): "start_date_2": 2000, } with override_current_transaction(quota.transaction): - form = forms.QuotaUpdateForm(data=data, instance=quota, initial={}) + form = forms.QuotaUpdateForm( + data=data, + instance=quota, + request=session_request_with_workbasket, + initial={}, + geo_area_options=[], + existing_origins=[], + ) assert not form.is_valid() - assert "Please select a valid category" in form.errors["category"] + assert forms.SAFEGUARD_HELP_TEXT in form.errors["category"] + + +def test_update_quota_form_safeguard_disabled(session_request_with_workbasket): + """When a QuotaOrderNumber with the category safeguard is edited the + category cannot be changed.""" + quota = factories.QuotaOrderNumberFactory.create( + category=validators.QuotaCategory.SAFEGUARD, + ) + data = { + # if the widget is disabled the data is not submitted + "start_date_0": 1, + "start_date_1": 1, + "start_date_2": 2000, + } + with override_current_transaction(quota.transaction): + form = forms.QuotaUpdateForm( + data=data, + instance=quota, + request=session_request_with_workbasket, + initial={}, + geo_area_options=[], + existing_origins=[], + ) + assert form.is_valid() + assert quota.category == validators.QuotaCategory.SAFEGUARD def test_update_quota_form_safeguard_disabled(client_with_current_workbasket): @@ -125,3 +162,150 @@ def test_quota_definition_volume_validation(date_ranges): form.errors["__all__"][0] == "Current volume cannot be higher than initial volume" ) + + +def test_quota_update_react_form_cleaned_data(session_request_with_workbasket): + quota = factories.QuotaOrderNumberFactory.create() + geo_group = factories.GeoGroupFactory.create() + area_1 = factories.GeographicalMembershipFactory.create(geo_group=geo_group).member + area_2 = factories.GeographicalMembershipFactory.create(geo_group=geo_group).member + area_3 = factories.GeographicalMembershipFactory.create(geo_group=geo_group).member + # create geo area group with members to be excluded + data = { + "category": quota.category, + "start_date_0": 1, + "start_date_1": 1, + "start_date_2": 2000, + "end_date_0": 1, + "end_date_1": 1, + "end_date_2": 2010, + "origins-0-geographical_area": quota.quotaordernumberorigin_set.first().geographical_area.pk, + "origins-0-start_date_0": 1, + "origins-0-start_date_1": 1, + "origins-0-start_date_2": 2000, + "origins-0-end_date_0": 1, + "origins-0-end_date_1": 1, + "origins-0-end_date_2": 2010, + "origins-0-exclusions-0-geographical_area": area_1.pk, + "origins-0-exclusions-1-geographical_area": area_2.pk, + "submit": "Save", + } + + tx = Transaction.objects.last() + + with override_current_transaction(tx): + geo_area_options = ( + GeographicalArea.objects.all() + .prefetch_related("descriptions") + .with_latest_description() + .as_at_today_and_beyond() + .order_by("description") + ) + existing_origins = ( + quota.quotaordernumberorigin_set.current().with_latest_geo_area_description() + ) + form = forms.QuotaUpdateForm( + data=data, + instance=quota, + initial={}, + request=session_request_with_workbasket, + geo_area_options=geo_area_options, + existing_origins=existing_origins, + ) + assert form.is_valid() + + assert "valid_between" in form.cleaned_data["origins"][0].keys() + assert "exclusions" in form.cleaned_data["origins"][0].keys() + assert "geographical_area" in form.cleaned_data["origins"][0].keys() + + assert form.cleaned_data["origins"][0]["valid_between"] == TaricDateRange( + datetime.date(2000, 1, 1), + datetime.date(2010, 1, 1), + ) + + assert ( + form.cleaned_data["origins"][0]["geographical_area"] + == quota.quotaordernumberorigin_set.first().geographical_area + ) + + assert len(form.cleaned_data["origins"][0]["exclusions"]) == 2 + + +@pytest.mark.parametrize( + "field_name, error", + [ + ("some_field", "There is a problem"), + ("some_field", ValidationError("There is a problem")), + (None, {"some_field": "There is a problem"}), + ], +) +def test_quota_update_add_extra_error( + field_name, + error, + session_request_with_workbasket, +): + quota = factories.QuotaOrderNumberFactory.create() + data = { + "category": quota.category, + "start_date_0": 1, + "start_date_1": 1, + "start_date_2": 2000, + "submit": "Save", + } + with override_current_transaction(quota.transaction): + geo_area_options = ( + GeographicalArea.objects.all() + .prefetch_related("descriptions") + .with_latest_description() + .as_at_today_and_beyond() + .order_by("description") + ) + existing_origins = ( + quota.quotaordernumberorigin_set.current().with_latest_geo_area_description() + ) + form = forms.QuotaUpdateForm( + data=data, + instance=quota, + initial={}, + request=session_request_with_workbasket, + geo_area_options=geo_area_options, + existing_origins=existing_origins, + ) + form.add_extra_error(field_name, error) + + assert "There is a problem" in form.errors["some_field"] + + +def test_quota_update_add_extra_error_type_error(session_request_with_workbasket): + quota = factories.QuotaOrderNumberFactory.create() + data = { + "category": quota.category, + "start_date_0": 1, + "start_date_1": 1, + "start_date_2": 2000, + "submit": "Save", + } + with override_current_transaction(quota.transaction): + geo_area_options = ( + GeographicalArea.objects.all() + .prefetch_related("descriptions") + .with_latest_description() + .as_at_today_and_beyond() + .order_by("description") + ) + existing_origins = ( + quota.quotaordernumberorigin_set.current().with_latest_geo_area_description() + ) + form = forms.QuotaUpdateForm( + data=data, + instance=quota, + initial={}, + request=session_request_with_workbasket, + geo_area_options=geo_area_options, + existing_origins=existing_origins, + ) + with pytest.raises(TypeError): + form.add_extra_error( + "a_field", + {"some_field": "Error", "some_other_field": "Error"}, + ) diff --git a/quotas/tests/test_views.py b/quotas/tests/test_views.py index 09682e4b0..8aa839f19 100644 --- a/quotas/tests/test_views.py +++ b/quotas/tests/test_views.py @@ -1359,3 +1359,261 @@ def test_quota_order_number_create_success( assert ( soup.find("h1").text.strip() == f"Quota {quota.order_number} has been created" ) + + +def test_quota_update_existing_origins(client_with_current_workbasket, date_ranges): + quota = factories.QuotaOrderNumberFactory.create( + category=0, + valid_between=date_ranges.big_no_end, + ) + factories.QuotaOrderNumberOriginFactory.create(order_number=quota) + factories.QuotaOrderNumberOriginFactory.create(order_number=quota) + geo_area1 = factories.GeographicalAreaFactory.create() + geo_area2 = factories.GeographicalAreaFactory.create() + tx = geo_area2.transaction + ( + origin1, + origin2, + origin3, + ) = quota.quotaordernumberorigin_set.approved_up_to_transaction(tx) + + # sanity check + assert quota.quotaordernumberorigin_set.count() == 3 + + data = { + "start_date_0": date_ranges.big_no_end.lower.day, + "start_date_1": date_ranges.big_no_end.lower.month, + "start_date_2": date_ranges.big_no_end.lower.year, + "end_date_0": "", + "end_date_1": "", + "end_date_2": "", + "category": "1", # update category + # keep first origin data the same + "origins-0-pk": origin1.pk, + "origins-0-start_date_0": date_ranges.big_no_end.lower.day, + "origins-0-start_date_1": date_ranges.big_no_end.lower.month, + "origins-0-start_date_2": date_ranges.big_no_end.lower.year, + "origins-0-end_date_0": "", + "origins-0-end_date_1": "", + "origins-0-end_date_2": "", + "origins-0-geographical_area": origin1.geographical_area.pk, + # omit subform for origin2 to delete it + # change origin3 geo area + "origins-1-pk": origin3.pk, + "origins-1-start_date_0": date_ranges.big_no_end.lower.day, + "origins-1-start_date_1": date_ranges.big_no_end.lower.month, + "origins-1-start_date_2": date_ranges.big_no_end.lower.year, + "origins-1-end_date_0": "", + "origins-1-end_date_1": "", + "origins-1-end_date_2": "", + "origins-1-geographical_area": geo_area1.pk, + # add a new origin + "origins-2-pk": "", + "origins-2-start_date_0": date_ranges.big_no_end.lower.day, + "origins-2-start_date_1": date_ranges.big_no_end.lower.month, + "origins-2-start_date_2": date_ranges.big_no_end.lower.year, + "origins-2-end_date_0": "", + "origins-2-end_date_1": "", + "origins-2-end_date_2": "", + "origins-2-geographical_area": geo_area2.pk, + "submit": "Save", + } + url = reverse("quota-ui-edit", kwargs={"sid": quota.sid}) + response = client_with_current_workbasket.post(url, data) + + assert response.status_code == 302 + assert response.url == reverse("quota-ui-confirm-update", kwargs={"sid": quota.sid}) + + tx = Transaction.objects.last() + updated_quota = ( + models.QuotaOrderNumber.objects.approved_up_to_transaction(tx) + .filter(sid=quota.sid) + .first() + ) + assert updated_quota.category == 1 + assert updated_quota.valid_between == date_ranges.big_no_end + + assert updated_quota.origins.approved_up_to_transaction(tx).count() == 3 + assert {o.sid for o in updated_quota.origins.approved_up_to_transaction(tx)} == { + geo_area1.sid, + geo_area2.sid, + origin1.geographical_area.sid, + } + + +def test_quota_update_existing_origin_exclusions( + client_with_current_workbasket, + date_ranges, +): + # make a geo group with 3 member countries + country1 = factories.CountryFactory.create() + country2 = factories.CountryFactory.create() + country3 = factories.CountryFactory.create() + geo_group = factories.GeoGroupFactory.create() + membership1 = factories.GeographicalMembershipFactory.create( + member=country1, + geo_group=geo_group, + ) + membership2 = factories.GeographicalMembershipFactory.create( + member=country2, + geo_group=geo_group, + ) + membership3 = factories.GeographicalMembershipFactory.create( + member=country3, + geo_group=geo_group, + ) + + exclusion = factories.QuotaOrderNumberOriginExclusionFactory.create( + excluded_geographical_area=membership1.member, + ) + origin = exclusion.origin + quota = origin.order_number + + # sanity check + assert quota.quotaordernumberorigin_set.count() == 1 + + data = { + "start_date_0": date_ranges.big_no_end.lower.day, + "start_date_1": date_ranges.big_no_end.lower.month, + "start_date_2": date_ranges.big_no_end.lower.year, + "end_date_0": "", + "end_date_1": "", + "end_date_2": "", + "category": "1", # update category + "origins-0-pk": origin.pk, + "origins-0-start_date_0": date_ranges.big_no_end.lower.day, + "origins-0-start_date_1": date_ranges.big_no_end.lower.month, + "origins-0-start_date_2": date_ranges.big_no_end.lower.year, + "origins-0-end_date_0": "", + "origins-0-end_date_1": "", + "origins-0-end_date_2": "", + "origins-0-geographical_area": geo_group.pk, + # update existing + "origins-0-exclusions-0-pk": exclusion.pk, + "origins-0-exclusions-0-geographical_area": membership2.member.pk, + # add new + "origins-0-exclusions-1-pk": "", + "origins-0-exclusions-1-geographical_area": membership3.member.pk, + "submit": "Save", + } + url = reverse("quota-ui-edit", kwargs={"sid": quota.sid}) + response = client_with_current_workbasket.post(url, data) + + assert response.status_code == 302 + assert response.url == reverse("quota-ui-confirm-update", kwargs={"sid": quota.sid}) + + tx = Transaction.objects.last() + + updated_quota = ( + models.QuotaOrderNumber.objects.approved_up_to_transaction(tx) + .filter(sid=quota.sid) + .first() + ) + + assert updated_quota.origins.approved_up_to_transaction(tx).count() == 1 + updated_origin = ( + updated_quota.quotaordernumberorigin_set.approved_up_to_transaction(tx).first() + ) + assert { + e.excluded_geographical_area.sid + for e in updated_origin.quotaordernumberoriginexclusion_set.approved_up_to_transaction( + tx, + ) + } == { + membership2.member.sid, + membership3.member.sid, + } + + +def test_quota_update_existing_origin_exclusion_remove( + client_with_current_workbasket, + date_ranges, +): + country1 = factories.CountryFactory.create() + geo_group = factories.GeoGroupFactory.create() + membership1 = factories.GeographicalMembershipFactory.create( + member=country1, + geo_group=geo_group, + ) + + exclusion = factories.QuotaOrderNumberOriginExclusionFactory.create( + excluded_geographical_area=membership1.member, + ) + origin1 = exclusion.origin + quota = origin1.order_number + origin2 = factories.QuotaOrderNumberOriginFactory.create(order_number=quota) + + # sanity check + tx = Transaction.objects.last() + assert quota.quotaordernumberorigin_set.approved_up_to_transaction(tx).count() == 2 + assert ( + origin1.quotaordernumberoriginexclusion_set.approved_up_to_transaction( + tx, + ).count() + == 1 + ) + assert ( + origin2.quotaordernumberoriginexclusion_set.approved_up_to_transaction( + tx, + ).count() + == 0 + ) + + data = { + "start_date_0": date_ranges.big_no_end.lower.day, + "start_date_1": date_ranges.big_no_end.lower.month, + "start_date_2": date_ranges.big_no_end.lower.year, + "end_date_0": "", + "end_date_1": "", + "end_date_2": "", + "category": quota.category, + "origins-0-pk": origin1.pk, + "origins-0-start_date_0": date_ranges.big_no_end.lower.day, + "origins-0-start_date_1": date_ranges.big_no_end.lower.month, + "origins-0-start_date_2": date_ranges.big_no_end.lower.year, + "origins-0-end_date_0": "", + "origins-0-end_date_1": "", + "origins-0-end_date_2": "", + "origins-0-geographical_area": geo_group.pk, + # remove the first origin's exclusion + # remove the second origin + "submit": "Save", + } + + url = reverse("quota-ui-edit", kwargs={"sid": quota.sid}) + response = client_with_current_workbasket.post(url, data) + + assert response.status_code == 302 + assert response.url == reverse("quota-ui-confirm-update", kwargs={"sid": quota.sid}) + + last_tx = Transaction.objects.last() + + updated_quota = ( + models.QuotaOrderNumber.objects.approved_up_to_transaction(last_tx) + .filter(sid=quota.sid) + .first() + ) + + assert updated_quota.origins.approved_up_to_transaction(last_tx).count() == 1 + updated_origin = ( + updated_quota.quotaordernumberorigin_set.approved_up_to_transaction( + last_tx, + ).first() + ) + assert ( + updated_origin.quotaordernumberoriginexclusion_set.approved_up_to_transaction( + last_tx, + ).count() + == 0 + ) + # update quota + # update quota origin 1 + # delete quota origin 1 exclusion + # delete quota origin 2 + assert updated_origin.transaction.workbasket.tracked_models.count() == 4 + assert sorted( + [ + item.get_update_type_display() + for item in updated_origin.transaction.workbasket.tracked_models.all() + ], + ) == ["Delete", "Delete", "Update", "Update"] diff --git a/quotas/views.py b/quotas/views.py index 472cc78a6..18f823f6e 100644 --- a/quotas/views.py +++ b/quotas/views.py @@ -23,6 +23,7 @@ from common.views import TamatoListView from common.views import TrackedModelDetailMixin from common.views import TrackedModelDetailView +from geo_areas.models import GeographicalArea from geo_areas.utils import get_all_members_of_geo_groups from measures.models import Measure from quotas import business_rules @@ -120,7 +121,8 @@ class QuotaCreate(QuotaOrderNumberMixin, CreateTaricCreateView): def get_context_data(self, **kwargs): return super().get_context_data( - page_title="Create a new quota order number", **kwargs + page_title="Create a new quota order number", + **kwargs, ) @@ -295,27 +297,154 @@ class QuotaUpdateMixin( UpdateValidity, ) - @transaction.atomic - def get_result_object(self, form): - object = super().get_result_object(form) + def get_form_kwargs(self): + kwargs = super().get_form_kwargs() + kwargs["request"] = self.request + kwargs["geo_area_options"] = ( + GeographicalArea.objects.current() + .prefetch_related("descriptions") + .with_latest_description() + .as_at_today_and_beyond() + .order_by("description") + ) + kwargs[ + "existing_origins" + ] = ( + self.object.quotaordernumberorigin_set.current().with_latest_geo_area_description() + ) + return kwargs - existing_origins = ( - models.QuotaOrderNumberOrigin.objects.approved_up_to_transaction( - object.transaction, - ).filter( - order_number__sid=object.sid, + def update_origins(self, instance, form_origins): + existing_origin_pks = { + o.pk + for o in models.QuotaOrderNumberOrigin.objects.current().filter( + order_number__sid=instance.sid, ) + } + if form_origins: + submitted_origin_pks = {o["pk"] for o in form_origins} + deleted_origin_pks = existing_origin_pks.difference(submitted_origin_pks) + + for origin_pk in deleted_origin_pks: + origin = models.QuotaOrderNumberOrigin.objects.get( + pk=origin_pk, + ) + origin.new_version( + update_type=UpdateType.DELETE, + workbasket=WorkBasket.current(self.request), + transaction=instance.transaction, + ) + # Delete the exclusions as well + exclusions = models.QuotaOrderNumberOriginExclusion.objects.filter( + origin__pk=origin_pk, + ) + for exclusion in exclusions: + exclusion.new_version( + update_type=UpdateType.DELETE, + workbasket=WorkBasket.current(self.request), + transaction=instance.transaction, + ) + + for origin in form_origins: + # If origin exists + if origin.get("pk"): + existing_origin = models.QuotaOrderNumberOrigin.objects.get( + pk=origin.get("pk"), + ) + updated_origin = existing_origin.new_version( + workbasket=WorkBasket.current(self.request), + transaction=instance.transaction, + order_number=instance, + valid_between=origin["valid_between"], + geographical_area=origin["geographical_area"], + ) + + # It's a newly created origin + else: + updated_origin = models.QuotaOrderNumberOrigin.objects.create( + order_number=instance, + valid_between=origin["valid_between"], + geographical_area=origin["geographical_area"], + update_type=UpdateType.CREATE, + transaction=instance.transaction, + ) + + # whether it's edited or new we need to add/update exclusions + self.update_exclusions( + instance, + updated_origin, + origin.get("exclusions"), + ) + else: + # even if no changes were made we must update the existing + # origins to link to the updated order number + existing_origins = ( + models.QuotaOrderNumberOrigin.objects.approved_up_to_transaction( + instance.transaction, + ).filter( + order_number__sid=instance.sid, + ) + ) + for origin in existing_origins: + origin.new_version( + workbasket=WorkBasket.current(self.request), + transaction=instance.transaction, + order_number=instance, + ) + + def update_exclusions(self, quota, updated_origin, exclusions): + existing_exclusion_pks = { + e.pk + for e in models.QuotaOrderNumberOriginExclusion.objects.current().filter( + origin__sid=updated_origin.sid, + ) + } + submitted_exclusion_pks = {e["pk"] for e in exclusions} + deleted_exclusion_pks = existing_exclusion_pks.difference( + submitted_exclusion_pks, ) - # this will be needed even if origins have not been edited in the form - for origin in existing_origins: - origin.new_version( + for exclusion_pk in deleted_exclusion_pks: + exclusion = models.QuotaOrderNumberOriginExclusion.objects.get( + pk=exclusion_pk, + ) + exclusion.new_version( + update_type=UpdateType.DELETE, workbasket=WorkBasket.current(self.request), - transaction=object.transaction, - order_number=object, + transaction=quota.transaction, ) - return object + for exclusion in exclusions: + geo_area = GeographicalArea.objects.get(pk=exclusion["geographical_area"]) + if exclusion.get("pk"): + existing_exclusion = models.QuotaOrderNumberOriginExclusion.objects.get( + pk=exclusion.get("pk"), + ) + existing_exclusion.new_version( + workbasket=WorkBasket.current(self.request), + transaction=quota.transaction, + origin=updated_origin, + excluded_geographical_area=geo_area, + ) + + else: + models.QuotaOrderNumberOriginExclusion.objects.create( + origin=updated_origin, + excluded_geographical_area=geo_area, + update_type=UpdateType.CREATE, + transaction=quota.transaction, + ) + + @transaction.atomic + def get_result_object(self, form): + instance = super().get_result_object(form) + + # if JS is enabled we get data from the React form which includes origins and exclusions + form_origins = form.cleaned_data.get("origins") + + self.update_origins(instance, form_origins) + + return instance class QuotaUpdate( diff --git a/webpack.config.js b/webpack.config.js index 2b6e1b1bb..88c149abb 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -48,6 +48,18 @@ module.exports = { ] }, + // Babel + { + test: /\.m?js$/, + exclude: /node_modules/, + use: { + loader: "babel-loader", + options: { + presets: ["@babel/preset-env", "@babel/react"] + } + } + }, + // Extract compiled SCSS separately from JS { test: /\.s[ac]ss$/i, From d4e274e15baba717e26f8903bd67707fa44e3fb2 Mon Sep 17 00:00:00 2001 From: Edie Pearce Date: Wed, 14 Feb 2024 16:09:18 +0000 Subject: [PATCH 037/118] Move babel packages out of dev deps (#1155) --- package.json | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index d71a1ad96..93fbf62a7 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,8 @@ "npm": "^10.3.0" }, "dependencies": { + "@babel/preset-env": "^7.23.7", + "@babel/preset-react": "^7.23.3", "@babel/core": "^7.23.2", "@types/styled-components": "^5.1.29", "accessible-autocomplete": "^2.0.3", @@ -43,14 +45,11 @@ "test": "jest" }, "devDependencies": { - "@babel/preset-env": "^7.23.7", - "@babel/preset-react": "^7.23.3", "@testing-library/jest-dom": "^6.2.1", "@testing-library/react": "^14.1.2", "babel-jest": "^29.7.0", "jest": "^29.7.0", "jest-environment-jsdom": "^29.7.0", - "react-test-renderer": "^18.2.0", - "webpack-cli": "^4.7.2" + "react-test-renderer": "^18.2.0" } } \ No newline at end of file From 0b23decfd16252b84386fe8b2e3f43e4ab198059 Mon Sep 17 00:00:00 2001 From: Doug Mills Date: Tue, 6 Feb 2024 08:29:21 +0000 Subject: [PATCH 038/118] initial commit - ref doc data model --- reference_documents/apps.py | 1 - .../jinja2/reference_documents/index.jinja | 24 ++++ .../jinja2/reference_documents/overview.jinja | 24 ++++ .../migrations/0001_initial.py | 132 ++++++++++++++++++ .../0002_referencedocument_area_id.py | 19 +++ .../migrations/0003_auto_20240201_0940.py | 33 +++++ .../migrations/0004_auto_20240202_0936.py | 23 +++ .../migrations/0005_auto_20240202_1431.py | 41 ++++++ .../0006_alter_preferentialquota_volume.py | 18 +++ reference_documents/models.py | 101 +++++++++++++- reference_documents/urls.py | 9 +- reference_documents/views.py | 72 ++++++++++ settings/common.py | 1 + urls.py | 1 + 14 files changed, 496 insertions(+), 3 deletions(-) create mode 100644 reference_documents/jinja2/reference_documents/index.jinja create mode 100644 reference_documents/jinja2/reference_documents/overview.jinja create mode 100644 reference_documents/migrations/0001_initial.py create mode 100644 reference_documents/migrations/0002_referencedocument_area_id.py create mode 100644 reference_documents/migrations/0003_auto_20240201_0940.py create mode 100644 reference_documents/migrations/0004_auto_20240202_0936.py create mode 100644 reference_documents/migrations/0005_auto_20240202_1431.py create mode 100644 reference_documents/migrations/0006_alter_preferentialquota_volume.py diff --git a/reference_documents/apps.py b/reference_documents/apps.py index c8e519933..1d11db6f1 100644 --- a/reference_documents/apps.py +++ b/reference_documents/apps.py @@ -2,5 +2,4 @@ class ReferenceDocumentsConfig(AppConfig): - default_auto_field = "django.db.models.BigAutoField" name = "reference_documents" diff --git a/reference_documents/jinja2/reference_documents/index.jinja b/reference_documents/jinja2/reference_documents/index.jinja new file mode 100644 index 000000000..b281f6a3d --- /dev/null +++ b/reference_documents/jinja2/reference_documents/index.jinja @@ -0,0 +1,24 @@ +{% extends "layouts/layout.jinja" %} +{% from "components/table/macro.njk" import govukTable %} + +{% set page_title = 'Reference Documents Index' %} + +{% block breadcrumb %} + {{ breadcrumbs(request, [ + {'text': "Reference Documents"} + ]) }} +{% endblock %} + +{% block content %} +

    + Reference Documents +

    + You will find a list of reference documents below that can be viewed. + +
    + {{ govukTable({ "head": reference_document_headers, "rows": reference_documents }) }} +
    +{% endblock %} + + + diff --git a/reference_documents/jinja2/reference_documents/overview.jinja b/reference_documents/jinja2/reference_documents/overview.jinja new file mode 100644 index 000000000..fc442ceb7 --- /dev/null +++ b/reference_documents/jinja2/reference_documents/overview.jinja @@ -0,0 +1,24 @@ +{% extends "layouts/layout.jinja" %} +{% from "components/table/macro.njk" import govukTable %} + +{% set page_title = 'Reference Documents versions Overview' %} + +{% block breadcrumb %} + {{ breadcrumbs(request, [ + {'text': "Reference Documents"} + ]) }} +{% endblock %} + +{% block content %} +

    + Reference Document Overview +

    + You will find a list of reference document versions below that can be viewed. + +
    + {{ govukTable({ "head": reference_document_versions_headers, "rows": reference_documents_versions }) }} +
    +{% endblock %} + + + diff --git a/reference_documents/migrations/0001_initial.py b/reference_documents/migrations/0001_initial.py new file mode 100644 index 000000000..64b49e847 --- /dev/null +++ b/reference_documents/migrations/0001_initial.py @@ -0,0 +1,132 @@ +# Generated by Django 3.2.23 on 2024-01-29 14:52 + +import django.db.models.deletion +import django_fsm +from django.db import migrations +from django.db import models + + +class Migration(migrations.Migration): + initial = True + + dependencies = [] + + operations = [ + migrations.CreateModel( + name="ReferenceDocument", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "title", + models.CharField( + db_index=True, + help_text="Short name for this workbasket", + max_length=255, + unique=True, + ), + ), + ], + ), + migrations.CreateModel( + name="ReferenceDocumentVersion", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("version", models.FloatField()), + ("published_date", models.DateField()), + ("entry_into_force_date", models.DateField()), + ( + "status", + django_fsm.FSMField( + choices=[ + ("EDITING", "Editing"), + ("IN_REVIEW", "In Review"), + ("PUBLISHED", "Published"), + ], + db_index=True, + default="EDITING", + editable=False, + max_length=50, + ), + ), + ( + "reference_document", + models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, + related_name="reference_document_versions", + to="reference_documents.referencedocument", + ), + ), + ], + ), + migrations.CreateModel( + name="PreferentialRate", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("commodity_code", models.CharField(db_index=True, max_length=10)), + ("duty_rate", models.CharField(max_length=255)), + ("order", models.IntegerField()), + ( + "reference_document_version", + models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, + related_name="preferential_rates", + to="reference_documents.referencedocumentversion", + ), + ), + ], + ), + migrations.CreateModel( + name="PreferentialQuota", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("quota_order_number", models.CharField(db_index=True, max_length=6)), + ("commodity_code", models.CharField(db_index=True, max_length=10)), + ("quota_duty_rate", models.CharField(max_length=255)), + ("volume", models.FloatField()), + ("quota_period_open", models.DateField()), + ("quota_period_close", models.DateField()), + ("measurement", models.CharField(max_length=255)), + ("order", models.IntegerField()), + ( + "reference_document_version", + models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, + related_name="preferential_quotas", + to="reference_documents.referencedocumentversion", + ), + ), + ], + ), + ] diff --git a/reference_documents/migrations/0002_referencedocument_area_id.py b/reference_documents/migrations/0002_referencedocument_area_id.py new file mode 100644 index 000000000..18f449577 --- /dev/null +++ b/reference_documents/migrations/0002_referencedocument_area_id.py @@ -0,0 +1,19 @@ +# Generated by Django 3.2.23 on 2024-01-30 16:02 + +from django.db import migrations +from django.db import models + + +class Migration(migrations.Migration): + dependencies = [ + ("reference_documents", "0001_initial"), + ] + + operations = [ + migrations.AddField( + model_name="referencedocument", + name="area_id", + field=models.CharField(db_index=True, default="ZZ", max_length=4), + preserve_default=False, + ), + ] diff --git a/reference_documents/migrations/0003_auto_20240201_0940.py b/reference_documents/migrations/0003_auto_20240201_0940.py new file mode 100644 index 000000000..f7b8ec51d --- /dev/null +++ b/reference_documents/migrations/0003_auto_20240201_0940.py @@ -0,0 +1,33 @@ +# Generated by Django 3.2.23 on 2024-02-01 09:40 + +from django.db import migrations +from django.db import models + + +class Migration(migrations.Migration): + dependencies = [ + ("reference_documents", "0002_referencedocument_area_id"), + ] + + operations = [ + migrations.AddField( + model_name="preferentialrate", + name="valid_end_day", + field=models.IntegerField(blank=True, null=True), + ), + migrations.AddField( + model_name="preferentialrate", + name="valid_end_month", + field=models.IntegerField(blank=True, null=True), + ), + migrations.AddField( + model_name="preferentialrate", + name="valid_start_day", + field=models.IntegerField(blank=True, null=True), + ), + migrations.AddField( + model_name="preferentialrate", + name="valid_start_month", + field=models.IntegerField(blank=True, null=True), + ), + ] diff --git a/reference_documents/migrations/0004_auto_20240202_0936.py b/reference_documents/migrations/0004_auto_20240202_0936.py new file mode 100644 index 000000000..c59bbfbdb --- /dev/null +++ b/reference_documents/migrations/0004_auto_20240202_0936.py @@ -0,0 +1,23 @@ +# Generated by Django 3.2.23 on 2024-02-02 09:36 + +from django.db import migrations +from django.db import models + + +class Migration(migrations.Migration): + dependencies = [ + ("reference_documents", "0003_auto_20240201_0940"), + ] + + operations = [ + migrations.AlterField( + model_name="referencedocumentversion", + name="entry_into_force_date", + field=models.DateField(blank=True, null=True), + ), + migrations.AlterField( + model_name="referencedocumentversion", + name="published_date", + field=models.DateField(blank=True, null=True), + ), + ] diff --git a/reference_documents/migrations/0005_auto_20240202_1431.py b/reference_documents/migrations/0005_auto_20240202_1431.py new file mode 100644 index 000000000..7787303df --- /dev/null +++ b/reference_documents/migrations/0005_auto_20240202_1431.py @@ -0,0 +1,41 @@ +# Generated by Django 3.2.23 on 2024-02-02 14:31 + +from django.db import migrations +from django.db import models + + +class Migration(migrations.Migration): + dependencies = [ + ("reference_documents", "0004_auto_20240202_0936"), + ] + + operations = [ + migrations.RemoveField( + model_name="preferentialquota", + name="quota_period_close", + ), + migrations.RemoveField( + model_name="preferentialquota", + name="quota_period_open", + ), + migrations.AddField( + model_name="preferentialquota", + name="valid_end_day", + field=models.IntegerField(blank=True, null=True), + ), + migrations.AddField( + model_name="preferentialquota", + name="valid_end_month", + field=models.IntegerField(blank=True, null=True), + ), + migrations.AddField( + model_name="preferentialquota", + name="valid_start_day", + field=models.IntegerField(blank=True, null=True), + ), + migrations.AddField( + model_name="preferentialquota", + name="valid_start_month", + field=models.IntegerField(blank=True, null=True), + ), + ] diff --git a/reference_documents/migrations/0006_alter_preferentialquota_volume.py b/reference_documents/migrations/0006_alter_preferentialquota_volume.py new file mode 100644 index 000000000..1f898a734 --- /dev/null +++ b/reference_documents/migrations/0006_alter_preferentialquota_volume.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.23 on 2024-02-02 14:48 + +from django.db import migrations +from django.db import models + + +class Migration(migrations.Migration): + dependencies = [ + ("reference_documents", "0005_auto_20240202_1431"), + ] + + operations = [ + migrations.AlterField( + model_name="preferentialquota", + name="volume", + field=models.CharField(max_length=255), + ), + ] diff --git a/reference_documents/models.py b/reference_documents/models.py index 6b2021999..253119f0b 100644 --- a/reference_documents/models.py +++ b/reference_documents/models.py @@ -1 +1,100 @@ -# Create your models here. +from django.db import models +from django_fsm import FSMField + + +class ReferenceDocumentVersionStatus(models.TextChoices): + # Reference document version can be edited + EDITING = "EDITING", "Editing" + # Reference document version ius locked and in review + IN_REVIEW = "IN_REVIEW", "In Review" + # reference document version has been approved and published + PUBLISHED = "PUBLISHED", "Published" + + +class ReferenceDocument(models.Model): + title = models.CharField( + max_length=255, + help_text="Short name for this workbasket", + db_index=True, + unique=True, + ) + + area_id = models.CharField( + max_length=4, + db_index=True, + ) + + +class ReferenceDocumentVersion(models.Model): + version = models.FloatField() + published_date = models.DateField(blank=True, null=True) + entry_into_force_date = models.DateField(blank=True, null=True) + + reference_document = models.ForeignKey( + "reference_documents.ReferenceDocument", + on_delete=models.PROTECT, + related_name="reference_document_versions", + ) + status = FSMField( + default=ReferenceDocumentVersionStatus.EDITING, + choices=ReferenceDocumentVersionStatus.choices, + db_index=True, + protected=False, + editable=False, + ) + + +class PreferentialRate(models.Model): + commodity_code = models.CharField( + max_length=10, + db_index=True, + ) + duty_rate = models.CharField( + max_length=255, + ) + order = models.IntegerField() + + reference_document_version = models.ForeignKey( + "reference_documents.ReferenceDocumentVersion", + on_delete=models.PROTECT, + related_name="preferential_rates", + ) + + valid_start_day = models.IntegerField(blank=True, null=True) + valid_start_month = models.IntegerField(blank=True, null=True) + valid_end_day = models.IntegerField(blank=True, null=True) + valid_end_month = models.IntegerField(blank=True, null=True) + + +class PreferentialQuota(models.Model): + quota_order_number = models.CharField( + max_length=6, + db_index=True, + ) + commodity_code = models.CharField( + max_length=10, + db_index=True, + ) + quota_duty_rate = models.CharField( + max_length=255, + ) + volume = models.CharField( + max_length=255, + ) + + valid_start_day = models.IntegerField(blank=True, null=True) + valid_start_month = models.IntegerField(blank=True, null=True) + valid_end_day = models.IntegerField(blank=True, null=True) + valid_end_month = models.IntegerField(blank=True, null=True) + + measurement = models.CharField( + max_length=255, + ) + + order = models.IntegerField() + + reference_document_version = models.ForeignKey( + "reference_documents.ReferenceDocumentVersion", + on_delete=models.PROTECT, + related_name="preferential_quotas", + ) diff --git a/reference_documents/urls.py b/reference_documents/urls.py index 3c823912b..3ad58e9b2 100644 --- a/reference_documents/urls.py +++ b/reference_documents/urls.py @@ -1,7 +1,13 @@ from django.urls import path - +from rest_framework import routers from reference_documents import views + +app_name = "reference_documents" + +api_router = routers.DefaultRouter() + + urlpatterns = [ path( "reference-documents/", @@ -13,4 +19,5 @@ views.ReferenceDocumentsDetailView.as_view(), name="reference_documents-ui-detail", ), + path("reference_documents/", views.ReferenceDocumentList.as_view(), name="index"), ] diff --git a/reference_documents/views.py b/reference_documents/views.py index e0b090905..cf4a7667a 100644 --- a/reference_documents/views.py +++ b/reference_documents/views.py @@ -1,3 +1,62 @@ +from django.contrib.auth.mixins import PermissionRequiredMixin +from django.views.generic import ListView + +from geo_areas.models import GeographicalArea +from geo_areas.models import GeographicalAreaDescription +from reference_documents.models import ReferenceDocument + + +class ReferenceDocumentList(PermissionRequiredMixin, ListView): + """UI endpoint for viewing and filtering workbaskets.""" + + template_name = "reference_documents/index.jinja" + permission_required = "reference_documents.view_reference_document" + model = ReferenceDocument + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + reference_documents = [] + + for reference in ReferenceDocument.objects.all().order_by("area_id"): + if reference.reference_document_versions.count() == 0: + reference_documents.append( + [ + {"text": "None"}, + { + "text": f"{reference.area_id} - ({self.get_name_by_area_id(reference.area_id)})", + }, + {"text": 0}, + {"text": 0}, + {"text": ""}, + ], + ) + + else: + reference_documents.append( + [ + {"text": reference.reference_document_versions.last().version}, + { + "text": f"{reference.area_id} - ({self.get_name_by_area_id(reference.area_id)})", + }, + { + "text": reference.reference_document_versions.last().preferential_rates.count(), + }, + { + "text": reference.reference_document_versions.last().preferential_quotas.count(), + }, + {"text": ""}, + ], + ) + + context["reference_documents"] = reference_documents + context["reference_document_headers"] = [ + {"text": "Latest Version"}, + {"text": "Country"}, + {"text": "Duties"}, + {"text": "Quotas"}, + {"text": "Actions"}, + ] + return context # Create your views here. from datetime import date @@ -95,3 +154,16 @@ def get_context_data(self, *args, **kwargs): "quotas": self.get_tariff_quota_data(), } return context + + def get_name_by_area_id(self, area_id): + geo_area = ( + GeographicalArea.objects.latest_approved().filter(area_id=area_id).first() + ) + if geo_area: + geo_area_name = ( + GeographicalAreaDescription.objects.latest_approved() + .filter(described_geographicalarea_id=geo_area.trackedmodel_ptr_id) + .last() + ) + return geo_area_name.description if geo_area_name else "None" + return "None" diff --git a/settings/common.py b/settings/common.py index 7573c09af..fc1dfda20 100644 --- a/settings/common.py +++ b/settings/common.py @@ -124,6 +124,7 @@ "importer", "notifications", # XXX need to keep this for migrations. delete later. + "reference_documents", "publishing", "taric", "workbaskets", diff --git a/urls.py b/urls.py index 8203aa32e..ac6ae8240 100644 --- a/urls.py +++ b/urls.py @@ -38,6 +38,7 @@ path("", include("reports.urls")), path("", include("taric_parsers.urls")), path("", include("workbaskets.urls", namespace="workbaskets")), + path("", include("reference_documents.urls", namespace="reference_documents")), ] if not settings.MAINTENANCE_MODE: From 2e5e86e7890f5545e007d7cc460ed5a353796514 Mon Sep 17 00:00:00 2001 From: Doug Mills Date: Thu, 8 Feb 2024 08:34:04 +0000 Subject: [PATCH 039/118] wip commit --- reference_documents/alignment_checks.py | 36 +++++ .../reference_document_versions/details.jinja | 27 ++++ .../{overview.jinja => details.jinja} | 2 +- reference_documents/models.py | 50 +++++- reference_documents/urls.py | 10 ++ reference_documents/views.py | 147 +++++++++++++++++- 6 files changed, 267 insertions(+), 5 deletions(-) create mode 100644 reference_documents/alignment_checks.py create mode 100644 reference_documents/jinja2/reference_document_versions/details.jinja rename reference_documents/jinja2/reference_documents/{overview.jinja => details.jinja} (93%) diff --git a/reference_documents/alignment_checks.py b/reference_documents/alignment_checks.py new file mode 100644 index 000000000..fd25204e9 --- /dev/null +++ b/reference_documents/alignment_checks.py @@ -0,0 +1,36 @@ +from commodities.models import GoodsNomenclature +from geo_areas.models import GeographicalArea +from reference_documents.models import PreferentialRate + + +class BaseCheck: + def run_check(self): + raise NotImplemented("Please implement on child classes") + + +class BasePreferentialRateCheck(BaseCheck): + def __init__(self, preferential_rate: PreferentialRate): + self.preferential_rate = preferential_rate + + def comm_code(self): + return GoodsNomenclature.objects.latest_approved().get( + item_id=self.preferential_rate.commodity_code, + ) + + def geo_area(self): + return GeographicalArea.objects.latest_approved().get( + area_id=self.preferential_rate.reference_document_version.reference_document.area_id, + ) + + def run_check(self): + raise NotImplementedError("Please implement on child classes") + + +class CheckPreferentialRateCommCode(BasePreferentialRateCheck): + def run_check(self): + measures = self.comm_code().measures.get(geographical_area=self.geo_area()) + + return ( + len(measures) > 0, + f"{len(measures)} measures matched required preferential rate", + ) diff --git a/reference_documents/jinja2/reference_document_versions/details.jinja b/reference_documents/jinja2/reference_document_versions/details.jinja new file mode 100644 index 000000000..c2cc712d8 --- /dev/null +++ b/reference_documents/jinja2/reference_document_versions/details.jinja @@ -0,0 +1,27 @@ +{% extends "layouts/layout.jinja" %} +{% from "components/table/macro.njk" import govukTable %} + +{% set page_title = 'Reference Documents version details' %} + +{% block breadcrumb %} + {{ breadcrumbs(request, [ + {'text': "Reference Documents"} + ]) }} +{% endblock %} + +{% block content %} +

    + Reference Document version Overview +

    + Reference document version details. + +
    + {{ govukTable({ "head": reference_document_version_duties_headers, "rows": reference_document_version_duties }) }} +
    +
    + {{ govukTable({ "head": reference_document_version_quotas_headers, "rows": reference_document_version_quotas }) }} +
    +{% endblock %} + + + diff --git a/reference_documents/jinja2/reference_documents/overview.jinja b/reference_documents/jinja2/reference_documents/details.jinja similarity index 93% rename from reference_documents/jinja2/reference_documents/overview.jinja rename to reference_documents/jinja2/reference_documents/details.jinja index fc442ceb7..0c301c051 100644 --- a/reference_documents/jinja2/reference_documents/overview.jinja +++ b/reference_documents/jinja2/reference_documents/details.jinja @@ -16,7 +16,7 @@ You will find a list of reference document versions below that can be viewed.
    - {{ govukTable({ "head": reference_document_versions_headers, "rows": reference_documents_versions }) }} + {{ govukTable({ "head": reference_document_versions_headers, "rows": reference_document_versions }) }}
    {% endblock %} diff --git a/reference_documents/models.py b/reference_documents/models.py index 253119f0b..94c712c05 100644 --- a/reference_documents/models.py +++ b/reference_documents/models.py @@ -1,6 +1,9 @@ from django.db import models +from django.db.models import fields from django_fsm import FSMField +from common.models import TimestampedMixin + class ReferenceDocumentVersionStatus(models.TextChoices): # Reference document version can be edited @@ -11,7 +14,7 @@ class ReferenceDocumentVersionStatus(models.TextChoices): PUBLISHED = "PUBLISHED", "Published" -class ReferenceDocument(models.Model): +class ReferenceDocument(models.Model, TimestampedMixin): title = models.CharField( max_length=255, help_text="Short name for this workbasket", @@ -25,7 +28,7 @@ class ReferenceDocument(models.Model): ) -class ReferenceDocumentVersion(models.Model): +class ReferenceDocumentVersion(models.Model, TimestampedMixin): version = models.FloatField() published_date = models.DateField(blank=True, null=True) entry_into_force_date = models.DateField(blank=True, null=True) @@ -98,3 +101,46 @@ class PreferentialQuota(models.Model): on_delete=models.PROTECT, related_name="preferential_quotas", ) + + +class AlignmentReport(models.Model, TimestampedMixin): + reference_document_version = models.ForeignKey( + "reference_documents.ReferenceDocumentVersion", + on_delete=models.PROTECT, + related_name="alignment_reports", + ) + + +class AlignmentReportCheck(models.Model): + created_at = models.DateTimeField(auto_now_add=True) + + alignment_report = models.ForeignKey( + "reference_documents.AlignmentReport", + on_delete=models.PROTECT, + related_name="alignment_report_checks", + ) + + check_name = fields.CharField(max_length=255) + """A string identifying the type of check carried out.""" + + successful = fields.BooleanField() + """True if the check was successful.""" + + message = fields.TextField(null=True) + """The text content returned by the check, if any.""" + + preferential_quota = models.ForeignKey( + "reference_documents.PreferentialQuota", + on_delete=models.PROTECT, + related_name="alignment_report_checks", + blank=True, + null=True, + ) + + preferential_rate = models.ForeignKey( + "reference_documents.PreferentialRate", + on_delete=models.PROTECT, + related_name="alignment_report_checks", + blank=True, + null=True, + ) diff --git a/reference_documents/urls.py b/reference_documents/urls.py index 3ad58e9b2..84fcb43ff 100644 --- a/reference_documents/urls.py +++ b/reference_documents/urls.py @@ -20,4 +20,14 @@ name="reference_documents-ui-detail", ), path("reference_documents/", views.ReferenceDocumentList.as_view(), name="index"), + path( + "reference_documents//", + views.ReferenceDocumentDetails.as_view(), + name="details", + ), + path( + "reference_document_versions//", + views.ReferenceDocumentVersionDetails.as_view(), + name="version_details", + ), ] diff --git a/reference_documents/views.py b/reference_documents/views.py index cf4a7667a..7095b8e78 100644 --- a/reference_documents/views.py +++ b/reference_documents/views.py @@ -1,9 +1,11 @@ from django.contrib.auth.mixins import PermissionRequiredMixin +from django.views.generic import DetailView from django.views.generic import ListView from geo_areas.models import GeographicalArea from geo_areas.models import GeographicalAreaDescription from reference_documents.models import ReferenceDocument +from reference_documents.models import ReferenceDocumentVersion class ReferenceDocumentList(PermissionRequiredMixin, ListView): @@ -27,7 +29,9 @@ def get_context_data(self, **kwargs): }, {"text": 0}, {"text": 0}, - {"text": ""}, + { + "html": f'Details', + }, ], ) @@ -44,7 +48,9 @@ def get_context_data(self, **kwargs): { "text": reference.reference_document_versions.last().preferential_quotas.count(), }, - {"text": ""}, + { + "html": f'Details', + }, ], ) @@ -167,3 +173,140 @@ def get_name_by_area_id(self, area_id): ) return geo_area_name.description if geo_area_name else "None" return "None" + + +class ReferenceDocumentDetails(PermissionRequiredMixin, DetailView): + template_name = "reference_documents/details.jinja" + permission_required = "reference_documents.view_reference_document" + model = ReferenceDocument + + def get_context_data(self, *args, **kwargs): + context = super(ReferenceDocumentDetails, self).get_context_data( + *args, + **kwargs, + ) + + context["reference_document_versions_headers"] = [ + {"text": "Version"}, + {"text": "Duties"}, + {"text": "Quotas"}, + {"text": "Actions"}, + ] + reference_document_versions = [] + + print(self.request) + + for version in context["object"].reference_document_versions.order_by( + "version", + ): + reference_document_versions.append( + [ + { + "text": version.version, + }, + { + "text": version.preferential_rates.count(), + }, + { + "text": version.preferential_quotas.count(), + }, + { + "html": f'version details', + }, + ], + ) + + context["reference_document_versions"] = reference_document_versions + + return context + + +class ReferenceDocumentVersionDetails(PermissionRequiredMixin, DetailView): + template_name = "reference_document_versions/details.jinja" + permission_required = "reference_documents.view_reference_document" + model = ReferenceDocumentVersion + + def get_context_data(self, *args, **kwargs): + context = super(ReferenceDocumentVersionDetails, self).get_context_data( + *args, + **kwargs, + ) + + context["reference_document_version_duties_headers"] = [ + {"text": "Comm Code"}, + {"text": "Duty Rate"}, + {"text": "Validity"}, + {"text": "Actions"}, + ] + + context["reference_document_version_quotas_headers"] = [ + {"text": "Order Number"}, + {"text": "Comm Code"}, + {"text": "Rate"}, + {"text": "Volume"}, + {"text": "Validity"}, + {"text": "Actions"}, + ] + + reference_document_version_duties = [] + reference_document_version_quotas = [] + + print(self.request) + + for duty in context["object"].preferential_rates.order_by("order"): + validity = "" + + if duty.valid_start_day: + validity = f"{duty.valid_start_day}/{duty.valid_start_month} - {duty.valid_end_day}/{duty.valid_end_month}" + + reference_document_version_duties.append( + [ + { + "text": duty.commodity_code, + }, + { + "text": duty.duty_rate, + }, + { + "text": validity, + }, + { + "text": "", + }, + ], + ) + + for quota in context["object"].preferential_quotas.order_by("order"): + validity = "" + + if quota.valid_start_day: + validity = f"{quota.valid_start_day}/{quota.valid_start_month} - {quota.valid_end_day}/{quota.valid_end_month}" + + reference_document_version_quotas.append( + [ + { + "text": quota.quota_order_number, + }, + { + "text": quota.commodity_code, + }, + { + "text": quota.quota_duty_rate, + }, + { + "text": f"{quota.volume} {quota.measurement}", + }, + { + "text": validity, + }, + { + "text": "", + }, + ], + ) + + context["reference_document_version_duties"] = reference_document_version_duties + + context["reference_document_version_quotas"] = reference_document_version_quotas + + return context From 353ffcb4a543573435610cd942769667451b7bef Mon Sep 17 00:00:00 2001 From: Doug Mills Date: Tue, 6 Feb 2024 08:29:21 +0000 Subject: [PATCH 040/118] initial commit - ref doc data model --- .../jinja2/reference_documents/overview.jinja | 24 +++++++++++++++++++ reference_documents/models.py | 1 - 2 files changed, 24 insertions(+), 1 deletion(-) create mode 100644 reference_documents/jinja2/reference_documents/overview.jinja diff --git a/reference_documents/jinja2/reference_documents/overview.jinja b/reference_documents/jinja2/reference_documents/overview.jinja new file mode 100644 index 000000000..fc442ceb7 --- /dev/null +++ b/reference_documents/jinja2/reference_documents/overview.jinja @@ -0,0 +1,24 @@ +{% extends "layouts/layout.jinja" %} +{% from "components/table/macro.njk" import govukTable %} + +{% set page_title = 'Reference Documents versions Overview' %} + +{% block breadcrumb %} + {{ breadcrumbs(request, [ + {'text': "Reference Documents"} + ]) }} +{% endblock %} + +{% block content %} +

    + Reference Document Overview +

    + You will find a list of reference document versions below that can be viewed. + +
    + {{ govukTable({ "head": reference_document_versions_headers, "rows": reference_documents_versions }) }} +
    +{% endblock %} + + + diff --git a/reference_documents/models.py b/reference_documents/models.py index 94c712c05..ccb1eb64b 100644 --- a/reference_documents/models.py +++ b/reference_documents/models.py @@ -1,7 +1,6 @@ from django.db import models from django.db.models import fields from django_fsm import FSMField - from common.models import TimestampedMixin From ed19ef1b3801b21529785e278f3051bf2d628599 Mon Sep 17 00:00:00 2001 From: Doug Mills Date: Tue, 13 Feb 2024 11:33:34 +0000 Subject: [PATCH 041/118] wip commit --- reference_documents/alignment_checks.py | 80 ++++++++++++-- .../migrations/0001_initial.py | 101 ++++++++++++++++-- .../0002_referencedocument_area_id.py | 19 ---- .../migrations/0003_auto_20240201_0940.py | 33 ------ .../migrations/0004_auto_20240202_0936.py | 23 ---- .../migrations/0005_auto_20240202_1431.py | 41 ------- .../0006_alter_preferentialquota_volume.py | 18 ---- reference_documents/models.py | 7 +- 8 files changed, 168 insertions(+), 154 deletions(-) delete mode 100644 reference_documents/migrations/0002_referencedocument_area_id.py delete mode 100644 reference_documents/migrations/0003_auto_20240201_0940.py delete mode 100644 reference_documents/migrations/0004_auto_20240202_0936.py delete mode 100644 reference_documents/migrations/0005_auto_20240202_1431.py delete mode 100644 reference_documents/migrations/0006_alter_preferentialquota_volume.py diff --git a/reference_documents/alignment_checks.py b/reference_documents/alignment_checks.py index fd25204e9..c7f43eb59 100644 --- a/reference_documents/alignment_checks.py +++ b/reference_documents/alignment_checks.py @@ -1,5 +1,8 @@ +from datetime import date + from commodities.models import GoodsNomenclature from geo_areas.models import GeographicalArea +from geo_areas.models import GeographicalAreaDescription from reference_documents.models import PreferentialRate @@ -13,13 +16,53 @@ def __init__(self, preferential_rate: PreferentialRate): self.preferential_rate = preferential_rate def comm_code(self): - return GoodsNomenclature.objects.latest_approved().get( + goods = GoodsNomenclature.objects.latest_approved().filter( item_id=self.preferential_rate.commodity_code, + valid_between__contains=self.ref_doc_version_eif_date(), + suffix=80, ) + if len(goods) == 0: + return None + + return goods.first() + def geo_area(self): - return GeographicalArea.objects.latest_approved().get( - area_id=self.preferential_rate.reference_document_version.reference_document.area_id, + return ( + GeographicalArea.objects.latest_approved() + .filter( + area_id=self.preferential_rate.reference_document_version.reference_document.area_id, + ) + .first() + ) + + def geo_area_description(self): + geo_area_desc = ( + GeographicalAreaDescription.objects.latest_approved() + .filter(described_geographicalarea=self.geo_area()) + .last() + ) + return geo_area_desc.description + + def ref_doc_version_eif_date(self): + eif_date = ( + self.preferential_rate.reference_document_version.entry_into_force_date + ) + + # todo : make sure EIf dates are all populated correctly - and remove this + if eif_date is None: + eif_date = date.today() + + return eif_date + + def related_measures(self): + return ( + self.comm_code() + .measures.latest_approved() + .filter( + geographical_area=self.geo_area(), + valid_between__contains=self.ref_doc_version_eif_date(), + ) ) def run_check(self): @@ -28,9 +71,30 @@ def run_check(self): class CheckPreferentialRateCommCode(BasePreferentialRateCheck): def run_check(self): - measures = self.comm_code().measures.get(geographical_area=self.geo_area()) + # comm code live on EIF date + if not self.comm_code(): + print( + "FAIL", + self.preferential_rate.commodity_code, + self.geo_area_description(), + "comm code not live", + ) + return False - return ( - len(measures) > 0, - f"{len(measures)} measures matched required preferential rate", - ) + measures = self.related_measures() + + if len(measures) == 1: + print("PASS", self.comm_code(), self.geo_area_description()) + return True + + if len(measures) == 0: + print("FAIL", self.comm_code(), self.geo_area_description()) + return False + + if len(measures) > 1: + print( + "WARNING - multiple measures", + self.comm_code(), + self.geo_area_description(), + ) + return True diff --git a/reference_documents/migrations/0001_initial.py b/reference_documents/migrations/0001_initial.py index 64b49e847..d94be24d1 100644 --- a/reference_documents/migrations/0001_initial.py +++ b/reference_documents/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 3.2.23 on 2024-01-29 14:52 +# Generated by Django 3.2.23 on 2024-02-08 09:34 import django.db.models.deletion import django_fsm @@ -12,18 +12,35 @@ class Migration(migrations.Migration): dependencies = [] operations = [ + migrations.CreateModel( + name="AlignmentReport", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("created_at", models.DateTimeField(auto_now_add=True)), + ], + ), migrations.CreateModel( name="ReferenceDocument", fields=[ ( "id", - models.BigAutoField( + models.AutoField( auto_created=True, primary_key=True, serialize=False, verbose_name="ID", ), ), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), ( "title", models.CharField( @@ -33,6 +50,7 @@ class Migration(migrations.Migration): unique=True, ), ), + ("area_id", models.CharField(db_index=True, max_length=4)), ], ), migrations.CreateModel( @@ -40,16 +58,18 @@ class Migration(migrations.Migration): fields=[ ( "id", - models.BigAutoField( + models.AutoField( auto_created=True, primary_key=True, serialize=False, verbose_name="ID", ), ), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), ("version", models.FloatField()), - ("published_date", models.DateField()), - ("entry_into_force_date", models.DateField()), + ("published_date", models.DateField(blank=True, null=True)), + ("entry_into_force_date", models.DateField(blank=True, null=True)), ( "status", django_fsm.FSMField( @@ -79,7 +99,7 @@ class Migration(migrations.Migration): fields=[ ( "id", - models.BigAutoField( + models.AutoField( auto_created=True, primary_key=True, serialize=False, @@ -89,6 +109,10 @@ class Migration(migrations.Migration): ("commodity_code", models.CharField(db_index=True, max_length=10)), ("duty_rate", models.CharField(max_length=255)), ("order", models.IntegerField()), + ("valid_start_day", models.IntegerField(blank=True, null=True)), + ("valid_start_month", models.IntegerField(blank=True, null=True)), + ("valid_end_day", models.IntegerField(blank=True, null=True)), + ("valid_end_month", models.IntegerField(blank=True, null=True)), ( "reference_document_version", models.ForeignKey( @@ -104,7 +128,7 @@ class Migration(migrations.Migration): fields=[ ( "id", - models.BigAutoField( + models.AutoField( auto_created=True, primary_key=True, serialize=False, @@ -114,9 +138,11 @@ class Migration(migrations.Migration): ("quota_order_number", models.CharField(db_index=True, max_length=6)), ("commodity_code", models.CharField(db_index=True, max_length=10)), ("quota_duty_rate", models.CharField(max_length=255)), - ("volume", models.FloatField()), - ("quota_period_open", models.DateField()), - ("quota_period_close", models.DateField()), + ("volume", models.CharField(max_length=255)), + ("valid_start_day", models.IntegerField(blank=True, null=True)), + ("valid_start_month", models.IntegerField(blank=True, null=True)), + ("valid_end_day", models.IntegerField(blank=True, null=True)), + ("valid_end_month", models.IntegerField(blank=True, null=True)), ("measurement", models.CharField(max_length=255)), ("order", models.IntegerField()), ( @@ -129,4 +155,59 @@ class Migration(migrations.Migration): ), ], ), + migrations.CreateModel( + name="AlignmentReportCheck", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("check_name", models.CharField(max_length=255)), + ("successful", models.BooleanField()), + ("message", models.TextField(null=True)), + ( + "alignment_report", + models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, + related_name="alignment_report_checks", + to="reference_documents.alignmentreport", + ), + ), + ( + "preferential_quota", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.PROTECT, + related_name="alignment_report_checks", + to="reference_documents.preferentialquota", + ), + ), + ( + "preferential_rate", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.PROTECT, + related_name="alignment_report_checks", + to="reference_documents.preferentialrate", + ), + ), + ], + ), + migrations.AddField( + model_name="alignmentreport", + name="reference_document_version", + field=models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, + related_name="alignment_reports", + to="reference_documents.referencedocumentversion", + ), + ), ] diff --git a/reference_documents/migrations/0002_referencedocument_area_id.py b/reference_documents/migrations/0002_referencedocument_area_id.py deleted file mode 100644 index 18f449577..000000000 --- a/reference_documents/migrations/0002_referencedocument_area_id.py +++ /dev/null @@ -1,19 +0,0 @@ -# Generated by Django 3.2.23 on 2024-01-30 16:02 - -from django.db import migrations -from django.db import models - - -class Migration(migrations.Migration): - dependencies = [ - ("reference_documents", "0001_initial"), - ] - - operations = [ - migrations.AddField( - model_name="referencedocument", - name="area_id", - field=models.CharField(db_index=True, default="ZZ", max_length=4), - preserve_default=False, - ), - ] diff --git a/reference_documents/migrations/0003_auto_20240201_0940.py b/reference_documents/migrations/0003_auto_20240201_0940.py deleted file mode 100644 index f7b8ec51d..000000000 --- a/reference_documents/migrations/0003_auto_20240201_0940.py +++ /dev/null @@ -1,33 +0,0 @@ -# Generated by Django 3.2.23 on 2024-02-01 09:40 - -from django.db import migrations -from django.db import models - - -class Migration(migrations.Migration): - dependencies = [ - ("reference_documents", "0002_referencedocument_area_id"), - ] - - operations = [ - migrations.AddField( - model_name="preferentialrate", - name="valid_end_day", - field=models.IntegerField(blank=True, null=True), - ), - migrations.AddField( - model_name="preferentialrate", - name="valid_end_month", - field=models.IntegerField(blank=True, null=True), - ), - migrations.AddField( - model_name="preferentialrate", - name="valid_start_day", - field=models.IntegerField(blank=True, null=True), - ), - migrations.AddField( - model_name="preferentialrate", - name="valid_start_month", - field=models.IntegerField(blank=True, null=True), - ), - ] diff --git a/reference_documents/migrations/0004_auto_20240202_0936.py b/reference_documents/migrations/0004_auto_20240202_0936.py deleted file mode 100644 index c59bbfbdb..000000000 --- a/reference_documents/migrations/0004_auto_20240202_0936.py +++ /dev/null @@ -1,23 +0,0 @@ -# Generated by Django 3.2.23 on 2024-02-02 09:36 - -from django.db import migrations -from django.db import models - - -class Migration(migrations.Migration): - dependencies = [ - ("reference_documents", "0003_auto_20240201_0940"), - ] - - operations = [ - migrations.AlterField( - model_name="referencedocumentversion", - name="entry_into_force_date", - field=models.DateField(blank=True, null=True), - ), - migrations.AlterField( - model_name="referencedocumentversion", - name="published_date", - field=models.DateField(blank=True, null=True), - ), - ] diff --git a/reference_documents/migrations/0005_auto_20240202_1431.py b/reference_documents/migrations/0005_auto_20240202_1431.py deleted file mode 100644 index 7787303df..000000000 --- a/reference_documents/migrations/0005_auto_20240202_1431.py +++ /dev/null @@ -1,41 +0,0 @@ -# Generated by Django 3.2.23 on 2024-02-02 14:31 - -from django.db import migrations -from django.db import models - - -class Migration(migrations.Migration): - dependencies = [ - ("reference_documents", "0004_auto_20240202_0936"), - ] - - operations = [ - migrations.RemoveField( - model_name="preferentialquota", - name="quota_period_close", - ), - migrations.RemoveField( - model_name="preferentialquota", - name="quota_period_open", - ), - migrations.AddField( - model_name="preferentialquota", - name="valid_end_day", - field=models.IntegerField(blank=True, null=True), - ), - migrations.AddField( - model_name="preferentialquota", - name="valid_end_month", - field=models.IntegerField(blank=True, null=True), - ), - migrations.AddField( - model_name="preferentialquota", - name="valid_start_day", - field=models.IntegerField(blank=True, null=True), - ), - migrations.AddField( - model_name="preferentialquota", - name="valid_start_month", - field=models.IntegerField(blank=True, null=True), - ), - ] diff --git a/reference_documents/migrations/0006_alter_preferentialquota_volume.py b/reference_documents/migrations/0006_alter_preferentialquota_volume.py deleted file mode 100644 index 1f898a734..000000000 --- a/reference_documents/migrations/0006_alter_preferentialquota_volume.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 3.2.23 on 2024-02-02 14:48 - -from django.db import migrations -from django.db import models - - -class Migration(migrations.Migration): - dependencies = [ - ("reference_documents", "0005_auto_20240202_1431"), - ] - - operations = [ - migrations.AlterField( - model_name="preferentialquota", - name="volume", - field=models.CharField(max_length=255), - ), - ] diff --git a/reference_documents/models.py b/reference_documents/models.py index ccb1eb64b..1214d8b55 100644 --- a/reference_documents/models.py +++ b/reference_documents/models.py @@ -27,7 +27,9 @@ class ReferenceDocument(models.Model, TimestampedMixin): ) -class ReferenceDocumentVersion(models.Model, TimestampedMixin): +class ReferenceDocumentVersion(models.Model): + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) version = models.FloatField() published_date = models.DateField(blank=True, null=True) entry_into_force_date = models.DateField(blank=True, null=True) @@ -102,7 +104,8 @@ class PreferentialQuota(models.Model): ) -class AlignmentReport(models.Model, TimestampedMixin): +class AlignmentReport(models.Model): + created_at = models.DateTimeField(auto_now_add=True) reference_document_version = models.ForeignKey( "reference_documents.ReferenceDocumentVersion", on_delete=models.PROTECT, From 34d3f5e6468898ab4b2e47f9c8596ad2eac58ca5 Mon Sep 17 00:00:00 2001 From: Doug Mills Date: Wed, 14 Feb 2024 11:42:17 +0000 Subject: [PATCH 042/118] wip commit --- .../migrations/0001_initial.py | 213 ------------------ reference_documents/models.py | 5 +- 2 files changed, 3 insertions(+), 215 deletions(-) delete mode 100644 reference_documents/migrations/0001_initial.py diff --git a/reference_documents/migrations/0001_initial.py b/reference_documents/migrations/0001_initial.py deleted file mode 100644 index d94be24d1..000000000 --- a/reference_documents/migrations/0001_initial.py +++ /dev/null @@ -1,213 +0,0 @@ -# Generated by Django 3.2.23 on 2024-02-08 09:34 - -import django.db.models.deletion -import django_fsm -from django.db import migrations -from django.db import models - - -class Migration(migrations.Migration): - initial = True - - dependencies = [] - - operations = [ - migrations.CreateModel( - name="AlignmentReport", - fields=[ - ( - "id", - models.AutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ("created_at", models.DateTimeField(auto_now_add=True)), - ], - ), - migrations.CreateModel( - name="ReferenceDocument", - fields=[ - ( - "id", - models.AutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ("created_at", models.DateTimeField(auto_now_add=True)), - ("updated_at", models.DateTimeField(auto_now=True)), - ( - "title", - models.CharField( - db_index=True, - help_text="Short name for this workbasket", - max_length=255, - unique=True, - ), - ), - ("area_id", models.CharField(db_index=True, max_length=4)), - ], - ), - migrations.CreateModel( - name="ReferenceDocumentVersion", - fields=[ - ( - "id", - models.AutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ("created_at", models.DateTimeField(auto_now_add=True)), - ("updated_at", models.DateTimeField(auto_now=True)), - ("version", models.FloatField()), - ("published_date", models.DateField(blank=True, null=True)), - ("entry_into_force_date", models.DateField(blank=True, null=True)), - ( - "status", - django_fsm.FSMField( - choices=[ - ("EDITING", "Editing"), - ("IN_REVIEW", "In Review"), - ("PUBLISHED", "Published"), - ], - db_index=True, - default="EDITING", - editable=False, - max_length=50, - ), - ), - ( - "reference_document", - models.ForeignKey( - on_delete=django.db.models.deletion.PROTECT, - related_name="reference_document_versions", - to="reference_documents.referencedocument", - ), - ), - ], - ), - migrations.CreateModel( - name="PreferentialRate", - fields=[ - ( - "id", - models.AutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ("commodity_code", models.CharField(db_index=True, max_length=10)), - ("duty_rate", models.CharField(max_length=255)), - ("order", models.IntegerField()), - ("valid_start_day", models.IntegerField(blank=True, null=True)), - ("valid_start_month", models.IntegerField(blank=True, null=True)), - ("valid_end_day", models.IntegerField(blank=True, null=True)), - ("valid_end_month", models.IntegerField(blank=True, null=True)), - ( - "reference_document_version", - models.ForeignKey( - on_delete=django.db.models.deletion.PROTECT, - related_name="preferential_rates", - to="reference_documents.referencedocumentversion", - ), - ), - ], - ), - migrations.CreateModel( - name="PreferentialQuota", - fields=[ - ( - "id", - models.AutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ("quota_order_number", models.CharField(db_index=True, max_length=6)), - ("commodity_code", models.CharField(db_index=True, max_length=10)), - ("quota_duty_rate", models.CharField(max_length=255)), - ("volume", models.CharField(max_length=255)), - ("valid_start_day", models.IntegerField(blank=True, null=True)), - ("valid_start_month", models.IntegerField(blank=True, null=True)), - ("valid_end_day", models.IntegerField(blank=True, null=True)), - ("valid_end_month", models.IntegerField(blank=True, null=True)), - ("measurement", models.CharField(max_length=255)), - ("order", models.IntegerField()), - ( - "reference_document_version", - models.ForeignKey( - on_delete=django.db.models.deletion.PROTECT, - related_name="preferential_quotas", - to="reference_documents.referencedocumentversion", - ), - ), - ], - ), - migrations.CreateModel( - name="AlignmentReportCheck", - fields=[ - ( - "id", - models.AutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ("created_at", models.DateTimeField(auto_now_add=True)), - ("check_name", models.CharField(max_length=255)), - ("successful", models.BooleanField()), - ("message", models.TextField(null=True)), - ( - "alignment_report", - models.ForeignKey( - on_delete=django.db.models.deletion.PROTECT, - related_name="alignment_report_checks", - to="reference_documents.alignmentreport", - ), - ), - ( - "preferential_quota", - models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.PROTECT, - related_name="alignment_report_checks", - to="reference_documents.preferentialquota", - ), - ), - ( - "preferential_rate", - models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.PROTECT, - related_name="alignment_report_checks", - to="reference_documents.preferentialrate", - ), - ), - ], - ), - migrations.AddField( - model_name="alignmentreport", - name="reference_document_version", - field=models.ForeignKey( - on_delete=django.db.models.deletion.PROTECT, - related_name="alignment_reports", - to="reference_documents.referencedocumentversion", - ), - ), - ] diff --git a/reference_documents/models.py b/reference_documents/models.py index 1214d8b55..9e7987320 100644 --- a/reference_documents/models.py +++ b/reference_documents/models.py @@ -1,7 +1,6 @@ from django.db import models from django.db.models import fields from django_fsm import FSMField -from common.models import TimestampedMixin class ReferenceDocumentVersionStatus(models.TextChoices): @@ -13,7 +12,9 @@ class ReferenceDocumentVersionStatus(models.TextChoices): PUBLISHED = "PUBLISHED", "Published" -class ReferenceDocument(models.Model, TimestampedMixin): +class ReferenceDocument(models.Model): + created_at = models.DateTimeField(auto_now_add=True) + title = models.CharField( max_length=255, help_text="Short name for this workbasket", From edb180497b6c565c0a5e844a60e695f51db511bc Mon Sep 17 00:00:00 2001 From: Doug Mills Date: Tue, 6 Feb 2024 08:29:21 +0000 Subject: [PATCH 043/118] initial commit - ref doc data model --- .../migrations/0001_initial.py | 132 ++++++++++++++++++ .../0002_referencedocument_area_id.py | 19 +++ .../migrations/0003_auto_20240201_0940.py | 33 +++++ .../migrations/0004_auto_20240202_0936.py | 23 +++ .../migrations/0005_auto_20240202_1431.py | 41 ++++++ .../0006_alter_preferentialquota_volume.py | 18 +++ reference_documents/views.py | 6 + 7 files changed, 272 insertions(+) create mode 100644 reference_documents/migrations/0001_initial.py create mode 100644 reference_documents/migrations/0002_referencedocument_area_id.py create mode 100644 reference_documents/migrations/0003_auto_20240201_0940.py create mode 100644 reference_documents/migrations/0004_auto_20240202_0936.py create mode 100644 reference_documents/migrations/0005_auto_20240202_1431.py create mode 100644 reference_documents/migrations/0006_alter_preferentialquota_volume.py diff --git a/reference_documents/migrations/0001_initial.py b/reference_documents/migrations/0001_initial.py new file mode 100644 index 000000000..64b49e847 --- /dev/null +++ b/reference_documents/migrations/0001_initial.py @@ -0,0 +1,132 @@ +# Generated by Django 3.2.23 on 2024-01-29 14:52 + +import django.db.models.deletion +import django_fsm +from django.db import migrations +from django.db import models + + +class Migration(migrations.Migration): + initial = True + + dependencies = [] + + operations = [ + migrations.CreateModel( + name="ReferenceDocument", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "title", + models.CharField( + db_index=True, + help_text="Short name for this workbasket", + max_length=255, + unique=True, + ), + ), + ], + ), + migrations.CreateModel( + name="ReferenceDocumentVersion", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("version", models.FloatField()), + ("published_date", models.DateField()), + ("entry_into_force_date", models.DateField()), + ( + "status", + django_fsm.FSMField( + choices=[ + ("EDITING", "Editing"), + ("IN_REVIEW", "In Review"), + ("PUBLISHED", "Published"), + ], + db_index=True, + default="EDITING", + editable=False, + max_length=50, + ), + ), + ( + "reference_document", + models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, + related_name="reference_document_versions", + to="reference_documents.referencedocument", + ), + ), + ], + ), + migrations.CreateModel( + name="PreferentialRate", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("commodity_code", models.CharField(db_index=True, max_length=10)), + ("duty_rate", models.CharField(max_length=255)), + ("order", models.IntegerField()), + ( + "reference_document_version", + models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, + related_name="preferential_rates", + to="reference_documents.referencedocumentversion", + ), + ), + ], + ), + migrations.CreateModel( + name="PreferentialQuota", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("quota_order_number", models.CharField(db_index=True, max_length=6)), + ("commodity_code", models.CharField(db_index=True, max_length=10)), + ("quota_duty_rate", models.CharField(max_length=255)), + ("volume", models.FloatField()), + ("quota_period_open", models.DateField()), + ("quota_period_close", models.DateField()), + ("measurement", models.CharField(max_length=255)), + ("order", models.IntegerField()), + ( + "reference_document_version", + models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, + related_name="preferential_quotas", + to="reference_documents.referencedocumentversion", + ), + ), + ], + ), + ] diff --git a/reference_documents/migrations/0002_referencedocument_area_id.py b/reference_documents/migrations/0002_referencedocument_area_id.py new file mode 100644 index 000000000..18f449577 --- /dev/null +++ b/reference_documents/migrations/0002_referencedocument_area_id.py @@ -0,0 +1,19 @@ +# Generated by Django 3.2.23 on 2024-01-30 16:02 + +from django.db import migrations +from django.db import models + + +class Migration(migrations.Migration): + dependencies = [ + ("reference_documents", "0001_initial"), + ] + + operations = [ + migrations.AddField( + model_name="referencedocument", + name="area_id", + field=models.CharField(db_index=True, default="ZZ", max_length=4), + preserve_default=False, + ), + ] diff --git a/reference_documents/migrations/0003_auto_20240201_0940.py b/reference_documents/migrations/0003_auto_20240201_0940.py new file mode 100644 index 000000000..f7b8ec51d --- /dev/null +++ b/reference_documents/migrations/0003_auto_20240201_0940.py @@ -0,0 +1,33 @@ +# Generated by Django 3.2.23 on 2024-02-01 09:40 + +from django.db import migrations +from django.db import models + + +class Migration(migrations.Migration): + dependencies = [ + ("reference_documents", "0002_referencedocument_area_id"), + ] + + operations = [ + migrations.AddField( + model_name="preferentialrate", + name="valid_end_day", + field=models.IntegerField(blank=True, null=True), + ), + migrations.AddField( + model_name="preferentialrate", + name="valid_end_month", + field=models.IntegerField(blank=True, null=True), + ), + migrations.AddField( + model_name="preferentialrate", + name="valid_start_day", + field=models.IntegerField(blank=True, null=True), + ), + migrations.AddField( + model_name="preferentialrate", + name="valid_start_month", + field=models.IntegerField(blank=True, null=True), + ), + ] diff --git a/reference_documents/migrations/0004_auto_20240202_0936.py b/reference_documents/migrations/0004_auto_20240202_0936.py new file mode 100644 index 000000000..c59bbfbdb --- /dev/null +++ b/reference_documents/migrations/0004_auto_20240202_0936.py @@ -0,0 +1,23 @@ +# Generated by Django 3.2.23 on 2024-02-02 09:36 + +from django.db import migrations +from django.db import models + + +class Migration(migrations.Migration): + dependencies = [ + ("reference_documents", "0003_auto_20240201_0940"), + ] + + operations = [ + migrations.AlterField( + model_name="referencedocumentversion", + name="entry_into_force_date", + field=models.DateField(blank=True, null=True), + ), + migrations.AlterField( + model_name="referencedocumentversion", + name="published_date", + field=models.DateField(blank=True, null=True), + ), + ] diff --git a/reference_documents/migrations/0005_auto_20240202_1431.py b/reference_documents/migrations/0005_auto_20240202_1431.py new file mode 100644 index 000000000..7787303df --- /dev/null +++ b/reference_documents/migrations/0005_auto_20240202_1431.py @@ -0,0 +1,41 @@ +# Generated by Django 3.2.23 on 2024-02-02 14:31 + +from django.db import migrations +from django.db import models + + +class Migration(migrations.Migration): + dependencies = [ + ("reference_documents", "0004_auto_20240202_0936"), + ] + + operations = [ + migrations.RemoveField( + model_name="preferentialquota", + name="quota_period_close", + ), + migrations.RemoveField( + model_name="preferentialquota", + name="quota_period_open", + ), + migrations.AddField( + model_name="preferentialquota", + name="valid_end_day", + field=models.IntegerField(blank=True, null=True), + ), + migrations.AddField( + model_name="preferentialquota", + name="valid_end_month", + field=models.IntegerField(blank=True, null=True), + ), + migrations.AddField( + model_name="preferentialquota", + name="valid_start_day", + field=models.IntegerField(blank=True, null=True), + ), + migrations.AddField( + model_name="preferentialquota", + name="valid_start_month", + field=models.IntegerField(blank=True, null=True), + ), + ] diff --git a/reference_documents/migrations/0006_alter_preferentialquota_volume.py b/reference_documents/migrations/0006_alter_preferentialquota_volume.py new file mode 100644 index 000000000..1f898a734 --- /dev/null +++ b/reference_documents/migrations/0006_alter_preferentialquota_volume.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.23 on 2024-02-02 14:48 + +from django.db import migrations +from django.db import models + + +class Migration(migrations.Migration): + dependencies = [ + ("reference_documents", "0005_auto_20240202_1431"), + ] + + operations = [ + migrations.AlterField( + model_name="preferentialquota", + name="volume", + field=models.CharField(max_length=255), + ), + ] diff --git a/reference_documents/views.py b/reference_documents/views.py index 7095b8e78..8ccd5b7e4 100644 --- a/reference_documents/views.py +++ b/reference_documents/views.py @@ -1,11 +1,17 @@ from django.contrib.auth.mixins import PermissionRequiredMixin +<<<<<<< HEAD from django.views.generic import DetailView +======= +>>>>>>> eb2cd348 (initial commit - ref doc data model) from django.views.generic import ListView from geo_areas.models import GeographicalArea from geo_areas.models import GeographicalAreaDescription from reference_documents.models import ReferenceDocument +<<<<<<< HEAD from reference_documents.models import ReferenceDocumentVersion +======= +>>>>>>> eb2cd348 (initial commit - ref doc data model) class ReferenceDocumentList(PermissionRequiredMixin, ListView): From 25f803d0e515c887c405c7028d6fce21c5bdd794 Mon Sep 17 00:00:00 2001 From: Doug Mills Date: Thu, 8 Feb 2024 08:34:04 +0000 Subject: [PATCH 044/118] wip commit --- reference_documents/alignment_checks.py | 2 ++ .../jinja2/reference_documents/overview.jinja | 24 ------------------- reference_documents/models.py | 3 --- reference_documents/views.py | 6 ----- 4 files changed, 2 insertions(+), 33 deletions(-) delete mode 100644 reference_documents/jinja2/reference_documents/overview.jinja diff --git a/reference_documents/alignment_checks.py b/reference_documents/alignment_checks.py index c7f43eb59..bda1123c1 100644 --- a/reference_documents/alignment_checks.py +++ b/reference_documents/alignment_checks.py @@ -1,3 +1,5 @@ +from commodities.models import GoodsNomenclature +from geo_areas.models import GeographicalArea from datetime import date from commodities.models import GoodsNomenclature diff --git a/reference_documents/jinja2/reference_documents/overview.jinja b/reference_documents/jinja2/reference_documents/overview.jinja deleted file mode 100644 index fc442ceb7..000000000 --- a/reference_documents/jinja2/reference_documents/overview.jinja +++ /dev/null @@ -1,24 +0,0 @@ -{% extends "layouts/layout.jinja" %} -{% from "components/table/macro.njk" import govukTable %} - -{% set page_title = 'Reference Documents versions Overview' %} - -{% block breadcrumb %} - {{ breadcrumbs(request, [ - {'text': "Reference Documents"} - ]) }} -{% endblock %} - -{% block content %} -

    - Reference Document Overview -

    - You will find a list of reference document versions below that can be viewed. - -
    - {{ govukTable({ "head": reference_document_versions_headers, "rows": reference_documents_versions }) }} -
    -{% endblock %} - - - diff --git a/reference_documents/models.py b/reference_documents/models.py index 9e7987320..001990da8 100644 --- a/reference_documents/models.py +++ b/reference_documents/models.py @@ -1,5 +1,4 @@ from django.db import models -from django.db.models import fields from django_fsm import FSMField @@ -13,8 +12,6 @@ class ReferenceDocumentVersionStatus(models.TextChoices): class ReferenceDocument(models.Model): - created_at = models.DateTimeField(auto_now_add=True) - title = models.CharField( max_length=255, help_text="Short name for this workbasket", diff --git a/reference_documents/views.py b/reference_documents/views.py index 8ccd5b7e4..7095b8e78 100644 --- a/reference_documents/views.py +++ b/reference_documents/views.py @@ -1,17 +1,11 @@ from django.contrib.auth.mixins import PermissionRequiredMixin -<<<<<<< HEAD from django.views.generic import DetailView -======= ->>>>>>> eb2cd348 (initial commit - ref doc data model) from django.views.generic import ListView from geo_areas.models import GeographicalArea from geo_areas.models import GeographicalAreaDescription from reference_documents.models import ReferenceDocument -<<<<<<< HEAD from reference_documents.models import ReferenceDocumentVersion -======= ->>>>>>> eb2cd348 (initial commit - ref doc data model) class ReferenceDocumentList(PermissionRequiredMixin, ListView): From 641cfe3b7aa136e4a1ac2e187e9a2db7cc894640 Mon Sep 17 00:00:00 2001 From: Doug Mills Date: Tue, 6 Feb 2024 08:29:21 +0000 Subject: [PATCH 045/118] initial commit - ref doc data model --- .../jinja2/reference_documents/overview.jinja | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 reference_documents/jinja2/reference_documents/overview.jinja diff --git a/reference_documents/jinja2/reference_documents/overview.jinja b/reference_documents/jinja2/reference_documents/overview.jinja new file mode 100644 index 000000000..fc442ceb7 --- /dev/null +++ b/reference_documents/jinja2/reference_documents/overview.jinja @@ -0,0 +1,24 @@ +{% extends "layouts/layout.jinja" %} +{% from "components/table/macro.njk" import govukTable %} + +{% set page_title = 'Reference Documents versions Overview' %} + +{% block breadcrumb %} + {{ breadcrumbs(request, [ + {'text': "Reference Documents"} + ]) }} +{% endblock %} + +{% block content %} +

    + Reference Document Overview +

    + You will find a list of reference document versions below that can be viewed. + +
    + {{ govukTable({ "head": reference_document_versions_headers, "rows": reference_documents_versions }) }} +
    +{% endblock %} + + + From af15b0dbdd9f3c47ab7fd38b6ef8ce2c37624990 Mon Sep 17 00:00:00 2001 From: Doug Mills Date: Wed, 14 Feb 2024 12:58:21 +0000 Subject: [PATCH 046/118] wip commit --- .../migrations/0001_initial.py | 100 ++++++++++++++++-- .../0002_referencedocument_area_id.py | 19 ---- .../migrations/0003_auto_20240201_0940.py | 33 ------ .../migrations/0004_auto_20240202_0936.py | 23 ---- .../migrations/0005_auto_20240202_1431.py | 41 ------- .../0006_alter_preferentialquota_volume.py | 18 ---- 6 files changed, 90 insertions(+), 144 deletions(-) delete mode 100644 reference_documents/migrations/0002_referencedocument_area_id.py delete mode 100644 reference_documents/migrations/0003_auto_20240201_0940.py delete mode 100644 reference_documents/migrations/0004_auto_20240202_0936.py delete mode 100644 reference_documents/migrations/0005_auto_20240202_1431.py delete mode 100644 reference_documents/migrations/0006_alter_preferentialquota_volume.py diff --git a/reference_documents/migrations/0001_initial.py b/reference_documents/migrations/0001_initial.py index 64b49e847..c56b2b4c0 100644 --- a/reference_documents/migrations/0001_initial.py +++ b/reference_documents/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 3.2.23 on 2024-01-29 14:52 +# Generated by Django 3.2.23 on 2024-02-14 11:45 import django.db.models.deletion import django_fsm @@ -12,18 +12,34 @@ class Migration(migrations.Migration): dependencies = [] operations = [ + migrations.CreateModel( + name="AlignmentReport", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("created_at", models.DateTimeField(auto_now_add=True)), + ], + ), migrations.CreateModel( name="ReferenceDocument", fields=[ ( "id", - models.BigAutoField( + models.AutoField( auto_created=True, primary_key=True, serialize=False, verbose_name="ID", ), ), + ("created_at", models.DateTimeField(auto_now_add=True)), ( "title", models.CharField( @@ -33,6 +49,7 @@ class Migration(migrations.Migration): unique=True, ), ), + ("area_id", models.CharField(db_index=True, max_length=4)), ], ), migrations.CreateModel( @@ -40,16 +57,18 @@ class Migration(migrations.Migration): fields=[ ( "id", - models.BigAutoField( + models.AutoField( auto_created=True, primary_key=True, serialize=False, verbose_name="ID", ), ), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), ("version", models.FloatField()), - ("published_date", models.DateField()), - ("entry_into_force_date", models.DateField()), + ("published_date", models.DateField(blank=True, null=True)), + ("entry_into_force_date", models.DateField(blank=True, null=True)), ( "status", django_fsm.FSMField( @@ -79,7 +98,7 @@ class Migration(migrations.Migration): fields=[ ( "id", - models.BigAutoField( + models.AutoField( auto_created=True, primary_key=True, serialize=False, @@ -89,6 +108,10 @@ class Migration(migrations.Migration): ("commodity_code", models.CharField(db_index=True, max_length=10)), ("duty_rate", models.CharField(max_length=255)), ("order", models.IntegerField()), + ("valid_start_day", models.IntegerField(blank=True, null=True)), + ("valid_start_month", models.IntegerField(blank=True, null=True)), + ("valid_end_day", models.IntegerField(blank=True, null=True)), + ("valid_end_month", models.IntegerField(blank=True, null=True)), ( "reference_document_version", models.ForeignKey( @@ -104,7 +127,7 @@ class Migration(migrations.Migration): fields=[ ( "id", - models.BigAutoField( + models.AutoField( auto_created=True, primary_key=True, serialize=False, @@ -114,9 +137,11 @@ class Migration(migrations.Migration): ("quota_order_number", models.CharField(db_index=True, max_length=6)), ("commodity_code", models.CharField(db_index=True, max_length=10)), ("quota_duty_rate", models.CharField(max_length=255)), - ("volume", models.FloatField()), - ("quota_period_open", models.DateField()), - ("quota_period_close", models.DateField()), + ("volume", models.CharField(max_length=255)), + ("valid_start_day", models.IntegerField(blank=True, null=True)), + ("valid_start_month", models.IntegerField(blank=True, null=True)), + ("valid_end_day", models.IntegerField(blank=True, null=True)), + ("valid_end_month", models.IntegerField(blank=True, null=True)), ("measurement", models.CharField(max_length=255)), ("order", models.IntegerField()), ( @@ -129,4 +154,59 @@ class Migration(migrations.Migration): ), ], ), + migrations.CreateModel( + name="AlignmentReportCheck", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("check_name", models.CharField(max_length=255)), + ("successful", models.BooleanField()), + ("message", models.TextField(null=True)), + ( + "alignment_report", + models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, + related_name="alignment_report_checks", + to="reference_documents.alignmentreport", + ), + ), + ( + "preferential_quota", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.PROTECT, + related_name="alignment_report_checks", + to="reference_documents.preferentialquota", + ), + ), + ( + "preferential_rate", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.PROTECT, + related_name="alignment_report_checks", + to="reference_documents.preferentialrate", + ), + ), + ], + ), + migrations.AddField( + model_name="alignmentreport", + name="reference_document_version", + field=models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, + related_name="alignment_reports", + to="reference_documents.referencedocumentversion", + ), + ), ] diff --git a/reference_documents/migrations/0002_referencedocument_area_id.py b/reference_documents/migrations/0002_referencedocument_area_id.py deleted file mode 100644 index 18f449577..000000000 --- a/reference_documents/migrations/0002_referencedocument_area_id.py +++ /dev/null @@ -1,19 +0,0 @@ -# Generated by Django 3.2.23 on 2024-01-30 16:02 - -from django.db import migrations -from django.db import models - - -class Migration(migrations.Migration): - dependencies = [ - ("reference_documents", "0001_initial"), - ] - - operations = [ - migrations.AddField( - model_name="referencedocument", - name="area_id", - field=models.CharField(db_index=True, default="ZZ", max_length=4), - preserve_default=False, - ), - ] diff --git a/reference_documents/migrations/0003_auto_20240201_0940.py b/reference_documents/migrations/0003_auto_20240201_0940.py deleted file mode 100644 index f7b8ec51d..000000000 --- a/reference_documents/migrations/0003_auto_20240201_0940.py +++ /dev/null @@ -1,33 +0,0 @@ -# Generated by Django 3.2.23 on 2024-02-01 09:40 - -from django.db import migrations -from django.db import models - - -class Migration(migrations.Migration): - dependencies = [ - ("reference_documents", "0002_referencedocument_area_id"), - ] - - operations = [ - migrations.AddField( - model_name="preferentialrate", - name="valid_end_day", - field=models.IntegerField(blank=True, null=True), - ), - migrations.AddField( - model_name="preferentialrate", - name="valid_end_month", - field=models.IntegerField(blank=True, null=True), - ), - migrations.AddField( - model_name="preferentialrate", - name="valid_start_day", - field=models.IntegerField(blank=True, null=True), - ), - migrations.AddField( - model_name="preferentialrate", - name="valid_start_month", - field=models.IntegerField(blank=True, null=True), - ), - ] diff --git a/reference_documents/migrations/0004_auto_20240202_0936.py b/reference_documents/migrations/0004_auto_20240202_0936.py deleted file mode 100644 index c59bbfbdb..000000000 --- a/reference_documents/migrations/0004_auto_20240202_0936.py +++ /dev/null @@ -1,23 +0,0 @@ -# Generated by Django 3.2.23 on 2024-02-02 09:36 - -from django.db import migrations -from django.db import models - - -class Migration(migrations.Migration): - dependencies = [ - ("reference_documents", "0003_auto_20240201_0940"), - ] - - operations = [ - migrations.AlterField( - model_name="referencedocumentversion", - name="entry_into_force_date", - field=models.DateField(blank=True, null=True), - ), - migrations.AlterField( - model_name="referencedocumentversion", - name="published_date", - field=models.DateField(blank=True, null=True), - ), - ] diff --git a/reference_documents/migrations/0005_auto_20240202_1431.py b/reference_documents/migrations/0005_auto_20240202_1431.py deleted file mode 100644 index 7787303df..000000000 --- a/reference_documents/migrations/0005_auto_20240202_1431.py +++ /dev/null @@ -1,41 +0,0 @@ -# Generated by Django 3.2.23 on 2024-02-02 14:31 - -from django.db import migrations -from django.db import models - - -class Migration(migrations.Migration): - dependencies = [ - ("reference_documents", "0004_auto_20240202_0936"), - ] - - operations = [ - migrations.RemoveField( - model_name="preferentialquota", - name="quota_period_close", - ), - migrations.RemoveField( - model_name="preferentialquota", - name="quota_period_open", - ), - migrations.AddField( - model_name="preferentialquota", - name="valid_end_day", - field=models.IntegerField(blank=True, null=True), - ), - migrations.AddField( - model_name="preferentialquota", - name="valid_end_month", - field=models.IntegerField(blank=True, null=True), - ), - migrations.AddField( - model_name="preferentialquota", - name="valid_start_day", - field=models.IntegerField(blank=True, null=True), - ), - migrations.AddField( - model_name="preferentialquota", - name="valid_start_month", - field=models.IntegerField(blank=True, null=True), - ), - ] diff --git a/reference_documents/migrations/0006_alter_preferentialquota_volume.py b/reference_documents/migrations/0006_alter_preferentialquota_volume.py deleted file mode 100644 index 1f898a734..000000000 --- a/reference_documents/migrations/0006_alter_preferentialquota_volume.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 3.2.23 on 2024-02-02 14:48 - -from django.db import migrations -from django.db import models - - -class Migration(migrations.Migration): - dependencies = [ - ("reference_documents", "0005_auto_20240202_1431"), - ] - - operations = [ - migrations.AlterField( - model_name="preferentialquota", - name="volume", - field=models.CharField(max_length=255), - ), - ] From d1d441260d7c5d250bb11ad30cc9c64b534e6f52 Mon Sep 17 00:00:00 2001 From: Doug Mills Date: Tue, 20 Feb 2024 11:26:44 +0000 Subject: [PATCH 047/118] added alignment report, reference document and reference document version views, refactored the checks and ran the checks several times against reference document versions. --- common/static/common/scss/application.scss | 1 + reference_documents/checks/base.py | 175 ++++++++++++++++++ reference_documents/checks/check_runner.py | 43 +++++ .../checks/preferential_quotas.py | 1 + .../checks/preferential_rates.py | 45 +++++ reference_documents/checks/utils.py | 21 +++ .../jinja2/alignment_reports/details.jinja | 25 +++ .../alignment_reports.jinja | 25 +++ .../reference_document_versions/details.jinja | 2 +- .../migrations/0002_auto_20240215_1056.py | 32 ++++ .../migrations/0003_auto_20240219_0951.py | 36 ++++ reference_documents/models.py | 23 ++- .../scss/_reference_documents.scss | 8 + reference_documents/urls.py | 15 ++ reference_documents/views.py | 136 +++++++++++++- webpack.config.js | 1 + 16 files changed, 581 insertions(+), 8 deletions(-) create mode 100644 reference_documents/checks/base.py create mode 100644 reference_documents/checks/check_runner.py create mode 100644 reference_documents/checks/preferential_quotas.py create mode 100644 reference_documents/checks/preferential_rates.py create mode 100644 reference_documents/checks/utils.py create mode 100644 reference_documents/jinja2/alignment_reports/details.jinja create mode 100644 reference_documents/jinja2/reference_document_versions/alignment_reports.jinja create mode 100644 reference_documents/migrations/0002_auto_20240215_1056.py create mode 100644 reference_documents/migrations/0003_auto_20240219_0951.py create mode 100644 reference_documents/static/reference_documents/scss/_reference_documents.scss diff --git a/common/static/common/scss/application.scss b/common/static/common/scss/application.scss index 3ac96053c..b9cc920eb 100644 --- a/common/static/common/scss/application.scss +++ b/common/static/common/scss/application.scss @@ -27,6 +27,7 @@ $govuk-image-url-function: frontend-image-url; @import "publishing"; @import "regulations"; @import "workbaskets"; +@import "reference_documents"; @import "versions"; @import "fake-link"; @import "violations"; diff --git a/reference_documents/checks/base.py b/reference_documents/checks/base.py new file mode 100644 index 000000000..2f1c6ceb0 --- /dev/null +++ b/reference_documents/checks/base.py @@ -0,0 +1,175 @@ +from datetime import date + +from commodities.models import GoodsNomenclature +from commodities.models.dc import CommodityCollectionLoader +from commodities.models.dc import CommodityTreeSnapshot +from commodities.models.dc import SnapshotMoment +from common.models import Transaction +from geo_areas.models import GeographicalArea +from geo_areas.models import GeographicalAreaDescription +from reference_documents.models import PreferentialQuota +from reference_documents.models import PreferentialRate + + +class BaseCheck: + def __init__(self): + self.dependent_on_passing_check = None + + def run_check(self): + raise NotImplemented("Please implement on child classes") + + +class BasePreferentialQuotaCheck(BaseCheck): + def __init__(self, preferential_quota: PreferentialQuota): + super().__init__() + self.preferential_quota = preferential_quota + + +class BasePreferentialRateCheck(BaseCheck): + def __init__(self, preferential_rate: PreferentialRate): + super().__init__() + self.preferential_rate = preferential_rate + + def get_snapshot(self) -> CommodityTreeSnapshot: + # not liking having to use CommodityTreeSnapshot, but it does to the job + item_id = self.comm_code().item_id + while item_id[-2:] == "00": + item_id = item_id[0 : len(item_id) - 2] + + commodities_collection = CommodityCollectionLoader( + prefix=item_id, + ).load(current_only=True) + + latest_transaction = Transaction.objects.order_by("created_at").last() + + snapshot = CommodityTreeSnapshot( + commodities=commodities_collection.commodities, + moment=SnapshotMoment(transaction=latest_transaction), + ) + + return snapshot + + def comm_code(self): + goods = GoodsNomenclature.objects.latest_approved().filter( + item_id=self.preferential_rate.commodity_code, + valid_between__contains=self.ref_doc_version_eif_date(), + suffix=80, + ) + + if len(goods) == 0: + return None + + return goods.first() + + def geo_area(self): + return ( + GeographicalArea.objects.latest_approved() + .filter( + area_id=self.preferential_rate.reference_document_version.reference_document.area_id, + ) + .first() + ) + + def geo_area_description(self): + geo_area_desc = ( + GeographicalAreaDescription.objects.latest_approved() + .filter(described_geographicalarea=self.geo_area()) + .last() + ) + return geo_area_desc.description + + def ref_doc_version_eif_date(self): + eif_date = ( + self.preferential_rate.reference_document_version.entry_into_force_date + ) + + # todo : make sure EIf dates are all populated correctly - and remove this + if eif_date is None: + eif_date = date.today() + + return eif_date + + def related_measures(self, comm_code_item_id=None): + if comm_code_item_id: + good = GoodsNomenclature.objects.latest_approved().filter( + item_id=comm_code_item_id, + valid_between__contains=self.ref_doc_version_eif_date(), + suffix=80, + ) + + if len(good) == 1: + return ( + good.first() + .measures.latest_approved() + .filter( + geographical_area=self.geo_area(), + valid_between__contains=self.ref_doc_version_eif_date(), + measure_type__sid__in=[ + 142, + 143, + ], # note : these are the measure types used to identify preferential tariffs + ) + ) + else: + return [] + else: + return ( + self.comm_code() + .measures.latest_approved() + .filter( + geographical_area=self.geo_area(), + valid_between__contains=self.ref_doc_version_eif_date(), + measure_type__sid__in=[ + 142, + 143, + ], # note : these are the measure types used to identify preferential tariffs + ) + ) + + def recursive_comm_code_check( + self, + snapshot: CommodityTreeSnapshot, + parent_item_id, + parent_item_suffix, + level=1, + ): + # find comm code from snapshot + child_commodities = [] + for commodity in snapshot.commodities: + if ( + commodity.item_id == parent_item_id + and commodity.suffix == parent_item_suffix + ): + child_commodities = snapshot.get_children(commodity) + break + + if len(child_commodities) == 0: + print(f'{"-" * level} no more children') + return False + + results = [] + for child_commodity in child_commodities: + related_measures = self.related_measures(child_commodity.item_id) + + if len(related_measures) == 0: + print(f'{"-" * level} FAIL : {child_commodity.item_id}') + results.append( + self.recursive_comm_code_check( + snapshot, + child_commodity.item_id, + child_commodity.suffix, + level + 1, + ), + ) + elif len(related_measures) == 1: + print(f'{"-" * level} PASS : {child_commodity.item_id}') + results.append(True) + else: + # Multiple measures + print(f'{"-" * level} PASS : multiple : {child_commodity.item_id}') + results.append(True) + + return False in results + + def run_check(self): + raise NotImplementedError("Please implement on child classes") diff --git a/reference_documents/checks/check_runner.py b/reference_documents/checks/check_runner.py new file mode 100644 index 000000000..43d6c1223 --- /dev/null +++ b/reference_documents/checks/check_runner.py @@ -0,0 +1,43 @@ +from reference_documents.checks.base import BasePreferentialQuotaCheck +from reference_documents.checks.base import BasePreferentialRateCheck +from reference_documents.checks.preferential_quotas import * # noqa +from reference_documents.checks.preferential_rates import * # noqa +from reference_documents.checks.utils import utils +from reference_documents.models import AlignmentReport +from reference_documents.models import AlignmentReportCheck +from reference_documents.models import ReferenceDocumentVersion + + +class Checks: + def __init__(self, reference_document_version: ReferenceDocumentVersion): + self.reference_document_version = reference_document_version + self.alignment_report = AlignmentReport.objects.create( + reference_document_version=self.reference_document_version, + ) + + @staticmethod + def get_checks_for(check_class): + return utils().get_child_checks(check_class) + + def run(self): + for check in Checks.get_checks_for(BasePreferentialRateCheck): + for pref_rate in self.reference_document_version.preferential_rates.all(): + self.capture_check_result(check(pref_rate), pref_rate=pref_rate) + + for check in Checks.get_checks_for(BasePreferentialQuotaCheck): + for pref_quota in self.reference_document_version.preferential_quotas.all(): + self.capture_check_result(check(pref_quota), pref_quota=pref_quota) + + def capture_check_result(self, check, pref_rate=None, pref_quota=None): + status, message = check.run_check() + + kwargs = { + "alignment_report": self.alignment_report, + "check_name": check.__class__.__name__, + "preferential_rate": pref_rate, + "preferential_quota": pref_quota, + "status": status, + "message": message, + } + + AlignmentReportCheck.objects.create(**kwargs) diff --git a/reference_documents/checks/preferential_quotas.py b/reference_documents/checks/preferential_quotas.py new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/reference_documents/checks/preferential_quotas.py @@ -0,0 +1 @@ + diff --git a/reference_documents/checks/preferential_rates.py b/reference_documents/checks/preferential_rates.py new file mode 100644 index 000000000..840b33b21 --- /dev/null +++ b/reference_documents/checks/preferential_rates.py @@ -0,0 +1,45 @@ +from reference_documents.checks.base import BasePreferentialRateCheck +from reference_documents.models import AlignmentReportCheckStatus + + +class MeasureExists(BasePreferentialRateCheck): + def run_check(self): + # comm code live on EIF date + if not self.comm_code(): + message = f"{self.preferential_rate.commodity_code} {self.geo_area_description()} comm code not live" + print("FAIL", message) + return AlignmentReportCheckStatus.FAIL, message + + # query measures + measures = self.related_measures() + + # this is ok - there is a single measure matching the expected query + if len(measures) == 1: + return AlignmentReportCheckStatus.PASS, "" + + # this is not inline with expected measures presence - check comm code children + if len(measures) == 0: + # check 1 level down for presence of measures + match = self.recursive_comm_code_check( + self.get_snapshot(), + self.preferential_rate.commodity_code, + 80, + ) + + message = f"{self.comm_code()}, {self.geo_area_description()}" + + if match: + message += f"\nmatched with children" + print("PASS", message) + + return AlignmentReportCheckStatus.PASS, message + else: + message += f"\nno expected measures found on good code or children" + print("FAIL", message) + + return AlignmentReportCheckStatus.FAIL, message + + if len(measures) > 1: + message = f"multiple measures match {self.comm_code()}, {self.geo_area_description()}" + print("WARNING", message) + return AlignmentReportCheckStatus.WARNING, message diff --git a/reference_documents/checks/utils.py b/reference_documents/checks/utils.py new file mode 100644 index 000000000..242f15289 --- /dev/null +++ b/reference_documents/checks/utils.py @@ -0,0 +1,21 @@ +from reference_documents.checks.base import BaseCheck + + +class utils: + def get_child_checks(self, check_class: BaseCheck.__class__): + result = [] + + check_classes = self.subclasses_for(check_class) + for check_class in check_classes: + result.append(check_class) + + return result + + def subclasses_for(self, cls) -> list: + all_subclasses = [] + + for subclass in cls.__subclasses__(): + all_subclasses.append(subclass) + all_subclasses.extend(self.subclasses_for(subclass)) + + return all_subclasses diff --git a/reference_documents/jinja2/alignment_reports/details.jinja b/reference_documents/jinja2/alignment_reports/details.jinja new file mode 100644 index 000000000..59de1a733 --- /dev/null +++ b/reference_documents/jinja2/alignment_reports/details.jinja @@ -0,0 +1,25 @@ +{% extends "layouts/layout.jinja" %} +{% from "components/table/macro.njk" import govukTable %} + +{% set page_title = 'Reference Documents version details' %} + +{% block breadcrumb %} + {{ breadcrumbs(request, [ + {'text': "Reference Document Version Overview"} + ]) }} +{% endblock %} + +{% block content %} +

    + Alignment reports +

    + Reference document version details. + +
    + {{ govukTable({ "head": alignment_report_headers, "rows": alignment_reports }) }} +
    + +{% endblock %} + + + diff --git a/reference_documents/jinja2/reference_document_versions/alignment_reports.jinja b/reference_documents/jinja2/reference_document_versions/alignment_reports.jinja new file mode 100644 index 000000000..59de1a733 --- /dev/null +++ b/reference_documents/jinja2/reference_document_versions/alignment_reports.jinja @@ -0,0 +1,25 @@ +{% extends "layouts/layout.jinja" %} +{% from "components/table/macro.njk" import govukTable %} + +{% set page_title = 'Reference Documents version details' %} + +{% block breadcrumb %} + {{ breadcrumbs(request, [ + {'text': "Reference Document Version Overview"} + ]) }} +{% endblock %} + +{% block content %} +

    + Alignment reports +

    + Reference document version details. + +
    + {{ govukTable({ "head": alignment_report_headers, "rows": alignment_reports }) }} +
    + +{% endblock %} + + + diff --git a/reference_documents/jinja2/reference_document_versions/details.jinja b/reference_documents/jinja2/reference_document_versions/details.jinja index c2cc712d8..7f32bbb29 100644 --- a/reference_documents/jinja2/reference_document_versions/details.jinja +++ b/reference_documents/jinja2/reference_document_versions/details.jinja @@ -5,7 +5,7 @@ {% block breadcrumb %} {{ breadcrumbs(request, [ - {'text': "Reference Documents"} + {'text': "Reference Document Version"} ]) }} {% endblock %} diff --git a/reference_documents/migrations/0002_auto_20240215_1056.py b/reference_documents/migrations/0002_auto_20240215_1056.py new file mode 100644 index 000000000..f2d8b7bb1 --- /dev/null +++ b/reference_documents/migrations/0002_auto_20240215_1056.py @@ -0,0 +1,32 @@ +# Generated by Django 3.2.23 on 2024-02-15 10:56 + +import django_fsm +from django.db import migrations + + +class Migration(migrations.Migration): + dependencies = [ + ("reference_documents", "0001_initial"), + ] + + operations = [ + migrations.RemoveField( + model_name="alignmentreportcheck", + name="successful", + ), + migrations.AddField( + model_name="alignmentreportcheck", + name="status", + field=django_fsm.FSMField( + choices=[ + ("PASS", "Passing"), + ("FAIL", "Failed"), + ("WARNING", "Warning"), + ], + db_index=True, + default="FAIL", + editable=False, + max_length=50, + ), + ), + ] diff --git a/reference_documents/migrations/0003_auto_20240219_0951.py b/reference_documents/migrations/0003_auto_20240219_0951.py new file mode 100644 index 000000000..5d621f081 --- /dev/null +++ b/reference_documents/migrations/0003_auto_20240219_0951.py @@ -0,0 +1,36 @@ +# Generated by Django 3.2.23 on 2024-02-19 09:51 + +import django.db.models.deletion +from django.db import migrations +from django.db import models + + +class Migration(migrations.Migration): + dependencies = [ + ("reference_documents", "0002_auto_20240215_1056"), + ] + + operations = [ + migrations.AlterField( + model_name="alignmentreportcheck", + name="preferential_quota", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.PROTECT, + related_name="preferential_quota_checks", + to="reference_documents.preferentialquota", + ), + ), + migrations.AlterField( + model_name="alignmentreportcheck", + name="preferential_rate", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.PROTECT, + related_name="preferential_rate_checks", + to="reference_documents.preferentialrate", + ), + ), + ] diff --git a/reference_documents/models.py b/reference_documents/models.py index 001990da8..e914d3120 100644 --- a/reference_documents/models.py +++ b/reference_documents/models.py @@ -11,6 +11,15 @@ class ReferenceDocumentVersionStatus(models.TextChoices): PUBLISHED = "PUBLISHED", "Published" +class AlignmentReportCheckStatus(models.TextChoices): + # Reference document version can be edited + PASS = "PASS", "Passing" + # Reference document version ius locked and in review + FAIL = "FAIL", "Failed" + # reference document version has been approved and published + WARNING = "WARNING", "Warning" + + class ReferenceDocument(models.Model): title = models.CharField( max_length=255, @@ -123,16 +132,20 @@ class AlignmentReportCheck(models.Model): check_name = fields.CharField(max_length=255) """A string identifying the type of check carried out.""" - successful = fields.BooleanField() - """True if the check was successful.""" - + status = FSMField( + default=AlignmentReportCheckStatus.FAIL, + choices=AlignmentReportCheckStatus.choices, + db_index=True, + protected=False, + editable=False, + ) message = fields.TextField(null=True) """The text content returned by the check, if any.""" preferential_quota = models.ForeignKey( "reference_documents.PreferentialQuota", on_delete=models.PROTECT, - related_name="alignment_report_checks", + related_name="preferential_quota_checks", blank=True, null=True, ) @@ -140,7 +153,7 @@ class AlignmentReportCheck(models.Model): preferential_rate = models.ForeignKey( "reference_documents.PreferentialRate", on_delete=models.PROTECT, - related_name="alignment_report_checks", + related_name="preferential_rate_checks", blank=True, null=True, ) diff --git a/reference_documents/static/reference_documents/scss/_reference_documents.scss b/reference_documents/static/reference_documents/scss/_reference_documents.scss new file mode 100644 index 000000000..59f254e15 --- /dev/null +++ b/reference_documents/static/reference_documents/scss/_reference_documents.scss @@ -0,0 +1,8 @@ +.check-passing { + color: #1d640f; + font-weight: bold; +} +.check-failing { + color: #671111; + font-weight: bold; +} \ No newline at end of file diff --git a/reference_documents/urls.py b/reference_documents/urls.py index 84fcb43ff..782551173 100644 --- a/reference_documents/urls.py +++ b/reference_documents/urls.py @@ -20,6 +20,11 @@ name="reference_documents-ui-detail", ), path("reference_documents/", views.ReferenceDocumentList.as_view(), name="index"), + path( + "reference_documents/", + views.ReferenceDocumentList.as_view(), + name="index", + ), path( "reference_documents//", views.ReferenceDocumentDetails.as_view(), @@ -30,4 +35,14 @@ views.ReferenceDocumentVersionDetails.as_view(), name="version_details", ), + path( + "reference_document_version_alignment_reports//", + views.ReferenceDocumentVersionAlignmentReportsDetailsView.as_view(), + name="reference_document_version_alignment_reports", + ), + path( + "alignment_reports//", + views.AlignmentReportsDetailsView.as_view(), + name="alignment_reports", + ), ] diff --git a/reference_documents/views.py b/reference_documents/views.py index 7095b8e78..8173d4a14 100644 --- a/reference_documents/views.py +++ b/reference_documents/views.py @@ -4,6 +4,8 @@ from geo_areas.models import GeographicalArea from geo_areas.models import GeographicalAreaDescription +from reference_documents.models import AlignmentReport +from reference_documents.models import AlignmentReportCheckStatus from reference_documents.models import ReferenceDocument from reference_documents.models import ReferenceDocumentVersion @@ -211,7 +213,8 @@ def get_context_data(self, *args, **kwargs): "text": version.preferential_quotas.count(), }, { - "html": f'version details', + "html": f'version details
    ' + f'Alignment reports', }, ], ) @@ -236,6 +239,7 @@ def get_context_data(self, *args, **kwargs): {"text": "Comm Code"}, {"text": "Duty Rate"}, {"text": "Validity"}, + {"text": "Checks"}, {"text": "Actions"}, ] @@ -245,13 +249,14 @@ def get_context_data(self, *args, **kwargs): {"text": "Rate"}, {"text": "Volume"}, {"text": "Validity"}, + {"text": "Checks"}, {"text": "Actions"}, ] reference_document_version_duties = [] reference_document_version_quotas = [] - print(self.request) + latest_alignment_report = context["object"].alignment_reports.last() for duty in context["object"].preferential_rates.order_by("order"): validity = "" @@ -259,6 +264,25 @@ def get_context_data(self, *args, **kwargs): if duty.valid_start_day: validity = f"{duty.valid_start_day}/{duty.valid_start_month} - {duty.valid_end_day}/{duty.valid_end_month}" + failure_count = ( + duty.preferential_rate_checks.all() + .filter( + alignment_report=latest_alignment_report, + status=AlignmentReportCheckStatus.FAIL, + ) + .count() + ) + check_count = ( + duty.preferential_rate_checks.all() + .filter(alignment_report=latest_alignment_report) + .count() + ) + + if failure_count > 0: + checks_output = f'
    FAILED {failure_count} of {check_count}
    ' + else: + checks_output = f'
    PASSED {check_count} of {check_count}
    ' + reference_document_version_duties.append( [ { @@ -270,6 +294,9 @@ def get_context_data(self, *args, **kwargs): { "text": validity, }, + { + "html": checks_output, + }, { "text": "", }, @@ -282,6 +309,25 @@ def get_context_data(self, *args, **kwargs): if quota.valid_start_day: validity = f"{quota.valid_start_day}/{quota.valid_start_month} - {quota.valid_end_day}/{quota.valid_end_month}" + failure_count = ( + quota.preferential_quota_checks.all() + .filter( + alignment_report=latest_alignment_report, + status=AlignmentReportCheckStatus.FAIL, + ) + .count() + ) + check_count = ( + quota.preferential_quota_checks.all() + .filter(alignment_report=latest_alignment_report) + .count() + ) + + if failure_count > 0: + checks_output = f'
    FAILED {failure_count} of {check_count}
    ' + else: + checks_output = f'
    PASSED {check_count} of {check_count}
    ' + reference_document_version_quotas.append( [ { @@ -299,6 +345,9 @@ def get_context_data(self, *args, **kwargs): { "text": validity, }, + { + "html": checks_output, + }, { "text": "", }, @@ -310,3 +359,86 @@ def get_context_data(self, *args, **kwargs): context["reference_document_version_quotas"] = reference_document_version_quotas return context + + +class ReferenceDocumentVersionAlignmentReportsDetailsView( + PermissionRequiredMixin, + DetailView, +): + template_name = "reference_document_versions/alignment_reports.jinja" + permission_required = "reference_documents.view_reference_document" + model = ReferenceDocumentVersion + + def get_context_data(self, *args, **kwargs): + context = super( + ReferenceDocumentVersionAlignmentReportsDetailsView, + self, + ).get_context_data( + *args, + **kwargs, + ) + + context["alignment_report_headers"] = [ + {"text": "Created"}, + {"text": "Passed"}, + {"text": "failed"}, + {"text": "Percent"}, + {"text": "Actions"}, + ] + + alignment_reports = [] + for report in context["object"].alignment_reports.order_by("-created_at"): + failure_count = ( + report.alignment_report_checks.all() + .filter(status=AlignmentReportCheckStatus.FAIL) + .count() + ) + pass_count = ( + report.alignment_report_checks.all() + .filter(status=AlignmentReportCheckStatus.PASS) + .count() + ) + + if pass_count > 0: + pass_percentage = round( + (pass_count / (pass_count + failure_count)) * 100, + 2, + ) + else: + pass_percentage = 100 + + alignment_reports.append( + [ + { + "text": report.created_at.strftime("%d/%m/%Y %H:%M"), + }, + { + "text": pass_count, + }, + { + "text": failure_count, + }, + { + "text": f"{pass_percentage} %", + }, + { + "html": f"Details", + }, + ], + ) + + context["alignment_reports"] = alignment_reports + + return context + + +class AlignmentReportsDetailsView(PermissionRequiredMixin, DetailView): + template_name = "alignment_reports/details.jinja" + permission_required = "reference_documents.view_reference_document" + model = AlignmentReport + + def get_context_data(self, *args, **kwargs): + context = super(AlignmentReportsDetailsView, self).get_context_data( + *args, + **kwargs, + ) diff --git a/webpack.config.js b/webpack.config.js index 88c149abb..b900d501f 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -80,6 +80,7 @@ module.exports = { 'publishing/static/publishing/scss', 'regulations/static/regulations/scss', 'workbaskets/static/workbaskets/scss', + 'reference_documents/static/reference_documents/scss', ], }, }, From 8fe80b81fe50a546834f1ae1c9205c6c11d1673b Mon Sep 17 00:00:00 2001 From: Doug Mills Date: Tue, 20 Feb 2024 15:14:11 +0000 Subject: [PATCH 048/118] added alignment report, reference document and reference document version views, refactored the checks and ran the checks several times against reference document versions. --- reference_documents/urls.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/reference_documents/urls.py b/reference_documents/urls.py index 782551173..875faa942 100644 --- a/reference_documents/urls.py +++ b/reference_documents/urls.py @@ -1,25 +1,21 @@ from django.urls import path from rest_framework import routers -from reference_documents import views - -app_name = "reference_documents" +from reference_documents import views api_router = routers.DefaultRouter() - urlpatterns = [ path( - "reference-documents/", + "reference-documents-example/", views.ReferenceDocumentsListView.as_view(), name="reference_documents-ui-list", ), path( - f"reference-documents/albania/", + f"reference-documents-example-albania/", views.ReferenceDocumentsDetailView.as_view(), name="reference_documents-ui-detail", ), - path("reference_documents/", views.ReferenceDocumentList.as_view(), name="index"), path( "reference_documents/", views.ReferenceDocumentList.as_view(), From fef34d9dfa504941bfd35e649aefb1d549b68eb0 Mon Sep 17 00:00:00 2001 From: Doug Mills Date: Wed, 21 Feb 2024 14:57:10 +0000 Subject: [PATCH 049/118] prep for merge to mega branch --- common/forms.py | 9 +- .../{workbasket_action.jinja => index.jinja} | 0 common/views.py | 5 +- .../includes/tabs/preferential_quotas.jinja | 23 ++ .../includes/tabs/preferential_rates.jinja | 20 ++ .../details.jinja} | 0 .../index.jinja} | 2 +- .../new_details.jinja | 47 ++++ reference_documents/urls.py | 29 ++- reference_documents/views/example_views.py | 110 ++++++++++ .../views/reference_document_version_views.py | 205 ++++++++++++++++++ .../views/reference_document_views.py | 122 +++++++++++ settings/common.py | 1 - urls.py | 1 - 14 files changed, 557 insertions(+), 17 deletions(-) rename common/jinja2/common/{workbasket_action.jinja => index.jinja} (100%) create mode 100644 reference_documents/jinja2/includes/tabs/preferential_quotas.jinja create mode 100644 reference_documents/jinja2/includes/tabs/preferential_rates.jinja rename reference_documents/jinja2/{reference_documents/detail.jinja => reference_document_examples/details.jinja} (100%) rename reference_documents/jinja2/{reference_documents/list.jinja => reference_document_examples/index.jinja} (92%) create mode 100644 reference_documents/jinja2/reference_document_versions/new_details.jinja create mode 100644 reference_documents/views/example_views.py create mode 100644 reference_documents/views/reference_document_version_views.py create mode 100644 reference_documents/views/reference_document_views.py diff --git a/common/forms.py b/common/forms.py index 8e8bd4a28..76428cdf9 100644 --- a/common/forms.py +++ b/common/forms.py @@ -250,8 +250,12 @@ class HMRCCDSManagerActions(TextChoices): class CommonUserActions(TextChoices): SEARCH = "SEARCH", "Search the tariff" + + +class ReferenceDocumentsActions(TextChoices): # Change this to be dependent on permissions later - VIEW_REF_DOCS = "VIEW_REF_DOCS", "View reference documents" + REF_DOCS_EXAMPLES = "REF_DOCS_EXAMPLES", "View example reference document index" + REF_DOCS = "REF_DOCS", "View reference documents" class ImportUserActions(TextChoices): @@ -280,6 +284,9 @@ def __init__(self, *args, **kwargs): choices += CommonUserActions.choices + if self.user.has_perm("reference_documents.view_reference_document"): + choices += ReferenceDocumentsActions.choices + if self.user.has_perm("common.add_trackedmodel") or self.user.has_perm( "common.change_trackedmodel", ): diff --git a/common/jinja2/common/workbasket_action.jinja b/common/jinja2/common/index.jinja similarity index 100% rename from common/jinja2/common/workbasket_action.jinja rename to common/jinja2/common/index.jinja diff --git a/common/views.py b/common/views.py index def5fc2da..288bcda87 100644 --- a/common/views.py +++ b/common/views.py @@ -48,7 +48,7 @@ class HomeView(FormView, View): - template_name = "common/workbasket_action.jinja" + template_name = "common/index.jinja" form_class = forms.HomeForm REDIRECT_MAPPING = { @@ -58,7 +58,8 @@ class HomeView(FormView, View): "PROCESS_ENVELOPES": "publishing:envelope-queue-ui-list", "SEARCH": "search-page", "IMPORT": "commodity_importer-ui-list", - "VIEW_REF_DOCS": "reference_documents-ui-list", + "REF_DOCS_EXAMPLES": "reference_documents:example-ui-index", + "REF_DOCS": "reference_documents:index", "WORKBASKET_LIST_ALL": "workbaskets:workbasket-ui-list-all", } diff --git a/reference_documents/jinja2/includes/tabs/preferential_quotas.jinja b/reference_documents/jinja2/includes/tabs/preferential_quotas.jinja new file mode 100644 index 000000000..a687d517c --- /dev/null +++ b/reference_documents/jinja2/includes/tabs/preferential_quotas.jinja @@ -0,0 +1,23 @@ +{% from "components/table/macro.njk" import govukTable %} +{% from 'macros/create_link.jinja' import create_link %} + +
    +
    + {% for key, value in reference_document_version_quotas.items() %} +

    Order Number {{ key }}

    + {{ govukTable({ + "head": reference_document_version_quotas_headers, + "rows": value + }) }} + {% endfor %} +
    + +
    + +
    +
    diff --git a/reference_documents/jinja2/includes/tabs/preferential_rates.jinja b/reference_documents/jinja2/includes/tabs/preferential_rates.jinja new file mode 100644 index 000000000..abd737378 --- /dev/null +++ b/reference_documents/jinja2/includes/tabs/preferential_rates.jinja @@ -0,0 +1,20 @@ +{% from "components/table/macro.njk" import govukTable %} +{% from 'macros/create_link.jinja' import create_link %} + +
    +
    + {{ govukTable({ + "head": reference_document_version_duties_headers, + "rows": reference_document_version_duties + }) }} +
    + +
    + +
    +
    diff --git a/reference_documents/jinja2/reference_documents/detail.jinja b/reference_documents/jinja2/reference_document_examples/details.jinja similarity index 100% rename from reference_documents/jinja2/reference_documents/detail.jinja rename to reference_documents/jinja2/reference_document_examples/details.jinja diff --git a/reference_documents/jinja2/reference_documents/list.jinja b/reference_documents/jinja2/reference_document_examples/index.jinja similarity index 92% rename from reference_documents/jinja2/reference_documents/list.jinja rename to reference_documents/jinja2/reference_document_examples/index.jinja index 70a49fa35..e7ea9cc94 100644 --- a/reference_documents/jinja2/reference_documents/list.jinja +++ b/reference_documents/jinja2/reference_document_examples/index.jinja @@ -10,7 +10,7 @@ {%- set table_rows = [] -%} {% for ref_doc in object_list %} {% set ref_doc_link -%} - {{ ref_doc.name }} + {{ ref_doc.name }} {%- endset %} {{ table_rows.append([ {"html": ref_doc_link}, diff --git a/reference_documents/jinja2/reference_document_versions/new_details.jinja b/reference_documents/jinja2/reference_document_versions/new_details.jinja new file mode 100644 index 000000000..77fcdff14 --- /dev/null +++ b/reference_documents/jinja2/reference_document_versions/new_details.jinja @@ -0,0 +1,47 @@ +{% extends "layouts/layout.jinja" %} + +{% from "components/table/macro.njk" import govukTable %} +{% from "components/tabs/macro.njk" import govukTabs %} + +{% set page_title = "Preferential duty rates" %} + +{% set preferential_rates_html %} + {% include "includes/tabs/preferential_rates.jinja" %} +{% endset %} +{% set preferential_quotas_html %} + {% include "includes/tabs/preferential_quotas.jinja" %} +{% endset %} + +{% set tabs = { + "items": [ + { + "label": "Preferential duty rates", + "id": "core-data", + "panel": { + "html": preferential_rates_html + } + }, + { + "label": "Tariff quotas", + "id": "tariff-quotas", + "panel": { + "html": preferential_quotas_html + } + }, + ] + } %} + +{% block content %} +

    {{ page_title }}

    +

    Title:

    +

    {{ ref_doc_title }}

    +

    Version:

    +

    {{ object.version }}

    +

    Date published:

    +

    {{ object.published_date }}

    +

    Entry in to force date:

    +

    {{ object.entry_into_force_date or 'unknown' }}

    + + {{ govukTabs(tabs) }} + +{% endblock %} diff --git a/reference_documents/urls.py b/reference_documents/urls.py index f8a9a2f00..5d56f03ec 100644 --- a/reference_documents/urls.py +++ b/reference_documents/urls.py @@ -1,46 +1,53 @@ from django.urls import path from rest_framework import routers -from reference_documents import views +from reference_documents.views import alignment_report_views +from reference_documents.views import example_views +from reference_documents.views import reference_document_version_views +from reference_documents.views import reference_document_views app_name = "reference_documents" api_router = routers.DefaultRouter() urlpatterns = [ + # Example views path( "reference-documents-example/", - views.ReferenceDocumentsListView.as_view(), - name="reference_documents-ui-list", + example_views.ReferenceDocumentsListView.as_view(), + name="example-ui-index", ), path( f"reference-documents-example-albania/", - views.ReferenceDocumentsDetailView.as_view(), - name="reference_documents-ui-detail", + example_views.ReferenceDocumentsDetailView.as_view(), + name="example-ui-details", ), + # Reference document views path( "reference_documents/", - views.ReferenceDocumentList.as_view(), + reference_document_views.ReferenceDocumentList.as_view(), name="index", ), path( "reference_documents//", - views.ReferenceDocumentDetails.as_view(), + reference_document_views.ReferenceDocumentDetails.as_view(), name="details", ), + # reference document version views path( "reference_document_versions//", - views.ReferenceDocumentVersionDetails.as_view(), + reference_document_version_views.ReferenceDocumentVersionDetails.as_view(), name="version_details", ), + # Alignment report views path( "reference_document_version_alignment_reports//", - views.ReferenceDocumentVersionAlignmentReportsDetailsView.as_view(), - name="reference_document_version_alignment_reports", + alignment_report_views.ReferenceDocumentVersionAlignmentReportsDetailsView.as_view(), + name="version_alignment_reports", ), path( "alignment_reports//", - views.AlignmentReportsDetailsView.as_view(), + alignment_report_views.AlignmentReportsDetailsView.as_view(), name="alignment_reports", ), ] diff --git a/reference_documents/views/example_views.py b/reference_documents/views/example_views.py new file mode 100644 index 000000000..906f463bc --- /dev/null +++ b/reference_documents/views/example_views.py @@ -0,0 +1,110 @@ +from datetime import date + +from django.db.models import Q +from django.views.generic import TemplateView + +from geo_areas.models import GeographicalArea +from geo_areas.models import GeographicalAreaDescription +from measures.models import Measure + + +class ReferenceDocumentsListView(TemplateView): + template_name = "reference_document_examples/index.jinja" + + def get_context_data(self, *args, **kwargs): + context = super().get_context_data(*args, **kwargs) + context["object_list"] = [ + { + "name": "The Albania Preferential Tariff", + "version": 1.4, + "date_published": date(2023, 4, 12).strftime("%d %b %Y"), + "regulation_id": "TBC", + "geo_area_id": GeographicalArea.objects.get(area_id="AL").area_id, + }, + ] + return context + + +class ReferenceDocumentsDetailView(TemplateView): + template_name = "reference_document_examples/details.jinja" + + def get_pref_duty_rates(self): + """Returns a list of measures associated with the Albania Preferential + Tariff.""" + # Measures with type 142, for Albania, Valid up to and including 12th April 2023 + + date_filter_query = Q(valid_between__contains=date(2023, 4, 12)) | Q( + valid_between__startswith__lt=date(2023, 4, 12), + ) + pref_duty_measure_list = Measure.objects.filter( + date_filter_query, + measure_type__sid="142", + geographical_area__area_id="AL", + )[:20] + + return pref_duty_measure_list + + def get_tariff_quota_data(self): + """Returns a dict of quota order numbers, and their linked definitions + that are associated with the Albania Preferential Tariff.""" + # Measures with type 143, for Albania, with descriptions that are valid for 2023 only. + + # measures + albanian_measures = ( + Measure.objects.filter( + measure_type__sid="143", + geographical_area__area_id="AL", + ) + .exclude(order_number=None) + .order_by("-valid_between")[:30] + ) + + # order_numbers of measures + albanian_order_numbers = [] + for measure in albanian_measures: + albanian_order_numbers.append(measure.order_number) + + # remove the duplicates + albanian_order_numbers = list(dict.fromkeys(albanian_order_numbers)) + + quotas = [] + + for order_number in albanian_order_numbers: + comm_codes = [] + for measure in albanian_measures: + if measure.order_number == order_number: + comm_codes.append(measure.goods_nomenclature) + + quotas.append({"order_number": order_number, "comm_codes": comm_codes}) + + # Get the current definition for each order number in the quotas list + for quota in quotas: + for definition in quota["order_number"].definitions.current(): + if definition.valid_between.upper.year == 2023: + quota["definition"] = definition + return quotas + + def get_context_data(self, *args, **kwargs): + context = super().get_context_data(*args, **kwargs) + + context["ref_doc"] = { + "name": "The Albania Preferential Tariff", + "version": 1.4, + "date_published": date(2023, 4, 12).strftime("%d %b %Y"), + "pref_duty_measure_list": self.get_pref_duty_rates(), + "quotas": self.get_tariff_quota_data(), + } + return context + + def get_name_by_area_id(self, area_id): + geo_area = ( + GeographicalArea.objects.latest_approved().filter(area_id=area_id).first() + ) + if geo_area: + geo_area_name = ( + GeographicalAreaDescription.objects.latest_approved() + .filter(described_geographicalarea_id=geo_area.trackedmodel_ptr_id) + .last() + ) + return geo_area_name.description if geo_area_name else "None" + return "None" diff --git a/reference_documents/views/reference_document_version_views.py b/reference_documents/views/reference_document_version_views.py new file mode 100644 index 000000000..e1b79dfa1 --- /dev/null +++ b/reference_documents/views/reference_document_version_views.py @@ -0,0 +1,205 @@ +from django.contrib.auth.mixins import PermissionRequiredMixin +from django.views.generic import DetailView + +from commodities.models import GoodsNomenclature +from geo_areas.models import GeographicalAreaDescription +from reference_documents.models import AlignmentReportCheckStatus +from reference_documents.models import ReferenceDocumentVersion + + +class ReferenceDocumentVersionDetails(PermissionRequiredMixin, DetailView): + template_name = "reference_document_versions/new_details.jinja" + permission_required = "reference_documents.view_reference_document" + model = ReferenceDocumentVersion + + def get_country_by_area_id(self, area_id): + description = ( + GeographicalAreaDescription.objects.latest_approved() + .filter(described_geographicalarea__area_id=area_id) + .order_by("-validity_start") + .first() + ) + if description: + return description.description + else: + return f"{area_id} (unknown description)" + + def get_tap_comm_code(self, duty): + if duty.reference_document_version.entry_into_force_date is not None: + contains_date = duty.reference_document_version.entry_into_force_date + else: + contains_date = duty.reference_document_version.published_date + + goods = GoodsNomenclature.objects.latest_approved().filter( + item_id=duty.commodity_code, + valid_between__contains=contains_date, + suffix=80, + ) + + if len(goods) == 0: + return None + + return goods.first() + + def get_context_data(self, *args, **kwargs): + context = super(ReferenceDocumentVersionDetails, self).get_context_data( + *args, + **kwargs, + ) + + # title + context[ + "ref_doc_title" + ] = f'Reference Document for {self.get_country_by_area_id(context["object"].reference_document.area_id)}' + + context["reference_document_version_duties_headers"] = [ + {"text": "Comm Code"}, + {"text": "Duty Rate"}, + {"text": "Validity"}, + {"text": "Checks"}, + {"text": "Actions"}, + ] + + context["reference_document_version_quotas_headers"] = [ + {"text": "Comm Code"}, + {"text": "Rate"}, + {"text": "Volume"}, + {"text": "Validity"}, + {"text": "Checks"}, + {"text": "Actions"}, + ] + + reference_document_version_duties = [] + reference_document_version_quotas = {} + + latest_alignment_report = context["object"].alignment_reports.last() + + for duty in context["object"].preferential_rates.order_by("order"): + validity = "" + + if duty.valid_start_day: + validity = f"{duty.valid_start_day}/{duty.valid_start_month} - {duty.valid_end_day}/{duty.valid_end_month}" + + failure_count = ( + duty.preferential_rate_checks.all() + .filter( + alignment_report=latest_alignment_report, + status=AlignmentReportCheckStatus.FAIL, + ) + .count() + ) + + check_count = ( + duty.preferential_rate_checks.all() + .filter( + alignment_report=latest_alignment_report, + ) + .count() + ) + + if failure_count > 0: + checks_output = f'
    FAIL
    ' + elif check_count == 0: + checks_output = f"N/A" + else: + checks_output = f'
    PASS
    ' + + comm_code = self.get_tap_comm_code(duty) + + if comm_code: + comm_code_link = f'{comm_code.item_id}' + else: + comm_code_link = f"{duty.commodity_code}" + + reference_document_version_duties.append( + [ + { + "html": comm_code_link, + }, + { + "text": duty.duty_rate, + }, + { + "text": validity, + }, + { + "html": checks_output, + }, + { + "text": "", + }, + ], + ) + + # order numbers + for quota in context["object"].preferential_quotas.order_by("order"): + validity = "" + + if quota.valid_start_day: + validity = f"{quota.valid_start_day}/{quota.valid_start_month} - {quota.valid_end_day}/{quota.valid_end_month}" + + failure_count = ( + quota.preferential_quota_checks.all() + .filter( + alignment_report=latest_alignment_report, + status=AlignmentReportCheckStatus.FAIL, + ) + .count() + ) + + check_count = ( + quota.preferential_quota_checks.all() + .filter( + alignment_report=latest_alignment_report, + ) + .count() + ) + + if failure_count > 0: + checks_output = f'
    FAIL
    ' + elif check_count == 0: + checks_output = f"N/A" + else: + checks_output = f'
    PASS
    ' + + comm_code = self.get_tap_comm_code(quota) + if comm_code: + comm_code_link = f'{comm_code.structure_code}' + else: + comm_code_link = f"{quota.commodity_code}" + + row_to_add = [ + { + "html": comm_code_link, + }, + { + "text": quota.quota_duty_rate, + }, + { + "text": f"{quota.volume} {quota.measurement}", + }, + { + "text": validity, + }, + { + "html": checks_output, + }, + { + "text": "", + }, + ] + + if quota.quota_order_number in reference_document_version_quotas.keys(): + reference_document_version_quotas[quota.quota_order_number].append( + row_to_add, + ) + else: + reference_document_version_quotas[quota.quota_order_number] = [ + row_to_add, + ] + + context["reference_document_version_duties"] = reference_document_version_duties + + context["reference_document_version_quotas"] = reference_document_version_quotas + + return context diff --git a/reference_documents/views/reference_document_views.py b/reference_documents/views/reference_document_views.py new file mode 100644 index 000000000..6e18eaf4a --- /dev/null +++ b/reference_documents/views/reference_document_views.py @@ -0,0 +1,122 @@ +from django.contrib.auth.mixins import PermissionRequiredMixin +from django.views.generic import DetailView +from django.views.generic import ListView + +from geo_areas.models import GeographicalAreaDescription +from reference_documents.models import ReferenceDocument + + +class ReferenceDocumentList(PermissionRequiredMixin, ListView): + """UI endpoint for viewing and filtering workbaskets.""" + + template_name = "reference_documents/index.jinja" + permission_required = "reference_documents.view_reference_document" + model = ReferenceDocument + + def get_name_by_area_id(self, area_id): + description = ( + GeographicalAreaDescription.objects.latest_approved() + .filter(described_geographicalarea__area_id=area_id) + .order_by("-validity_start") + .first() + ) + if description: + return description.description + else: + return f"{area_id} (unknown description)" + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + reference_documents = [] + + for reference in ReferenceDocument.objects.all().order_by("area_id"): + if reference.reference_document_versions.count() == 0: + reference_documents.append( + [ + {"text": "None"}, + { + "text": f"{reference.area_id} - ({self.get_name_by_area_id(reference.area_id)})", + }, + {"text": 0}, + {"text": 0}, + { + "html": f'Details', + }, + ], + ) + + else: + reference_documents.append( + [ + {"text": reference.reference_document_versions.last().version}, + { + "text": f"{reference.area_id} - ({self.get_name_by_area_id(reference.area_id)})", + }, + { + "text": reference.reference_document_versions.last().preferential_rates.count(), + }, + { + "text": reference.reference_document_versions.last().preferential_quotas.count(), + }, + { + "html": f'Details', + }, + ], + ) + + context["reference_documents"] = reference_documents + context["reference_document_headers"] = [ + {"text": "Latest Version"}, + {"text": "Country"}, + {"text": "Duties"}, + {"text": "Quotas"}, + {"text": "Actions"}, + ] + return context + + +class ReferenceDocumentDetails(PermissionRequiredMixin, DetailView): + template_name = "reference_documents/details.jinja" + permission_required = "reference_documents.view_reference_document" + model = ReferenceDocument + + def get_context_data(self, *args, **kwargs): + context = super(ReferenceDocumentDetails, self).get_context_data( + *args, + **kwargs, + ) + + context["reference_document_versions_headers"] = [ + {"text": "Version"}, + {"text": "Duties"}, + {"text": "Quotas"}, + {"text": "Actions"}, + ] + reference_document_versions = [] + + print(self.request) + + for version in context["object"].reference_document_versions.order_by( + "version", + ): + reference_document_versions.append( + [ + { + "text": version.version, + }, + { + "text": version.preferential_rates.count(), + }, + { + "text": version.preferential_quotas.count(), + }, + { + "html": f'version details
    ' + f'Alignment reports', + }, + ], + ) + + context["reference_document_versions"] = reference_document_versions + + return context diff --git a/settings/common.py b/settings/common.py index fc1dfda20..b5f87e259 100644 --- a/settings/common.py +++ b/settings/common.py @@ -128,7 +128,6 @@ "publishing", "taric", "workbaskets", - "reference_documents", "exporter.apps.ExporterConfig", "crispy_forms", "crispy_forms_gds", diff --git a/urls.py b/urls.py index ac6ae8240..ab9650a14 100644 --- a/urls.py +++ b/urls.py @@ -33,7 +33,6 @@ path("", include("measures.urls")), path("", include("publishing.urls", namespace="publishing")), path("", include("quotas.urls")), - path("", include("reference_documents.urls")), path("", include("regulations.urls")), path("", include("reports.urls")), path("", include("taric_parsers.urls")), From c430ba10c0ad2776cebbfbc12aaefc28fef7118b Mon Sep 17 00:00:00 2001 From: Doug Mills Date: Wed, 21 Feb 2024 14:57:19 +0000 Subject: [PATCH 050/118] prep for merge to mega branch --- reference_documents/views.py | 441 ------------------ .../views/alignment_report_views.py | 89 ++++ 2 files changed, 89 insertions(+), 441 deletions(-) delete mode 100644 reference_documents/views.py create mode 100644 reference_documents/views/alignment_report_views.py diff --git a/reference_documents/views.py b/reference_documents/views.py deleted file mode 100644 index 4549e7348..000000000 --- a/reference_documents/views.py +++ /dev/null @@ -1,441 +0,0 @@ -from datetime import date - -from django.contrib.auth.mixins import PermissionRequiredMixin -from django.db.models import Q -from django.views.generic import DetailView -from django.views.generic import ListView -from django.views.generic import TemplateView - -from geo_areas.models import GeographicalArea -from geo_areas.models import GeographicalAreaDescription -from measures.models import Measure -from reference_documents.models import AlignmentReport -from reference_documents.models import AlignmentReportCheckStatus -from reference_documents.models import ReferenceDocument -from reference_documents.models import ReferenceDocumentVersion - - -class ReferenceDocumentList(PermissionRequiredMixin, ListView): - """UI endpoint for viewing and filtering workbaskets.""" - - template_name = "reference_documents/index.jinja" - permission_required = "reference_documents.view_reference_document" - model = ReferenceDocument - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - reference_documents = [] - - for reference in ReferenceDocument.objects.all().order_by("area_id"): - if reference.reference_document_versions.count() == 0: - reference_documents.append( - [ - {"text": "None"}, - { - "text": f"{reference.area_id} - ({self.get_name_by_area_id(reference.area_id)})", - }, - {"text": 0}, - {"text": 0}, - { - "html": f'Details', - }, - ], - ) - - else: - reference_documents.append( - [ - {"text": reference.reference_document_versions.last().version}, - { - "text": f"{reference.area_id} - ({self.get_name_by_area_id(reference.area_id)})", - }, - { - "text": reference.reference_document_versions.last().preferential_rates.count(), - }, - { - "text": reference.reference_document_versions.last().preferential_quotas.count(), - }, - { - "html": f'Details', - }, - ], - ) - - context["reference_documents"] = reference_documents - context["reference_document_headers"] = [ - {"text": "Latest Version"}, - {"text": "Country"}, - {"text": "Duties"}, - {"text": "Quotas"}, - {"text": "Actions"}, - ] - return context - - -class ReferenceDocumentsListView(TemplateView): - template_name = "reference_documents/list.jinja" - - def get_context_data(self, *args, **kwargs): - context = super().get_context_data(*args, **kwargs) - context["object_list"] = [ - { - "name": "The Albania Preferential Tariff", - "version": 1.4, - "date_published": date(2023, 4, 12).strftime("%d %b %Y"), - "regulation_id": "TBC", - "geo_area_id": GeographicalArea.objects.get(area_id="AL").area_id, - }, - ] - return context - - -class ReferenceDocumentsDetailView(TemplateView): - template_name = "reference_documents/detail.jinja" - - def get_pref_duty_rates(self): - """Returns a list of measures associated with the Albania Preferential - Tariff.""" - # Measures with type 142, for Albania, Valid up to and including 12th April 2023 - - date_filter_query = Q(valid_between__contains=date(2023, 4, 12)) | Q( - valid_between__startswith__lt=date(2023, 4, 12), - ) - pref_duty_measure_list = Measure.objects.filter( - date_filter_query, - measure_type__sid="142", - geographical_area__area_id="AL", - )[:20] - - return pref_duty_measure_list - - def get_tariff_quota_data(self): - """Returns a dict of quota order numbers, and their linked definitions - that are associated with the Albania Preferential Tariff.""" - # Measures with type 143, for Albania, with descriptions that are valid for 2023 only. - - # measures - albanian_measures = ( - Measure.objects.filter( - measure_type__sid="143", - geographical_area__area_id="AL", - ) - .exclude(order_number=None) - .order_by("-valid_between")[:30] - ) - - # order_numbers of measures - albanian_order_numbers = [] - for measure in albanian_measures: - albanian_order_numbers.append(measure.order_number) - - # remove the duplicates - albanian_order_numbers = list(dict.fromkeys(albanian_order_numbers)) - - quotas = [] - - for order_number in albanian_order_numbers: - comm_codes = [] - for measure in albanian_measures: - if measure.order_number == order_number: - comm_codes.append(measure.goods_nomenclature) - - quotas.append({"order_number": order_number, "comm_codes": comm_codes}) - - # Get the current definition for each order number in the quotas list - for quota in quotas: - for definition in quota["order_number"].definitions.current(): - if definition.valid_between.upper.year == 2023: - quota["definition"] = definition - return quotas - - def get_context_data(self, *args, **kwargs): - context = super().get_context_data(*args, **kwargs) - - context["ref_doc"] = { - "name": "The Albania Preferential Tariff", - "version": 1.4, - "date_published": date(2023, 4, 12).strftime("%d %b %Y"), - "pref_duty_measure_list": self.get_pref_duty_rates(), - "quotas": self.get_tariff_quota_data(), - } - return context - - def get_name_by_area_id(self, area_id): - geo_area = ( - GeographicalArea.objects.latest_approved().filter(area_id=area_id).first() - ) - if geo_area: - geo_area_name = ( - GeographicalAreaDescription.objects.latest_approved() - .filter(described_geographicalarea_id=geo_area.trackedmodel_ptr_id) - .last() - ) - return geo_area_name.description if geo_area_name else "None" - return "None" - - -class ReferenceDocumentDetails(PermissionRequiredMixin, DetailView): - template_name = "reference_documents/details.jinja" - permission_required = "reference_documents.view_reference_document" - model = ReferenceDocument - - def get_context_data(self, *args, **kwargs): - context = super(ReferenceDocumentDetails, self).get_context_data( - *args, - **kwargs, - ) - - context["reference_document_versions_headers"] = [ - {"text": "Version"}, - {"text": "Duties"}, - {"text": "Quotas"}, - {"text": "Actions"}, - ] - reference_document_versions = [] - - print(self.request) - - for version in context["object"].reference_document_versions.order_by( - "version", - ): - reference_document_versions.append( - [ - { - "text": version.version, - }, - { - "text": version.preferential_rates.count(), - }, - { - "text": version.preferential_quotas.count(), - }, - { - "html": f'version details
    ' - f'Alignment reports', - }, - ], - ) - - context["reference_document_versions"] = reference_document_versions - - return context - - -class ReferenceDocumentVersionDetails(PermissionRequiredMixin, DetailView): - template_name = "reference_document_versions/details.jinja" - permission_required = "reference_documents.view_reference_document" - model = ReferenceDocumentVersion - - def get_context_data(self, *args, **kwargs): - context = super(ReferenceDocumentVersionDetails, self).get_context_data( - *args, - **kwargs, - ) - - context["reference_document_version_duties_headers"] = [ - {"text": "Comm Code"}, - {"text": "Duty Rate"}, - {"text": "Validity"}, - {"text": "Checks"}, - {"text": "Actions"}, - ] - - context["reference_document_version_quotas_headers"] = [ - {"text": "Order Number"}, - {"text": "Comm Code"}, - {"text": "Rate"}, - {"text": "Volume"}, - {"text": "Validity"}, - {"text": "Checks"}, - {"text": "Actions"}, - ] - - reference_document_version_duties = [] - reference_document_version_quotas = [] - - latest_alignment_report = context["object"].alignment_reports.last() - - for duty in context["object"].preferential_rates.order_by("order"): - validity = "" - - if duty.valid_start_day: - validity = f"{duty.valid_start_day}/{duty.valid_start_month} - {duty.valid_end_day}/{duty.valid_end_month}" - - failure_count = ( - duty.preferential_rate_checks.all() - .filter( - alignment_report=latest_alignment_report, - status=AlignmentReportCheckStatus.FAIL, - ) - .count() - ) - check_count = ( - duty.preferential_rate_checks.all() - .filter(alignment_report=latest_alignment_report) - .count() - ) - - if failure_count > 0: - checks_output = f'
    FAILED {failure_count} of {check_count}
    ' - else: - checks_output = f'
    PASSED {check_count} of {check_count}
    ' - - reference_document_version_duties.append( - [ - { - "text": duty.commodity_code, - }, - { - "text": duty.duty_rate, - }, - { - "text": validity, - }, - { - "html": checks_output, - }, - { - "text": "", - }, - ], - ) - - for quota in context["object"].preferential_quotas.order_by("order"): - validity = "" - - if quota.valid_start_day: - validity = f"{quota.valid_start_day}/{quota.valid_start_month} - {quota.valid_end_day}/{quota.valid_end_month}" - - failure_count = ( - quota.preferential_quota_checks.all() - .filter( - alignment_report=latest_alignment_report, - status=AlignmentReportCheckStatus.FAIL, - ) - .count() - ) - check_count = ( - quota.preferential_quota_checks.all() - .filter(alignment_report=latest_alignment_report) - .count() - ) - - if failure_count > 0: - checks_output = f'
    FAILED {failure_count} of {check_count}
    ' - else: - checks_output = f'
    PASSED {check_count} of {check_count}
    ' - - reference_document_version_quotas.append( - [ - { - "text": quota.quota_order_number, - }, - { - "text": quota.commodity_code, - }, - { - "text": quota.quota_duty_rate, - }, - { - "text": f"{quota.volume} {quota.measurement}", - }, - { - "text": validity, - }, - { - "html": checks_output, - }, - { - "text": "", - }, - ], - ) - - context["reference_document_version_duties"] = reference_document_version_duties - - context["reference_document_version_quotas"] = reference_document_version_quotas - - return context - - -class ReferenceDocumentVersionAlignmentReportsDetailsView( - PermissionRequiredMixin, - DetailView, -): - template_name = "reference_document_versions/alignment_reports.jinja" - permission_required = "reference_documents.view_reference_document" - model = ReferenceDocumentVersion - - def get_context_data(self, *args, **kwargs): - context = super( - ReferenceDocumentVersionAlignmentReportsDetailsView, - self, - ).get_context_data( - *args, - **kwargs, - ) - - context["alignment_report_headers"] = [ - {"text": "Created"}, - {"text": "Passed"}, - {"text": "failed"}, - {"text": "Percent"}, - {"text": "Actions"}, - ] - - alignment_reports = [] - for report in context["object"].alignment_reports.order_by("-created_at"): - failure_count = ( - report.alignment_report_checks.all() - .filter(status=AlignmentReportCheckStatus.FAIL) - .count() - ) - pass_count = ( - report.alignment_report_checks.all() - .filter(status=AlignmentReportCheckStatus.PASS) - .count() - ) - - if pass_count > 0: - pass_percentage = round( - (pass_count / (pass_count + failure_count)) * 100, - 2, - ) - else: - pass_percentage = 100 - - alignment_reports.append( - [ - { - "text": report.created_at.strftime("%d/%m/%Y %H:%M"), - }, - { - "text": pass_count, - }, - { - "text": failure_count, - }, - { - "text": f"{pass_percentage} %", - }, - { - "html": f"Details", - }, - ], - ) - - context["alignment_reports"] = alignment_reports - - return context - - -class AlignmentReportsDetailsView(PermissionRequiredMixin, DetailView): - template_name = "alignment_reports/details.jinja" - permission_required = "reference_documents.view_reference_document" - model = AlignmentReport - - def get_context_data(self, *args, **kwargs): - context = super(AlignmentReportsDetailsView, self).get_context_data( - *args, - **kwargs, - ) diff --git a/reference_documents/views/alignment_report_views.py b/reference_documents/views/alignment_report_views.py new file mode 100644 index 000000000..b59aa751d --- /dev/null +++ b/reference_documents/views/alignment_report_views.py @@ -0,0 +1,89 @@ +from django.contrib.auth.mixins import PermissionRequiredMixin +from django.views.generic import DetailView + +from reference_documents.models import AlignmentReport +from reference_documents.models import AlignmentReportCheckStatus +from reference_documents.models import ReferenceDocumentVersion + + +class ReferenceDocumentVersionAlignmentReportsDetailsView( + PermissionRequiredMixin, + DetailView, +): + template_name = "reference_document_versions/alignment_reports.jinja" + permission_required = "reference_documents.view_reference_document" + model = ReferenceDocumentVersion + + def get_context_data(self, *args, **kwargs): + context = super( + ReferenceDocumentVersionAlignmentReportsDetailsView, + self, + ).get_context_data( + *args, + **kwargs, + ) + + context["alignment_report_headers"] = [ + {"text": "Created"}, + {"text": "Passed"}, + {"text": "failed"}, + {"text": "Percent"}, + {"text": "Actions"}, + ] + + alignment_reports = [] + for report in context["object"].alignment_reports.order_by("-created_at"): + failure_count = ( + report.alignment_report_checks.all() + .filter(status=AlignmentReportCheckStatus.FAIL) + .count() + ) + pass_count = ( + report.alignment_report_checks.all() + .filter(status=AlignmentReportCheckStatus.PASS) + .count() + ) + + if pass_count > 0: + pass_percentage = round( + (pass_count / (pass_count + failure_count)) * 100, + 2, + ) + else: + pass_percentage = 100 + + alignment_reports.append( + [ + { + "text": report.created_at.strftime("%d/%m/%Y %H:%M"), + }, + { + "text": pass_count, + }, + { + "text": failure_count, + }, + { + "text": f"{pass_percentage} %", + }, + { + "html": f"Details", + }, + ], + ) + + context["alignment_reports"] = alignment_reports + + return context + + +class AlignmentReportsDetailsView(PermissionRequiredMixin, DetailView): + template_name = "alignment_reports/details.jinja" + permission_required = "reference_documents.view_reference_document" + model = AlignmentReport + + def get_context_data(self, *args, **kwargs): + context = super(AlignmentReportsDetailsView, self).get_context_data( + *args, + **kwargs, + ) From b9b54e7fb7d709db222cb5525e858dbe600e852e Mon Sep 17 00:00:00 2001 From: Doug Mills Date: Fri, 23 Feb 2024 10:50:28 +0000 Subject: [PATCH 051/118] prep for merge to mega branch --- .../includes/tabs/preferential_quotas.jinja | 22 +++++- .../jinja2/preferential_quotas/edit.jinja | 14 ++++ .../reference_document_versions/edit.jinja | 14 ++++ .../migrations/0001_initial.py | 49 +++++++++--- .../migrations/0002_auto_20240215_1056.py | 32 -------- .../migrations/0003_auto_20240219_0951.py | 36 --------- reference_documents/models.py | 22 ++++-- reference_documents/urls.py | 17 +++++ .../views/preferential_quotas.py | 37 +++++++++ .../views/reference_document_version_views.py | 75 ++++++++++++++----- .../views/reference_document_views.py | 6 ++ 11 files changed, 215 insertions(+), 109 deletions(-) create mode 100644 reference_documents/jinja2/preferential_quotas/edit.jinja create mode 100644 reference_documents/jinja2/reference_document_versions/edit.jinja delete mode 100644 reference_documents/migrations/0002_auto_20240215_1056.py delete mode 100644 reference_documents/migrations/0003_auto_20240219_0951.py create mode 100644 reference_documents/views/preferential_quotas.py diff --git a/reference_documents/jinja2/includes/tabs/preferential_quotas.jinja b/reference_documents/jinja2/includes/tabs/preferential_quotas.jinja index a687d517c..7b97ce0e6 100644 --- a/reference_documents/jinja2/includes/tabs/preferential_quotas.jinja +++ b/reference_documents/jinja2/includes/tabs/preferential_quotas.jinja @@ -4,10 +4,26 @@
    {% for key, value in reference_document_version_quotas.items() %} -

    Order Number {{ key }}

    - {{ govukTable({ + {% if value["quota_order_number"] != None %} +

    Order Number + + {{ value["quota_order_number"] }} + +

    +
    + validity: from {{ value["quota_order_number"].valid_between.lower }} + {% if value["quota_order_number"].valid_between.upper == None %} + no end date defined + {% else %} + to {{ value["quota_order_number"].valid_between.upper }} + {% endif %} +
    + {% else %} +

    Order Number {{ value["quota_order_number"] }}

    + {% endif %} + {{ govukTable({ "head": reference_document_version_quotas_headers, - "rows": value + "rows": value['data_rows'] }) }} {% endfor %}
    diff --git a/reference_documents/jinja2/preferential_quotas/edit.jinja b/reference_documents/jinja2/preferential_quotas/edit.jinja new file mode 100644 index 000000000..f9bc245a2 --- /dev/null +++ b/reference_documents/jinja2/preferential_quotas/edit.jinja @@ -0,0 +1,14 @@ +{% extends "layouts/layout.jinja" %} + +{% set page_title = "Preferential duty rates" %} + +{% block content %} + +
    + + {{ form.as_p() }} + +
    + +{% endblock %} + diff --git a/reference_documents/jinja2/reference_document_versions/edit.jinja b/reference_documents/jinja2/reference_document_versions/edit.jinja new file mode 100644 index 000000000..f9bc245a2 --- /dev/null +++ b/reference_documents/jinja2/reference_document_versions/edit.jinja @@ -0,0 +1,14 @@ +{% extends "layouts/layout.jinja" %} + +{% set page_title = "Preferential duty rates" %} + +{% block content %} + +
    + + {{ form.as_p() }} + +
    + +{% endblock %} + diff --git a/reference_documents/migrations/0001_initial.py b/reference_documents/migrations/0001_initial.py index c56b2b4c0..4470166cc 100644 --- a/reference_documents/migrations/0001_initial.py +++ b/reference_documents/migrations/0001_initial.py @@ -1,10 +1,12 @@ -# Generated by Django 3.2.23 on 2024-02-14 11:45 +# Generated by Django 3.2.23 on 2024-02-22 15:02 import django.db.models.deletion import django_fsm from django.db import migrations from django.db import models +import common.fields + class Migration(migrations.Migration): initial = True @@ -108,10 +110,15 @@ class Migration(migrations.Migration): ("commodity_code", models.CharField(db_index=True, max_length=10)), ("duty_rate", models.CharField(max_length=255)), ("order", models.IntegerField()), - ("valid_start_day", models.IntegerField(blank=True, null=True)), - ("valid_start_month", models.IntegerField(blank=True, null=True)), - ("valid_end_day", models.IntegerField(blank=True, null=True)), - ("valid_end_month", models.IntegerField(blank=True, null=True)), + ( + "valid_between", + common.fields.TaricDateRangeField( + blank=True, + db_index=True, + default=None, + null=True, + ), + ), ( "reference_document_version", models.ForeignKey( @@ -138,10 +145,15 @@ class Migration(migrations.Migration): ("commodity_code", models.CharField(db_index=True, max_length=10)), ("quota_duty_rate", models.CharField(max_length=255)), ("volume", models.CharField(max_length=255)), - ("valid_start_day", models.IntegerField(blank=True, null=True)), - ("valid_start_month", models.IntegerField(blank=True, null=True)), - ("valid_end_day", models.IntegerField(blank=True, null=True)), - ("valid_end_month", models.IntegerField(blank=True, null=True)), + ( + "valid_between", + common.fields.TaricDateRangeField( + blank=True, + db_index=True, + default=None, + null=True, + ), + ), ("measurement", models.CharField(max_length=255)), ("order", models.IntegerField()), ( @@ -168,7 +180,20 @@ class Migration(migrations.Migration): ), ("created_at", models.DateTimeField(auto_now_add=True)), ("check_name", models.CharField(max_length=255)), - ("successful", models.BooleanField()), + ( + "status", + django_fsm.FSMField( + choices=[ + ("PASS", "Passing"), + ("FAIL", "Failed"), + ("WARNING", "Warning"), + ], + db_index=True, + default="FAIL", + editable=False, + max_length=50, + ), + ), ("message", models.TextField(null=True)), ( "alignment_report", @@ -184,7 +209,7 @@ class Migration(migrations.Migration): blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, - related_name="alignment_report_checks", + related_name="preferential_quota_checks", to="reference_documents.preferentialquota", ), ), @@ -194,7 +219,7 @@ class Migration(migrations.Migration): blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, - related_name="alignment_report_checks", + related_name="preferential_rate_checks", to="reference_documents.preferentialrate", ), ), diff --git a/reference_documents/migrations/0002_auto_20240215_1056.py b/reference_documents/migrations/0002_auto_20240215_1056.py deleted file mode 100644 index f2d8b7bb1..000000000 --- a/reference_documents/migrations/0002_auto_20240215_1056.py +++ /dev/null @@ -1,32 +0,0 @@ -# Generated by Django 3.2.23 on 2024-02-15 10:56 - -import django_fsm -from django.db import migrations - - -class Migration(migrations.Migration): - dependencies = [ - ("reference_documents", "0001_initial"), - ] - - operations = [ - migrations.RemoveField( - model_name="alignmentreportcheck", - name="successful", - ), - migrations.AddField( - model_name="alignmentreportcheck", - name="status", - field=django_fsm.FSMField( - choices=[ - ("PASS", "Passing"), - ("FAIL", "Failed"), - ("WARNING", "Warning"), - ], - db_index=True, - default="FAIL", - editable=False, - max_length=50, - ), - ), - ] diff --git a/reference_documents/migrations/0003_auto_20240219_0951.py b/reference_documents/migrations/0003_auto_20240219_0951.py deleted file mode 100644 index 5d621f081..000000000 --- a/reference_documents/migrations/0003_auto_20240219_0951.py +++ /dev/null @@ -1,36 +0,0 @@ -# Generated by Django 3.2.23 on 2024-02-19 09:51 - -import django.db.models.deletion -from django.db import migrations -from django.db import models - - -class Migration(migrations.Migration): - dependencies = [ - ("reference_documents", "0002_auto_20240215_1056"), - ] - - operations = [ - migrations.AlterField( - model_name="alignmentreportcheck", - name="preferential_quota", - field=models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.PROTECT, - related_name="preferential_quota_checks", - to="reference_documents.preferentialquota", - ), - ), - migrations.AlterField( - model_name="alignmentreportcheck", - name="preferential_rate", - field=models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.PROTECT, - related_name="preferential_rate_checks", - to="reference_documents.preferentialrate", - ), - ), - ] diff --git a/reference_documents/models.py b/reference_documents/models.py index 6e691056a..82f5323a6 100644 --- a/reference_documents/models.py +++ b/reference_documents/models.py @@ -2,6 +2,8 @@ from django.db.models import fields from django_fsm import FSMField +from common.fields import TaricDateRangeField + class ReferenceDocumentVersionStatus(models.TextChoices): # Reference document version can be edited @@ -74,10 +76,12 @@ class PreferentialRate(models.Model): related_name="preferential_rates", ) - valid_start_day = models.IntegerField(blank=True, null=True) - valid_start_month = models.IntegerField(blank=True, null=True) - valid_end_day = models.IntegerField(blank=True, null=True) - valid_end_month = models.IntegerField(blank=True, null=True) + valid_between = TaricDateRangeField( + db_index=True, + null=True, + blank=True, + default=None, + ) class PreferentialQuota(models.Model): @@ -96,10 +100,12 @@ class PreferentialQuota(models.Model): max_length=255, ) - valid_start_day = models.IntegerField(blank=True, null=True) - valid_start_month = models.IntegerField(blank=True, null=True) - valid_end_day = models.IntegerField(blank=True, null=True) - valid_end_month = models.IntegerField(blank=True, null=True) + valid_between = TaricDateRangeField( + db_index=True, + null=True, + blank=True, + default=None, + ) measurement = models.CharField( max_length=255, diff --git a/reference_documents/urls.py b/reference_documents/urls.py index 5d56f03ec..2f0448f90 100644 --- a/reference_documents/urls.py +++ b/reference_documents/urls.py @@ -3,6 +3,7 @@ from reference_documents.views import alignment_report_views from reference_documents.views import example_views +from reference_documents.views import preferential_quotas from reference_documents.views import reference_document_version_views from reference_documents.views import reference_document_views @@ -39,6 +40,11 @@ reference_document_version_views.ReferenceDocumentVersionDetails.as_view(), name="version_details", ), + path( + "reference_document_versions/edit//", + reference_document_version_views.ReferenceDocumentVersionEditView.as_view(), + name="reference_document_version_edit", + ), # Alignment report views path( "reference_document_version_alignment_reports//", @@ -50,4 +56,15 @@ alignment_report_views.AlignmentReportsDetailsView.as_view(), name="alignment_reports", ), + # Preferential Quotas + path( + "preferential_quotas/delete//", + preferential_quotas.PreferentialQuotaDeleteView.as_view(), + name="preferential_quotas_delete", + ), + path( + "preferential_quotas/edit//", + preferential_quotas.PreferentialQuotaEditView.as_view(), + name="preferential_quotas_edit", + ), ] diff --git a/reference_documents/views/preferential_quotas.py b/reference_documents/views/preferential_quotas.py new file mode 100644 index 000000000..6a454325b --- /dev/null +++ b/reference_documents/views/preferential_quotas.py @@ -0,0 +1,37 @@ +from django.contrib.auth.mixins import PermissionRequiredMixin +from django.shortcuts import redirect +from django.urls import reverse +from django.views.generic import UpdateView + +from reference_documents.models import PreferentialQuota + + +class PreferentialQuotaEditView(PermissionRequiredMixin, UpdateView): + template_name = "preferential_quotas/edit.jinja" + permission_required = "reference_documents.edit_reference_document" + model = PreferentialQuota + fields = [ + "quota_order_number", + "commodity_code", + "quota_duty_rate", + "volume", + "measurement", + "valid_between", + ] + + def post(self, request, *args, **kwargs): + quota = self.get_object() + quota.save() + return redirect( + reverse( + "reference_documents:version_details", + args=[quota.reference_document_version.pk], + ) + + "#tariff-quotas", + ) + + +class PreferentialQuotaDeleteView(PermissionRequiredMixin, UpdateView): + template_name = "preferential_quotas/delete.jinja" + permission_required = "reference_documents.edit_reference_document" + model = PreferentialQuota diff --git a/reference_documents/views/reference_document_version_views.py b/reference_documents/views/reference_document_version_views.py index e1b79dfa1..f24788ca3 100644 --- a/reference_documents/views/reference_document_version_views.py +++ b/reference_documents/views/reference_document_version_views.py @@ -1,8 +1,12 @@ from django.contrib.auth.mixins import PermissionRequiredMixin +from django.urls import reverse +from django.urls import reverse_lazy from django.views.generic import DetailView +from django.views.generic import UpdateView from commodities.models import GoodsNomenclature from geo_areas.models import GeographicalAreaDescription +from quotas.models import QuotaOrderNumber from reference_documents.models import AlignmentReportCheckStatus from reference_documents.models import ReferenceDocumentVersion @@ -41,6 +45,25 @@ def get_tap_comm_code(self, duty): return goods.first() + def get_tap_order_number(self, quota): + # todo: This needs to consider the validity period(s) + # may need to handle in the pre processing of the data e.g. where the volume defines multiple periods + + if quota.reference_document_version.entry_into_force_date is not None: + contains_date = quota.reference_document_version.entry_into_force_date + else: + contains_date = quota.reference_document_version.published_date + + quota_order_number = QuotaOrderNumber.objects.latest_approved().filter( + order_number=quota.quota_order_number, + valid_between__contains=contains_date, + ) + + if len(quota_order_number) == 0: + return None + + return quota_order_number.first() + def get_context_data(self, *args, **kwargs): context = super(ReferenceDocumentVersionDetails, self).get_context_data( *args, @@ -75,11 +98,6 @@ def get_context_data(self, *args, **kwargs): latest_alignment_report = context["object"].alignment_reports.last() for duty in context["object"].preferential_rates.order_by("order"): - validity = "" - - if duty.valid_start_day: - validity = f"{duty.valid_start_day}/{duty.valid_start_month} - {duty.valid_end_day}/{duty.valid_end_month}" - failure_count = ( duty.preferential_rate_checks.all() .filter( @@ -120,7 +138,7 @@ def get_context_data(self, *args, **kwargs): "text": duty.duty_rate, }, { - "text": validity, + "text": duty.valid_between, }, { "html": checks_output, @@ -133,11 +151,6 @@ def get_context_data(self, *args, **kwargs): # order numbers for quota in context["object"].preferential_quotas.order_by("order"): - validity = "" - - if quota.valid_start_day: - validity = f"{quota.valid_start_day}/{quota.valid_start_month} - {quota.valid_end_day}/{quota.valid_end_month}" - failure_count = ( quota.preferential_quota_checks.all() .filter( @@ -162,6 +175,8 @@ def get_context_data(self, *args, **kwargs): else: checks_output = f'
    PASS
    ' + quota_order_number = self.get_tap_order_number(quota) + comm_code = self.get_tap_comm_code(quota) if comm_code: comm_code_link = f'{comm_code.structure_code}' @@ -179,27 +194,51 @@ def get_context_data(self, *args, **kwargs): "text": f"{quota.volume} {quota.measurement}", }, { - "text": validity, + "text": quota.valid_between, }, { "html": checks_output, }, { - "text": "", + "html": f"Edit " + f"Delete", }, ] if quota.quota_order_number in reference_document_version_quotas.keys(): - reference_document_version_quotas[quota.quota_order_number].append( + reference_document_version_quotas[quota.quota_order_number][ + "data_rows" + ].append( row_to_add, ) else: - reference_document_version_quotas[quota.quota_order_number] = [ - row_to_add, - ] + reference_document_version_quotas[quota.quota_order_number] = { + "data_rows": [row_to_add], + "quota_order_number": quota_order_number, + } context["reference_document_version_duties"] = reference_document_version_duties - context["reference_document_version_quotas"] = reference_document_version_quotas return context + + +class ReferenceDocumentVersionEditView(PermissionRequiredMixin, UpdateView): + template_name = "reference_document_versions/edit.jinja" + permission_required = "reference_documents.edit_reference_document" + model = ReferenceDocumentVersion + fields = ["version", "published_date", "entry_into_force_date"] + + # def post(self, request, *args, **kwargs): + # reference_document_version = self.get_object() + # reference_document_version.save() + # return redirect(reverse("reference_documents:details", args=[reference_document_version.reference_document.pk])) + + def form_valid(self, form): + return super(ReferenceDocumentVersionEditView, self).form_valid(form) + + def get_success_url(self): + return reverse_lazy( + "reference_documents:details", + args=[self.object.id], + ) diff --git a/reference_documents/views/reference_document_views.py b/reference_documents/views/reference_document_views.py index 6e18eaf4a..7b000b722 100644 --- a/reference_documents/views/reference_document_views.py +++ b/reference_documents/views/reference_document_views.py @@ -1,4 +1,5 @@ from django.contrib.auth.mixins import PermissionRequiredMixin +from django.urls import reverse from django.views.generic import DetailView from django.views.generic import ListView @@ -90,6 +91,7 @@ def get_context_data(self, *args, **kwargs): {"text": "Version"}, {"text": "Duties"}, {"text": "Quotas"}, + {"text": "EIF date"}, {"text": "Actions"}, ] reference_document_versions = [] @@ -110,8 +112,12 @@ def get_context_data(self, *args, **kwargs): { "text": version.preferential_quotas.count(), }, + { + "text": version.entry_into_force_date, + }, { "html": f'version details
    ' + f'Edit
    ' f'Alignment reports', }, ], From e10118a8b196752a8f87a637687f36600a4cbbdf Mon Sep 17 00:00:00 2001 From: Doug Mills <110824173+dougmills-DIT@users.noreply.github.com> Date: Fri, 23 Feb 2024 12:53:49 +0000 Subject: [PATCH 052/118] Tp2000 1186 ref doc data model (#1164) * TP2000-1168 Add sub-quota, blocking period & suspension period nested review tabs (#1133) * Add sub-quotas nested review tab * Add quota blocking periods nested review tab * Add quota suspension periods nested review tab * Use tab title instead of model verbose name * Add blocking period and suspension period SID to table * Feat: expand expiring quotas report to include tabs (#1131) * feat: invoke UI changes to reports and create new URL path to handle reports with multiple tabs (#1134) * feat: Add both CSV and excel types for charts exporting (#1136) * TP2000-1185 Add maintenance mode (#1137) * Add MAINTENANCE_MODE setting and middleware * Fix middleware removal and recursive redirect * Add template view and url * Add tests * Update contact us form link for other pages * Update text wording * Remove database route during maintenance * Update maintenance page template/url name --------- Co-authored-by: Dale Cannon * Increment message id & record sequence number correctly (#1083) * record seq number & message id fix * fix taricXMLRenderer, pass in value of counter * feat: implement URLs for quota reports to ease navigation (#1135) * Update readme with maintenance mode instructions. (#1140) * TP2000-1130 Move current workbasket from Session to custom User model (#1123) * Update User model references * Use custom User model * TP2000-1152-handling-invalid-workbaskets (#1113) * Update middleware to check for workbasket changing state * Update to use decorator rather than middleware, add pytest fixtures * Update tests that require a session workbasket to run * Move views and urls to workbasket app and update template * Add tests for when workbasket status changes * Tidy up following Pauls comments * Update models and templates to find workbasket in user model * Update test fixtures for workbasket being in user model * Tidy up and test updates * Update referencing to User model * Updating bdd tests for new user model * Add and update view and model unit tests * Update require_current_workbasket decorator docstring * Add docstring, move template for NoActiveWorkBasket view * Amend current workbasket id retrieval in template * Amend custom User model migration * Remake migration adding current_workbasket field to User model * Remove unused ValidateSessionWorkBasketMiddleware * Make current_workbasket optional * Add User model to admin * Use historical models to fix migration tests * Move ContentType data migration so it may be applied * Rename function to remove a users current workbasket * Amend docstrings * Remove reference to session middleware that is no longer used * Update workbaskets models following Pauls review * Bring back user workbasket middleware as extra security * Move User model from workbaskets app to common app * Add forgotten content type data migration * Remove setup_content_type fixture following patch to migrator fixture * Amend middleware util method name * Remove uneeded DoesNotExist try except block --------- Co-authored-by: Dale Cannon * Bump aiohttp from 3.9.1 to 3.9.2 (#1142) Bumps [aiohttp](https://github.com/aio-libs/aiohttp) from 3.9.1 to 3.9.2. - [Release notes](https://github.com/aio-libs/aiohttp/releases) - [Changelog](https://github.com/aio-libs/aiohttp/blob/master/CHANGES.rst) - [Commits](https://github.com/aio-libs/aiohttp/compare/v3.9.1...v3.9.2) --- updated-dependencies: - dependency-name: aiohttp dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * initial commit - ref doc data model * update importer model matching to account for end dated objects. (#1146) * update importer model matching to account for end dated objects. * update importer model matching to account for end dated objects. * Tp2000 1211 (#1148) * update govuk dependency since its been deleted at source * update govuk dependency since its been deleted at source * initial commit - ref doc data model * wip commit * Bump django from 3.2.23 to 3.2.24 (#1150) Bumps [django](https://github.com/django/django) from 3.2.23 to 3.2.24. - [Commits](https://github.com/django/django/compare/3.2.23...3.2.24) --- updated-dependencies: - dependency-name: django dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Tp2000 652 force rule check after real edit (#1130) * added a check that if tracked models have been updated since the last checks business rules need run again * data migration to add timestamps to tracked models and transaction checks * tests for real edits * tests for data migrations * TP2000-1219 Prevent maintenance mode errors (#1152) * Remove authbroker middleware when in maintenance mode * Skip applying migrations in init script * Prevent maintenance mode template attempts to access user attribute on request object * Update privacy policy link * initial commit - ref doc data model * wip commit * initial commit - ref doc data model * wip commit * wip commit * wip commit * added alignment report, reference document and reference document version views, refactored the checks and ran the checks several times against reference document versions. * TP2000-1168 Add sub-quota, blocking period & suspension period nested review tabs (#1133) * Add sub-quotas nested review tab * Add quota blocking periods nested review tab * Add quota suspension periods nested review tab * Use tab title instead of model verbose name * Add blocking period and suspension period SID to table * Feat: expand expiring quotas report to include tabs (#1131) * feat: invoke UI changes to reports and create new URL path to handle reports with multiple tabs (#1134) * feat: Add both CSV and excel types for charts exporting (#1136) * TP2000-1185 Add maintenance mode (#1137) * Add MAINTENANCE_MODE setting and middleware * Fix middleware removal and recursive redirect * Add template view and url * Add tests * Update contact us form link for other pages * Update text wording * Remove database route during maintenance * Update maintenance page template/url name --------- Co-authored-by: Dale Cannon * Increment message id & record sequence number correctly (#1083) * record seq number & message id fix * fix taricXMLRenderer, pass in value of counter * feat: implement URLs for quota reports to ease navigation (#1135) * Update readme with maintenance mode instructions. (#1140) * TP2000-1130 Move current workbasket from Session to custom User model (#1123) * Update User model references * Use custom User model * TP2000-1152-handling-invalid-workbaskets (#1113) * Update middleware to check for workbasket changing state * Update to use decorator rather than middleware, add pytest fixtures * Update tests that require a session workbasket to run * Move views and urls to workbasket app and update template * Add tests for when workbasket status changes * Tidy up following Pauls comments * Update models and templates to find workbasket in user model * Update test fixtures for workbasket being in user model * Tidy up and test updates * Update referencing to User model * Updating bdd tests for new user model * Add and update view and model unit tests * Update require_current_workbasket decorator docstring * Add docstring, move template for NoActiveWorkBasket view * Amend current workbasket id retrieval in template * Amend custom User model migration * Remake migration adding current_workbasket field to User model * Remove unused ValidateSessionWorkBasketMiddleware * Make current_workbasket optional * Add User model to admin * Use historical models to fix migration tests * Move ContentType data migration so it may be applied * Rename function to remove a users current workbasket * Amend docstrings * Remove reference to session middleware that is no longer used * Update workbaskets models following Pauls review * Bring back user workbasket middleware as extra security * Move User model from workbaskets app to common app * Add forgotten content type data migration * Remove setup_content_type fixture following patch to migrator fixture * Amend middleware util method name * Remove uneeded DoesNotExist try except block --------- Co-authored-by: Dale Cannon * Bump aiohttp from 3.9.1 to 3.9.2 (#1142) Bumps [aiohttp](https://github.com/aio-libs/aiohttp) from 3.9.1 to 3.9.2. - [Release notes](https://github.com/aio-libs/aiohttp/releases) - [Changelog](https://github.com/aio-libs/aiohttp/blob/master/CHANGES.rst) - [Commits](https://github.com/aio-libs/aiohttp/compare/v3.9.1...v3.9.2) --- updated-dependencies: - dependency-name: aiohttp dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * update importer model matching to account for end dated objects. (#1146) * update importer model matching to account for end dated objects. * update importer model matching to account for end dated objects. * Tp2000 1211 (#1148) * update govuk dependency since its been deleted at source * update govuk dependency since its been deleted at source * Bump django from 3.2.23 to 3.2.24 (#1150) Bumps [django](https://github.com/django/django) from 3.2.23 to 3.2.24. - [Commits](https://github.com/django/django/compare/3.2.23...3.2.24) --- updated-dependencies: - dependency-name: django dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Tp2000 652 force rule check after real edit (#1130) * added a check that if tracked models have been updated since the last checks business rules need run again * data migration to add timestamps to tracked models and transaction checks * tests for real edits * tests for data migrations * TP2000-1219 Prevent maintenance mode errors (#1152) * Remove authbroker middleware when in maintenance mode * Skip applying migrations in init script * Prevent maintenance mode template attempts to access user attribute on request object * Update privacy policy link * Formatting updates and adding end date field to footnote create (#1154) * TP2000-1114: React enhanced forms proof of concept (#1091) * Add react * Start to build origins form in react * Build quota origin form with initial data * Enable adding/removing of origins * Repopulate form initial in case of error on submit * Pass errors from django to react * Create origins * Add aria attribute * Reinstate geo area descriptions in form * Organise JS, code comments * Add key for react list * Simplify if statement * Add exclusions formset * Add jest for react testing * Amend gitignore * Fix error re-rendering component after submit fail * Move state management into top level component * Pass origin index to exclusions formset * Submit origin pk * Update constants.py * Test form cleaned_data * Update quota origins to use with_latest_description * Use description from annotated query * Update origins and add test * Update origin exclusions * Don't remove empty data * Fix exclusions not pre-populating * Add jest snapshot tests * Add react tests * Add jest tests to github actions * Fix query not returning origin exclusions * Fix disabled widget error * Fix origins no longer being linked to quota when order number updated * Update tests for workbasket change * Add tests for add_extra_error form method * Fix incorrect exclusion being removed * Clean up babel config * Remove unused field * Create exclusions for updated and new origins * Make sure exclusions are updated/deleted * Move current() queryset into init * Fix geographical area invalid choice error in test * Move babel packages out of dev deps (#1155) * initial commit - ref doc data model * wip commit * initial commit - ref doc data model * wip commit * wip commit * initial commit - ref doc data model * wip commit * initial commit - ref doc data model * wip commit * added alignment report, reference document and reference document version views, refactored the checks and ran the checks several times against reference document versions. * added alignment report, reference document and reference document version views, refactored the checks and ran the checks several times against reference document versions. * prep for merge to mega branch * prep for merge to mega branch * prep for merge to mega branch --------- Signed-off-by: dependabot[bot] Co-authored-by: Dale Cannon <118175145+dalecannon@users.noreply.github.com> Co-authored-by: Tash Boyse <57753415+nboyse@users.noreply.github.com> Co-authored-by: Matthew McKenzie <97194636+mattjamc@users.noreply.github.com> Co-authored-by: Dale Cannon Co-authored-by: A Gleeson Co-authored-by: Paul Pepper <85895113+paulpepper-trade@users.noreply.github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Edie Pearce --- .github/workflows/jest.yml | 21 + .profile | 9 +- README.rst | 26 + .../tests/bdd/test_edit_additional_codes.py | 36 +- additional_codes/tests/test_forms.py | 17 +- additional_codes/tests/test_views.py | 8 +- babel.config.json | 11 + .../tests/bdd/test_edit_certificates.py | 23 +- certificates/tests/test_forms.py | 24 +- certificates/tests/test_views.py | 10 +- checks/migrations/0006_auto_20231211_1642.py | 32 + .../0007_transactioncheck_timestamps.py | 44 + checks/models.py | 2 +- checks/tests/test_migrations.py | 45 + commodities/tests/test_migrations.py | 16 +- commodities/tests/test_views.py | 27 +- common/admin.py | 32 + common/forms.py | 19 +- common/jinja2/common/403.jinja | 2 +- common/jinja2/common/500.jinja | 2 +- .../common/confirm_create_description.jinja | 2 +- .../common/confirm_update_description.jinja | 2 +- common/jinja2/common/edit_description.jinja | 2 +- .../{workbasket_action.jinja => index.jinja} | 0 common/jinja2/common/maintenance.jinja | 32 + common/jinja2/components/breadcrumbs.jinja | 4 +- .../includes/common/main-menu-link.jinja | 2 +- .../includes/common/tabs/descriptions.jinja | 1 + common/jinja2/layouts/layout.jinja | 6 +- common/middleware.py | 18 + common/migrations/0001_initial.py | 123 + .../0008_user_current_workbasket.py | 45 + .../0009_tracked_model_timestamp.py | 28 + .../0010_set_tracked_model_datetime.py | 38 + common/models/__init__.py | 2 + common/models/trackedmodel.py | 5 +- common/models/user.py | 21 + common/models/utils.py | 28 +- common/renderers.py | 2 +- common/serializers.py | 18 +- common/static/common/js/application.js | 6 +- .../QuotaOriginFormset/DeleteButton.js | 12 + .../QuotaOriginExclusionForm.js | 41 + .../QuotaOriginExclusionFormset.js | 17 + .../QuotaOriginFormset/QuotaOriginForm.js | 97 + .../js/components/QuotaOriginFormset/index.js | 106 + .../tests/__snapshots__/index.test.js.snap | 1528 ++ .../QuotaOriginFormset/tests/index.test.js | 176 + common/static/common/scss/_button.scss | 9 + common/static/common/scss/application.scss | 1 + common/tests/bdd/test_edit_view.py | 19 +- common/tests/factories.py | 5 +- common/tests/test_filters.py | 7 +- common/tests/test_forms.py | 4 +- common/tests/test_migrations.py | 35 +- common/tests/test_serializers.py | 106 + common/tests/test_views.py | 22 +- common/urls.py | 1 + common/views.py | 9 +- conftest.py | 160 +- footnotes/filters.py | 2 +- footnotes/forms.py | 18 +- .../footnotes/tabs/descriptions.jinja | 1 + footnotes/tests/bdd/test_edit_footnote.py | 23 +- footnotes/tests/test_forms.py | 4 +- footnotes/tests/test_views.py | 8 +- geo_areas/tests/test_forms.py | 18 +- geo_areas/tests/test_views.py | 58 +- govuk_frontend_jinja/__init__.py | 0 govuk_frontend_jinja/flask_ext.py | 29 + govuk_frontend_jinja/templates.py | 302 + importer/forms.py | 4 +- .../jinja2/eu-importer/notify-success.jinja | 6 +- importer/management/commands/chunk_taric.py | 4 +- importer/management/commands/import_taric.py | 4 +- importer/management/util.py | 4 +- importer/tests/test_views.py | 4 +- jest-setup.js | 1 + jest.config.js | 7 + measures/tests/conftest.py | 4 +- measures/tests/test_filters.py | 18 +- measures/tests/test_forms.py | 20 +- measures/tests/test_migrations.py | 217 +- measures/tests/test_views.py | 239 +- measures/views.py | 2 +- package-lock.json | 12733 +++++++++++++--- package.json | 21 +- pii-secret-exclude.txt | 3 + publishing/tests/test_migrations.py | 2 +- publishing/tests/test_views.py | 58 +- quotas/constants.py | 2 + quotas/forms.py | 185 +- .../includes/quotas/quota-edit-origins.jinja | 42 +- .../includes/quotas/tabs/core_data.jinja | 4 +- .../quota-origins/macros/origin_display.jinja | 2 +- quotas/models.py | 17 - quotas/tests/test_forms.py | 194 +- quotas/tests/test_views.py | 343 +- quotas/views.py | 159 +- reference_documents/alignment_checks.py | 102 + reference_documents/apps.py | 1 - reference_documents/checks/base.py | 175 + reference_documents/checks/check_runner.py | 43 + .../checks/preferential_quotas.py | 1 + .../checks/preferential_rates.py | 45 + reference_documents/checks/utils.py | 21 + .../jinja2/alignment_reports/details.jinja | 25 + .../includes/tabs/preferential_quotas.jinja | 39 + .../includes/tabs/preferential_rates.jinja | 20 + .../jinja2/preferential_quotas/edit.jinja | 14 + .../details.jinja} | 0 .../index.jinja} | 2 +- .../alignment_reports.jinja | 25 + .../reference_document_versions/details.jinja | 27 + .../reference_document_versions/edit.jinja | 14 + .../new_details.jinja | 47 + .../jinja2/reference_documents/details.jinja | 24 + .../jinja2/reference_documents/index.jinja | 24 + .../jinja2/reference_documents/overview.jinja | 24 + .../migrations/0001_initial.py | 237 + reference_documents/models.py | 169 +- .../scss/_reference_documents.scss | 8 + reference_documents/urls.py | 68 +- .../views/alignment_report_views.py | 89 + .../{views.py => views/example_views.py} | 19 +- .../views/preferential_quotas.py | 37 + .../views/reference_document_version_views.py | 244 + .../views/reference_document_views.py | 128 + regulations/tests/test_views.py | 11 +- reports/jinja2/generics/table.jinja | 71 +- .../reports/report_chart_timescale.jinja | 5 +- reports/jinja2/reports/report_table.jinja | 7 +- reports/reports/base_table.py | 45 +- reports/reports/cds_approved.py | 2 +- reports/reports/cds_approved_7_day_avg.py | 2 +- reports/reports/cds_rejections.py | 4 +- ...piring_quotas_with_no_definition_period.py | 260 +- reports/reports/quotas_cannot_be_used.py | 11 +- ...piring_quotas_with_no_definition_period.py | 134 +- reports/tests/test_report_utils.py | 26 + reports/tests/test_report_views.py | 32 +- reports/urls.py | 12 +- reports/views.py | 106 +- requirements.txt | 5 +- sample.env | 2 + settings/common.py | 32 +- taric_parsers/forms.py | 4 +- taric_parsers/parsers/taric_parser.py | 29 +- taric_parsers/tasks.py | 4 +- taric_parsers/tests/test_views.py | 4 +- urls.py | 8 +- webpack.config.js | 13 + .../includes/workbaskets/navigation.jinja | 2 +- .../review-quota-blocking-periods.jinja | 37 + .../review-quota-suspension-periods.jinja | 35 + .../workbaskets/review-sub-quotas.jinja | 35 + workbaskets/jinja2/workbaskets/checks.jinja | 2 +- workbaskets/jinja2/workbaskets/compare.jinja | 8 +- .../jinja2/workbaskets/delete_changes.jinja | 2 +- .../workbaskets/delete_changes_confirm.jinja | 4 +- .../workbaskets/delete_workbasket.jinja | 2 +- .../jinja2/workbaskets/edit-details.jinja | 2 +- .../jinja2/workbaskets/edit-workbasket.jinja | 8 +- .../workbaskets/no_active_workbasket.jinja | 37 + .../jinja2/workbaskets/review-quotas.jinja | 15 + workbaskets/jinja2/workbaskets/review.jinja | 8 +- .../workbaskets/summary-workbasket.jinja | 4 +- .../jinja2/workbaskets/taric/transaction.xml | 2 +- .../jinja2/workbaskets/violation_detail.jinja | 2 +- .../jinja2/workbaskets/violations.jinja | 2 +- workbaskets/migrations/0001_initial.py | 24 +- .../0002_change_status_per_ADR008.py | 25 + workbaskets/models.py | 60 +- workbaskets/tests/test_models.py | 7 + workbaskets/tests/test_views.py | 397 +- workbaskets/urls.py | 20 + workbaskets/views/decorators.py | 20 +- workbaskets/views/ui.py | 78 +- 178 files changed, 17892 insertions(+), 3355 deletions(-) create mode 100644 .github/workflows/jest.yml mode change 100644 => 100755 .profile create mode 100644 babel.config.json create mode 100644 checks/migrations/0006_auto_20231211_1642.py create mode 100644 checks/migrations/0007_transactioncheck_timestamps.py create mode 100644 checks/tests/test_migrations.py create mode 100644 common/admin.py rename common/jinja2/common/{workbasket_action.jinja => index.jinja} (100%) create mode 100644 common/jinja2/common/maintenance.jinja create mode 100644 common/middleware.py create mode 100644 common/migrations/0008_user_current_workbasket.py create mode 100644 common/migrations/0009_tracked_model_timestamp.py create mode 100644 common/migrations/0010_set_tracked_model_datetime.py create mode 100644 common/models/user.py create mode 100644 common/static/common/js/components/QuotaOriginFormset/DeleteButton.js create mode 100644 common/static/common/js/components/QuotaOriginFormset/QuotaOriginExclusionForm.js create mode 100644 common/static/common/js/components/QuotaOriginFormset/QuotaOriginExclusionFormset.js create mode 100644 common/static/common/js/components/QuotaOriginFormset/QuotaOriginForm.js create mode 100644 common/static/common/js/components/QuotaOriginFormset/index.js create mode 100644 common/static/common/js/components/QuotaOriginFormset/tests/__snapshots__/index.test.js.snap create mode 100644 common/static/common/js/components/QuotaOriginFormset/tests/index.test.js create mode 100644 govuk_frontend_jinja/__init__.py create mode 100644 govuk_frontend_jinja/flask_ext.py create mode 100644 govuk_frontend_jinja/templates.py create mode 100644 jest-setup.js create mode 100644 jest.config.js create mode 100644 reference_documents/alignment_checks.py create mode 100644 reference_documents/checks/base.py create mode 100644 reference_documents/checks/check_runner.py create mode 100644 reference_documents/checks/preferential_quotas.py create mode 100644 reference_documents/checks/preferential_rates.py create mode 100644 reference_documents/checks/utils.py create mode 100644 reference_documents/jinja2/alignment_reports/details.jinja create mode 100644 reference_documents/jinja2/includes/tabs/preferential_quotas.jinja create mode 100644 reference_documents/jinja2/includes/tabs/preferential_rates.jinja create mode 100644 reference_documents/jinja2/preferential_quotas/edit.jinja rename reference_documents/jinja2/{reference_documents/detail.jinja => reference_document_examples/details.jinja} (100%) rename reference_documents/jinja2/{reference_documents/list.jinja => reference_document_examples/index.jinja} (92%) create mode 100644 reference_documents/jinja2/reference_document_versions/alignment_reports.jinja create mode 100644 reference_documents/jinja2/reference_document_versions/details.jinja create mode 100644 reference_documents/jinja2/reference_document_versions/edit.jinja create mode 100644 reference_documents/jinja2/reference_document_versions/new_details.jinja create mode 100644 reference_documents/jinja2/reference_documents/details.jinja create mode 100644 reference_documents/jinja2/reference_documents/index.jinja create mode 100644 reference_documents/jinja2/reference_documents/overview.jinja create mode 100644 reference_documents/migrations/0001_initial.py create mode 100644 reference_documents/static/reference_documents/scss/_reference_documents.scss create mode 100644 reference_documents/views/alignment_report_views.py rename reference_documents/{views.py => views/example_views.py} (83%) create mode 100644 reference_documents/views/preferential_quotas.py create mode 100644 reference_documents/views/reference_document_version_views.py create mode 100644 reference_documents/views/reference_document_views.py create mode 100644 workbaskets/jinja2/includes/workbaskets/review-quota-blocking-periods.jinja create mode 100644 workbaskets/jinja2/includes/workbaskets/review-quota-suspension-periods.jinja create mode 100644 workbaskets/jinja2/includes/workbaskets/review-sub-quotas.jinja create mode 100644 workbaskets/jinja2/workbaskets/no_active_workbasket.jinja diff --git a/.github/workflows/jest.yml b/.github/workflows/jest.yml new file mode 100644 index 000000000..514b865cf --- /dev/null +++ b/.github/workflows/jest.yml @@ -0,0 +1,21 @@ +name: CI/CD + +on: + push: + branches: [master] + pull_request: + branches: [master] + +jobs: + test: + name: "Run jest tests" + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + + - name: Set up Node + uses: ./.github/actions/setup-node + + - name: Run tests + run: npm run test diff --git a/.profile b/.profile old mode 100644 new mode 100755 index dcebbe256..ee12fb823 --- a/.profile +++ b/.profile @@ -3,8 +3,13 @@ # ref - https://docs.cloudfoundry.org/devguide/deploy-apps/deploy-app.html echo "---- RUNNING release tasks (.profile) ------" -echo "---- Apply Migrations ------" -python manage.py migrate + +if [[ "$MAINTENANCE_MODE" != "True" && "$MAINTENANCE_MODE" != "true" ]] ; then + echo "---- Apply Migrations ------" + python manage.py migrate +else + echo "---- Skip Applying Migrations (Maintenance Mode) ------" +fi echo "---- Collect Static Files ------" OUTPUT=$(python manage.py collectstatic --noinput --clear) diff --git a/README.rst b/README.rst index 044607d81..ee16932d0 100644 --- a/README.rst +++ b/README.rst @@ -497,6 +497,32 @@ We use a shared service accross the department for virus scanning to run locally 3. add CLAM_AV_DOMAIN without http(s):// 4. set CLAM_AV_USERNAME,CLAM_AV_PASSWORD as the username and password found in the config.py in the dit-clamav-rest project + +Application maintenance mode +---------------------------- + +The application can be put into a "maintenance mode" type of operation. By doing +so, all user web access is routed to a maintenance view and the default database +route removes the application's access to the database. This prevents +inadvertent changes by users, via the application UI, to application data while +in maintenance mode. Note, however, that this would not restrict other forms of +data update, such as active Celery tasks - Celery and other similar processes +need to be scaled down separately. + +The process for transitioning the application into and back out of maintenance +mode is as follows: + +1. Set the application’s `MAINTENANCE_MODE` environment variable to `True`. + +2. Restart the application so that it picks up the new value of `MAINTENANCE_MODE`. + +3. Complete maintenance activities. + +4. Set the value of the `MAINTENANCE_MODE` environment variable to `False`. + +5. Restart the application. + + How to contribute ----------------- diff --git a/additional_codes/tests/bdd/test_edit_additional_codes.py b/additional_codes/tests/bdd/test_edit_additional_codes.py index 1eaab475e..1aaf049ae 100644 --- a/additional_codes/tests/bdd/test_edit_additional_codes.py +++ b/additional_codes/tests/bdd/test_edit_additional_codes.py @@ -15,13 +15,24 @@ @pytest.fixture @when("I edit additional code X000") -def model_edit_page(client, additional_code_X000): - return client.get(additional_code_X000.get_url("edit")) +def model_edit_page(client_with_current_workbasket, additional_code_X000): + return client_with_current_workbasket.get(additional_code_X000.get_url("edit")) + + +@pytest.fixture +@when("I edit additional code X000") +def model_edit_page_invalid_user( + client_with_current_workbasket_no_permissions, + additional_code_X000, +): + return client_with_current_workbasket_no_permissions.get( + additional_code_X000.get_url("edit"), + ) @then("I am not permitted to edit") -def edit_permission_denied(model_edit_page): - assert model_edit_page.status_code == 403 +def edit_permission_denied(model_edit_page_invalid_user): + assert model_edit_page_invalid_user.status_code == 403 @then("I see an edit form") @@ -31,8 +42,12 @@ def edit_permission_granted(model_edit_page): @pytest.fixture @when("I set the end date before the start date on additional code X000") -def end_date_before_start(client, response, additional_code_X000): - response["response"] = client.post( +def end_date_before_start( + client_with_current_workbasket, + response, + additional_code_X000, +): + response["response"] = client_with_current_workbasket.post( additional_code_X000.get_url("edit"), validity_period_post_data( start=date(2021, 1, 1), @@ -44,8 +59,13 @@ def end_date_before_start(client, response, additional_code_X000): @when( "I set the start date of additional code X000 to overlap the previous additional code", ) -def submit_overlapping(client, response, additional_code_X000, old_additional_code): - response["response"] = client.post( +def submit_overlapping( + client_with_current_workbasket, + response, + additional_code_X000, + old_additional_code, +): + response["response"] = client_with_current_workbasket.post( additional_code_X000.get_url("edit"), validity_period_post_data( start=old_additional_code.valid_between.lower, diff --git a/additional_codes/tests/test_forms.py b/additional_codes/tests/test_forms.py index 147da14ef..8e82bef3c 100644 --- a/additional_codes/tests/test_forms.py +++ b/additional_codes/tests/test_forms.py @@ -7,7 +7,7 @@ # https://uktrade.atlassian.net/browse/TP2000-296 -def test_additional_code_create_sid(session_with_workbasket, date_ranges): +def test_additional_code_create_sid(session_request_with_workbasket, date_ranges): """Tests that additional code type is NOT considered when generating a new sid.""" type_1 = factories.AdditionalCodeTypeFactory.create() @@ -21,7 +21,10 @@ def test_additional_code_create_sid(session_with_workbasket, date_ranges): "start_date_1": date_ranges.normal.lower.month, "start_date_2": date_ranges.normal.lower.year, } - form = forms.AdditionalCodeCreateForm(data=data, request=session_with_workbasket) + form = forms.AdditionalCodeCreateForm( + data=data, + request=session_request_with_workbasket, + ) assert form.is_valid() @@ -30,7 +33,10 @@ def test_additional_code_create_sid(session_with_workbasket, date_ranges): assert new_additional_code.sid != additional_code.sid -def test_additional_code_create_valid_data(session_with_workbasket, date_ranges): +def test_additional_code_create_valid_data( + session_request_with_workbasket, + date_ranges, +): """Tests that AdditionalCodeCreateForm.is_valid() returns True when passed required fields and additional_code_description values in cleaned data.""" code_type = factories.AdditionalCodeTypeFactory.create() @@ -42,7 +48,10 @@ def test_additional_code_create_valid_data(session_with_workbasket, date_ranges) "start_date_1": date_ranges.normal.lower.month, "start_date_2": date_ranges.normal.lower.year, } - form = forms.AdditionalCodeCreateForm(data=data, request=session_with_workbasket) + form = forms.AdditionalCodeCreateForm( + data=data, + request=session_request_with_workbasket, + ) assert form.is_valid() assert form.cleaned_data["additional_code_description"].description == "description" diff --git a/additional_codes/tests/test_views.py b/additional_codes/tests/test_views.py index 6c8e60cfe..0b68d1ff1 100644 --- a/additional_codes/tests/test_views.py +++ b/additional_codes/tests/test_views.py @@ -133,7 +133,7 @@ def test_additional_codes_detail_views( view, url_pattern, valid_user_client, - session_with_workbasket, + session_request_with_workbasket, ): """Verify that additional code detail views are under the url additional_codes/ and don't return an error.""" @@ -228,7 +228,7 @@ def test_additional_code_details_list_no_measures(valid_user_client): assert num_measures == 0 -def test_additional_code_description_create(valid_user_client): +def test_additional_code_description_create(client_with_current_workbasket): """Tests that `AdditionalCodeDescriptionCreate` view returns 200 and creates a description for the current version of an additional code.""" additional_code = factories.AdditionalCodeFactory.create() @@ -250,10 +250,10 @@ def test_additional_code_description_create(valid_user_client): } with override_current_transaction(Transaction.objects.last()): - get_response = valid_user_client.get(url) + get_response = client_with_current_workbasket.get(url) assert get_response.status_code == 200 - post_response = valid_user_client.post(url, data) + post_response = client_with_current_workbasket.post(url, data) assert post_response.status_code == 302 assert AdditionalCodeDescription.objects.filter( diff --git a/babel.config.json b/babel.config.json new file mode 100644 index 000000000..66fae7d96 --- /dev/null +++ b/babel.config.json @@ -0,0 +1,11 @@ +{ + "presets": [ + "@babel/env", + [ + "@babel/preset-react", + { + "runtime": "automatic" // defaults to classic + } + ] + ] +} \ No newline at end of file diff --git a/certificates/tests/bdd/test_edit_certificates.py b/certificates/tests/bdd/test_edit_certificates.py index e4054e4d3..76f1508f6 100644 --- a/certificates/tests/bdd/test_edit_certificates.py +++ b/certificates/tests/bdd/test_edit_certificates.py @@ -10,13 +10,24 @@ @pytest.fixture @when("I edit certificate X000") -def model_edit_page(client, certificate_X000): - return client.get(certificate_X000.get_url("edit")) +def model_edit_page(client_with_current_workbasket, certificate_X000): + return client_with_current_workbasket.get(certificate_X000.get_url("edit")) + + +@pytest.fixture +@when("I edit certificate X000") +def model_edit_page_invalid_user( + client_with_current_workbasket_no_permissions, + certificate_X000, +): + return client_with_current_workbasket_no_permissions.get( + certificate_X000.get_url("edit"), + ) @then("I am not permitted to edit") -def edit_permission_denied(model_edit_page): - assert model_edit_page.status_code == 403 +def edit_permission_denied(model_edit_page_invalid_user): + assert model_edit_page_invalid_user.status_code == 403 @then("I see an edit form") @@ -26,8 +37,8 @@ def edit_permission_granted(model_edit_page): @pytest.fixture @when("I set the end date before the start date on certificate X000") -def end_date_before_start(client, response, certificate_X000): - response["response"] = client.post( +def end_date_before_start(client_with_current_workbasket, response, certificate_X000): + response["response"] = client_with_current_workbasket.post( certificate_X000.get_url("edit"), { "start_date_0": "1", diff --git a/certificates/tests/test_forms.py b/certificates/tests/test_forms.py index 6d3159759..a747d5f65 100644 --- a/certificates/tests/test_forms.py +++ b/certificates/tests/test_forms.py @@ -10,7 +10,7 @@ def test_form_save_creates_new_certificate( - session_with_workbasket, + session_request_with_workbasket, ): """Tests that the certificate create form creates a new certificate, and that two certificates of the same type are created with different sid's.""" @@ -35,7 +35,7 @@ def test_form_save_creates_new_certificate( } form = forms.CertificateCreateForm( data=certificate_b_data, - request=session_with_workbasket, + request=session_request_with_workbasket, ) certificate_b = form.save(commit=False) @@ -45,7 +45,7 @@ def test_form_save_creates_new_certificate( def test_certificate_type_does_not_increment_id( - session_with_workbasket, + session_request_with_workbasket, ): """Tests that when two certificates are made with different types, the sids are not incremented.""" @@ -74,7 +74,7 @@ def test_certificate_type_does_not_increment_id( for certificate in certificates: form = forms.CertificateCreateForm( data=certificate, - request=session_with_workbasket, + request=session_request_with_workbasket, ) saved_certificate = form.save(commit=False) completed_certificates.append(saved_certificate) @@ -87,7 +87,7 @@ def test_certificate_type_does_not_increment_id( assert completed_certificates[1].sid == "001" -def test_certificate_create_form_validates_data(session_with_workbasket): +def test_certificate_create_form_validates_data(session_request_with_workbasket): """A test to check that the create form validates data and ciphers out incorrect submissions.""" @@ -101,7 +101,7 @@ def test_certificate_create_form_validates_data(session_with_workbasket): } form = forms.CertificateCreateForm( data=certificate_data, - request=session_with_workbasket, + request=session_request_with_workbasket, ) error_string = [ "Select a valid choice. That choice is not one of the available choices.", @@ -119,7 +119,7 @@ def test_certificate_create_form_validates_data(session_with_workbasket): assert not form.is_valid() -def test_certificate_create_with_custom_sid(session_with_workbasket): +def test_certificate_create_with_custom_sid(session_request_with_workbasket): """Tests that a certificate can be created with a custom sid inputted by the user.""" certificate_type = factories.CertificateTypeFactory.create() @@ -133,14 +133,14 @@ def test_certificate_create_with_custom_sid(session_with_workbasket): } form = forms.CertificateCreateForm( data=data, - request=session_with_workbasket, + request=session_request_with_workbasket, ) certificate = form.save(commit=False) assert certificate.sid == "A01" -def test_certificate_create_ignores_non_numeric_sid(session_with_workbasket): +def test_certificate_create_ignores_non_numeric_sid(session_request_with_workbasket): """Tests that a certificate is created with a numeric sid when a certificate of the same type with a non-numeric sid already exists.""" certificate_type = factories.CertificateTypeFactory.create() @@ -154,14 +154,14 @@ def test_certificate_create_ignores_non_numeric_sid(session_with_workbasket): } form = forms.CertificateCreateForm( data=data, - request=session_with_workbasket, + request=session_request_with_workbasket, ) certificate = form.save(commit=False) assert certificate.sid == "001" -def test_validation_error_raised_for_duplicate_sid(session_with_workbasket): +def test_validation_error_raised_for_duplicate_sid(session_request_with_workbasket): """Tests that a validation error is raised on create when a certificate of the same type with the same sid already exists.""" certificate_type = factories.CertificateTypeFactory.create() @@ -176,7 +176,7 @@ def test_validation_error_raised_for_duplicate_sid(session_with_workbasket): } form = forms.CertificateCreateForm( data=data, - request=session_with_workbasket, + request=session_request_with_workbasket, ) assert not form.is_valid() diff --git a/certificates/tests/test_views.py b/certificates/tests/test_views.py index cb27d9695..9ccd0849f 100644 --- a/certificates/tests/test_views.py +++ b/certificates/tests/test_views.py @@ -46,7 +46,7 @@ def test_certificate_description_delete_form(use_delete_form): def test_certificate_create_form_creates_certificate_description_object( - valid_user_api_client, + api_client_with_current_workbasket, ): # Post a form create_url = reverse("certificate-ui-create") @@ -60,7 +60,7 @@ def test_certificate_create_form_creates_certificate_description_object( "description": "A participation certificate", } - valid_user_api_client.post(create_url, form_data) + api_client_with_current_workbasket.post(create_url, form_data) # get the certificate we have made, and the certificate description matching our description on the form certificate = models.Certificate.objects.all()[0] certificate_description = models.CertificateDescription.objects.filter( @@ -84,7 +84,7 @@ def test_certificate_detail_views( view, url_pattern, valid_user_client, - session_with_workbasket, + session_request_with_workbasket, ): """Verify that certificate detail views are under the url certificates/ and don't return an error.""" @@ -128,7 +128,7 @@ def test_description_create_get_initial(): assert initial["described_certificate"] == new_version -def test_description_create_get_context_data(valid_user_api_client): +def test_description_create_get_context_data(api_client_with_current_workbasket): """Test that posting to certificate create endpoint with valid data returns a 302 and creates new description matching certificate.""" certificate = factories.CertificateFactory.create(description=None) @@ -145,7 +145,7 @@ def test_description_create_get_context_data(valid_user_api_client): "validity_start_2": 2022, } assert not models.CertificateDescription.objects.exists() - response = valid_user_api_client.post(url, post_data) + response = api_client_with_current_workbasket.post(url, post_data) assert response.status_code == 302 assert models.CertificateDescription.objects.filter( diff --git a/checks/migrations/0006_auto_20231211_1642.py b/checks/migrations/0006_auto_20231211_1642.py new file mode 100644 index 000000000..a417c684c --- /dev/null +++ b/checks/migrations/0006_auto_20231211_1642.py @@ -0,0 +1,32 @@ +# Generated by Django 3.2.23 on 2023-12-11 16:42 + +import django.utils.timezone +from django.db import migrations +from django.db import models + +epoch_time = 0000000000 + + +class Migration(migrations.Migration): + dependencies = [ + ("checks", "0005_trackedmodelcheck_processing_time"), + ("common", "0010_set_tracked_model_datetime"), + ("workbaskets", "0008_datarow_dataupload"), + ] + + operations = [ + migrations.AddField( + model_name="transactioncheck", + name="created_at", + field=models.DateTimeField( + auto_now_add=True, + default=django.utils.timezone.datetime.fromtimestamp(epoch_time), + ), + preserve_default=False, + ), + migrations.AddField( + model_name="transactioncheck", + name="updated_at", + field=models.DateTimeField(auto_now=True), + ), + ] diff --git a/checks/migrations/0007_transactioncheck_timestamps.py b/checks/migrations/0007_transactioncheck_timestamps.py new file mode 100644 index 000000000..f259965e3 --- /dev/null +++ b/checks/migrations/0007_transactioncheck_timestamps.py @@ -0,0 +1,44 @@ +# Generated by Django 3.2.18 on 2023-03-28 15:39 + +from django.db import migrations +from django.db.models import Max +from django.db.models import Min +from django.db.transaction import atomic + +from workbaskets.validators import WorkflowStatus + + +@atomic +def generate_timestamps(apps, schema_editor): + TransactionCheck = apps.get_model("checks", "transactioncheck") + + transaction_checks_to_update = TransactionCheck.objects.filter( + transaction__workbasket__status=WorkflowStatus.EDITING, + completed=True, + ) + + for check in transaction_checks_to_update: + if not check.model_checks.all(): + continue + aggregated_checks = check.model_checks.aggregate( + first_created_at=Min("created_at"), + last_updated_at=Max("updated_at"), + ) + check.completed = True + check.successful = False + check.created_at = aggregated_checks["first_created_at"] + check.updated_at = aggregated_checks["last_updated_at"] + check.save() + + +class Migration(migrations.Migration): + dependencies = [ + ("checks", "0006_auto_20231211_1642"), + ] + + operations = [ + migrations.RunPython( + generate_timestamps, + reverse_code=migrations.RunPython.noop, + ), + ] diff --git a/checks/models.py b/checks/models.py index 6d061c863..0bec9a901 100644 --- a/checks/models.py +++ b/checks/models.py @@ -7,7 +7,7 @@ from common.models.transactions import Transaction -class TransactionCheck(models.Model): +class TransactionCheck(TimestampedMixin): """ Represents an in-progress or completed check of a transaction for correctness. diff --git a/checks/tests/test_migrations.py b/checks/tests/test_migrations.py new file mode 100644 index 000000000..cfd96a3cb --- /dev/null +++ b/checks/tests/test_migrations.py @@ -0,0 +1,45 @@ +import pytest + +from checks.models import TransactionCheck +from checks.tests.factories import TrackedModelCheckFactory + + +@pytest.mark.django_db() +def test_timestamp_migration(migrator): + migrator.reset() + old_state = migrator.apply_initial_migration( + ( + "checks", + "0006_auto_20231211_1642", + ), + ) + migrator.apply_tested_migration( + ( + "tests", + "0003_auto_20210714_1522", + ), + ) + tracked_model_check_1 = TrackedModelCheckFactory.create( + transaction_check__completed=True, + transaction_check__successful=True, + successful=True, + ) + + transaction_check = TransactionCheck.objects.get( + pk=tracked_model_check_1.transaction_check.transaction.pk, + ) + + migrator.apply_tested_migration( + ( + "checks", + "0007_transactioncheck_timestamps", + ), + ) + + new_transaction_check = TransactionCheck.objects.get(pk=transaction_check.pk) + assert new_transaction_check.completed == True + assert new_transaction_check.successful == False + assert new_transaction_check.created_at == tracked_model_check_1.created_at + assert new_transaction_check.updated_at >= tracked_model_check_1.updated_at + + migrator.reset() diff --git a/commodities/tests/test_migrations.py b/commodities/tests/test_migrations.py index 9f528c3ab..7ed54bcc5 100644 --- a/commodities/tests/test_migrations.py +++ b/commodities/tests/test_migrations.py @@ -2,13 +2,12 @@ import pytest -from common.tests.factories import QueuedWorkBasketFactory from common.util import TaricDateRange from common.validators import UpdateType @pytest.mark.django_db() -def test_main_migration_works(migrator, setup_content_types): +def test_main_migration_works(migrator): """Ensures that the description date fix for TOPS-745 migration works.""" # before migration @@ -16,7 +15,7 @@ def test_main_migration_works(migrator, setup_content_types): ("commodities", "0011_TOPS_745_migration_dependencies"), ) - setup_content_types(old_state.apps) + target_workbasket_id = 238 GoodsNomenclatureDescription = old_state.apps.get_model( "commodities", @@ -26,12 +25,13 @@ def test_main_migration_works(migrator, setup_content_types): Transaction = old_state.apps.get_model("common", "Transaction") Workbasket = old_state.apps.get_model("workbaskets", "WorkBasket") VersionGroup = old_state.apps.get_model("common", "VersionGroup") + User = old_state.apps.get_model("common", "User") - QueuedWorkBasketFactory.create().save() - workbasket = Workbasket.objects.create(id=238, author_id=1) + user = User.objects.create() + workbasket = Workbasket.objects.create(id=target_workbasket_id, author=user) new_transaction = Transaction.objects.create( workbasket=workbasket, - order=Transaction.objects.order_by("order").last().order + 1, + order=1, ) gn_older_version = GoodsNomenclature.objects.create( @@ -86,14 +86,12 @@ def test_main_migration_works(migrator, setup_content_types): @pytest.mark.django_db() -def test_main_migration_ignores_if_no_data(migrator, setup_content_types): +def test_main_migration_ignores_if_no_data(migrator): # before migration old_state = migrator.apply_initial_migration( ("commodities", "0011_TOPS_745_migration_dependencies"), ) - setup_content_types(old_state.apps) - GoodsNomenclatureDescription = old_state.apps.get_model( "commodities", "GoodsNomenclatureDescription", diff --git a/commodities/tests/test_views.py b/commodities/tests/test_views.py index dffc16191..0c546a35d 100644 --- a/commodities/tests/test_views.py +++ b/commodities/tests/test_views.py @@ -111,7 +111,7 @@ def test_commodities_detail_views( url_pattern, valid_user_client, requests_mock, - session_with_workbasket, + session_request_with_workbasket, ): """Verify that commodity detail views are under the url commodities/ and don't return an error.""" @@ -378,7 +378,7 @@ def test_commodity_measures_sorting_measure_type( assert measure_sids == [measure1.sid, measure2.sid, measure3.sid] -def test_add_commodity_footnote(valid_user_client, date_ranges): +def test_add_commodity_footnote(client_with_current_workbasket, date_ranges): commodity = factories.GoodsNomenclatureFactory.create( valid_between=date_ranges.big_no_end, ) @@ -396,7 +396,7 @@ def test_add_commodity_footnote(valid_user_client, date_ranges): # sanity check assert commodity.footnote_associations.count() == 0 - response = valid_user_client.post(url, data) + response = client_with_current_workbasket.post(url, data) assert response.status_code == 302 assert commodity.footnote_associations.count() == 1 @@ -411,7 +411,10 @@ def test_add_commodity_footnote(valid_user_client, date_ranges): assert new_association.goods_nomenclature == commodity -def test_add_commodity_footnote_NIG22_failure(valid_user_client, date_ranges): +def test_add_commodity_footnote_NIG22_failure( + client_with_current_workbasket, + date_ranges, +): """ Tests failure of NIG22: @@ -433,7 +436,7 @@ def test_add_commodity_footnote_NIG22_failure(valid_user_client, date_ranges): "end_date": "", } - response = valid_user_client.post(url, data) + response = client_with_current_workbasket.post(url, data) assert response.status_code == 200 @@ -443,12 +446,12 @@ def test_add_commodity_footnote_NIG22_failure(valid_user_client, date_ranges): ) -def test_add_commodity_footnote_form_page(valid_user_client, date_ranges): +def test_add_commodity_footnote_form_page(client_with_current_workbasket, date_ranges): commodity = factories.GoodsNomenclatureFactory.create( valid_between=date_ranges.big_no_end, ) url = reverse("commodity-ui-add-footnote", kwargs={"sid": commodity.sid}) - response = valid_user_client.get(url) + response = client_with_current_workbasket.get(url) assert response.status_code == 200 @@ -505,7 +508,7 @@ def test_commodity_footnotes_page(valid_user_client): assert not footnote_descriptions.difference(page_footnote_descriptions) -def test_commodity_footnote_update_success(valid_user_client, date_ranges): +def test_commodity_footnote_update_success(client_with_current_workbasket, date_ranges): commodity = factories.GoodsNomenclatureFactory.create() footnote1 = factories.FootnoteFactory.create() association1 = factories.FootnoteAssociationGoodsNomenclatureFactory.create( @@ -521,7 +524,7 @@ def test_commodity_footnote_update_success(valid_user_client, date_ranges): "start_date_2": date_ranges.later.lower.year, "end_date": "", } - response = valid_user_client.post(url, data) + response = client_with_current_workbasket.post(url, data) tx = Transaction.objects.last() updated_association = ( FootnoteAssociationGoodsNomenclature.objects.approved_up_to_transaction( @@ -532,7 +535,7 @@ def test_commodity_footnote_update_success(valid_user_client, date_ranges): assert response.url == updated_association.get_url("confirm-update") -def test_footnote_association_delete(valid_user_client): +def test_footnote_association_delete(client_with_current_workbasket): commodity = factories.GoodsNomenclatureFactory.create() footnote1 = factories.FootnoteFactory.create() association1 = factories.FootnoteAssociationGoodsNomenclatureFactory.create( @@ -540,7 +543,7 @@ def test_footnote_association_delete(valid_user_client): goods_nomenclature=commodity, ) url = association1.get_url("delete") - response = valid_user_client.post(url, {"submit": "Delete"}) + response = client_with_current_workbasket.post(url, {"submit": "Delete"}) assert response.status_code == 302 assert response.url == reverse( @@ -554,7 +557,7 @@ def test_footnote_association_delete(valid_user_client): assert tx.workbasket.tracked_models.first().goods_nomenclature == commodity assert tx.workbasket.tracked_models.first().update_type == UpdateType.DELETE - confirm_response = valid_user_client.get(response.url) + confirm_response = client_with_current_workbasket.get(response.url) soup = BeautifulSoup( confirm_response.content.decode(response.charset), "html.parser", diff --git a/common/admin.py b/common/admin.py new file mode 100644 index 000000000..2016e1c17 --- /dev/null +++ b/common/admin.py @@ -0,0 +1,32 @@ +from django.contrib import admin +from django.contrib.auth import forms as auth_forms +from django.contrib.auth import get_user_model +from django.contrib.auth.admin import UserAdmin + +User = get_user_model() + + +class UserCreationForm(auth_forms.UserCreationForm): + class Meta(auth_forms.UserCreationForm.Meta): + model = User + fields = auth_forms.UserCreationForm.Meta.fields + + +class UserChangeForm(auth_forms.UserChangeForm): + class Meta(auth_forms.UserChangeForm.Meta): + model = User + fields = auth_forms.UserCreationForm.Meta.fields + + +class UserAdmin(UserAdmin): + model = User + form = UserChangeForm + add_form = UserCreationForm + readonly_fields = ("current_workbasket",) + fieldsets = UserAdmin.fieldsets + ( + ("Workbasket", {"fields": ("current_workbasket",)}), + ) + add_fieldsets = UserAdmin.add_fieldsets + + +admin.site.register(User, UserAdmin) diff --git a/common/forms.py b/common/forms.py index 7adc18fc8..76428cdf9 100644 --- a/common/forms.py +++ b/common/forms.py @@ -250,8 +250,12 @@ class HMRCCDSManagerActions(TextChoices): class CommonUserActions(TextChoices): SEARCH = "SEARCH", "Search the tariff" + + +class ReferenceDocumentsActions(TextChoices): # Change this to be dependent on permissions later - VIEW_REF_DOCS = "VIEW_REF_DOCS", "View reference documents" + REF_DOCS_EXAMPLES = "REF_DOCS_EXAMPLES", "View example reference document index" + REF_DOCS = "REF_DOCS", "View reference documents" class ImportUserActions(TextChoices): @@ -280,6 +284,9 @@ def __init__(self, *args, **kwargs): choices += CommonUserActions.choices + if self.user.has_perm("reference_documents.view_reference_document"): + choices += ReferenceDocumentsActions.choices + if self.user.has_perm("common.add_trackedmodel") or self.user.has_perm( "common.change_trackedmodel", ): @@ -757,17 +764,15 @@ def unprefix_formset_data(prefix, data): for i in range(0, num_items): subform_initial = {} for k, v in formset_data.items(): - if v: - if k.startswith(f"{prefix}-{i}-"): - subform_initial[k.split(f"{prefix}-{i}-")[1]] = v + if k.startswith(f"{prefix}-{i}-"): + subform_initial[k.split(f"{prefix}-{i}-")[1]] = v if subform_initial: output.append(subform_initial) for k, v in formset_data.items(): subform_initial = {} - if v: - if k.startswith(f"{prefix}-__prefix__-"): - subform_initial[k.split(f"{prefix}-__prefix__-")[1]] = v + if k.startswith(f"{prefix}-__prefix__-"): + subform_initial[k.split(f"{prefix}-__prefix__-")[1]] = v if subform_initial: output.append(subform_initial) diff --git a/common/jinja2/common/403.jinja b/common/jinja2/common/403.jinja index f0efb1268..76038c36c 100644 --- a/common/jinja2/common/403.jinja +++ b/common/jinja2/common/403.jinja @@ -11,7 +11,7 @@

    You do not have access to this part of the service

    - Contact the Tariff Application Platform (TAP) team to change your access rights. + Contact the Tariff Application Platform (TAP) team to change your access rights.

    You will need to describe what you were doing before seeing this error message.

    The team will reply to you within two working days.

    diff --git a/common/jinja2/common/500.jinja b/common/jinja2/common/500.jinja index d418a33fd..2d957ab19 100644 --- a/common/jinja2/common/500.jinja +++ b/common/jinja2/common/500.jinja @@ -12,7 +12,7 @@

    Sorry, there is a problem with this service

    + href="https://forms.office.com/Pages/ResponsePage.aspx?id=7Beij6oz-0atlt_mgAa7husF7H2qg6ZMi_-_m4b1eedUMjBNRllURlk0R0dFS1FHQkVBMFhNWjROViQlQCN0PWcu"> Contact the Tariff Application Platform (TAP) team who will help to resolve this problem. diff --git a/common/jinja2/common/confirm_create_description.jinja b/common/jinja2/common/confirm_create_description.jinja index 6012f63f7..2727c55e1 100644 --- a/common/jinja2/common/confirm_create_description.jinja +++ b/common/jinja2/common/confirm_create_description.jinja @@ -36,7 +36,7 @@

    diff --git a/common/jinja2/common/confirm_update_description.jinja b/common/jinja2/common/confirm_update_description.jinja index 9f0fc8863..9feecdc9c 100644 --- a/common/jinja2/common/confirm_update_description.jinja +++ b/common/jinja2/common/confirm_update_description.jinja @@ -36,7 +36,7 @@ diff --git a/common/jinja2/common/edit_description.jinja b/common/jinja2/common/edit_description.jinja index 0a2eb1fe0..7a3739775 100644 --- a/common/jinja2/common/edit_description.jinja +++ b/common/jinja2/common/edit_description.jinja @@ -2,7 +2,7 @@ {% from "components/details/macro.njk" import govukDetails %} {% from "components/table/macro.njk" import govukTable %} -{% set page_title = "Edit " ~ object._meta.verbose_name %} +{% set page_title = "Edit " ~ object._meta.verbose_name ~ " details"%} {% set described_object = object.get_described_object() %} diff --git a/common/jinja2/common/workbasket_action.jinja b/common/jinja2/common/index.jinja similarity index 100% rename from common/jinja2/common/workbasket_action.jinja rename to common/jinja2/common/index.jinja diff --git a/common/jinja2/common/maintenance.jinja b/common/jinja2/common/maintenance.jinja new file mode 100644 index 000000000..3aafad122 --- /dev/null +++ b/common/jinja2/common/maintenance.jinja @@ -0,0 +1,32 @@ +{% extends "layouts/layout.jinja" %} + +{% set page_title = "Sorry, the service is unavailable" %} + +{% set workbasket_html %}{% endset %} + +{% block header %} +{{ govukHeader({ + "homepageUrl": "https://gov.uk/", + "serviceName": service_name, + "serviceUrl": "/", +}) }} +{% endblock %} + +{% block breadcrumb %}{% endblock %} + +{% block content %} +
    +
    +
    +
    +

    Sorry, the service is unavailable

    +

    You will be able to use the service later.

    +

    + Contact the Tariff Application Platform (TAP) team if you have any queries. {# /PS-IGNORE #} +

    +

    The team will reply to you within 2 working days.

    +
    +
    +
    +
    +{% endblock %} diff --git a/common/jinja2/components/breadcrumbs.jinja b/common/jinja2/components/breadcrumbs.jinja index 0f09a507a..c59eb1608 100644 --- a/common/jinja2/components/breadcrumbs.jinja +++ b/common/jinja2/components/breadcrumbs.jinja @@ -5,9 +5,9 @@
  • Home
  • - {% if request.session.workbasket %} + {% if request.user.current_workbasket %}
  • - Workbasket {{ request.session.workbasket.id }} + Workbasket {{ request.user.current_workbasket.id }}
  • {% endif %} {% for crumb in breadcrumbs_list %} diff --git a/common/jinja2/includes/common/main-menu-link.jinja b/common/jinja2/includes/common/main-menu-link.jinja index cbcce5ed3..4f3cdfe55 100644 --- a/common/jinja2/includes/common/main-menu-link.jinja +++ b/common/jinja2/includes/common/main-menu-link.jinja @@ -1,3 +1,3 @@ -{% if request.session.workbasket %} +{% if request.user.current_workbasket %}
  • Return to workbasket
  • {% endif %} diff --git a/common/jinja2/includes/common/tabs/descriptions.jinja b/common/jinja2/includes/common/tabs/descriptions.jinja index cda4201ed..0af17abbd 100644 --- a/common/jinja2/includes/common/tabs/descriptions.jinja +++ b/common/jinja2/includes/common/tabs/descriptions.jinja @@ -24,6 +24,7 @@

    Descriptions

    Create a new description

    +

    There must be at least one description.

    {% set head = [ {"text": "Start date", "classes": "govuk-!-width-one-eighth"}, {"text": "Description"}, diff --git a/common/jinja2/layouts/layout.jinja b/common/jinja2/layouts/layout.jinja index 331d29103..1327a955e 100644 --- a/common/jinja2/layouts/layout.jinja +++ b/common/jinja2/layouts/layout.jinja @@ -20,11 +20,11 @@ {% block header %} {% set workbasket_html %} - {% if request.session.workbasket %} + {% if request.user.current_workbasket %} Workbasket {{ request.session.workbasket.id }} {{ workbasket_svg }} + >Workbasket {{ request.user.current_workbasket.id }} {{ workbasket_svg }} {% endif %} {% endset %} @@ -66,7 +66,7 @@ "meta": { "items": [ { - "href": "https://workspace.trade.gov.uk/working-at-dit/policies-and-guidance/policies/tariff-application-privacy-policy/", + "href": "https://workspace.trade.gov.uk/working-at-dbt/policies-and-guidance/policies/tariff-application-privacy-policy/", "text": "Privacy policy" }, { diff --git a/common/middleware.py b/common/middleware.py new file mode 100644 index 000000000..25d1b8f43 --- /dev/null +++ b/common/middleware.py @@ -0,0 +1,18 @@ +from django.conf import settings +from django.shortcuts import redirect +from django.urls import reverse + + +class MaintenanceModeMiddleware: + """If MAINTENANCE_MODE env variable is True, reroute all user requests to + MaintenanceModeView.""" + + def __init__(self, get_response): + self.get_response = get_response + + def __call__(self, request): + if settings.MAINTENANCE_MODE and request.path_info != reverse("maintenance"): + return redirect(reverse("maintenance")) + + response = self.get_response(request) + return response diff --git a/common/migrations/0001_initial.py b/common/migrations/0001_initial.py index b0cdcd195..4eaa62b09 100644 --- a/common/migrations/0001_initial.py +++ b/common/migrations/0001_initial.py @@ -10,9 +10,132 @@ class Migration(migrations.Migration): dependencies = [ ("contenttypes", "0002_remove_content_type_name"), ("workbaskets", "0001_initial"), + ("auth", "0012_alter_user_first_name_max_length"), ] operations = [ + # The initial migration for Django's default auth user model has been added here so that it can be substituted for a custom user model without having to truncate the django_migrations table. + migrations.CreateModel( + name="User", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("password", models.CharField(max_length=128, verbose_name="password")), + ( + "last_login", + models.DateTimeField( + blank=True, + null=True, + verbose_name="last login", + ), + ), + ( + "is_superuser", + models.BooleanField( + default=False, + help_text="Designates that this user has all permissions without explicitly assigning them.", + verbose_name="superuser status", + ), + ), + ( + "username", + models.CharField( + error_messages={ + "unique": "A user with that username already exists.", + }, + help_text="Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.", + max_length=150, + unique=True, + validators=[ + django.contrib.auth.validators.UnicodeUsernameValidator(), + ], + verbose_name="username", + ), + ), + ( + "first_name", + models.CharField( + blank=True, + max_length=150, + verbose_name="first name", + ), + ), + ( + "last_name", + models.CharField( + blank=True, + max_length=150, + verbose_name="last name", + ), + ), + ( + "email", + models.EmailField( + blank=True, + max_length=254, + verbose_name="email address", + ), + ), + ( + "is_staff", + models.BooleanField( + default=False, + help_text="Designates whether the user can log into this admin site.", + verbose_name="staff status", + ), + ), + ( + "is_active", + models.BooleanField( + default=True, + help_text="Designates whether this user should be treated as active. Unselect this instead of deleting accounts.", + verbose_name="active", + ), + ), + ( + "date_joined", + models.DateTimeField( + default=django.utils.timezone.now, + verbose_name="date joined", + ), + ), + ( + "groups", + models.ManyToManyField( + blank=True, + help_text="The groups this user belongs to. A user will get all permissions granted to each of their groups.", + related_name="user_set", + related_query_name="user", + to="auth.Group", + verbose_name="groups", + ), + ), + ( + "user_permissions", + models.ManyToManyField( + blank=True, + help_text="Specific permissions for this user.", + related_name="user_set", + related_query_name="user", + to="auth.Permission", + verbose_name="user permissions", + ), + ), + ], + options={ + "db_table": "auth_user", + }, + managers=[ + ("objects", django.contrib.auth.models.UserManager()), + ], + ), migrations.CreateModel( name="TrackedModel", fields=[ diff --git a/common/migrations/0008_user_current_workbasket.py b/common/migrations/0008_user_current_workbasket.py new file mode 100644 index 000000000..782f73d94 --- /dev/null +++ b/common/migrations/0008_user_current_workbasket.py @@ -0,0 +1,45 @@ +# Generated by Django 3.2.23 on 2024-01-24 15:41 + +import django.db.models.deletion +from django.db import migrations +from django.db import models + + +def change_user_content_type(apps, schema_editor): + """ + The addition of the new current_workbasket field marks the move to a custom + user model from this point in migration history onwards. + + As a result, the auth.User content type must be updated to reflect + common.User (custom user model) to preserve existing references. + """ + + ContentType = apps.get_model("contenttypes", "ContentType") + ct = ContentType.objects.filter( + app_label="auth", + model="user", + ).first() + if ct: + ct.app_label = "common" + ct.save() + + +class Migration(migrations.Migration): + dependencies = [ + ("workbaskets", "0008_datarow_dataupload"), + ("common", "0007_auto_20221114_1040_fix_missing_current_versions"), + ] + + operations = [ + migrations.RunPython(change_user_content_type), + migrations.AddField( + model_name="user", + name="current_workbasket", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + to="workbaskets.workbasket", + ), + ), + ] diff --git a/common/migrations/0009_tracked_model_timestamp.py b/common/migrations/0009_tracked_model_timestamp.py new file mode 100644 index 000000000..e9954cdd4 --- /dev/null +++ b/common/migrations/0009_tracked_model_timestamp.py @@ -0,0 +1,28 @@ +# Generated by Django 3.2.23 on 2023-12-11 16:42 + +import django.utils.timezone +from django.db import migrations +from django.db import models + + +class Migration(migrations.Migration): + dependencies = [ + ("common", "0008_user_current_workbasket"), + ] + + operations = [ + migrations.AddField( + model_name="trackedmodel", + name="created_at", + field=models.DateTimeField( + auto_now_add=True, + default=django.utils.timezone.now, + ), + preserve_default=False, + ), + migrations.AddField( + model_name="trackedmodel", + name="updated_at", + field=models.DateTimeField(auto_now=True), + ), + ] diff --git a/common/migrations/0010_set_tracked_model_datetime.py b/common/migrations/0010_set_tracked_model_datetime.py new file mode 100644 index 000000000..17eb282d0 --- /dev/null +++ b/common/migrations/0010_set_tracked_model_datetime.py @@ -0,0 +1,38 @@ +# Generated by Django 3.2.18 on 2023-03-28 15:39 +import logging + +from django.conf import settings +from django.core.paginator import Paginator +from django.db import migrations +from django.db.transaction import atomic + +logger = logging.getLogger(__name__) + + +@atomic +def generate_timestamps(apps, schema_editor): + TrackedModel = apps.get_model("common", "trackedmodel") + all_models = TrackedModel.objects.select_related("transaction").all() + paginator = Paginator(all_models, settings.DATA_MIGRATION_BATCH_SIZE) + logger.info( + "Running Tracked Model migration in batches, total batches: %s.", + paginator.num_pages, + ) + for page_num in range(1, paginator.num_pages + 1): + logger.info("Batch number: %s", page_num) + for tracked_model in paginator.page(page_num).object_list: + tracked_model.created_at = tracked_model.transaction.created_at + tracked_model.save() + + +class Migration(migrations.Migration): + dependencies = [ + ("common", "0009_tracked_model_timestamp"), + ] + + operations = [ + migrations.RunPython( + generate_timestamps, + reverse_code=migrations.RunPython.noop, + ), + ] diff --git a/common/models/__init__.py b/common/models/__init__.py index a55b86f6f..e94541f1b 100644 --- a/common/models/__init__.py +++ b/common/models/__init__.py @@ -10,6 +10,7 @@ from common.models.trackedmodel import TrackedModel from common.models.trackedmodel import VersionGroup from common.models.transactions import Transaction +from common.models.user import User __all__ = [ "ApplicabilityCode", @@ -19,6 +20,7 @@ "TimestampedMixin", "TrackedModel", "Transaction", + "User", "ValidityMixin", "ValidityStartMixin", "DescriptionMixin", diff --git a/common/models/trackedmodel.py b/common/models/trackedmodel.py index d2cc3947c..1a22da4a3 100644 --- a/common/models/trackedmodel.py +++ b/common/models/trackedmodel.py @@ -27,6 +27,7 @@ from common.models import TimestampedMixin from common.models.managers import CurrentTrackedModelManager from common.models.managers import TrackedModelManager +from common.models.mixins import TimestampedMixin from common.models.tracked_qs import TrackedModelQuerySet from common.models.tracked_utils import get_deferred_set_fields from common.models.tracked_utils import get_models_linked_to @@ -55,7 +56,7 @@ class VersionGroup(TimestampedMixin): Cls = TypeVar("Cls", bound="TrackedModel") -class TrackedModel(PolymorphicModel): +class TrackedModel(PolymorphicModel, TimestampedMixin): transaction = models.ForeignKey( "common.Transaction", on_delete=models.PROTECT, @@ -391,6 +392,8 @@ def auto_value_fields(cls) -> Set[Field]: "update_type", "trackedmodel_ptr", "transaction", + "created_at", + "updated_at", } def copy( diff --git a/common/models/user.py b/common/models/user.py new file mode 100644 index 000000000..9fdd40c23 --- /dev/null +++ b/common/models/user.py @@ -0,0 +1,21 @@ +from django.contrib.auth.models import AbstractUser +from django.db import models + + +class User(AbstractUser): + """Custom user model.""" + + current_workbasket = models.ForeignKey( + "workbaskets.WorkBasket", + on_delete=models.SET_NULL, + null=True, + blank=True, + ) + + class Meta: + db_table = "auth_user" + + def remove_current_workbasket(self): + """Remove the user's assigned current workbasket.""" + self.current_workbasket = None + self.save() diff --git a/common/models/utils.py b/common/models/utils.py index c624159e0..dd8b7b6f7 100644 --- a/common/models/utils.py +++ b/common/models/utils.py @@ -4,6 +4,7 @@ import wrapt from django.db.models import Value +from django.shortcuts import redirect from django.urls import reverse _thread_locals = threading.local() @@ -93,9 +94,9 @@ def override_current_transaction(transaction=None): set_current_transaction(old_transaction) -def is_session_workbasket_valid(request): - """Returns True if the workbasket in the session is valid (i.e. exists and - has status EDITING.)""" +def is_current_workbasket_valid(request): + """Returns True if a user's current workbasket is valid (i.e. exists and has + status EDITING.)""" from workbaskets.models import WorkBasket from workbaskets.validators import WorkflowStatus @@ -108,15 +109,13 @@ def is_session_workbasket_valid(request): return False -class ValidateSessionWorkBasketMiddleware: +class ValidateUserWorkBasketMiddleware: """ WorkBasket middleware that: - - - Validates that any workbasket in the user's session is valid. - - Removes invalid workbaskets from the user's session. - + - Validates that a user's assigned current workbasket is valid. + - Removes invalid workbaskets from the user. This middleware should always be placed before any other middleware in - settings.MIDDLEWARE that references session workbaskets (for instance, + settings.MIDDLEWARE that references workbaskets (for instance, TransactionMiddleware). """ @@ -124,14 +123,11 @@ def __init__(self, get_response): self.get_response = get_response def __call__(self, request): - from workbaskets.models import WorkBasket + # If a user has an invalid workbasket then redirect them to the notice page + # letting them know it has been removed, otherwise continue. - # A current, editable workbasket is necessary in order to set the - # current transaction (below), so abandon this middleware's action and - # return early if there is no current editable workbasket in the - # session. - if not is_session_workbasket_valid(request): - WorkBasket.remove_current_from_session(request.session) + if not is_current_workbasket_valid(request): + redirect("workbaskets:no-active-workbasket") return self.get_response(request) diff --git a/common/renderers.py b/common/renderers.py index 76fa7645f..5d4f46d29 100644 --- a/common/renderers.py +++ b/common/renderers.py @@ -20,5 +20,5 @@ def get_template_context(self, *args, **kwargs): context["envelope_id"] = f"{counter_generator()():06}" context["message_counter"] = counter_generator() - context["counter_generator"] = counter_generator + context["counter_generator"] = counter_generator() return context diff --git a/common/serializers.py b/common/serializers.py index 9a4d6a574..a4c579e25 100644 --- a/common/serializers.py +++ b/common/serializers.py @@ -7,8 +7,8 @@ from typing import Optional from django.conf import settings +from django.contrib.auth import get_user_model from django.contrib.auth.models import Group -from django.contrib.auth.models import User from django.template.loader import render_to_string from drf_extra_fields.fields import DateRangeField from lxml import etree @@ -24,6 +24,8 @@ from common.util import get_taric_template from common.util import parse_xml +User = get_user_model() + logger = logging.getLogger(__name__) @@ -198,7 +200,8 @@ def __init__( self, output: IO, envelope_id: int, - message_counter: Counter = counter_generator(), + sequence_counter: Counter = None, + message_counter: Counter = None, max_envelope_size: Optional[int] = None, format: str = "xml", newline: bool = False, @@ -206,13 +209,20 @@ def __init__( """ :param output: The output stream to write to. :param envelope_id: The id of the envelope. + :param sequence_counter: A counter for the record number sequence :param message_counter: A counter for the message ids. :param max_envelope_size: The maximum size of an envelope, if None then no limit. :param format: Format to serialize to, defaults to xml. :param newline: Whether to add a newline after the envelope. """ self.output = output - self.message_counter = message_counter + # Not set as default in params as the counter value persits between different instantiations of the Serilzer + self.sequence_counter = ( + sequence_counter if sequence_counter else counter_generator() + ) + self.message_counter = ( + message_counter if message_counter else counter_generator() + ) self.envelope_id = envelope_id self.envelope_size = 0 self.max_envelope_size = max_envelope_size @@ -280,7 +290,7 @@ def render_envelope_body( context={"format": self.format}, ).data, "transaction_id": transaction_id, - "counter_generator": counter_generator, + "counter_generator": self.sequence_counter, "message_counter": self.message_counter, }, ) diff --git a/common/static/common/js/application.js b/common/static/common/js/application.js index 18f3a5034..82ef1e73f 100644 --- a/common/static/common/js/application.js +++ b/common/static/common/js/application.js @@ -13,6 +13,9 @@ import initConditionalMeasureConditions from './conditionalMeasureConditions'; import initFilterDisabledToggleForComCode from './conditionalDisablingFilters' import initOpenCloseAccordionSection from './openCloseAccordion'; import initTapDebounce from './buttonDebounce'; +import { setupQuotaOriginFormset } from './components/QuotaOriginFormset/index'; + + showHideCheckboxes(); // Initialise accessible-autocomplete components without a `name` attr in order // to avoid the "dummy" autocomplete field being submitted as part of the form @@ -26,4 +29,5 @@ initConditionalMeasureConditions(); initAutocompleteProgressiveEnhancement(); initFilterDisabledToggleForComCode(); initOpenCloseAccordionSection(); -initTapDebounce(); \ No newline at end of file +initTapDebounce(); +setupQuotaOriginFormset(); \ No newline at end of file diff --git a/common/static/common/js/components/QuotaOriginFormset/DeleteButton.js b/common/static/common/js/components/QuotaOriginFormset/DeleteButton.js new file mode 100644 index 000000000..12b264c47 --- /dev/null +++ b/common/static/common/js/components/QuotaOriginFormset/DeleteButton.js @@ -0,0 +1,12 @@ +import React from 'react'; + + +function DeleteButton({ renderCondition, name, func, item, parent }) { + if (renderCondition) { + return ( + + ) + } +} + +export { DeleteButton } \ No newline at end of file diff --git a/common/static/common/js/components/QuotaOriginFormset/QuotaOriginExclusionForm.js b/common/static/common/js/components/QuotaOriginFormset/QuotaOriginExclusionForm.js new file mode 100644 index 000000000..158e12147 --- /dev/null +++ b/common/static/common/js/components/QuotaOriginFormset/QuotaOriginExclusionForm.js @@ -0,0 +1,41 @@ +import React from 'react'; +import { Select } from 'govuk-react' +import { DeleteButton } from './DeleteButton' + + +function QuotaOriginExclusionForm({ exclusion, origin, options, originIndex, index, removeExclusion, errors }) { + + return ( +
    +
    Exclusion {index + 1}
    +
    + + +
    +
    + +
    +
    ) +} + +export { QuotaOriginExclusionForm } \ No newline at end of file diff --git a/common/static/common/js/components/QuotaOriginFormset/QuotaOriginExclusionFormset.js b/common/static/common/js/components/QuotaOriginFormset/QuotaOriginExclusionFormset.js new file mode 100644 index 000000000..da4b68178 --- /dev/null +++ b/common/static/common/js/components/QuotaOriginFormset/QuotaOriginExclusionFormset.js @@ -0,0 +1,17 @@ +import React from 'react'; +import { QuotaOriginExclusionForm } from './QuotaOriginExclusionForm' + + +function QuotaOriginExclusionFormset({ origin, originIndex, options, errors, addEmptyExclusion, removeExclusion }) { + + return ( +
    + {origin.exclusions.map((exclusion, i) => + + )} + +
    + ) +} + +export { QuotaOriginExclusionFormset } \ No newline at end of file diff --git a/common/static/common/js/components/QuotaOriginFormset/QuotaOriginForm.js b/common/static/common/js/components/QuotaOriginFormset/QuotaOriginForm.js new file mode 100644 index 000000000..90e726967 --- /dev/null +++ b/common/static/common/js/components/QuotaOriginFormset/QuotaOriginForm.js @@ -0,0 +1,97 @@ +import React from 'react'; +import { DateField, Fieldset, Select } from 'govuk-react' +import { QuotaOriginExclusionFormset } from './QuotaOriginExclusionFormset' +import { DeleteButton } from './DeleteButton' + + +function QuotaOriginForm({ origin, options, index, removeOrigin, addEmptyExclusion, removeExclusion, errors }) { + // If the form is submitted with no exclusions and fails validation + // the exclusions key will not exist on the origin so create it here + origin.exclusions = origin.exclusions || [] + + return ( +
    +

    Origin {index + 1}

    + +
    + + + Start date + + +
    +
    + + + End date + + +
    +
    + +
    +
    +

    Geographical exclusions

    + +
    + 0} name={"origin"} func={removeOrigin} item={origin} parent={null} /> +
    +
    ) +} + +export { QuotaOriginForm } \ No newline at end of file diff --git a/common/static/common/js/components/QuotaOriginFormset/index.js b/common/static/common/js/components/QuotaOriginFormset/index.js new file mode 100644 index 000000000..9936cf222 --- /dev/null +++ b/common/static/common/js/components/QuotaOriginFormset/index.js @@ -0,0 +1,106 @@ +import { useState } from 'react'; +import { createRoot } from 'react-dom/client'; +import React from 'react'; +import { QuotaOriginForm } from './QuotaOriginForm' + + +function QuotaOriginFormset({ data, options, errors }) { + const [origins, setOrigins] = useState([...data]); + const emptyOrigin = { + "id": "", + "pk": "", + "exclusions": [ + ], + "geo_area_name": "", + "geo_area_pk": "", + "start_date_0": "", + "start_date_1": "", + "start_date_2": "", + "end_date_0": "", + "end_date_1": "", + "end_date_2": "", + } + const emptyExclusion = { + "id": "", + "pk": "", + } + + const addEmptyOrigin = (e) => { + e.preventDefault(); + const newEmptyOrigin = { ...emptyOrigin } + newEmptyOrigin.id = Date.now() + setOrigins([...origins, { ...newEmptyOrigin }]); + } + + function removeOrigin(origin, _, e) { + e.preventDefault(); + const newOrigins = [...origins] + const index = origins.indexOf(origin) + if (index > -1) { + newOrigins.splice(index, 1) + setOrigins(newOrigins) + } + } + + function addEmptyExclusion(origin, e) { + e.preventDefault(); + // find parent origin and update exclusions + const updatedOrigin = { ...origin } + const newEmptyExclusion = { ...emptyExclusion } + newEmptyExclusion.id = Date.now() + const newExclusions = [...updatedOrigin.exclusions, newEmptyExclusion] + updatedOrigin.exclusions = newExclusions + + // update origins + const updatedOrigins = [...origins] + const index = origins.findIndex(o => o.id === origin.id) + if (index > -1) { + updatedOrigins.splice(index, 1, updatedOrigin) + setOrigins(updatedOrigins) + } + } + + function removeExclusion(exclusion, origin, e) { + e.preventDefault(); + // remove the exclusion from its parent origin + const newOrigin = { ...origin } + const exclusionIndex = newOrigin.exclusions.indexOf(exclusion) + if (exclusionIndex > -1) { + newOrigin.exclusions.splice(exclusionIndex, 1) + } + + // update the origin + const newOrigins = [...origins] + const index = newOrigins.indexOf(origin) + if (index > -1) { + newOrigins.splice(index, 1, newOrigin) + setOrigins(newOrigins) + } + } + + return ( +
    + {origins.map((origin, i) => + + )} + +
    + ) +} + +function init() { + const originsContainer = document.getElementById("quota_origins"); + const root = createRoot(originsContainer); + const origins = [...originsData]; + // originsData and geoAreasOptions come from template quotas/jinja2/includes/quotas/quota-edit-origins.jinja + // originsErrors are errors raised by django. see template quotas/jinja2/includes/quotas/quota-edit-origins.jinja + root.render( + + ); +} + +function setupQuotaOriginFormset() { + document.addEventListener('DOMContentLoaded', init()) +} + +export { setupQuotaOriginFormset, QuotaOriginFormset }; \ No newline at end of file diff --git a/common/static/common/js/components/QuotaOriginFormset/tests/__snapshots__/index.test.js.snap b/common/static/common/js/components/QuotaOriginFormset/tests/__snapshots__/index.test.js.snap new file mode 100644 index 000000000..6b1e2ba2a --- /dev/null +++ b/common/static/common/js/components/QuotaOriginFormset/tests/__snapshots__/index.test.js.snap @@ -0,0 +1,1528 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`QuotaOriginFormset renders empty formset when no initial data 1`] = ` +
    + +
    +`; + +exports[`QuotaOriginFormset renders formset with props 1`] = ` +
    +
    +

    + Origin + 1 +

    + +
    +
    + + + Start date + + +
    + + + +
    +
    +
    +
    +
    + + + End date + + + + Leave empty if a quota order number origin is needed for an unlimited time + +
    + + + +
    +
    +
    +
    + +
    +
    +

    + Geographical exclusions +

    +
    +
    +
    + Exclusion + 1 +
    +
    + + +
    +
    + +
    +
    +
    +
    + Exclusion + 2 +
    +
    + + +
    +
    + +
    +
    + +
    +
    +
    +
    +
    +

    + Origin + 2 +

    + +
    +
    + + + Start date + + +
    + + + +
    +
    +
    +
    +
    + + + End date + + + + Leave empty if a quota order number origin is needed for an unlimited time + +
    + + + +
    +
    +
    +
    + +
    +
    +

    + Geographical exclusions +

    +
    +
    +
    + Exclusion + 1 +
    +
    + + +
    +
    + +
    +
    + +
    +
    + +
    +
    + +
    +`; + +exports[`QuotaOriginFormset renders with formset errors 1`] = ` +
    +
    +

    + Origin + 1 +

    + +
    +
    + + + Start date + + +
    + + + +
    +
    +
    +
    +
    + + + End date + + + + Leave empty if a quota order number origin is needed for an unlimited time + + + The end date must be the same as or after the start date. + +
    + + + +
    +
    +
    +
    + +
    +
    +

    + Geographical exclusions +

    +
    +
    +
    + Exclusion + 1 +
    +
    + + +
    +
    + +
    +
    +
    +
    + Exclusion + 2 +
    +
    + + +
    +
    + +
    +
    + +
    +
    +
    +
    +
    +

    + Origin + 2 +

    + +
    +
    + + + Start date + + +
    + + + +
    +
    +
    +
    +
    + + + End date + + + + Leave empty if a quota order number origin is needed for an unlimited time + +
    + + + +
    +
    +
    +
    + +
    +
    +

    + Geographical exclusions +

    +
    +
    +
    + Exclusion + 1 +
    +
    + + +
    +
    + +
    +
    + +
    +
    + +
    +
    + +
    +`; diff --git a/common/static/common/js/components/QuotaOriginFormset/tests/index.test.js b/common/static/common/js/components/QuotaOriginFormset/tests/index.test.js new file mode 100644 index 000000000..c126b47ec --- /dev/null +++ b/common/static/common/js/components/QuotaOriginFormset/tests/index.test.js @@ -0,0 +1,176 @@ +import { fireEvent, render, screen } from "@testing-library/react"; +import renderer from 'react-test-renderer'; +import { QuotaOriginFormset } from '../index'; + + +const mockGeoAreaOptions = [ + { + "name": "All countries", + "value": 1 + }, + { + "name": "EU", + "value": 2 + }, + { + "name": "China", + "value": 3 + }, + { + "name": "South Korea", + "value": 4 + }, + { + "name": "Switzerland", + "value": 5 + }, +] + +const mockOrigins = [ + { + "id": 1, + "pk": 1, + "exclusions": [ + { "id": 3, "pk": 1 }, + { "id": 4, "pk": 2 }, + ], + "geographical_area": 1, + "start_date_0": 1, + "start_date_1": 1, + "start_date_2": 2000, + "end_date_0": 1, + "end_date_1": 1, + "end_date_2": 2010, + }, + { + "id": 2, + "pk": 2, + "exclusions": [ + { "id": 5, "pk": 3 }, + ], + "geographical_area": 2, + "start_date_0": 1, + "start_date_1": 1, + "start_date_2": 2000, + "end_date_0": 1, + "end_date_1": 1, + "end_date_2": 2010, + }, +] + +describe(QuotaOriginFormset, () => { + it('renders formset with props', () => { + + const mockOriginsErrors = {} + + const component = renderer.create( + , + ); + + let tree = component.toJSON(); + expect(tree).toMatchSnapshot(); + + }); + + it('renders empty formset when no initial data', () => { + + const mockOriginsErrors = {} + const mockOrigins = [] + + const component = renderer.create( + , + ); + + let tree = component.toJSON(); + expect(tree).toMatchSnapshot(); + + }); + + it('renders with formset errors', () => { + + const mockOriginsErrors = { + "origins-0-end_date": "The end date must be the same as or after the start date.", + }; + + const component = renderer.create( + , + ); + + let tree = component.toJSON(); + expect(tree).toMatchSnapshot(); + + }); + + it("should add empty origin form when add button is clicked", () => { + const mockOrigins = [] + const mockOriginsErrors = {} + + // render form with no origins + render(); + + // add an empty origin + fireEvent.click(screen.getByText("Add another origin")); + expect(screen.getByText("Origin 1")).toBeInTheDocument(); + expect(screen.queryByText("Origin 2")).not.toBeInTheDocument(); + }); + + it("should remove origin form when delete button is clicked", () => { + const mockOriginsErrors = {} + + // render form with 2 origins + render(); + + // delete the last origin + fireEvent.click(screen.getByText("Delete this origin")); + expect(screen.getByText("Origin 1")).toBeInTheDocument(); + expect(screen.queryByText("Origin 2")).not.toBeInTheDocument(); + }); + + it("should add empty exclusion form when add button is clicked", () => { + const mockOrigins = [ + { + "id": 1, + "pk": 1, + "exclusions": [ + ], + "geographical_area": 1, + "start_date_0": 1, + "start_date_1": 1, + "start_date_2": 2000, + "end_date_0": 1, + "end_date_1": 1, + "end_date_2": 2010, + }, + ] + const mockOriginsErrors = {} + + // render form with no origins + render(); + expect(screen.getByText("Origin 1")).toBeInTheDocument(); + expect(screen.queryByText("Origin 2")).not.toBeInTheDocument(); + expect(screen.queryByText("Exclusion 1")).not.toBeInTheDocument(); + expect(screen.queryByText("Delete this exclusion")).not.toBeInTheDocument(); + + // add an empty exclusion + fireEvent.click(screen.getByText("Add an exclusion")); + expect(screen.getByText("Exclusion 1")).toBeInTheDocument(); + expect(screen.getByText("Delete this exclusion")).toBeInTheDocument(); + expect(screen.queryByText("Exclusion 2")).not.toBeInTheDocument(); + }); + + it("should remove exclusion form when delete button is clicked", () => { + const mockOriginsErrors = {} + + // render form with 2 origins + // first has 2 exclusions + // second has 1 + render(); + expect(screen.getByText("Origin 1")).toBeInTheDocument(); + expect(screen.getByText("Origin 2")).toBeInTheDocument(); + expect(screen.getAllByText(/Exclusion [0-9]+/).length).toBe(3) + + // add an empty exclusion to first origin + fireEvent.click(screen.getAllByText("Add an exclusion")[0]); + expect(screen.getAllByText(/Exclusion [0-9]+/).length).toBe(4) + }); +}) diff --git a/common/static/common/scss/_button.scss b/common/static/common/scss/_button.scss index e3be8b0bc..fc71efcbf 100644 --- a/common/static/common/scss/_button.scss +++ b/common/static/common/scss/_button.scss @@ -7,4 +7,13 @@ button.no-background { cursor: pointer; overflow: hidden; outline: none; +} + +.report-button-inline { + float:right; + margin-top:-70px +} + +.float-elements-right { + float: right; } \ No newline at end of file diff --git a/common/static/common/scss/application.scss b/common/static/common/scss/application.scss index 3ac96053c..b9cc920eb 100644 --- a/common/static/common/scss/application.scss +++ b/common/static/common/scss/application.scss @@ -27,6 +27,7 @@ $govuk-image-url-function: frontend-image-url; @import "publishing"; @import "regulations"; @import "workbaskets"; +@import "reference_documents"; @import "versions"; @import "fake-link"; @import "violations"; diff --git a/common/tests/bdd/test_edit_view.py b/common/tests/bdd/test_edit_view.py index 90ff22996..163505683 100644 --- a/common/tests/bdd/test_edit_view.py +++ b/common/tests/bdd/test_edit_view.py @@ -21,13 +21,24 @@ def tracked_model(approved_transaction): @pytest.fixture @when("I edit a model") -def model_edit_page(client, tracked_model): - return client.get(tracked_model.get_url("edit")) +def model_edit_page(client_with_current_workbasket, tracked_model): + return client_with_current_workbasket.get(tracked_model.get_url("edit")) + + +@pytest.fixture +@when("I edit a model") +def model_edit_page_invalid_user( + client_with_current_workbasket_no_permissions, + tracked_model, +): + return client_with_current_workbasket_no_permissions.get( + tracked_model.get_url("edit"), + ) @then("I am not permitted to edit") -def edit_permission_denied(model_edit_page): - assert model_edit_page.status_code == 403 +def edit_permission_denied(model_edit_page_invalid_user): + assert model_edit_page_invalid_user.status_code == 403 @then("I see an edit form") diff --git a/common/tests/factories.py b/common/tests/factories.py index a2fe14464..a32463dc2 100644 --- a/common/tests/factories.py +++ b/common/tests/factories.py @@ -5,6 +5,7 @@ from itertools import product import factory +from django.contrib.auth import get_user_model from factory.fuzzy import FuzzyChoice from factory.fuzzy import FuzzyText from faker import Faker @@ -30,6 +31,8 @@ from quotas.validators import QuotaEventType from workbaskets.validators import WorkflowStatus +User = get_user_model() + def short_description(): return factory.Faker("text", max_nb_chars=500) @@ -102,7 +105,7 @@ class UserFactory(factory.django.DjangoModelFactory): """User factory.""" class Meta: - model = "auth.User" + model = User username = factory.sequence(lambda n: f"{factory.Faker('name')}{n}") email = factory.Faker("email") diff --git a/common/tests/test_filters.py b/common/tests/test_filters.py index 4135913ff..b0d1f085e 100644 --- a/common/tests/test_filters.py +++ b/common/tests/test_filters.py @@ -48,10 +48,10 @@ def test_search_queryset_returns_case_insensitive(): def test_filter_by_current_workbasket_mixin( valid_user_client, - session_workbasket, + user_workbasket, session_request, ): - with session_workbasket.new_transaction() as transaction: + with user_workbasket.new_transaction() as transaction: commodity_in_workbasket_1 = GoodsNomenclatureFactory.create( transaction=transaction, ) @@ -61,9 +61,6 @@ def test_filter_by_current_workbasket_mixin( commodity_not_in_workbasket_1 = GoodsNomenclatureFactory.create() commodity_not_in_workbasket_2 = GoodsNomenclatureFactory.create() - session = valid_user_client.session - session["workbasket"] = {"id": session_workbasket.pk} - session.save() qs = GoodsNomenclature.objects.all() self = CommodityFilter( diff --git a/common/tests/test_forms.py b/common/tests/test_forms.py index 75f2fed94..c342a8de1 100644 --- a/common/tests/test_forms.py +++ b/common/tests/test_forms.py @@ -183,7 +183,7 @@ def test_radio_nested_form_nested_formset_cleaned_data(): "measure-conditions-formset-0-applicable_duty": "test1", "measure-conditions-formset-1-applicable_duty": "test2", "measure-conditions-formset-2-applicable_duty": "test3", - "measure-conditions-formset-3-applicable_duty": "test4", + "measure-conditions-formset-3-applicable_duty": "", }, [ { @@ -196,7 +196,7 @@ def test_radio_nested_form_nested_formset_cleaned_data(): "applicable_duty": "test3", }, { - "applicable_duty": "test4", + "applicable_duty": "", }, ], ), diff --git a/common/tests/test_migrations.py b/common/tests/test_migrations.py index a215160ae..f6be7daef 100644 --- a/common/tests/test_migrations.py +++ b/common/tests/test_migrations.py @@ -3,20 +3,22 @@ import pytest +from common.models import TrackedModel from common.models.transactions import TransactionPartition +from common.tests import factories from common.util import TaricDateRange from common.validators import UpdateType +from workbaskets.validators import WorkflowStatus @pytest.mark.django_db() def test_missing_current_version_fix(migrator): migrator.reset() - """Ensures that the initial migration works.""" new_state = migrator.apply_initial_migration(("common", "0006_auto_20221114_1000")) # setup - user_class = new_state.apps.get_model("auth", "User") + user_class = new_state.apps.get_model("common", "User") workbasket_class = new_state.apps.get_model("workbaskets", "WorkBasket") measurement_unit_class = new_state.apps.get_model("measures", "MeasurementUnit") transaction_class = new_state.apps.get_model("common", "Transaction") @@ -75,3 +77,32 @@ def test_missing_current_version_fix(migrator): # assert assert measurement_unit.version_group.current_version_id == measurement_unit_id migrator.reset() + + +@pytest.mark.django_db() +def test_timestamp_migration(migrator): + migrator.reset() + """Ensures that the initial migration works.""" + migrator.apply_initial_migration( + ("common", "0009_tracked_model_timestamp"), + ) + + workbasket = factories.WorkBasketFactory.create( + status=WorkflowStatus.EDITING, + ) + transaction = factories.TransactionFactory.create(workbasket=workbasket) + trked1 = factories.FootnoteTypeFactory.create(transaction=transaction) + trked2 = factories.FootnoteTypeFactory.create(transaction=transaction) + + assert transaction.created_at + assert hasattr(trked1, "created_at") + assert hasattr(trked1, "updated_at") + assert trked1.created_at != transaction.created_at + + migrator.apply_tested_migration(("common", "0010_set_tracked_model_datetime")) + new_trked1 = TrackedModel.objects.get(pk=trked1.pk) + new_trked2 = TrackedModel.objects.get(pk=trked2.pk) + assert new_trked1.created_at == transaction.created_at + assert new_trked1.updated_at > transaction.updated_at + assert new_trked2.created_at == transaction.created_at + assert new_trked2.updated_at > transaction.updated_at diff --git a/common/tests/test_serializers.py b/common/tests/test_serializers.py index 3a81f201d..13ecc8d49 100644 --- a/common/tests/test_serializers.py +++ b/common/tests/test_serializers.py @@ -1,7 +1,9 @@ import io +import os import random import pytest +from bs4 import BeautifulSoup from lxml import etree from pytest_django.asserts import assertQuerysetEqual # noqa @@ -13,7 +15,9 @@ from common.tests.util import taric_xml_record_codes from exporter.serializers import MultiFileEnvelopeTransactionSerializer from exporter.serializers import RenderedTransactions +from exporter.util import dit_file_generator from taric.models import Envelope +from workbaskets.models import WorkBasket pytestmark = pytest.mark.django_db @@ -165,3 +169,105 @@ def create_output_constructor(): # TODO - it would be good to check the output more thoroughly than just the record code. # Some record codes are generated in the template, making issuperset required in this assertion. assert output_record_codes.issuperset(expected_record_codes[i]) + + +def test_transaction_envelope_serializer_counters(queued_workbasket): + """Test that the envelope serializer sets the counters in an envelope + correctly that the message id always starts from one in each envelope and + that the record sequence number increments.""" + approved_transaction = queued_workbasket.transactions.approved().last() + # add a tracked_models to the workbasket + + factories.AdditionalCodeTypeFactory(transaction=approved_transaction) + factories.AdditionalCodeDescriptionFactory(transaction=approved_transaction) + factories.RegulationFactory( + transaction=approved_transaction, + regulation_group=factories.RegulationGroupFactory( + transaction=approved_transaction, + ), + ) + factories.CertificateFactory( + transaction=approved_transaction, + certificate_type=factories.CertificateTypeFactory( + transaction=approved_transaction, + ), + description=factories.CertificateDescriptionFactory( + transaction=approved_transaction, + ), + ) + factories.FootnoteFactory( + transaction=approved_transaction, + description=factories.FootnoteDescriptionFactory( + transaction=approved_transaction, + ), + footnote_type=factories.FootnoteTypeFactory(transaction=approved_transaction), + ) + + # Make a envelope from the files + output_file_constructor = dit_file_generator("/tmp", 230001) + serializer = MultiFileEnvelopeTransactionSerializer( + output_file_constructor, + envelope_id=230001, + ) + + workbaskets = WorkBasket.objects.filter(pk=queued_workbasket.pk) + transactions = workbaskets.ordered_transactions() + + envelope = list(serializer.split_render_transactions(transactions))[0] + + assert len(envelope.transactions) > 0 + + envelope_file = envelope.output + envelope_file.seek(0, os.SEEK_SET) + soup = BeautifulSoup(envelope_file, "xml") + record_sequence_numbers = soup.find_all("oub:record.sequence.number") + message_id_numbers = soup.find_all("env:app.message") + + expected_value = 1 + for element in record_sequence_numbers: + actual_value = int(element.text) + assert actual_value == expected_value + expected_value += 1 + + expected_id = 1 + for element in message_id_numbers: + actual_value = int(element["id"]) + assert actual_value == expected_id + expected_id += 1 + + workbasket = factories.QueuedWorkBasketFactory.create() + approved_transaction2 = workbasket.transactions.approved().last() + factories.AdditionalCodeTypeFactory(transaction=approved_transaction2) + factories.AdditionalCodeDescriptionFactory(transaction=approved_transaction2) + factories.FootnoteFactory( + transaction=approved_transaction2, + description=factories.FootnoteDescriptionFactory( + transaction=approved_transaction2, + ), + footnote_type=factories.FootnoteTypeFactory(transaction=approved_transaction2), + ) + + # Make a envelope from the files + output_file_constructor = dit_file_generator("/tmp", 230002) + serializer = MultiFileEnvelopeTransactionSerializer( + output_file_constructor, + envelope_id=230002, + ) + + workbaskets = WorkBasket.objects.filter(pk=workbasket.pk) + transactions = workbaskets.ordered_transactions() + + envelope_2 = list(serializer.split_render_transactions(transactions))[0] + + assert len(envelope.transactions) > 0 + + envelope_file_2 = envelope_2.output + envelope_file_2.seek(0, os.SEEK_SET) + soup_2 = BeautifulSoup(envelope_file_2, "xml") + message_id_numbers_2 = soup_2.find_all("env:app.message") + + expected_id_2 = 1 + for element in message_id_numbers_2: + actual_value = int(element.get("id")) + assert actual_value == expected_id_2 + expected_id_2 += 1 diff --git a/common/tests/test_views.py b/common/tests/test_views.py index dda2ffede..caa1617a7 100644 --- a/common/tests/test_views.py +++ b/common/tests/test_views.py @@ -4,6 +4,8 @@ from bs4 import BeautifulSoup from django.conf import settings from django.contrib.auth.models import Permission +from django.test import modify_settings +from django.test import override_settings from django.urls import reverse from common.tests import factories @@ -193,7 +195,7 @@ def test_index_displays_footer_links(valid_user_client): assert "Privacy policy" in a_tags[0].text assert ( a_tags[0].attrs["href"] - == "https://workspace.trade.gov.uk/working-at-dit/policies-and-guidance/policies/tariff-application-privacy-policy/" + == "https://workspace.trade.gov.uk/working-at-dbt/policies-and-guidance/policies/tariff-application-privacy-policy/" ) @@ -241,3 +243,21 @@ def test_accessibility_statement_view_returns_200(valid_user_client): "Accessibility statement for the Tariff application platform" in page.select("h1")[0].text ) + + +@override_settings(MAINTENANCE_MODE=True) +@modify_settings( + MIDDLEWARE={ + "append": "common.middleware.MaintenanceModeMiddleware", + }, +) +def test_user_redirect_during_maintenance_mode(valid_user_client): + response = valid_user_client.get(reverse("home")) + assert response.status_code == 302 + assert response.url == reverse("maintenance") + + +def test_maintenance_mode_page_content(valid_user_client): + response = valid_user_client.get(reverse("maintenance")) + assert response.status_code == 200 + assert "Sorry, the service is unavailable" in str(response.content) diff --git a/common/urls.py b/common/urls.py index 06dc8cbc7..76697e5cd 100644 --- a/common/urls.py +++ b/common/urls.py @@ -26,4 +26,5 @@ path("login", views.LoginView.as_view(), name="login"), path("logout", views.LogoutView.as_view(), name="logout"), path("api-auth/", include("rest_framework.urls")), + path("maintenance/", views.MaintenanceView.as_view(), name="maintenance"), ] diff --git a/common/views.py b/common/views.py index c8e696fdd..288bcda87 100644 --- a/common/views.py +++ b/common/views.py @@ -48,7 +48,7 @@ class HomeView(FormView, View): - template_name = "common/workbasket_action.jinja" + template_name = "common/index.jinja" form_class = forms.HomeForm REDIRECT_MAPPING = { @@ -58,7 +58,8 @@ class HomeView(FormView, View): "PROCESS_ENVELOPES": "publishing:envelope-queue-ui-list", "SEARCH": "search-page", "IMPORT": "commodity_importer-ui-list", - "VIEW_REF_DOCS": "reference_documents-ui-list", + "REF_DOCS_EXAMPLES": "reference_documents:example-ui-index", + "REF_DOCS": "reference_documents:index", "WORKBASKET_LIST_ALL": "workbaskets:workbasket-ui-list-all", } @@ -465,5 +466,9 @@ def handler500(request, *args, **kwargs): return TemplateResponse(request=request, template="common/500.jinja", status=500) +class MaintenanceView(TemplateView): + template_name = "common/maintenance.jinja" + + class AccessibilityStatementView(TemplateView): template_name = "common/accessibility.jinja" diff --git a/conftest.py b/conftest.py index e2dc46965..581f8d5cd 100644 --- a/conftest.py +++ b/conftest.py @@ -156,7 +156,8 @@ def tap_migrator_factory(migrator_factory): its migration unit testing, continues to cause problems in migration unit tests. A couple of examples of the reported issue: https://code.djangoproject.com/ticket/10827 - https://github.com/wemake-services/django-test-migrations/blob/93db540c00a830767eeab5f90e2eef1747c940d4/django_test_migrations/migrator.py#L73 + /PS-IGNORE---https://github.com/wemake-services/django-test-migrations/blob/93db540c00a830767eeab5f90e2eef1747c940d4/django_test_migrations/migrator.py#L73 + An initial migration must reference ContentType instances (in the DB). This can occur when inserting Permission objects during @@ -321,6 +322,29 @@ def valid_user_client(client, valid_user): return client +@pytest.fixture +def client_with_current_workbasket(client, valid_user): + client.force_login(valid_user) + workbasket = factories.WorkBasketFactory.create( + status=WorkflowStatus.EDITING, + ) + workbasket.assign_to_user(valid_user) + return client + + +@pytest.fixture +def client_with_current_workbasket_no_permissions(client): + """Returns a client with a logged in user who has a current workbasket but + no permissions.""" + user = factories.UserFactory.create() + client.force_login(user) + workbasket = factories.WorkBasketFactory.create( + status=WorkflowStatus.EDITING, + ) + workbasket.assign_to_user(user) + return client + + @pytest.fixture def superuser(): user = factories.UserFactory.create(is_superuser=True, is_staff=True) @@ -360,6 +384,16 @@ def valid_user_api_client(api_client, valid_user) -> APIClient: return api_client +@pytest.fixture +def api_client_with_current_workbasket(api_client, valid_user) -> APIClient: + api_client.force_login(valid_user) + workbasket = factories.WorkBasketFactory.create( + status=WorkflowStatus.EDITING, + ) + workbasket.assign_to_user(valid_user) + return api_client + + @pytest.fixture def taric_schema(settings) -> etree.XMLSchema: with open(settings.PATH_XSD_TARIC) as xsd_file: @@ -405,25 +439,21 @@ def published_footnote_type(queued_workbasket): @pytest.fixture @given("there is a current workbasket") -def session_workbasket(client, new_workbasket) -> WorkBasket: - # The valid_user_client.session property returns a new session instance on - # each reference, so first get a single session instance via the property. - session = client.session - new_workbasket.save_to_session(session) - session.save() +def user_workbasket(client, valid_user, new_workbasket) -> WorkBasket: + """Returns a workbasket which has been assigned to a valid logged-in + user.""" + client.force_login(valid_user) + new_workbasket.assign_to_user(valid_user) return new_workbasket @pytest.fixture -def session_empty_workbasket(valid_user_client) -> WorkBasket: +def user_empty_workbasket(client, valid_user) -> WorkBasket: + client.force_login(valid_user) workbasket = factories.WorkBasketFactory.create( status=WorkflowStatus.EDITING, ) - # The valid_user_client.session property returns a new session instance on - # each reference, so first get a single session instance via the property. - session = valid_user_client.session - workbasket.save_to_session(session) - session.save() + workbasket.assign_to_user(valid_user) return workbasket @@ -481,7 +511,16 @@ def unapproved_checked_transaction(unapproved_transaction): @pytest.fixture(scope="function") def workbasket(): - return factories.WorkBasketFactory.create() + """ + Returns existing workbasket if one already exists otherwise creates a new + one. + + This is as some tests already have a workbasket when this is called. + """ + if WorkBasket.objects.all().count() > 0: + return WorkBasket.objects.first() + else: + return factories.WorkBasketFactory.create() @pytest.fixture( @@ -522,7 +561,7 @@ def description_factory(request): @pytest.fixture -def use_create_form(valid_user_api_client: APIClient): +def use_create_form(api_client_with_current_workbasket): """ use_create_form, ported from use_update_form. @@ -551,7 +590,7 @@ def use( assert create_url, f"No create page found for {Model}" # Initial rendering of url - response = valid_user_api_client.get(create_url) + response = api_client_with_current_workbasket.get(create_url) assert response.status_code == 200 initial_form = response.context_data["form"] @@ -567,7 +606,7 @@ def use( k: data.get(k) for k in Model.identifying_fields if "__" not in k } - response = valid_user_api_client.post(create_url, data) + response = api_client_with_current_workbasket.post(create_url, data) # Check that if we expect failure that the new data was not persisted if response.status_code not in (301, 302): @@ -588,7 +627,47 @@ def use( @pytest.fixture -def use_edit_view(valid_user_api_client: APIClient): +def use_edit_view(api_client_with_current_workbasket): + """ + Uses the default edit form and view for a model in a workbasket with EDITING + status. + + The ``object`` param is the TrackedModel instance that is to be edited and + saved, which should not create a new version. + ``data_changes`` should be a dictionary to apply to the object, effectively + applying edits. + + Will raise :class:`~django.core.exceptions.ValidationError` if the form + contains errors. + """ + + def use(obj: TrackedModel, data_changes: dict[str, str]): + Model = type(obj) + obj_count = Model.objects.filter(**obj.get_identifying_fields()).count() + url = obj.get_url("edit") + + # Check initial form rendering. + get_response = api_client_with_current_workbasket.get(url) + assert get_response.status_code == 200 + + # Edit and submit the data. + initial_form = get_response.context_data["form"] + form_data = get_form_data(initial_form) + form_data.update(data_changes) + post_response = api_client_with_current_workbasket.post(url, form_data) + + # POSTing a real edits form should never create new object instances. + assert Model.objects.filter(**obj.get_identifying_fields()).count() == obj_count + if post_response.status_code not in (301, 302): + raise ValidationError( + f"Form contained errors: {dict(post_response.context_data['form'].errors)}", + ) + + return use + + +@pytest.fixture +def use_edit_view_no_workbasket(valid_user_api_client): """ Uses the default edit form and view for a model in a workbasket with EDITING status. @@ -628,7 +707,7 @@ def use(obj: TrackedModel, data_changes: dict[str, str]): @pytest.fixture -def use_update_form(valid_user_api_client: APIClient): +def use_update_form(api_client_with_current_workbasket): """ Uses the default create form and view for a model with update_type=UPDATE. @@ -653,7 +732,7 @@ def use(object: TrackedModel, new_data: Callable[[TrackedModel], dict[str, Any]] # Visit the edit page and ensure it is a success edit_url = object.get_url("edit") assert edit_url, f"No edit page found for {object}" - response = valid_user_api_client.get(edit_url) + response = api_client_with_current_workbasket.get(edit_url) assert response.status_code == 200 # Get the data out of the edit page @@ -664,7 +743,7 @@ def use(object: TrackedModel, new_data: Callable[[TrackedModel], dict[str, Any]] realised_data = new_data(object) assert set(realised_data.keys()).issubset(data.keys()) data.update(realised_data) - response = valid_user_api_client.post(edit_url, data) + response = api_client_with_current_workbasket.post(edit_url, data) # Check that if we expect failure that the new data was not persisted if response.status_code not in (301, 302): @@ -682,7 +761,7 @@ def use(object: TrackedModel, new_data: Callable[[TrackedModel], dict[str, Any]] ) # Check that what we asked to be changed has been persisted - response = valid_user_api_client.get(edit_url) + response = api_client_with_current_workbasket.get(edit_url) assert response.status_code == 200 data = get_form_data(response.context_data["form"]) for key in realised_data: @@ -704,7 +783,7 @@ def use(object: TrackedModel, new_data: Callable[[TrackedModel], dict[str, Any]] @pytest.fixture -def use_delete_form(valid_user_api_client: APIClient): +def use_delete_form(api_client_with_current_workbasket): """ Uses the default delete form and view for a model to delete an object, and returns the deleted version of the object. @@ -725,12 +804,12 @@ def use(object: TrackedModel): # Visit the delete page and ensure it is a success delete_url = object.get_url("delete") assert delete_url, f"No delete page found for {object}" - response = valid_user_api_client.get(delete_url) + response = api_client_with_current_workbasket.get(delete_url) assert response.status_code == 200 # Get the data out of the delete page data = get_form_data(response.context_data["form"]) - response = valid_user_api_client.post(delete_url, data) + response = api_client_with_current_workbasket.post(delete_url, data) # Check that if we expect failure that the new data was not persisted if response.status_code not in (301, 302): @@ -748,7 +827,7 @@ def use(object: TrackedModel): ) # Check that the delete persisted and we can't delete again - response = valid_user_api_client.get(delete_url) + response = api_client_with_current_workbasket.get(delete_url) assert response.status_code == 404 # Check that if success was expected that the new version was persisted @@ -1142,7 +1221,7 @@ def make_record( dependency.delete() record = factory_instance.create( - **{f"{reference_field_name}_id": non_existent_id} + **{f"{reference_field_name}_id": non_existent_id}, ) try: @@ -1427,19 +1506,35 @@ def unordered_transactions(): @pytest.fixture -def session_request(client): +def session_request(client, valid_user): session = client.session session.save() request = RequestFactory() request.session = session + request.user = valid_user return request @pytest.fixture -def session_with_workbasket(session_request, workbasket): - session_request.session.update({"workbasket": {"id": workbasket.pk}}) - return session_request +def session_request_with_workbasket(client, valid_user): + """ + Returns a request object which has a valid user and session associated. + + The valid user has a current workbasket. + """ + client.force_login(valid_user) + workbasket = factories.WorkBasketFactory.create( + status=WorkflowStatus.EDITING, + ) + workbasket.assign_to_user(valid_user) + + session = client.session + session.save() + request = RequestFactory() + request.session = session + request.user = valid_user + return request @pytest.fixture @@ -1891,7 +1986,8 @@ def factory_method(workbasket=None, **kwargs): return_value=MagicMock(id=factory.Faker("uuid4")), ): packaged_workbasket = factories.QueuedPackagedWorkBasketFactory( - workbasket=workbasket, **kwargs + workbasket=workbasket, + **kwargs, ) return packaged_workbasket diff --git a/footnotes/filters.py b/footnotes/filters.py index 94206b678..5fc091c28 100644 --- a/footnotes/filters.py +++ b/footnotes/filters.py @@ -60,7 +60,7 @@ class FootnoteFilter( choices=type_choices(models.FootnoteType.objects.latest_approved()), widget=CheckboxSelectMultiple, field_name="footnote_type__footnote_type_id", - label="Footnote Type", + label="Footnote type", help_text="Select all that apply", required=False, ) diff --git a/footnotes/forms.py b/footnotes/forms.py index 5d34b7dd6..39dc026fa 100644 --- a/footnotes/forms.py +++ b/footnotes/forms.py @@ -35,11 +35,19 @@ def __init__(self, *args, **kwargs): if self.instance.pk: self.fields["code"].disabled = True - self.fields["code"].help_text = "You can't edit this" + self.fields[ + "code" + ].help_text = ( + "Footnote IDs are automatically generated and cannot be edited." + ) self.fields["code"].initial = str(self.instance) self.fields["footnote_type"].disabled = True - self.fields["footnote_type"].help_text = "You can't edit this" + self.fields[ + "footnote_type" + ].help_text = ( + "Once a footnote is published, you cannot edit the footnote type." + ) self.helper = FormHelper(self) self.helper.label_size = Size.SMALL @@ -91,8 +99,8 @@ class Meta: footnote_type = forms.ModelChoiceField( label="Footnote type", help_text=( - "Selecting the right footnote type will determine whether it can " - "be associated with measures, commodity codes, or both." + "The footnote type will determine whether it can be" + "associated with measures, commodity codes, or both." ), queryset=models.FootnoteType.objects.latest_approved(), empty_label="Select a footnote type", @@ -150,6 +158,7 @@ def __init__(self, *args, **kwargs): self.helper.layout = Layout( "footnote_type", "start_date", + "end_date", Field.textarea("description", rows=5), DescriptionHelpBox(), Submit( @@ -187,6 +196,7 @@ def __init__(self, *args, **kwargs): self.helper.layout = Layout( "footnote_type", "start_date", + "end_date", Submit( "submit", "Save", diff --git a/footnotes/jinja2/includes/footnotes/tabs/descriptions.jinja b/footnotes/jinja2/includes/footnotes/tabs/descriptions.jinja index 13be9ab35..391fce037 100644 --- a/footnotes/jinja2/includes/footnotes/tabs/descriptions.jinja +++ b/footnotes/jinja2/includes/footnotes/tabs/descriptions.jinja @@ -26,6 +26,7 @@

    Descriptions

    Create a new description

    +

    There must be at least one description.

    {% set head = [ {"text": "Start date", "classes": "govuk-!-width-one-eighth"}, {"text": "Description"}, diff --git a/footnotes/tests/bdd/test_edit_footnote.py b/footnotes/tests/bdd/test_edit_footnote.py index bee668a39..ed1dca5e6 100644 --- a/footnotes/tests/bdd/test_edit_footnote.py +++ b/footnotes/tests/bdd/test_edit_footnote.py @@ -17,8 +17,19 @@ @pytest.fixture @when("I edit footnote NC000") -def footnote_edit_screen(client, footnote_NC000): - return client.get(footnote_NC000.get_url("edit")) +def footnote_edit_screen(client_with_current_workbasket, footnote_NC000): + return client_with_current_workbasket.get(footnote_NC000.get_url("edit")) + + +@pytest.fixture +@when("I edit footnote NC000") +def footnote_edit_screen_invalid_user( + client_with_current_workbasket_no_permissions, + footnote_NC000, +): + return client_with_current_workbasket_no_permissions.get( + footnote_NC000.get_url("edit"), + ) @then("I see an edit form") @@ -28,8 +39,8 @@ def edit_permission_granted(footnote_edit_screen): @pytest.fixture @when("I set the end date before the start date on footnote NC000") -def end_date_before_start(client, response, footnote_NC000): - response["response"] = client.post( +def end_date_before_start(client_with_current_workbasket, response, footnote_NC000): + response["response"] = client_with_current_workbasket.post( footnote_NC000.get_url("edit"), validity_period_post_data( start=date(2021, 1, 1), @@ -39,8 +50,8 @@ def end_date_before_start(client, response, footnote_NC000): @when("I set the start date of footnote NC000 to predate the footnote type") -def submit_predating(client, response, footnote_NC000): - response["response"] = client.post( +def submit_predating(client_with_current_workbasket, response, footnote_NC000): + response["response"] = client_with_current_workbasket.post( footnote_NC000.get_url("edit"), validity_period_post_data( start=footnote_NC000.footnote_type.valid_between.lower - timedelta(days=1), diff --git a/footnotes/tests/test_forms.py b/footnotes/tests/test_forms.py index 2e5f4a356..34c96210b 100644 --- a/footnotes/tests/test_forms.py +++ b/footnotes/tests/test_forms.py @@ -18,7 +18,7 @@ # https://uktrade.atlassian.net/browse/TP-851 def test_form_save_creates_new_footnote_id_and_footnote_type_id_combo( - session_with_workbasket, + session_request_with_workbasket, ): """Tests that when two non-overlapping footnotes of the same type are created that these are created with a different footnote_id, to avoid @@ -41,7 +41,7 @@ def test_form_save_creates_new_footnote_id_and_footnote_type_id_combo( "start_date_2": 2022, "description": "A note on feet", } - form = forms.FootnoteCreateForm(data=data, request=session_with_workbasket) + form = forms.FootnoteCreateForm(data=data, request=session_request_with_workbasket) new_footnote = form.save(commit=False) assert earlier.footnote_id != new_footnote.footnote_id diff --git a/footnotes/tests/test_views.py b/footnotes/tests/test_views.py index 9810edac7..a432a0edf 100644 --- a/footnotes/tests/test_views.py +++ b/footnotes/tests/test_views.py @@ -129,7 +129,7 @@ def test_footnote_detail_views( view, url_pattern, valid_user_client, - session_with_workbasket, + session_request_with_workbasket, ): """Verify that measure detail views are under the url footnotes/ and don't return an error.""" @@ -228,7 +228,7 @@ def test_footnote_type_api_list_view(valid_user_client): ) -def test_footnote_description_create(valid_user_client): +def test_footnote_description_create(client_with_current_workbasket): """Tests that `FootnoteDescriptionCreate` view returns 200 and creates a description for the current version of an footnote.""" footnote = factories.FootnoteFactory.create(description=None) @@ -251,10 +251,10 @@ def test_footnote_description_create(valid_user_client): } with override_current_transaction(Transaction.objects.last()): - get_response = valid_user_client.get(url) + get_response = client_with_current_workbasket.get(url) assert get_response.status_code == 200 - post_response = valid_user_client.post(url, data) + post_response = client_with_current_workbasket.post(url, data) assert post_response.status_code == 302 assert FootnoteDescription.objects.filter(described_footnote=new_version).exists() diff --git a/geo_areas/tests/test_forms.py b/geo_areas/tests/test_forms.py index bcaaa4a86..01ec5644f 100644 --- a/geo_areas/tests/test_forms.py +++ b/geo_areas/tests/test_forms.py @@ -197,7 +197,7 @@ def test_geographical_membership_add_form_invalid_selection(date_ranges): def test_geographical_membership_edit_form_valid_deletion( date_ranges, - session_with_workbasket, + session_request_with_workbasket, ): country = factories.CountryFactory.create() area_group = factories.GeoGroupFactory.create(valid_between=date_ranges.normal) @@ -215,14 +215,14 @@ def test_geographical_membership_edit_form_valid_deletion( form = forms.GeographicalAreaEditForm( data=form_data, instance=area_group, - request=session_with_workbasket, + request=session_request_with_workbasket, ) assert form.is_valid() def test_geographical_membership_edit_form_invalid_deletion( date_ranges, - session_with_workbasket, + session_request_with_workbasket, ): country = factories.CountryFactory.create() area_group = factories.GeoGroupFactory.create(valid_between=date_ranges.normal) @@ -243,7 +243,7 @@ def test_geographical_membership_edit_form_invalid_deletion( form = forms.GeographicalAreaEditForm( data=form_data, instance=area_group, - request=session_with_workbasket, + request=session_request_with_workbasket, ) assert not form.is_valid() assert ( @@ -254,7 +254,7 @@ def test_geographical_membership_edit_form_invalid_deletion( def test_geographical_membership_edit_form_valid_end_date( date_ranges, - session_with_workbasket, + session_request_with_workbasket, ): country = factories.CountryFactory.create() area_group = factories.GeoGroupFactory.create(valid_between=date_ranges.normal) @@ -275,14 +275,14 @@ def test_geographical_membership_edit_form_valid_end_date( form = forms.GeographicalAreaEditForm( data=form_data, instance=area_group, - request=session_with_workbasket, + request=session_request_with_workbasket, ) assert form.is_valid() def test_geographical_membership_edit_form_invalid_end_date( date_ranges, - session_with_workbasket, + session_request_with_workbasket, ): country = factories.CountryFactory.create() area_group = factories.GeoGroupFactory.create(valid_between=date_ranges.normal) @@ -310,7 +310,7 @@ def test_geographical_membership_edit_form_invalid_end_date( form = forms.GeographicalAreaEditForm( data=invalid_end_date_1, instance=area_group, - request=session_with_workbasket, + request=session_request_with_workbasket, ) assert not form.is_valid() assert ( @@ -321,7 +321,7 @@ def test_geographical_membership_edit_form_invalid_end_date( form = forms.GeographicalAreaEditForm( data=invalid_end_date_2, instance=area_group, - request=session_with_workbasket, + request=session_request_with_workbasket, ) assert not form.is_valid() assert ( diff --git a/geo_areas/tests/test_views.py b/geo_areas/tests/test_views.py index f4ca956de..58f5acf17 100644 --- a/geo_areas/tests/test_views.py +++ b/geo_areas/tests/test_views.py @@ -49,7 +49,10 @@ def test_geo_area_description_delete_form(use_delete_form): ) -def test_geographical_area_description_create(valid_user_client, date_ranges): +def test_geographical_area_description_create( + client_with_current_workbasket, + date_ranges, +): """Tests that a geographical area description can be created.""" geo_area = factories.GeographicalAreaFactory.create( @@ -71,13 +74,13 @@ def test_geographical_area_description_create(valid_user_client, date_ranges): "geo_area-ui-description-create", kwargs={"sid": current_geo_area.sid}, ) - response = valid_user_client.post(url, form_data) + response = client_with_current_workbasket.post(url, form_data) assert response.status_code == 302 with override_current_transaction(Transaction.objects.last()): - new_desciption = current_geo_area.get_description() - assert new_desciption.description == form_data["description"] - assert new_desciption.validity_start == date_ranges.future.lower + new_description = current_geo_area.get_description() + assert new_description.description == form_data["description"] + assert new_description.validity_start == date_ranges.future.lower @pytest.mark.parametrize( @@ -92,7 +95,7 @@ def test_geographical_area_detail_views( view, url_pattern, valid_user_client, - session_with_workbasket, + session_request_with_workbasket, ): """Verify that geographical detail views are under the url geographical- areas and don't return an error.""" @@ -173,29 +176,29 @@ def test_geo_area_api_list_view(valid_user_client): ) -def test_geo_area_update_view_200(valid_user_client): +def test_geo_area_update_view_200(client_with_current_workbasket): geo_area = factories.GeographicalAreaFactory.create() url = reverse( "geo_area-ui-edit", kwargs={"sid": geo_area.sid}, ) - response = valid_user_client.get(url) + response = client_with_current_workbasket.get(url) assert response.status_code == 200 -def test_geo_area_edit_update_view_200(valid_user_client): +def test_geo_area_edit_update_view_200(client_with_current_workbasket): geo_area = factories.GeographicalAreaFactory.create() url = reverse( "geo_area-ui-edit-update", kwargs={"sid": geo_area.sid}, ) - response = valid_user_client.get(url) + response = client_with_current_workbasket.get(url) assert response.status_code == 200 def test_geo_area_update_view_edit_end_date( valid_user_client, - session_workbasket, + user_workbasket, date_ranges, ): """Tests that a geographical area's end date can be edited.""" @@ -224,7 +227,7 @@ def test_geo_area_update_view_edit_end_date( assert response.url == redirect_url geo_areas = GeographicalArea.objects.filter( - transaction__workbasket=session_workbasket, + transaction__workbasket=user_workbasket, ) for geo_area in geo_areas: assert geo_area.valid_between.upper == new_end_date @@ -233,7 +236,7 @@ def test_geo_area_update_view_edit_end_date( def test_geo_area_update_view_membership_add_country_or_region( valid_user_client, - session_workbasket, + user_workbasket, ): """Tests that a country or region can be added as a member of the area group being edited.""" @@ -269,7 +272,7 @@ def test_geo_area_update_view_membership_add_country_or_region( assert response.url == redirect_url workbasket = GeographicalMembership.objects.filter( - transaction__workbasket=session_workbasket, + transaction__workbasket=user_workbasket, ) for membership in workbasket: assert membership.valid_between == expected_valid_between @@ -278,7 +281,7 @@ def test_geo_area_update_view_membership_add_country_or_region( def test_geo_area_update_view_membership_add_to_group( valid_user_client, - session_workbasket, + user_workbasket, ): """Tests that the country or region being edited can be added as a member of an area group.""" @@ -313,7 +316,7 @@ def test_geo_area_update_view_membership_add_to_group( assert response.url == redirect_url workbasket = GeographicalMembership.objects.filter( - transaction__workbasket=session_workbasket, + transaction__workbasket=user_workbasket, ) for membership in workbasket: assert membership.valid_between == expected_valid_between @@ -322,7 +325,7 @@ def test_geo_area_update_view_membership_add_to_group( def test_geo_area_update_view_membership_edit_end_date( valid_user_client, - session_workbasket, + user_workbasket, date_ranges, ): """Tests that an end date for a geographical membership can be edited.""" @@ -357,7 +360,7 @@ def test_geo_area_update_view_membership_edit_end_date( assert response.url == redirect_url workbasket = GeographicalMembership.objects.filter( - transaction__workbasket=session_workbasket, + transaction__workbasket=user_workbasket, ) for membership in workbasket: assert membership.valid_between.upper == expected_end_date @@ -366,7 +369,7 @@ def test_geo_area_update_view_membership_edit_end_date( def test_geo_area_update_view_membership_deletion( valid_user_client, - session_workbasket, + user_workbasket, date_ranges, ): """Tests that a country or region can be deleted as a member of an area @@ -397,13 +400,13 @@ def test_geo_area_update_view_membership_deletion( assert response.url == redirect_url workbasket = GeographicalMembership.objects.filter( - transaction__workbasket=session_workbasket, + transaction__workbasket=user_workbasket, ) for membership in workbasket: assert membership.update_type == UpdateType.DELETE -def test_geo_area_create_view(valid_user_client, session_workbasket, date_ranges): +def test_geo_area_create_view(valid_user_client, user_workbasket, date_ranges): """Tests that a geographical area can be created.""" form_data = { "area_code": AreaCode.COUNTRY, @@ -424,7 +427,7 @@ def test_geo_area_create_view(valid_user_client, session_workbasket, date_ranges with override_current_transaction(Transaction.objects.last()): geo_area = GeographicalArea.objects.get( - transaction__workbasket=session_workbasket, + transaction__workbasket=user_workbasket, ) assert geo_area.update_type == UpdateType.CREATE assert geo_area.area_code == form_data["area_code"] @@ -434,17 +437,16 @@ def test_geo_area_create_view(valid_user_client, session_workbasket, date_ranges def test_geo_area_edit_create_view( - valid_user_client, - session_workbasket, - date_ranges, use_edit_view, + workbasket, + date_ranges, ): """Tests that geographical area CREATE instances can be edited.""" geo_area = factories.GeographicalAreaFactory.create( area_code=AreaCode.REGION, area_id="TR", valid_between=date_ranges.no_end, - transaction=session_workbasket.new_transaction(), + transaction=workbasket.new_transaction(), ) data_changes = {**date_post_data("end_date", date_ranges.normal.upper)} @@ -454,7 +456,7 @@ def test_geo_area_edit_create_view( def test_geographical_membership_create_view( valid_user_client, - session_workbasket, + user_workbasket, date_ranges, ): """Tests that multiple geographical memberships can be created.""" @@ -486,7 +488,7 @@ def test_geographical_membership_create_view( assert response.url == redirect_url memberships = GeographicalMembership.objects.filter( - transaction__workbasket=session_workbasket, + transaction__workbasket=user_workbasket, ) for i, membership in enumerate(memberships): assert membership.update_type == UpdateType.CREATE diff --git a/govuk_frontend_jinja/__init__.py b/govuk_frontend_jinja/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/govuk_frontend_jinja/flask_ext.py b/govuk_frontend_jinja/flask_ext.py new file mode 100644 index 000000000..d84e6781b --- /dev/null +++ b/govuk_frontend_jinja/flask_ext.py @@ -0,0 +1,29 @@ +from flask.templating import Environment as FlaskEnvironment +from jinja2 import select_autoescape + +from govuk_frontend_jinja.templates import Environment as NunjucksEnvironment +from govuk_frontend_jinja.templates import NunjucksExtension +from govuk_frontend_jinja.templates import NunjucksUndefined + + +class Environment(NunjucksEnvironment, FlaskEnvironment): + pass + + +def init_govuk_frontend(app): + """ + Use the govuk_frontend_jinja Jinja environment in a Flask app. + + >>> from flask import Flask + >>> app = Flask("cheeseshop_service") + >>> init_govuk_frontend(app) + """ + app.jinja_environment = Environment + app.select_jinja_autoescape = select_autoescape( + ("html", "htm", "xml", "xhtml", "njk"), + ) + jinja_options = app.jinja_options.copy() + jinja_options["extensions"].append(NunjucksExtension) + jinja_options["undefined"] = NunjucksUndefined + app.jinja_options = jinja_options + return app diff --git a/govuk_frontend_jinja/templates.py b/govuk_frontend_jinja/templates.py new file mode 100644 index 000000000..3b109ec74 --- /dev/null +++ b/govuk_frontend_jinja/templates.py @@ -0,0 +1,302 @@ +import builtins +import os.path as path +import re +from collections.abc import Sized + +import jinja2 +import jinja2.ext +from jinja2.lexer import Token +from markupsafe import Markup + + +def njk_to_j2(template): + # Some component templates (such as radios) use `items` as the key of + # an object element. However `items` is also the name of a dictionary + # method in Python, and Jinja2 will prefer to return this attribute + # over the dict item. Handle specially. + template = re.sub(r"\.items\b", ".items__njk", template) + + # Some component templates (such as radios) append the loop index to a + # string. As the loop index is an integer this causes a TypeError in + # Python. Jinja2 has an operator `~` for string concatenation that + # converts integers to strings. + template = template.replace("+ loop.index", "~ loop.index") + + # The Character Count component in version 3 concatenates the word count + # with the hint text. As the word count is an integer this causes a + # TypeError in Python. Jinja2 has an operator `~` for string + # concatenation that converts integers to strings. + template = template.replace( + "+ (params.maxlength or params.maxwords) +", + "~ (params.maxlength or params.maxwords) ~", + ) + + # Nunjucks uses elseif, Jinja uses elif + template = template.replace("elseif", "elif") + + # Some component templates (such as input) call macros with params as + # an object which has unqoted keys. This causes Jinja to silently + # ignore the values. + template = re.sub( + r"""^([ ]*)([^ '"#\r\n:]+?)\s*:""", + r"\1'\2':", + template, + flags=re.M, + ) + + # govukFieldset can accept a call block argument, however the Jinja + # compiler does not detect this as the macro body is included from + # the template file. A workaround is to patch the declaration of the + # macro to include an explicit caller argument. + template = template.replace( + "macro govukFieldset(params)", + "macro govukFieldset(params, caller=none)", + ) + + # Many components feature an attributes field, which is supposed to be + # a dictionary. In the template for these components, the keys and values + # are iterated. In Python, the default iterator for a dict is .keys(), but + # we want .items(). + # This only works because our undefined implements .items() + # We've tested this explicitly with: govukInput, govukCheckbox, govukTable, + # govukSummaryList + template = re.sub( + r"for attribute, value in (params|item|cell|action).attributes", + r"for attribute, value in \1.attributes.items()", + template, + flags=re.M, + ) + + # Some templates try to set a variable in an outer block, which is not + # supported in Jinja. We create a namespace in those templates to get + # around this. + template = re.sub( + r"""^([ ]*)({% set describedBy =( params.*describedBy if params.*describedBy else)? "" %})""", + r"\1{%- set nonlocal = namespace() -%}\n\1\2", + template, + flags=re.M, + ) + # Change any references to describedBy to be nonlocal.describedBy, + # unless describedBy is a dictionary key (i.e. quoted or dotted). + template = re.sub(r"""(?>> foo = ChainableUndefined(name='foo') + >>> str(foo.bar['baz']) + '' + >>> foo.bar['baz'] + 42 + Traceback (most recent call last): + ... + jinja2.exceptions.UndefinedError: 'foo' is undefined + """ + return self + + __getitem__ = __getattr__ + + # Allow treating undefined as an (empty) dictionary. + # This works because Undefined is an iterable. + def items(self): + return self + + # Allow escaping with Markup. This is required when + # autoescape is enabled. Debugging this issue was + # annoying; the error messages were not clear as to + # the cause of the issue (see upstream pull request + # for info https://github.com/pallets/jinja/pull/1047) + def __html__(self): + return str(self) + + # attempt to behave a bit like js's `undefined` when concatenation is attempted + def __add__(self, other): + if isinstance(other, str): + return "undefined" + other + return super().__add__(other) + + def __radd__(self, other): + if isinstance(other, str): + return other + "undefined" + return super().__radd__(other) + + +class NunjucksCodeGenerator(jinja2.compiler.CodeGenerator): + def visit_CondExpr(self, node, frame): + if not (self.filename or "").endswith(".njk"): + return super().visit_CondExpr(node, frame) + + def write_expr2(): + if node.expr2 is not None: + return self.visit(node.expr2, frame) + # rather than complaining about a missing else + # clause we just assume it to be the empty + # string for nunjucks compatibility + return self.write('""') + + self.write("(") + self.visit(node.expr1, frame) + self.write(" if ") + self.visit(node.test, frame) + self.write(" else ") + write_expr2() + self.write(")") + + +_njk_signature = "__njk" +_builtin_function_or_method_type = type({}.keys) + + +class Environment(jinja2.Environment): + code_generator_class = NunjucksCodeGenerator + + def __init__(self, *args, **kwargs): + kwargs.setdefault("extensions", [NunjucksExtension]) + kwargs.setdefault("undefined", NunjucksUndefined) + super().__init__(*args, **kwargs) + self.filters["indent_njk"] = indent_njk + + def join_path(self, template, parent): + """Enable the use of relative paths in template import statements.""" + if template.startswith(("./", "../")): + return path.normpath(path.join(path.dirname(parent), template)) + else: + return template + + def _handle_njk(method_name): + def inner(self, obj, argument): + if isinstance(argument, str) and argument.endswith(_njk_signature): + # a njk-originated access will always be assuming a dict lookup before an attr + final_method_name = "getitem" + final_argument = argument[: -len(_njk_signature)] + else: + final_argument = argument + final_method_name = method_name + + # pleasantly surprised that super() works in this context + retval = builtins.getattr(super(), final_method_name)(obj, final_argument) + + if ( + argument == f"length{_njk_signature}" + and isinstance(retval, jinja2.runtime.Undefined) + and isinstance(obj, Sized) + ): + return len(obj) + if ( + isinstance(argument, str) + and argument.endswith(_njk_signature) + and isinstance(retval, _builtin_function_or_method_type) + ): + # the lookup has probably gone looking for attributes and found a builtin method. because + # any njk-originated lookup will have been made to prefer dict lookups over attributes, we + # can be fairly sure there isn't a dict key matching this - so we should just call this a + # failure. + return self.undefined(obj=obj, name=final_argument) + return retval + + return inner + + getitem = _handle_njk("getitem") + + getattr = _handle_njk("getattr") diff --git a/importer/forms.py b/importer/forms.py index 8704ce357..a148a9079 100644 --- a/importer/forms.py +++ b/importer/forms.py @@ -9,7 +9,7 @@ from defusedxml.common import DTDForbidden from django import forms from django.conf import settings -from django.contrib.auth.models import User +from django.contrib.auth import get_user_model from django.core.exceptions import ValidationError from django.core.files.base import ContentFile from django.core.files.uploadedfile import InMemoryUploadedFile @@ -27,6 +27,8 @@ from workbaskets.validators import WorkflowStatus from workbaskets.validators import tops_jira_number_validator +User = get_user_model() + class ImporterV2FormMixin: """Mixin for taric parser forms, providing common taric_file clean and diff --git a/importer/jinja2/eu-importer/notify-success.jinja b/importer/jinja2/eu-importer/notify-success.jinja index 3bba97dbd..81f2eddbb 100644 --- a/importer/jinja2/eu-importer/notify-success.jinja +++ b/importer/jinja2/eu-importer/notify-success.jinja @@ -12,8 +12,8 @@ "items": [ {"text": "Home", "href": url("home")}, {"text": "Edit an existing workbasket", "href": url("workbaskets:workbasket-ui-list")}, - {"text": "Workbasket " ~ request.session.workbasket.id ~ " - Review goods", - "href": url("workbaskets:workbasket-ui-review-goods", kwargs={"pk": request.session.workbasket.id})}, + {"text": "Workbasket " ~ request.user.current_workbasket.id ~ " - Review goods", + "href": url("workbaskets:workbasket-ui-review-goods", kwargs={"pk": request.user.current_workbasket.id})}, {"text": page_title} ] }) }} @@ -30,7 +30,7 @@ }) }} diff --git a/importer/management/commands/chunk_taric.py b/importer/management/commands/chunk_taric.py index 9d659c360..514f18485 100644 --- a/importer/management/commands/chunk_taric.py +++ b/importer/management/commands/chunk_taric.py @@ -1,6 +1,6 @@ from typing import List -from django.contrib.auth.models import User +from django.contrib.auth import get_user_model from django.core.management import BaseCommand from importer import models @@ -8,6 +8,8 @@ from importer.management.util import ImporterCommandMixin from importer.namespaces import TARIC_RECORD_GROUPS +User = get_user_model() + def setup_batch( batch_name: str, diff --git a/importer/management/commands/import_taric.py b/importer/management/commands/import_taric.py index 7c1331ed7..eea00b069 100644 --- a/importer/management/commands/import_taric.py +++ b/importer/management/commands/import_taric.py @@ -1,7 +1,7 @@ from typing import List from typing import Sequence -from django.contrib.auth.models import User +from django.contrib.auth import get_user_model from django.core.management import BaseCommand from importer.management.commands.chunk_taric import chunk_taric @@ -12,6 +12,8 @@ from workbaskets.models import TRANSACTION_PARTITION_SCHEMES from workbaskets.validators import WorkflowStatus +User = get_user_model() + def import_taric( taric3_file: str, diff --git a/importer/management/util.py b/importer/management/util.py index 7be79682c..17a26334e 100644 --- a/importer/management/util.py +++ b/importer/management/util.py @@ -1,7 +1,9 @@ -from django.contrib.auth.models import User +from django.contrib.auth import get_user_model from django.core.exceptions import MultipleObjectsReturned from django.core.exceptions import ObjectDoesNotExist +User = get_user_model() + class ImporterCommandMixin: def get_user(self, username): diff --git a/importer/tests/test_views.py b/importer/tests/test_views.py index b6cf21a5c..d3c35376c 100644 --- a/importer/tests/test_views.py +++ b/importer/tests/test_views.py @@ -4,7 +4,7 @@ import pytest from bs4 import BeautifulSoup -from django.contrib.auth.models import User +from django.contrib.auth import get_user_model from django.core.files.uploadedfile import SimpleUploadedFile from django.test import Client from django.urls import reverse @@ -18,6 +18,8 @@ pytestmark = pytest.mark.django_db +User = get_user_model() + @pytest.mark.parametrize("url_name", ["import_batch-ui-list", "import_batch-ui-create"]) def test_import_urls_requires_superuser( diff --git a/jest-setup.js b/jest-setup.js new file mode 100644 index 000000000..02c423f5d --- /dev/null +++ b/jest-setup.js @@ -0,0 +1 @@ +import "@testing-library/jest-dom"; \ No newline at end of file diff --git a/jest.config.js b/jest.config.js new file mode 100644 index 000000000..9d486c080 --- /dev/null +++ b/jest.config.js @@ -0,0 +1,7 @@ +/** @type {import('jest').Config} */ + +module.exports = { + verbose: true, + setupFilesAfterEnv: ['/jest-setup.js'], + testEnvironment: "jest-environment-jsdom", +}; diff --git a/measures/tests/conftest.py b/measures/tests/conftest.py index 0db3ba8fa..107809d7c 100644 --- a/measures/tests/conftest.py +++ b/measures/tests/conftest.py @@ -274,12 +274,12 @@ def measure_edit_conditions_and_negative_action_data(measure_edit_conditions_dat @pytest.fixture -def measure_form(measure_form_data, session_with_workbasket, erga_omnes): +def measure_form(measure_form_data, session_request_with_workbasket, erga_omnes): with override_current_transaction(Transaction.objects.last()): return MeasureForm( data=measure_form_data, instance=Measure.objects.first(), - request=session_with_workbasket, + request=session_request_with_workbasket, initial={}, ) diff --git a/measures/tests/test_filters.py b/measures/tests/test_filters.py index dec411046..f7a3546fc 100644 --- a/measures/tests/test_filters.py +++ b/measures/tests/test_filters.py @@ -7,17 +7,16 @@ from common.validators import UpdateType from measures.filters import MeasureFilter from measures.models import Measure -from workbaskets.models import WorkBasket pytestmark = pytest.mark.django_db def test_filter_by_current_workbasket( valid_user_client, - session_workbasket: WorkBasket, + user_workbasket, session_request, ): - with session_workbasket.new_transaction() as transaction: + with user_workbasket.new_transaction() as transaction: measure_in_workbasket_1 = factories.MeasureFactory.create( transaction=transaction, ) @@ -27,9 +26,6 @@ def test_filter_by_current_workbasket( factories.MeasureFactory.create() factories.MeasureFactory.create() - session = valid_user_client.session - session["workbasket"] = {"id": session_workbasket.pk} - session.save() self = MeasureFilter( data={"measure_filters_modifier": "current"}, request=session_request, @@ -41,19 +37,15 @@ def test_filter_by_current_workbasket( name="measure_filters_modifier", value="current", ) - assert len(result) == len(session_workbasket.measures) - assert set(session_workbasket.measures) == set(result) + assert len(result) == len(user_workbasket.measures) + assert set(user_workbasket.measures) == set(result) def test_filter_by_certificates( valid_user_client, - session_workbasket: WorkBasket, + user_workbasket, session_request, ): - session = valid_user_client.session - session["workbasket"] = {"id": session_workbasket.pk} - session.save() - old_date_range = TaricDateRange(date(2021, 1, 1), date(2023, 1, 1)) new_date_range = TaricDateRange(date(2023, 1, 1)) diff --git a/measures/tests/test_forms.py b/measures/tests/test_forms.py index 29d26484b..3f3da2fbf 100644 --- a/measures/tests/test_forms.py +++ b/measures/tests/test_forms.py @@ -85,7 +85,7 @@ def test_measure_conditions_formset_invalid( def test_measure_form_invalid_conditions_data( measure_form_data, - session_with_workbasket, + session_request_with_workbasket, date_ranges, erga_omnes, duty_sentence_parser, @@ -109,7 +109,7 @@ def test_measure_form_invalid_conditions_data( data=form_data, initial=form_data, instance=measure, - request=session_with_workbasket, + request=session_request_with_workbasket, ) assert not measure_form.is_valid() @@ -1192,7 +1192,7 @@ def test_measure_forms_conditions_wizard_clears_unneeded_certificate(date_ranges assert form_expects_no_certificate.cleaned_data["required_certificate"] is None -def test_measure_form_valid_data(erga_omnes, session_with_workbasket): +def test_measure_form_valid_data(erga_omnes, session_request_with_workbasket): """Test that MeasureForm.is_valid returns True when passed required fields and geographical_area and sid fields in cleaned data.""" measure = factories.MeasureFactory.create() @@ -1212,7 +1212,7 @@ def test_measure_form_valid_data(erga_omnes, session_with_workbasket): data=data, initial={}, instance=Measure.objects.first(), - request=session_with_workbasket, + request=session_request_with_workbasket, ) assert form.is_valid() assert ( @@ -1226,7 +1226,7 @@ def test_measure_form_valid_data(erga_omnes, session_with_workbasket): def test_measure_form_initial_data_geo_area( initial_option, erga_omnes, - session_with_workbasket, + session_request_with_workbasket, ): group = factories.GeographicalAreaFactory.create(area_code=AreaCode.GROUP) country = factories.GeographicalAreaFactory.create() @@ -1250,14 +1250,14 @@ def test_measure_form_initial_data_geo_area( data=data, initial={}, instance=Measure.objects.first(), - request=session_with_workbasket, + request=session_request_with_workbasket, ) assert form.initial["geo_area"] == geo_area_to_choice[measure.geographical_area] def test_measure_form_cleaned_data_geo_exclusions_group( erga_omnes, - session_with_workbasket, + session_request_with_workbasket, ): """Test that MeasureForm accepts geo_area form group data and returns excluded countries in cleaned data.""" @@ -1285,7 +1285,7 @@ def test_measure_form_cleaned_data_geo_exclusions_group( data=data, initial=data, instance=Measure.objects.first(), - request=session_with_workbasket, + request=session_request_with_workbasket, ) assert form.is_valid() assert form.cleaned_data["exclusions"] == [excluded_country1, excluded_country2] @@ -1293,7 +1293,7 @@ def test_measure_form_cleaned_data_geo_exclusions_group( def test_measure_form_cleaned_data_geo_exclusions_erga_omnes( erga_omnes, - session_with_workbasket, + session_request_with_workbasket, ): """Test that MeasureForm accepts geo_area form erga omnes data and returns excluded countries in cleaned data.""" @@ -1320,7 +1320,7 @@ def test_measure_form_cleaned_data_geo_exclusions_erga_omnes( data=data, initial=data, instance=Measure.objects.first(), - request=session_with_workbasket, + request=session_request_with_workbasket, ) assert form.is_valid() assert form.cleaned_data["exclusions"] == [excluded_country1, excluded_country2] diff --git a/measures/tests/test_migrations.py b/measures/tests/test_migrations.py index d209617fb..fb9e7346c 100644 --- a/measures/tests/test_migrations.py +++ b/measures/tests/test_migrations.py @@ -4,17 +4,17 @@ import pytest from psycopg2._range import DateTimeTZRange -from common.tests.factories import DutyExpressionFactory -from common.tests.factories import GeographicalAreaFactory -from common.tests.factories import GoodsNomenclatureFactory -from common.tests.factories import MeasureTypeFactory -from common.tests.factories import QueuedWorkBasketFactory -from common.tests.factories import RegulationFactory -from common.tests.factories import RegulationGroupFactory +from common.validators import ApplicabilityCode +from common.validators import UpdateType +from measures.validators import ImportExportCode +from measures.validators import MeasureExplosionLevel +from measures.validators import MeasureTypeCombination +from measures.validators import OrderNumberCaptureCode +from workbaskets.validators import WorkflowStatus @pytest.mark.django_db() -def test_add_back_deleted_measures(migrator, setup_content_types): +def test_add_back_deleted_measures(migrator, date_ranges): from common.models.transactions import TransactionPartition """Ensures that the initial migration works.""" @@ -25,60 +25,190 @@ def test_add_back_deleted_measures(migrator, setup_content_types): ), ) - setup_content_types(old_state.apps) - # setup - target_workbasket_id = 545 - - measurement_class = old_state.apps.get_model("measures", "Measure") - - assert measurement_class.objects.filter(sid=20194965).exists() is False - assert measurement_class.objects.filter(sid=20194966).exists() is False - assert measurement_class.objects.filter(sid=20194967).exists() is False + DutyExpression = old_state.apps.get_model("measures", "DutyExpression") + ContentType = old_state.apps.get_model("contenttypes", "ContentType") + GeographicalArea = old_state.apps.get_model("geo_areas", "GeographicalArea") + GoodsNomenclature = old_state.apps.get_model("commodities", "GoodsNomenclature") + Measure = old_state.apps.get_model("measures", "Measure") + MeasureType = old_state.apps.get_model("measures", "MeasureType") + MeasureTypeSeries = old_state.apps.get_model("measures", "MeasureTypeSeries") + Regulation = old_state.apps.get_model("regulations", "Regulation") + Group = old_state.apps.get_model("regulations", "Group") + Transaction = old_state.apps.get_model("common", "Transaction") + User = old_state.apps.get_model("common", "User") + WorkBasket = old_state.apps.get_model("workbaskets", "WorkBasket") + VersionGroup = old_state.apps.get_model("common", "VersionGroup") + + goods_content_type = ContentType.objects.get(model="goodsnomenclature") + geo_area_content_type = ContentType.objects.get(model="geographicalarea") + regulation_group_content_type = ContentType.objects.get(model="group") + regulation_content_type = ContentType.objects.get(model="regulation") + measure_series_content_type = ContentType.objects.get(model="measuretypeseries") + measure_type_content_type = ContentType.objects.get(model="measuretype") + duty_expression_content_type = ContentType.objects.get(model="dutyexpression") + + assert not Measure.objects.filter(sid=20194965).exists() + assert not Measure.objects.filter(sid=20194966).exists() + assert not Measure.objects.filter(sid=20194967).exists() # mock up workbasket - new_work_basket = QueuedWorkBasketFactory.create(id=target_workbasket_id).save() + user = User.objects.create() + target_workbasket_id = 545 + workbasket = WorkBasket.objects.create( + id=target_workbasket_id, + author=user, + approver=user, + status=WorkflowStatus.QUEUED, + ) + transaction = Transaction.objects.create( + workbasket=workbasket, + order=1, + partition=TransactionPartition.REVISION, + composite_key=str(workbasket.id) + + "-" + + "1" + + "-" + + str(TransactionPartition.REVISION), + ) # create the three goods - goods_1 = GoodsNomenclatureFactory.create(item_id="0306920000").save( - force_write=True, + goods_1 = GoodsNomenclature.objects.create( + item_id="0306920000", + update_type=UpdateType.CREATE, + transaction=transaction, + version_group=VersionGroup.objects.create(), + valid_between=date_ranges.no_end, + statistical=False, + polymorphic_ctype=goods_content_type, ) - goods_2 = GoodsNomenclatureFactory.create(item_id="0307190000").save( - force_write=True, + version_group = goods_1.version_group + version_group.current_version_id = goods_1.id + version_group.save() + + goods_2 = GoodsNomenclature.objects.create( + item_id="0307190000", + update_type=UpdateType.CREATE, + transaction=transaction, + version_group=VersionGroup.objects.create(), + valid_between=date_ranges.no_end, + statistical=False, + polymorphic_ctype=goods_content_type, ) - goods_3 = GoodsNomenclatureFactory.create(item_id="0307490000").save( - force_write=True, + version_group = goods_2.version_group + version_group.current_version_id = goods_2.id + version_group.save() + + goods_3 = GoodsNomenclature.objects.create( + item_id="0307490000", + update_type=UpdateType.CREATE, + transaction=transaction, + version_group=VersionGroup.objects.create(), + valid_between=date_ranges.no_end, + statistical=False, + polymorphic_ctype=goods_content_type, ) + version_group = goods_3.version_group + version_group.current_version_id = goods_3.id + version_group.save() # create the geo area - new_geographical_area = GeographicalAreaFactory.create(sid=146).save( - force_write=True, + new_geographical_area = GeographicalArea.objects.create( + sid=146, + area_id="CA", + area_code=0, + update_type=UpdateType.CREATE, + transaction=transaction, + version_group=VersionGroup.objects.create(), + valid_between=date_ranges.no_end, + polymorphic_ctype=geo_area_content_type, ) + version_group = new_geographical_area.version_group + version_group.current_version_id = new_geographical_area.id + version_group.save() # create the regulation group - new_regulation_group = RegulationGroupFactory.create( + new_regulation_group = Group.objects.create( + update_type=UpdateType.CREATE, + transaction=transaction, + version_group=VersionGroup.objects.create(), valid_between=DateTimeTZRange( date.today() + timedelta(days=-1000), date.today() + timedelta(days=1000), ), - ).save(force_write=True) + polymorphic_ctype=regulation_group_content_type, + ) + version_group = new_regulation_group.version_group + version_group.current_version_id = new_regulation_group.id + version_group.save() # create the regulation - new_regulation = RegulationFactory.create( + new_regulation = Regulation.objects.create( + regulation_group=new_regulation_group, + regulation_id="C2100006", + approved=True, valid_between=DateTimeTZRange( date.today() + timedelta(days=-1000), date.today() + timedelta(days=1000), ), - regulation_group=new_regulation_group, - regulation_id="C2100006", - approved=True, - ).save(force_write=True) + update_type=UpdateType.CREATE, + transaction=transaction, + version_group=VersionGroup.objects.create(), + polymorphic_ctype=regulation_content_type, + ) + version_group = new_regulation.version_group + version_group.current_version_id = new_regulation.id + version_group.save() # create the measure type - new_measure_type = MeasureTypeFactory.create(sid=142).save(force_write=True) + new_measure_type_series = MeasureTypeSeries.objects.create( + id=157, + sid="C", + measure_type_combination=MeasureTypeCombination.SINGLE_MEASURE, + valid_between=date_ranges.no_end, + update_type=UpdateType.CREATE, + transaction=transaction, + version_group=VersionGroup.objects.create(), + polymorphic_ctype=measure_series_content_type, + ) + version_group = new_measure_type_series.version_group + version_group.current_version_id = new_measure_type_series.id + version_group.save() + + new_measure_type = MeasureType.objects.create( + sid=142, + trade_movement_code=ImportExportCode.IMPORT, + priority_code=1, + measure_component_applicability_code=ApplicabilityCode.MANDATORY, + origin_destination_code=ImportExportCode.IMPORT, + order_number_capture_code=OrderNumberCaptureCode.NOT_PERMITTED, + measure_explosion_level=MeasureExplosionLevel.HARMONISED_SYSTEM_CHAPTER, + measure_type_series=new_measure_type_series, + valid_between=date_ranges.no_end, + update_type=UpdateType.CREATE, + transaction=transaction, + version_group=VersionGroup.objects.create(), + polymorphic_ctype=measure_type_content_type, + ) + version_group = new_measure_type.version_group + version_group.current_version_id = new_measure_type.id + version_group.save() # create the duty expression - new_duty_expression = DutyExpressionFactory.create(sid=1).save(force_write=True) + new_duty_expression = DutyExpression.objects.create( + sid=1, + duty_amount_applicability_code=ApplicabilityCode.PERMITTED, + measurement_unit_applicability_code=ApplicabilityCode.PERMITTED, + monetary_unit_applicability_code=ApplicabilityCode.PERMITTED, + valid_between=date_ranges.no_end, + update_type=UpdateType.CREATE, + transaction=transaction, + version_group=VersionGroup.objects.create(), + polymorphic_ctype=duty_expression_content_type, + ) + version_group = new_duty_expression.version_group + version_group.current_version_id = new_duty_expression.id + version_group.save() # at this point all the appropriate elements are available within the database for the migration to create the # measures and conditions @@ -88,17 +218,15 @@ def test_add_back_deleted_measures(migrator, setup_content_types): ("measures", "0012_add_back_three_missing_measures_already_published"), ) - measurement_class = new_state.apps.get_model("measures", "Measure") + Measure = new_state.apps.get_model("measures", "Measure") measures_ids_to_check = [20194965, 20194966, 20194967] for measure_id_to_check in measures_ids_to_check: - # we should be able to get the measurements from the database now - assert ( - measurement_class.objects.filter(sid=measure_id_to_check).exists() is True - ) - assert measurement_class.objects.filter(sid=measure_id_to_check).count() == 1 - measure_to_check = measurement_class.objects.get(sid=measure_id_to_check) + # we should be able to get the measures from the database now + assert Measure.objects.filter(sid=measure_id_to_check).exists() is True + assert Measure.objects.filter(sid=measure_id_to_check).count() == 1 + measure_to_check = Measure.objects.get(sid=measure_id_to_check) # verify the transactions are on the correct partition assert measure_to_check.transaction.partition == TransactionPartition.REVISION # verify that the current version is as expected @@ -107,15 +235,12 @@ def test_add_back_deleted_measures(migrator, setup_content_types): == measure_to_check.trackedmodel_ptr_id ) - # verify that the current version is correct - migrator.reset() @pytest.mark.django_db() def test_add_back_deleted_measures_fails_silently_if_data_not_present( migrator, - setup_content_types, ): """Ensures that the initial migration works when no data to create measures are present, for local dev etc.""" @@ -127,8 +252,6 @@ def test_add_back_deleted_measures_fails_silently_if_data_not_present( ), ) - setup_content_types(old_state.apps) - measurement_class = old_state.apps.get_model("measures", "Measure") assert measurement_class.objects.filter(sid=20194965).exists() is False diff --git a/measures/tests/test_views.py b/measures/tests/test_views.py index a3f4ce8d5..619fb8798 100644 --- a/measures/tests/test_views.py +++ b/measures/tests/test_views.py @@ -135,7 +135,7 @@ def test_measure_delete(use_delete_form): use_delete_form(factories.MeasureFactory()) -def test_multiple_measure_delete_functionality(client, valid_user, session_workbasket): +def test_multiple_measure_delete_functionality(client, valid_user, user_workbasket): """Tests that MeasureMultipleDelete view's Post function takes a list of measures, and sets their update type to delete, clearing the session once completed.""" @@ -148,11 +148,6 @@ def test_multiple_measure_delete_functionality(client, valid_user, session_workb session = client.session session.update( { - "workbasket": { - "id": session_workbasket.pk, - "status": session_workbasket.status, - "title": session_workbasket.title, - }, "MULTIPLE_MEASURE_SELECTIONS": { measure_1.pk: True, measure_2.pk: True, @@ -165,7 +160,7 @@ def test_multiple_measure_delete_functionality(client, valid_user, session_workb response = client.post(url, data=post_data) workbasket_measures = Measure.objects.filter( - trackedmodel_ptr__transaction__workbasket_id=session_workbasket.id, + trackedmodel_ptr__transaction__workbasket_id=user_workbasket.id, ).order_by("sid") # on success, the page redirects to the list page @@ -176,7 +171,7 @@ def test_multiple_measure_delete_functionality(client, valid_user, session_workb assert measure.update_type == UpdateType.DELETE -def test_multiple_measure_delete_template(client, valid_user, session_workbasket): +def test_multiple_measure_delete_template(client, valid_user, user_workbasket): """Test that valid user receives a 200 on GET for MultipleMeasureDelete and correct measures display in html table.""" # Make a bunch of measures @@ -192,11 +187,6 @@ def test_multiple_measure_delete_template(client, valid_user, session_workbasket # Add a workbasket to the session, and add some selected measures to it. session.update( { - "workbasket": { - "id": session_workbasket.pk, - "status": session_workbasket.status, - "title": session_workbasket.title, - }, "MULTIPLE_MEASURE_SELECTIONS": { measure_1.pk: True, measure_2.pk: True, @@ -256,7 +246,7 @@ def test_measure_detail_views( view, url_pattern, valid_user_client, - session_with_workbasket, + session_request_with_workbasket, ): """Verify that measure detail views are under the url measures/ and don't return an error.""" @@ -514,8 +504,7 @@ def test_duties_validator( ) def test_measure_update_duty_sentence( update_data, - client, - valid_user, + client_with_current_workbasket, measure_form, duty_sentence_parser, ): @@ -533,8 +522,7 @@ def test_measure_update_duty_sentence( post_data.update(update_data) post_data["update_type"] = UpdateType.UPDATE url = reverse("measure-ui-edit", args=(measure_form.instance.sid,)) - client.force_login(valid_user) - response = client.post(url, data=post_data) + response = client_with_current_workbasket.post(url, data=post_data) assert response.status_code == 302 @@ -546,7 +534,6 @@ def test_measure_update_duty_sentence( components = measure.components.approved_up_to_transaction(tx).filter( component_measure__sid=measure_form.instance.sid, ) - assert components.exists() assert components.count() == 1 assert components.first().duty_amount == 10.000 @@ -557,8 +544,7 @@ def test_measure_update_duty_sentence( @patch("measures.forms.MeasureForm.save") def test_measure_form_save_called_on_measure_update( save, - client, - valid_user, + client_with_current_workbasket, measure_form, ): """Until work is done to make `TrackedModel` call new_version in save() we @@ -570,21 +556,20 @@ def test_measure_form_save_called_on_measure_update( post_data = {k: v for k, v in post_data.items() if v is not None} post_data["update_type"] = UpdateType.UPDATE url = reverse("measure-ui-edit", args=(measure_form.instance.sid,)) - client.force_login(valid_user) - client.post(url, data=post_data) + client_with_current_workbasket.post(url, data=post_data) save.assert_called_with(commit=False) -def test_measure_update_get_footnotes(session_with_workbasket): +def test_measure_update_get_footnotes(session_request_with_workbasket): association = factories.FootnoteAssociationMeasureFactory.create() - view = MeasureUpdate(request=session_with_workbasket) + view = MeasureUpdate(request=session_request_with_workbasket) footnotes = view.get_footnotes(association.footnoted_measure) assert len(footnotes) == 1 association.new_version( - WorkBasket.current(session_with_workbasket), + WorkBasket.current(session_request_with_workbasket), update_type=UpdateType.DELETE, ) @@ -595,7 +580,7 @@ def test_measure_update_get_footnotes(session_with_workbasket): def test_measure_update_form_creates_footnote_association( measure_form, - valid_user_client, + client_with_current_workbasket, ): """Test that editing a measure to add a new footnote doesn't require pressing "Add another footnote" button before submitting (saving) the @@ -609,7 +594,7 @@ def test_measure_update_form_creates_footnote_association( form_data["form-0-footnote"] = footnote.pk url = reverse("measure-ui-edit", kwargs={"sid": measure.sid}) - response = valid_user_client.post(url, form_data) + response = client_with_current_workbasket.post(url, form_data) assert response.status_code == 302 assert FootnoteAssociationMeasure.objects.filter( @@ -619,7 +604,10 @@ def test_measure_update_form_creates_footnote_association( # https://uktrade.atlassian.net/browse/TP2000-340 -def test_measure_update_updates_footnote_association(measure_form, client, valid_user): +def test_measure_update_updates_footnote_association( + measure_form, + client_with_current_workbasket, +): """Tests that when updating a measure with an existing footnote the MeasureFootnoteAssociation linking the measure and footnote is updated to point at the new, updated version of the measure.""" @@ -630,8 +618,7 @@ def test_measure_update_updates_footnote_association(measure_form, client, valid footnoted_measure=measure_form.instance, ) url = reverse("measure-ui-edit", args=(measure_form.instance.sid,)) - client.force_login(valid_user) - client.post(url, data=post_data) + client_with_current_workbasket.post(url, data=post_data) new_assoc = FootnoteAssociationMeasure.objects.last() ME70(new_assoc.transaction).validate(new_assoc) @@ -639,7 +626,10 @@ def test_measure_update_updates_footnote_association(measure_form, client, valid assert new_assoc.version_group == assoc.version_group -def test_measure_update_removes_footnote_association(valid_user_client, measure_form): +def test_measure_update_removes_footnote_association( + client_with_current_workbasket, + measure_form, +): """Test that when editing a measure to remove a footnote, the MeasureFootnoteAssociation, linking the measure and footnote, is updated to reflect this deletion.""" @@ -658,15 +648,15 @@ def test_measure_update_removes_footnote_association(valid_user_client, measure_ # Form stores data of footnotes on a measure in the session url = reverse("measure-ui-edit", kwargs={"sid": measure.sid}) - response = valid_user_client.get(url) + response = client_with_current_workbasket.get(url) assert response.status_code == 200 # Remove footnote2 from session to indicate its removal on form - session = valid_user_client.session + session = client_with_current_workbasket.session session[f"instance_footnotes_{measure.sid}"].remove(footnote2.pk) session.save() - response = valid_user_client.post(url, data=form_data) + response = client_with_current_workbasket.post(url, data=form_data) assert response.status_code == 302 with override_current_transaction(Transaction.objects.last()): @@ -680,7 +670,7 @@ def test_measure_update_removes_footnote_association(valid_user_client, measure_ def test_measure_update_create_conditions( - valid_user_client, + client_with_current_workbasket, measure_edit_conditions_data, duty_sentence_parser, erga_omnes, @@ -695,7 +685,10 @@ def test_measure_update_create_conditions( """ measure = Measure.objects.first() url = reverse("measure-ui-edit", args=(measure.sid,)) - response = valid_user_client.post(url, data=measure_edit_conditions_data) + response = client_with_current_workbasket.post( + url, + data=measure_edit_conditions_data, + ) assert response.status_code == 302 assert response.url == reverse("measure-ui-confirm-update", args=(measure.sid,)) @@ -737,8 +730,7 @@ def test_measure_update_create_conditions( def test_measure_update_edit_conditions( - client, - valid_user, + client_with_current_workbasket, measure_edit_conditions_data, duty_sentence_parser, erga_omnes, @@ -753,8 +745,7 @@ def test_measure_update_edit_conditions( """ measure = Measure.objects.first() url = reverse("measure-ui-edit", args=(measure.sid,)) - client.force_login(valid_user) - client.post(url, data=measure_edit_conditions_data) + client_with_current_workbasket.post(url, data=measure_edit_conditions_data) transaction_count = Transaction.objects.count() tx = Transaction.objects.last() measure_with_condition = Measure.objects.approved_up_to_transaction(tx).get( @@ -770,7 +761,7 @@ def test_measure_update_edit_conditions( measure_edit_conditions_data[ f"{MEASURE_CONDITIONS_FORMSET_PREFIX}-0-applicable_duty" ] = "10 GBP / 100 kg" - client.post(url, data=measure_edit_conditions_data) + client_with_current_workbasket.post(url, data=measure_edit_conditions_data) tx = Transaction.objects.last() updated_measure = Measure.objects.approved_up_to_transaction(tx).get( sid=measure.sid, @@ -831,8 +822,7 @@ def test_measure_update_edit_conditions( def test_measure_update_remove_conditions( - client, - valid_user, + client_with_current_workbasket, measure_edit_conditions_data, duty_sentence_parser, erga_omnes, @@ -847,11 +837,13 @@ def test_measure_update_remove_conditions( """ measure = Measure.objects.first() url = reverse("measure-ui-edit", args=(measure.sid,)) - client.force_login(valid_user) - client.post(url, data=measure_edit_conditions_data) + client_with_current_workbasket.post(url, data=measure_edit_conditions_data) measure_edit_conditions_data[f"{MEASURE_CONDITIONS_FORMSET_PREFIX}-0-DELETE"] = 1 - response = client.post(url, data=measure_edit_conditions_data) + response = client_with_current_workbasket.post( + url, + data=measure_edit_conditions_data, + ) assert response.status_code == 200 @@ -871,7 +863,10 @@ def test_measure_update_remove_conditions( ] = "" del measure_edit_conditions_data[f"{MEASURE_CONDITIONS_FORMSET_PREFIX}-0-DELETE"] transaction_count = Transaction.objects.count() - response = client.post(url, data=measure_edit_conditions_data) + response = client_with_current_workbasket.post( + url, + data=measure_edit_conditions_data, + ) assert response.status_code == 302 # We expect one transaction for the measure update and condition deletion @@ -886,8 +881,7 @@ def test_measure_update_remove_conditions( def test_measure_update_negative_condition( - client, - valid_user, + client_with_current_workbasket, measure_edit_conditions_and_negative_action_data, duty_sentence_parser, erga_omnes, @@ -902,8 +896,10 @@ def test_measure_update_negative_condition( measure = Measure.objects.first() url = reverse("measure-ui-edit", args=(measure.sid,)) - client.force_login(valid_user) - response = client.post(url, data=measure_edit_conditions_and_negative_action_data) + response = client_with_current_workbasket.post( + url, + data=measure_edit_conditions_and_negative_action_data, + ) assert response.status_code == 302 @@ -925,8 +921,7 @@ def test_measure_update_negative_condition( def test_measure_update_invalid_conditions( - client, - valid_user, + client_with_current_workbasket, measure_edit_conditions_and_negative_action_data, duty_sentence_parser, erga_omnes, @@ -946,8 +941,10 @@ def test_measure_update_invalid_conditions( measure = Measure.objects.first() url = reverse("measure-ui-edit", args=(measure.sid,)) - client.force_login(valid_user) - response = client.post(url, data=measure_edit_conditions_and_negative_action_data) + response = client_with_current_workbasket.post( + url, + data=measure_edit_conditions_and_negative_action_data, + ) assert response.status_code == 200 @@ -979,8 +976,7 @@ def test_measure_update_invalid_conditions( def test_measure_update_invalid_conditions_invalid_actions( - client, - valid_user, + client_with_current_workbasket, measure_edit_conditions_and_negative_action_data, duty_sentence_parser, erga_omnes, @@ -1010,8 +1006,10 @@ def test_measure_update_invalid_conditions_invalid_actions( measure = Measure.objects.first() url = reverse("measure-ui-edit", args=(measure.sid,)) - client.force_login(valid_user) - response = client.post(url, data=measure_edit_conditions_and_negative_action_data) + response = client_with_current_workbasket.post( + url, + data=measure_edit_conditions_and_negative_action_data, + ) assert response.status_code == 200 @@ -1027,7 +1025,7 @@ def test_measure_update_invalid_conditions_invalid_actions( ) -def test_measure_update_group_exclusion(client, valid_user, erga_omnes): +def test_measure_update_group_exclusion(client_with_current_workbasket, erga_omnes): """ Tests that measure edit view handles exclusion of one group from another group. @@ -1044,7 +1042,6 @@ def test_measure_update_group_exclusion(client, valid_user, erga_omnes): factories.GeographicalMembershipFactory.create(geo_group=erga_omnes, member=area_1) factories.GeographicalMembershipFactory.create(geo_group=erga_omnes, member=area_2) url = reverse("measure-ui-edit", args=(measure.sid,)) - client.force_login(valid_user) data = model_to_dict(measure) data = {k: v for k, v in data.items() if v is not None} start_date = data["valid_between"].lower @@ -1063,7 +1060,7 @@ def test_measure_update_group_exclusion(client, valid_user, erga_omnes): Transaction.objects.last(), ).exists() - client.post(url, data=data) + client_with_current_workbasket.post(url, data=data) measure_area_exclusions = ( MeasureExcludedGeographicalArea.objects.approved_up_to_transaction( Transaction.objects.last(), @@ -1083,16 +1080,14 @@ def test_measure_update_group_exclusion(client, valid_user, erga_omnes): assert area_2.sid in area_sids -def test_measure_edit_update_view(valid_user_client, erga_omnes): +def test_measure_edit_update_view(client_with_current_workbasket, erga_omnes): """Test that a measure UPDATE instance can be edited.""" measure = factories.MeasureFactory.create( update_type=UpdateType.UPDATE, - transaction=factories.UnapprovedTransactionFactory(), ) geo_area = factories.GeoGroupFactory.create() - url = reverse("measure-ui-edit-update", kwargs={"sid": measure.sid}) - response = valid_user_client.get(url) + response = client_with_current_workbasket.get(url) assert response.status_code == 200 data = model_to_dict(measure) @@ -1108,7 +1103,7 @@ def test_measure_edit_update_view(valid_user_client, erga_omnes): "submit": "submit", }, ) - response = valid_user_client.post(url, data=data) + response = client_with_current_workbasket.post(url, data=data) assert response.status_code == 302 with override_current_transaction(Transaction.objects.last()): @@ -1117,16 +1112,19 @@ def test_measure_edit_update_view(valid_user_client, erga_omnes): assert updated_measure.geographical_area == geo_area -def test_measure_edit_create_view(valid_user_client, duty_sentence_parser, erga_omnes): +def test_measure_edit_create_view( + client_with_current_workbasket, + duty_sentence_parser, + erga_omnes, +): """Test that a measure CREATE instance can be edited.""" measure = factories.MeasureFactory.create( update_type=UpdateType.CREATE, - transaction=factories.UnapprovedTransactionFactory(), ) geo_area = factories.CountryFactory.create() url = reverse("measure-ui-edit-create", kwargs={"sid": measure.sid}) - response = valid_user_client.get(url) + response = client_with_current_workbasket.get(url) assert response.status_code == 200 data = model_to_dict(measure) @@ -1144,7 +1142,7 @@ def test_measure_edit_create_view(valid_user_client, duty_sentence_parser, erga_ "submit": "submit", }, ) - response = valid_user_client.post(url, data=data) + response = client_with_current_workbasket.post(url, data=data) assert response.status_code == 302 with override_current_transaction(Transaction.objects.last()): @@ -1154,16 +1152,16 @@ def test_measure_edit_create_view(valid_user_client, duty_sentence_parser, erga_ @pytest.mark.django_db -def test_measure_form_wizard_start(valid_user_client): +def test_measure_form_wizard_start(client_with_current_workbasket): url = reverse("measure-ui-create", kwargs={"step": "start"}) - response = valid_user_client.get(url) + response = client_with_current_workbasket.get(url) assert response.status_code == 200 @unittest.mock.patch("measures.parsers.DutySentenceParser") def test_measure_form_wizard_finish( mock_duty_sentence_parser, - valid_user_client, + client_with_current_workbasket, regulation, duty_sentence_parser, erga_omnes, @@ -1249,10 +1247,10 @@ def test_measure_form_wizard_finish( "measure-ui-create", kwargs={"step": step_data["data"]["measure_create_wizard-current_step"]}, ) - response = valid_user_client.get(url) + response = client_with_current_workbasket.get(url) assert response.status_code == 200 - response = valid_user_client.post(url, step_data["data"]) + response = client_with_current_workbasket.post(url, step_data["data"]) assert response.status_code == 302 assert response.url == reverse( @@ -1260,7 +1258,7 @@ def test_measure_form_wizard_finish( kwargs={"step": step_data["next_step"]}, ) - complete_response = valid_user_client.get(response.url) + complete_response = client_with_current_workbasket.get(response.url) assert complete_response.status_code == 200 @@ -1725,7 +1723,7 @@ def test_measure_create_wizard_get_cleaned_data_for_step(session_request, measur def test_measure_create_wizard_quota_origins_conditional_step( - valid_user_client, + client_with_current_workbasket, quota_order_number, ): """ @@ -1766,10 +1764,10 @@ def test_measure_create_wizard_quota_origins_conditional_step( "measure-ui-create", kwargs={"step": step_data["data"]["measure_create_wizard-current_step"]}, ) - response = valid_user_client.get(url) + response = client_with_current_workbasket.get(url) assert response.status_code == 200 - response = valid_user_client.post(url, step_data["data"]) + response = client_with_current_workbasket.post(url, step_data["data"]) assert response.status_code == 302 assert response.url == reverse( @@ -1780,9 +1778,7 @@ def test_measure_create_wizard_quota_origins_conditional_step( def test_measure_form_creates_exclusions( erga_omnes, - session_with_workbasket, - valid_user, - client, + client_with_current_workbasket, ): excluded_country1 = factories.GeographicalAreaFactory.create() excluded_country2 = factories.GeographicalAreaFactory.create() @@ -1807,9 +1803,8 @@ def test_measure_form_creates_exclusions( "submit": "submit", } data.update(exclusions_data) - client.force_login(valid_user) url = reverse("measure-ui-edit", args=(measure.sid,)) - response = client.post(url, data) + response = client_with_current_workbasket.post(url, data) assert response.status_code == 302 assert measure.exclusions.all().count() == 2 assert not set( @@ -1839,7 +1834,7 @@ def test_measuretype_api_list_view(valid_user_client): def test_multiple_measure_start_and_end_date_edit_functionality( valid_user_client, - session_workbasket, + user_workbasket, mocked_diff_components, ): """Tests that MeasureEditWizard takes a list of measures, and sets their @@ -1859,11 +1854,6 @@ def test_multiple_measure_start_and_end_date_edit_functionality( session = valid_user_client.session session.update( { - "workbasket": { - "id": session_workbasket.pk, - "status": session_workbasket.status, - "title": session_workbasket.title, - }, "MULTIPLE_MEASURE_SELECTIONS": { measure_1.pk: 1, measure_2.pk: 1, @@ -1922,7 +1912,7 @@ def test_multiple_measure_start_and_end_date_edit_functionality( ) workbasket_measures = Measure.objects.filter( - trackedmodel_ptr__transaction__workbasket_id=session_workbasket.id, + trackedmodel_ptr__transaction__workbasket_id=user_workbasket.id, ).order_by("sid") complete_response = valid_user_client.get(response.url) @@ -1963,7 +1953,7 @@ def test_multiple_measure_edit_single_form_functionality( step, data, valid_user_client, - session_workbasket, + user_workbasket, mocked_diff_components, ): """Tests that MeasureEditWizard takes a list of measures, and sets their @@ -1977,11 +1967,6 @@ def test_multiple_measure_edit_single_form_functionality( session = valid_user_client.session session.update( { - "workbasket": { - "id": session_workbasket.pk, - "status": session_workbasket.status, - "title": session_workbasket.title, - }, "MULTIPLE_MEASURE_SELECTIONS": { measure_1.pk: 1, measure_2.pk: 1, @@ -2023,7 +2008,7 @@ def test_multiple_measure_edit_single_form_functionality( ) workbasket_measures = Measure.objects.filter( - trackedmodel_ptr__transaction__workbasket_id=session_workbasket.id, + trackedmodel_ptr__transaction__workbasket_id=user_workbasket.id, ).order_by("sid") complete_response = valid_user_client.get(response.url) @@ -2036,7 +2021,7 @@ def test_multiple_measure_edit_single_form_functionality( def test_multiple_measure_edit_only_regulation( valid_user_client, - session_workbasket, + user_workbasket, mocked_diff_components, ): """Tests the regulation step in MeasureEditWizard.""" @@ -2049,11 +2034,6 @@ def test_multiple_measure_edit_only_regulation( session = valid_user_client.session session.update( { - "workbasket": { - "id": session_workbasket.pk, - "status": session_workbasket.status, - "title": session_workbasket.title, - }, "MULTIPLE_MEASURE_SELECTIONS": { measure_1.pk: 1, measure_2.pk: 1, @@ -2098,7 +2078,7 @@ def test_multiple_measure_edit_only_regulation( ) workbasket_measures = Measure.objects.filter( - trackedmodel_ptr__transaction__workbasket_id=session_workbasket.id, + trackedmodel_ptr__transaction__workbasket_id=user_workbasket.id, ).order_by("sid") complete_response = valid_user_client.get(response.url) @@ -2110,7 +2090,7 @@ def test_multiple_measure_edit_only_regulation( assert measure.generating_regulation == regulation -def test_multiple_measure_edit_template(valid_user_client, session_workbasket): +def test_multiple_measure_edit_template(valid_user_client, user_workbasket): """Test that valid user receives a 200 on GET for MeasureEditWizard and correct measures display in html table.""" # Make a bunch of measures @@ -2124,11 +2104,6 @@ def test_multiple_measure_edit_template(valid_user_client, session_workbasket): # Add a workbasket to the session, and add some selected measures to it. session.update( { - "workbasket": { - "id": session_workbasket.pk, - "status": session_workbasket.status, - "title": session_workbasket.title, - }, "MULTIPLE_MEASURE_SELECTIONS": { measure_1.pk: True, measure_2.pk: True, @@ -2179,7 +2154,7 @@ def test_multiple_measure_edit_template(valid_user_client, session_workbasket): def test_measure_selection_update_view_updates_session( client, valid_user, - session_workbasket, + user_workbasket, ): # Make a bunch of measures measure_1 = factories.MeasureFactory.create() @@ -2226,7 +2201,7 @@ def test_measure_selection_update_view_updates_session( "foo", ], ) -def test_measure_list_redirect(form_action, valid_user_client, session_workbasket): +def test_measure_list_redirect(form_action, valid_user_client, user_workbasket): params = "page=2&start_date_modifier=exact&end_date_modifier=exact" url = f"{reverse('measure-ui-list')}?{params}" response = valid_user_client.post(url, {"form-action": form_action}) @@ -2271,7 +2246,7 @@ def test_measure_list_selected_measures_list(valid_user_client): def test_multiple_measure_edit_only_quota_order_number( valid_user_client, - session_workbasket, + user_workbasket, mocked_diff_components, ): """Tests the regulation step in MeasureEditWizard.""" @@ -2284,11 +2259,6 @@ def test_multiple_measure_edit_only_quota_order_number( session = valid_user_client.session session.update( { - "workbasket": { - "id": session_workbasket.pk, - "status": session_workbasket.status, - "title": session_workbasket.title, - }, "MULTIPLE_MEASURE_SELECTIONS": { measure_1.pk: 1, measure_2.pk: 1, @@ -2333,7 +2303,7 @@ def test_multiple_measure_edit_only_quota_order_number( ) workbasket_measures = Measure.objects.filter( - trackedmodel_ptr__transaction__workbasket_id=session_workbasket.id, + trackedmodel_ptr__transaction__workbasket_id=user_workbasket.id, ).order_by("sid") complete_response = valid_user_client.get(response.url) @@ -2347,7 +2317,7 @@ def test_multiple_measure_edit_only_quota_order_number( def test_multiple_measure_edit_only_duties( valid_user_client, - session_workbasket, + user_workbasket, duty_sentence_parser, ): """Tests the duties step in MeasureEditWizard.""" @@ -2360,11 +2330,6 @@ def test_multiple_measure_edit_only_duties( session = valid_user_client.session session.update( { - "workbasket": { - "id": session_workbasket.pk, - "status": session_workbasket.status, - "title": session_workbasket.title, - }, "MULTIPLE_MEASURE_SELECTIONS": { measure_1.pk: 1, measure_2.pk: 1, @@ -2409,7 +2374,7 @@ def test_multiple_measure_edit_only_duties( ) workbasket_measures = Measure.objects.filter( - trackedmodel_ptr__transaction__workbasket_id=session_workbasket.id, + trackedmodel_ptr__transaction__workbasket_id=user_workbasket.id, ).order_by("sid") complete_response = valid_user_client.get(response.url) @@ -2423,7 +2388,7 @@ def test_multiple_measure_edit_only_duties( def test_multiple_measure_edit_preserves_footnote_associations( valid_user_client, - session_workbasket, + user_workbasket, mocked_diff_components, ): """Tests that footnote associations are preserved in MeasureEditWizard.""" @@ -2439,11 +2404,6 @@ def test_multiple_measure_edit_preserves_footnote_associations( session = valid_user_client.session session.update( { - "workbasket": { - "id": session_workbasket.pk, - "status": session_workbasket.status, - "title": session_workbasket.title, - }, "MULTIPLE_MEASURE_SELECTIONS": { measure.pk: 1, }, @@ -2488,7 +2448,7 @@ def test_multiple_measure_edit_preserves_footnote_associations( ) workbasket_measures = Measure.objects.filter( - transaction__workbasket=session_workbasket, + transaction__workbasket=user_workbasket, ).order_by("sid") complete_response = valid_user_client.get(response.url) @@ -2504,7 +2464,7 @@ def test_multiple_measure_edit_preserves_footnote_associations( def test_multiple_measure_edit_geographical_area_exclusions( valid_user_client, - session_workbasket, + user_workbasket, mocked_diff_components, ): """Tests that the geographical area exclusions of multiple measures can be @@ -2517,9 +2477,6 @@ def test_multiple_measure_edit_geographical_area_exclusions( session = valid_user_client.session session.update( { - "workbasket": { - "id": session_workbasket.pk, - }, "MULTIPLE_MEASURE_SELECTIONS": { measure_1.pk: 1, measure_2.pk: 1, @@ -2567,7 +2524,7 @@ def test_multiple_measure_edit_geographical_area_exclusions( assert valid_user_client.session["MULTIPLE_MEASURE_SELECTIONS"] == {} workbasket_measures = Measure.objects.filter( - transaction__workbasket=session_workbasket, + transaction__workbasket=user_workbasket, ) assert workbasket_measures diff --git a/measures/views.py b/measures/views.py index 7290b4513..638382164 100644 --- a/measures/views.py +++ b/measures/views.py @@ -937,7 +937,7 @@ def done(self, form_list, **kwargs): cleaned_data = self.get_all_cleaned_data() created_measures = self.create_measures(cleaned_data) - created_measures[0].transaction.workbasket.save_to_session(self.request.session) + created_measures[0].transaction.workbasket.assign_to_user(self.request.user) context = self.get_context_data( form=None, diff --git a/package-lock.json b/package-lock.json index df6db8fe8..3a72fcc25 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,30 +1,45 @@ { "name": "tamato", "version": "0.1.0", - "lockfileVersion": 2, + "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "tamato", "version": "0.1.0", "dependencies": { + "@babel/core": "^7.23.2", + "@types/styled-components": "^5.1.29", "accessible-autocomplete": "^2.0.3", "ansi-regex": "^6.0.1", + "babel-loader": "^9.1.3", "chart.js": "^3.9.1", "chartjs-adapter-moment": "^1.0.0", "css-loader": "^5.2.6", "file-loader": "^6.2.0", "govuk-frontend": "^3.13.0", + "govuk-react": "^0.10.6", "mini-css-extract-plugin": "^1.6.0", "moment": "^2.29.4", + "react": "^18.2.0", + "react-dom": "^18.2.0", "sass": "^1.38.2", "sass-loader": "^12.1.0", "style-loader": "^3.0.0", + "styled-components": "^6.1.0", "webpack": "^5.76.0", "webpack-bundle-tracker": "^1.1.0", "webpack-cli": "^4.7.2" }, "devDependencies": { + "@babel/preset-env": "^7.23.7", + "@babel/preset-react": "^7.23.3", + "@testing-library/jest-dom": "^6.2.1", + "@testing-library/react": "^14.1.2", + "babel-jest": "^29.7.0", + "jest": "^29.7.0", + "jest-environment-jsdom": "^29.7.0", + "react-test-renderer": "^18.2.0", "webpack-cli": "^4.7.2" }, "engines": { @@ -32,3116 +47,10851 @@ "npm": "^10.3.0" } }, - "node_modules/@discoveryjs/json-ext": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz", - "integrity": "sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==", - "dev": true, - "engines": { - "node": ">=10.0.0" - } + "node_modules/@adobe/css-tools": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.3.2.tgz", + "integrity": "sha512-DA5a1C0gD/pLOvhv33YMrbf2FK3oUzwNl9oOJqE4XVjuEtt6XIakRcsd7eLiOSPkp1kTRQGICTA8cKra/vFbjw==", + "dev": true }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz", - "integrity": "sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==", + "node_modules/@ampproject/remapping": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.1.tgz", + "integrity": "sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==", "dependencies": { - "@jridgewell/set-array": "^1.0.1", - "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/gen-mapping": "^0.3.0", "@jridgewell/trace-mapping": "^0.3.9" }, "engines": { "node": ">=6.0.0" } }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", - "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==", + "node_modules/@babel/code-frame": { + "version": "7.22.13", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz", + "integrity": "sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==", + "dependencies": { + "@babel/highlight": "^7.22.13", + "chalk": "^2.4.2" + }, "engines": { - "node": ">=6.0.0" + "node": ">=6.9.0" } }, - "node_modules/@jridgewell/set-array": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", - "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", + "node_modules/@babel/compat-data": { + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.23.5.tgz", + "integrity": "sha512-uU27kfDRlhfKl+w1U6vp16IuvSLtjAxdArVXPa9BvLkrr7CYIsxH5adpHObeAGY/41+syctUWOZ140a2Rvkgjw==", "engines": { - "node": ">=6.0.0" + "node": ">=6.9.0" } }, - "node_modules/@jridgewell/source-map": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.2.tgz", - "integrity": "sha512-m7O9o2uR8k2ObDysZYzdfhb08VuEml5oWGiosa1VdaPZ/A6QyPkAJuwN0Q1lhULOf6B7MtQmHENS743hWtCrgw==", + "node_modules/@babel/core": { + "version": "7.23.2", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.23.2.tgz", + "integrity": "sha512-n7s51eWdaWZ3vGT2tD4T7J6eJs3QoBXydv7vkUM06Bf1cbVD2Kc2UrkzhiQwobfV7NwOnQXYL7UBJ5VPU+RGoQ==", "dependencies": { - "@jridgewell/gen-mapping": "^0.3.0", - "@jridgewell/trace-mapping": "^0.3.9" + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.22.13", + "@babel/generator": "^7.23.0", + "@babel/helper-compilation-targets": "^7.22.15", + "@babel/helper-module-transforms": "^7.23.0", + "@babel/helpers": "^7.23.2", + "@babel/parser": "^7.23.0", + "@babel/template": "^7.22.15", + "@babel/traverse": "^7.23.2", + "@babel/types": "^7.23.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/@jridgewell/sourcemap-codec": { - "version": "1.4.14", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", - "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==" - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.17", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.17.tgz", - "integrity": "sha512-MCNzAp77qzKca9+W/+I0+sEpaUnZoeasnghNeVc41VZCEKaCH73Vq3BZZ/SzWIgrqE4H4ceI+p+b6C0mHf9T4g==", + "node_modules/@babel/generator": { + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.0.tgz", + "integrity": "sha512-lN85QRR+5IbYrMWM6Y4pE/noaQtg4pNiqeNGX60eqOfo6gtEj6uw/JagelB8vVztSd7R6M5n1+PQkDbHbBRU4g==", "dependencies": { - "@jridgewell/resolve-uri": "3.1.0", - "@jridgewell/sourcemap-codec": "1.4.14" + "@babel/types": "^7.23.0", + "@jridgewell/gen-mapping": "^0.3.2", + "@jridgewell/trace-mapping": "^0.3.17", + "jsesc": "^2.5.1" + }, + "engines": { + "node": ">=6.9.0" } }, - "node_modules/@types/eslint": { - "version": "8.4.10", - "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.4.10.tgz", - "integrity": "sha512-Sl/HOqN8NKPmhWo2VBEPm0nvHnu2LL3v9vKo8MEq0EtbJ4eVzGPl41VNPvn5E1i5poMk4/XD8UriLHpJvEP/Nw==", + "node_modules/@babel/helper-annotate-as-pure": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.22.5.tgz", + "integrity": "sha512-LvBTxu8bQSQkcyKOU+a1btnNFQ1dMAd0R6PyW3arXes06F6QLWLIrd681bxRPIXlrMGR3XYnW9JyML7dP3qgxg==", + "dev": true, "dependencies": { - "@types/estree": "*", - "@types/json-schema": "*" + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" } }, - "node_modules/@types/eslint-scope": { - "version": "3.7.4", - "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.4.tgz", - "integrity": "sha512-9K4zoImiZc3HlIp6AVUDE4CWYx22a+lhSZMYNpbjW04+YF0KWj4pJXnEMjdnFTiQibFFmElcsasJXDbdI/EPhA==", + "node_modules/@babel/helper-builder-binary-assignment-operator-visitor": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.22.15.tgz", + "integrity": "sha512-QkBXwGgaoC2GtGZRoma6kv7Szfv06khvhFav67ZExau2RaXzy8MpHSMO2PNoP2XtmQphJQRHFfg77Bq731Yizw==", + "dev": true, "dependencies": { - "@types/eslint": "*", - "@types/estree": "*" + "@babel/types": "^7.22.15" + }, + "engines": { + "node": ">=6.9.0" } }, - "node_modules/@types/estree": { - "version": "0.0.51", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.51.tgz", - "integrity": "sha512-CuPgU6f3eT/XgKKPqKd/gLZV1Xmvf1a2R5POBOGQa6uv82xpls89HU5zKeVoyR8XzHd1RGNOlQlvUe3CFkjWNQ==" - }, - "node_modules/@types/json-schema": { - "version": "7.0.11", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz", - "integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==" - }, - "node_modules/@types/node": { - "version": "18.11.9", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.9.tgz", - "integrity": "sha512-CRpX21/kGdzjOpFsZSkcrXMGIBWMGNIHXXBVFSH+ggkftxg+XYP20TESbh+zFvFj3EQOl5byk0HTRn1IL6hbqg==" - }, - "node_modules/@webassemblyjs/ast": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.1.tgz", - "integrity": "sha512-ukBh14qFLjxTQNTXocdyksN5QdM28S1CxHt2rdskFyL+xFV7VremuBLVbmCePj+URalXBENx/9Lm7lnhihtCSw==", + "node_modules/@babel/helper-compilation-targets": { + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.23.6.tgz", + "integrity": "sha512-9JB548GZoQVmzrFgp8o7KxdgkTGm6xs9DW0o/Pim72UDjzr5ObUQ6ZzYPqA+g9OTS2bBQoctLJrky0RDCAWRgQ==", "dependencies": { - "@webassemblyjs/helper-numbers": "1.11.1", - "@webassemblyjs/helper-wasm-bytecode": "1.11.1" + "@babel/compat-data": "^7.23.5", + "@babel/helper-validator-option": "^7.23.5", + "browserslist": "^4.22.2", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" } }, - "node_modules/@webassemblyjs/floating-point-hex-parser": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.1.tgz", - "integrity": "sha512-iGRfyc5Bq+NnNuX8b5hwBrRjzf0ocrJPI6GWFodBFzmFnyvrQ83SHKhmilCU/8Jv67i4GJZBMhEzltxzcNagtQ==" - }, - "node_modules/@webassemblyjs/helper-api-error": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.1.tgz", - "integrity": "sha512-RlhS8CBCXfRUR/cwo2ho9bkheSXG0+NwooXcc3PAILALf2QLdFyj7KGsKRbVc95hZnhnERon4kW/D3SZpp6Tcg==" - }, - "node_modules/@webassemblyjs/helper-buffer": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.1.tgz", - "integrity": "sha512-gwikF65aDNeeXa8JxXa2BAk+REjSyhrNC9ZwdT0f8jc4dQQeDQ7G4m0f2QCLPJiMTTO6wfDmRmj/pW0PsUvIcA==" - }, - "node_modules/@webassemblyjs/helper-numbers": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.1.tgz", - "integrity": "sha512-vDkbxiB8zfnPdNK9Rajcey5C0w+QJugEglN0of+kmO8l7lDb77AnlKYQF7aarZuCrv+l0UvqL+68gSDr3k9LPQ==", + "node_modules/@babel/helper-create-class-features-plugin": { + "version": "7.23.7", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.23.7.tgz", + "integrity": "sha512-xCoqR/8+BoNnXOY7RVSgv6X+o7pmT5q1d+gGcRlXYkI+9B31glE4jeejhKVpA04O1AtzOt7OSQ6VYKP5FcRl9g==", + "dev": true, "dependencies": { - "@webassemblyjs/floating-point-hex-parser": "1.11.1", - "@webassemblyjs/helper-api-error": "1.11.1", - "@xtuc/long": "4.2.2" + "@babel/helper-annotate-as-pure": "^7.22.5", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-function-name": "^7.23.0", + "@babel/helper-member-expression-to-functions": "^7.23.0", + "@babel/helper-optimise-call-expression": "^7.22.5", + "@babel/helper-replace-supers": "^7.22.20", + "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" } }, - "node_modules/@webassemblyjs/helper-wasm-bytecode": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.1.tgz", - "integrity": "sha512-PvpoOGiJwXeTrSf/qfudJhwlvDQxFgelbMqtq52WWiXC6Xgg1IREdngmPN3bs4RoO83PnL/nFrxucXj1+BX62Q==" - }, - "node_modules/@webassemblyjs/helper-wasm-section": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.1.tgz", - "integrity": "sha512-10P9No29rYX1j7F3EVPX3JvGPQPae+AomuSTPiF9eBQeChHI6iqjMIwR9JmOJXwpnn/oVGDk7I5IlskuMwU/pg==", + "node_modules/@babel/helper-create-regexp-features-plugin": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.22.15.tgz", + "integrity": "sha512-29FkPLFjn4TPEa3RE7GpW+qbE8tlsu3jntNYNfcGsc49LphF1PQIiD+vMZ1z1xVOKt+93khA9tc2JBs3kBjA7w==", + "dev": true, "dependencies": { - "@webassemblyjs/ast": "1.11.1", - "@webassemblyjs/helper-buffer": "1.11.1", - "@webassemblyjs/helper-wasm-bytecode": "1.11.1", - "@webassemblyjs/wasm-gen": "1.11.1" + "@babel/helper-annotate-as-pure": "^7.22.5", + "regexpu-core": "^5.3.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" } }, - "node_modules/@webassemblyjs/ieee754": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.11.1.tgz", - "integrity": "sha512-hJ87QIPtAMKbFq6CGTkZYJivEwZDbQUgYd3qKSadTNOhVY7p+gfP6Sr0lLRVTaG1JjFj+r3YchoqRYxNH3M0GQ==", + "node_modules/@babel/helper-define-polyfill-provider": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.4.4.tgz", + "integrity": "sha512-QcJMILQCu2jm5TFPGA3lCpJJTeEP+mqeXooG/NZbg/h5FTFi6V0+99ahlRsW8/kRLyb24LZVCCiclDedhLKcBA==", + "dev": true, "dependencies": { - "@xtuc/ieee754": "^1.2.0" + "@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/@webassemblyjs/leb128": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.11.1.tgz", - "integrity": "sha512-BJ2P0hNZ0u+Th1YZXJpzW6miwqQUGcIHT1G/sf72gLVD9DZ5AdYTqPNbHZh6K1M5VmKvFXwGSWZADz+qBWxeRw==", - "dependencies": { - "@xtuc/long": "4.2.2" + "node_modules/@babel/helper-environment-visitor": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", + "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==", + "engines": { + "node": ">=6.9.0" } }, - "node_modules/@webassemblyjs/utf8": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.11.1.tgz", - "integrity": "sha512-9kqcxAEdMhiwQkHpkNiorZzqpGrodQQ2IGrHHxCy+Ozng0ofyMA0lTqiLkVs1uzTRejX+/O0EOT7KxqVPuXosQ==" + "node_modules/@babel/helper-function-name": { + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz", + "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==", + "dependencies": { + "@babel/template": "^7.22.15", + "@babel/types": "^7.23.0" + }, + "engines": { + "node": ">=6.9.0" + } }, - "node_modules/@webassemblyjs/wasm-edit": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.1.tgz", - "integrity": "sha512-g+RsupUC1aTHfR8CDgnsVRVZFJqdkFHpsHMfJuWQzWU3tvnLC07UqHICfP+4XyL2tnr1amvl1Sdp06TnYCmVkA==", + "node_modules/@babel/helper-hoist-variables": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", + "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", "dependencies": { - "@webassemblyjs/ast": "1.11.1", - "@webassemblyjs/helper-buffer": "1.11.1", - "@webassemblyjs/helper-wasm-bytecode": "1.11.1", - "@webassemblyjs/helper-wasm-section": "1.11.1", - "@webassemblyjs/wasm-gen": "1.11.1", - "@webassemblyjs/wasm-opt": "1.11.1", - "@webassemblyjs/wasm-parser": "1.11.1", - "@webassemblyjs/wast-printer": "1.11.1" + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" } }, - "node_modules/@webassemblyjs/wasm-gen": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.1.tgz", - "integrity": "sha512-F7QqKXwwNlMmsulj6+O7r4mmtAlCWfO/0HdgOxSklZfQcDu0TpLiD1mRt/zF25Bk59FIjEuGAIyn5ei4yMfLhA==", + "node_modules/@babel/helper-member-expression-to-functions": { + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.23.0.tgz", + "integrity": "sha512-6gfrPwh7OuT6gZyJZvd6WbTfrqAo7vm4xCzAXOusKqq/vWdKXphTpj5klHKNmRUU6/QRGlBsyU9mAIPaWHlqJA==", + "dev": true, "dependencies": { - "@webassemblyjs/ast": "1.11.1", - "@webassemblyjs/helper-wasm-bytecode": "1.11.1", - "@webassemblyjs/ieee754": "1.11.1", - "@webassemblyjs/leb128": "1.11.1", - "@webassemblyjs/utf8": "1.11.1" + "@babel/types": "^7.23.0" + }, + "engines": { + "node": ">=6.9.0" } }, - "node_modules/@webassemblyjs/wasm-opt": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.1.tgz", - "integrity": "sha512-VqnkNqnZlU5EB64pp1l7hdm3hmQw7Vgqa0KF/KCNO9sIpI6Fk6brDEiX+iCOYrvMuBWDws0NkTOxYEb85XQHHw==", + "node_modules/@babel/helper-module-imports": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.22.15.tgz", + "integrity": "sha512-0pYVBnDKZO2fnSPCrgM/6WMc7eS20Fbok+0r88fp+YtWVLZrp4CkafFGIp+W0VKw4a22sgebPT99y+FDNMdP4w==", "dependencies": { - "@webassemblyjs/ast": "1.11.1", - "@webassemblyjs/helper-buffer": "1.11.1", - "@webassemblyjs/wasm-gen": "1.11.1", - "@webassemblyjs/wasm-parser": "1.11.1" + "@babel/types": "^7.22.15" + }, + "engines": { + "node": ">=6.9.0" } }, - "node_modules/@webassemblyjs/wasm-parser": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.1.tgz", - "integrity": "sha512-rrBujw+dJu32gYB7/Lup6UhdkPx9S9SnobZzRVL7VcBH9Bt9bCBLEuX/YXOOtBsOZ4NQrRykKhffRWHvigQvOA==", + "node_modules/@babel/helper-module-transforms": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.23.3.tgz", + "integrity": "sha512-7bBs4ED9OmswdfDzpz4MpWgSrV7FXlc3zIagvLFjS5H+Mk7Snr21vQ6QwrsoCGMfNC4e4LQPdoULEt4ykz0SRQ==", "dependencies": { - "@webassemblyjs/ast": "1.11.1", - "@webassemblyjs/helper-api-error": "1.11.1", - "@webassemblyjs/helper-wasm-bytecode": "1.11.1", - "@webassemblyjs/ieee754": "1.11.1", - "@webassemblyjs/leb128": "1.11.1", - "@webassemblyjs/utf8": "1.11.1" + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-module-imports": "^7.22.15", + "@babel/helper-simple-access": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/helper-validator-identifier": "^7.22.20" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" } }, - "node_modules/@webassemblyjs/wast-printer": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.11.1.tgz", - "integrity": "sha512-IQboUWM4eKzWW+N/jij2sRatKMh99QEelo3Eb2q0qXkvPRISAj8Qxtmw5itwqK+TTkBuUIE45AxYPToqPtL5gg==", + "node_modules/@babel/helper-optimise-call-expression": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.22.5.tgz", + "integrity": "sha512-HBwaojN0xFRx4yIvpwGqxiV2tUfl7401jlok564NgB9EHS1y6QT17FmKWm4ztqjeVdXLuC4fSvHc5ePpQjoTbw==", + "dev": true, "dependencies": { - "@webassemblyjs/ast": "1.11.1", - "@xtuc/long": "4.2.2" + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" } }, - "node_modules/@webpack-cli/configtest": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@webpack-cli/configtest/-/configtest-1.2.0.tgz", - "integrity": "sha512-4FB8Tj6xyVkyqjj1OaTqCjXYULB9FMkqQ8yGrZjRDrYh0nOE+7Lhs45WioWQQMV+ceFlE368Ukhe6xdvJM9Egg==", + "node_modules/@babel/helper-plugin-utils": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.22.5.tgz", + "integrity": "sha512-uLls06UVKgFG9QD4OeFYLEGteMIAa5kpTPcFL28yuCIIzsf6ZyKZMllKVOCZFhiZ5ptnwX4mtKdWCBE/uT4amg==", "dev": true, - "peerDependencies": { - "webpack": "4.x.x || 5.x.x", - "webpack-cli": "4.x.x" + "engines": { + "node": ">=6.9.0" } }, - "node_modules/@webpack-cli/info": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@webpack-cli/info/-/info-1.5.0.tgz", - "integrity": "sha512-e8tSXZpw2hPl2uMJY6fsMswaok5FdlGNRTktvFk2sD8RjH0hE2+XistawJx1vmKteh4NmGmNUrp+Tb2w+udPcQ==", + "node_modules/@babel/helper-remap-async-to-generator": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.22.20.tgz", + "integrity": "sha512-pBGyV4uBqOns+0UvhsTO8qgl8hO89PmiDYv+/COyp1aeMcmfrfruz+/nCMFiYyFF/Knn0yfrC85ZzNFjembFTw==", "dev": true, "dependencies": { - "envinfo": "^7.7.3" + "@babel/helper-annotate-as-pure": "^7.22.5", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-wrap-function": "^7.22.20" + }, + "engines": { + "node": ">=6.9.0" }, "peerDependencies": { - "webpack-cli": "4.x.x" + "@babel/core": "^7.0.0" } }, - "node_modules/@webpack-cli/serve": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/@webpack-cli/serve/-/serve-1.7.0.tgz", - "integrity": "sha512-oxnCNGj88fL+xzV+dacXs44HcDwf1ovs3AuEzvP7mqXw7fQntqIhQ1BRmynh4qEKQSSSRSWVyXRjmTbZIX9V2Q==", + "node_modules/@babel/helper-replace-supers": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.22.20.tgz", + "integrity": "sha512-qsW0In3dbwQUbK8kejJ4R7IHVGwHJlV6lpG6UA7a9hSa2YEiAib+N1T2kr6PEeUT+Fl7najmSOS6SmAwCHK6Tw==", "dev": true, - "peerDependencies": { - "webpack-cli": "4.x.x" + "dependencies": { + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-member-expression-to-functions": "^7.22.15", + "@babel/helper-optimise-call-expression": "^7.22.5" }, - "peerDependenciesMeta": { - "webpack-dev-server": { - "optional": true - } + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" } }, - "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==" - }, - "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==" - }, - "node_modules/accessible-autocomplete": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/accessible-autocomplete/-/accessible-autocomplete-2.0.4.tgz", - "integrity": "sha512-2p0txrSpvs5wXFUeQJHMheDPTZVSEmiUHWlEPb7vJnv2Dd1xPfoLnBQQMfNbTSit2pL/9sSQYESuD2Yyohd4Yw==", + "node_modules/@babel/helper-simple-access": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.22.5.tgz", + "integrity": "sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w==", "dependencies": { - "preact": "^8.3.1" - } - }, - "node_modules/acorn": { - "version": "8.8.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.1.tgz", - "integrity": "sha512-7zFpHzhnqYKrkYdUjF1HI1bzd0VygEGX8lFk4k5zVMqHEoES+P+7TKI+EvLO9WVMJ8eekdO0aDEK044xTXwPPA==", - "bin": { - "acorn": "bin/acorn" + "@babel/types": "^7.22.5" }, "engines": { - "node": ">=0.4.0" + "node": ">=6.9.0" } }, - "node_modules/acorn-import-assertions": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.8.0.tgz", - "integrity": "sha512-m7VZ3jwz4eK6A4Vtt8Ew1/mNbP24u0FhdyfA7fSvnJR6LMdfOYnmuIrrJAgrYfYJ10F/otaHTtrtrtmHdMNzEw==", - "peerDependencies": { - "acorn": "^8" + "node_modules/@babel/helper-skip-transparent-expression-wrappers": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.22.5.tgz", + "integrity": "sha512-tK14r66JZKiC43p8Ki33yLBVJKlQDFoA8GYN67lWCDCqoL6EMMSuM9b+Iff2jHaM/RRFYl7K+iiru7hbRqNx8Q==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" } }, - "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "node_modules/@babel/helper-split-export-declaration": { + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", + "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", "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" + "@babel/types": "^7.22.5" }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" + "engines": { + "node": ">=6.9.0" } }, - "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==", - "peerDependencies": { - "ajv": "^6.9.1" + "node_modules/@babel/helper-string-parser": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz", + "integrity": "sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==", + "engines": { + "node": ">=6.9.0" } }, - "node_modules/ansi-regex": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", - "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "node_modules/@babel/helper-validator-identifier": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", + "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", "engines": { - "node": ">=12" + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.23.5.tgz", + "integrity": "sha512-85ttAOMLsr53VgXkTbkx8oA6YTfT4q7/HzXSLEYmjcSTJPMPQtvq1BD79Byep5xMUYbGRzEpDsjUf3dyp54IKw==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-wrap-function": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.22.20.tgz", + "integrity": "sha512-pms/UwkOpnQe/PDAEdV/d7dVCoBbB+R4FvYoHGZz+4VPcg7RtYy2KP7S2lbuWM6FCSgob5wshfGESbC/hzNXZw==", + "dev": true, + "dependencies": { + "@babel/helper-function-name": "^7.22.5", + "@babel/template": "^7.22.15", + "@babel/types": "^7.22.19" }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" + "engines": { + "node": ">=6.9.0" } }, - "node_modules/anymatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz", - "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==", + "node_modules/@babel/helpers": { + "version": "7.23.2", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.23.2.tgz", + "integrity": "sha512-lzchcp8SjTSVe/fPmLwtWVBFC7+Tbn8LGHDVfDp9JGxpAY5opSaEFgt8UQvrnECWOTdji2mOWMz1rOhkHscmGQ==", "dependencies": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" + "@babel/template": "^7.22.15", + "@babel/traverse": "^7.23.2", + "@babel/types": "^7.23.0" }, "engines": { - "node": ">= 8" + "node": ">=6.9.0" } }, - "node_modules/big.js": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", - "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", + "node_modules/@babel/highlight": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.20.tgz", + "integrity": "sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==", + "dependencies": { + "@babel/helper-validator-identifier": "^7.22.20", + "chalk": "^2.4.2", + "js-tokens": "^4.0.0" + }, "engines": { - "node": "*" + "node": ">=6.9.0" } }, - "node_modules/binary-extensions": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", - "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "node_modules/@babel/parser": { + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.0.tgz", + "integrity": "sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw==", + "bin": { + "parser": "bin/babel-parser.js" + }, "engines": { - "node": ">=8" + "node": ">=6.0.0" } }, - "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/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { + "version": "7.23.3", + "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.23.3.tgz", + "integrity": "sha512-iRkKcCqb7iGnq9+3G6rZ+Ciz5VywC4XNRHe57lKM+jOeYAoR0lVqdeeDRfh0tQcTfw/+vBhHn926FmQhLtlFLQ==", + "dev": true, "dependencies": { - "fill-range": "^7.0.1" + "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { - "node": ">=8" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" } }, - "node_modules/browserslist": { - "version": "4.21.4", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.4.tgz", - "integrity": "sha512-CBHJJdDmgjl3daYjN5Cp5kbTf1mUhZoS+beLklHIvkOWscs83YAhLlF3Wsh/lciQYAcbBJgTOD44VtG31ZM4Hw==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - } - ], + "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.23.3.tgz", + "integrity": "sha512-WwlxbfMNdVEpQjZmK5mhm7oSwD3dS6eU+Iwsi4Knl9wAletWem7kaRsGOG+8UEbRyqxY4SS5zvtfXwX+jMxUwQ==", + "dev": true, "dependencies": { - "caniuse-lite": "^1.0.30001400", - "electron-to-chromium": "^1.4.251", - "node-releases": "^2.0.6", - "update-browserslist-db": "^1.0.9" - }, - "bin": { - "browserslist": "cli.js" + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", + "@babel/plugin-transform-optional-chaining": "^7.23.3" }, "engines": { - "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.13.0" } }, - "node_modules/buffer-from": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", - "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==" - }, - "node_modules/caniuse-lite": { - "version": "1.0.30001434", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001434.tgz", - "integrity": "sha512-aOBHrLmTQw//WFa2rcF1If9fa3ypkC1wzqqiKHgfdrXTWcU8C4gKVZT77eQAPWN1APys3+uQ0Df07rKauXGEYA==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/caniuse-lite" - } - ] - }, - "node_modules/chart.js": { - "version": "3.9.1", - "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-3.9.1.tgz", - "integrity": "sha512-Ro2JbLmvg83gXF5F4sniaQ+lTbSv18E+TIf2cOeiH1Iqd2PGFOtem+DUufMZsCJwFE7ywPOpfXFBwRTGq7dh6w==" - }, - "node_modules/chartjs-adapter-moment": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/chartjs-adapter-moment/-/chartjs-adapter-moment-1.0.0.tgz", - "integrity": "sha512-PqlerEvQcc5hZLQ/NQWgBxgVQ4TRdvkW3c/t+SUEQSj78ia3hgLkf2VZ2yGJtltNbEEFyYGm+cA6XXevodYvWA==", - "peerDependencies": { - "chart.js": "^3.0.0", - "moment": "^2.10.2" - } - }, - "node_modules/chokidar": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", - "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", - "funding": [ - { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - ], + "node_modules/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": { + "version": "7.23.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.23.7.tgz", + "integrity": "sha512-LlRT7HgaifEpQA1ZgLVOIJZZFVPWN5iReq/7/JixwBtwcoeVGDBD53ZV28rrsLYOZs1Y/EHhA8N/Z6aazHR8cw==", + "dev": true, "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" + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { - "node": ">= 8.10.0" + "node": ">=6.9.0" }, - "optionalDependencies": { - "fsevents": "~2.3.2" + "peerDependencies": { + "@babel/core": "^7.0.0" } }, - "node_modules/chrome-trace-event": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz", - "integrity": "sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg==", + "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.0" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/clone-deep": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", - "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", + "node_modules/@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", "dev": true, "dependencies": { - "is-plain-object": "^2.0.4", - "kind-of": "^6.0.2", - "shallow-clone": "^3.0.0" + "@babel/helper-plugin-utils": "^7.8.0" }, - "engines": { - "node": ">=6" + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/colorette": { - "version": "2.0.19", - "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.19.tgz", - "integrity": "sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ==", - "dev": true - }, - "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==" + "node_modules/@babel/plugin-syntax-bigint": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", + "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } }, - "node_modules/cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", "dev": true, "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" + "@babel/helper-plugin-utils": "^7.12.13" }, - "engines": { - "node": ">= 8" + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/css-loader": { - "version": "5.2.7", - "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-5.2.7.tgz", - "integrity": "sha512-Q7mOvpBNBG7YrVGMxRxcBJZFL75o+cH2abNASdibkj/fffYD8qWbInZrD0S9ccI6vZclF3DsHE7njGlLtaHbhg==", + "node_modules/@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", + "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", + "dev": true, "dependencies": { - "icss-utils": "^5.1.0", - "loader-utils": "^2.0.0", - "postcss": "^8.2.15", - "postcss-modules-extract-imports": "^3.0.0", - "postcss-modules-local-by-default": "^4.0.0", - "postcss-modules-scope": "^3.0.0", - "postcss-modules-values": "^4.0.0", - "postcss-value-parser": "^4.1.0", - "schema-utils": "^3.0.0", - "semver": "^7.3.5" + "@babel/helper-plugin-utils": "^7.14.5" }, "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" + "node": ">=6.9.0" }, "peerDependencies": { - "webpack": "^4.27.0 || ^5.0.0" + "@babel/core": "^7.0.0-0" } }, - "node_modules/cssesc": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", - "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", - "bin": { - "cssesc": "bin/cssesc" + "node_modules/@babel/plugin-syntax-dynamic-import": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz", + "integrity": "sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" }, - "engines": { - "node": ">=4" + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/electron-to-chromium": { - "version": "1.4.284", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.284.tgz", - "integrity": "sha512-M8WEXFuKXMYMVr45fo8mq0wUrrJHheiKZf6BArTKk9ZBYCKJEOU5H8cdWgDT+qCVZf7Na4lVUaZsA+h6uA9+PA==" - }, - "node_modules/emojis-list": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", - "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==", - "engines": { - "node": ">= 4" + "node_modules/@babel/plugin-syntax-export-namespace-from": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-export-namespace-from/-/plugin-syntax-export-namespace-from-7.8.3.tgz", + "integrity": "sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.3" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/enhanced-resolve": { - "version": "5.10.0", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.10.0.tgz", - "integrity": "sha512-T0yTFjdpldGY8PmuXXR0PyQ1ufZpEGiHVrp7zHKB7jdR4qlmZHhONVM5AQOAWXuF/w3dnHbEQVrNptJgt7F+cQ==", + "node_modules/@babel/plugin-syntax-import-assertions": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.23.3.tgz", + "integrity": "sha512-lPgDSU+SJLK3xmFDTV2ZRQAiM7UuUjGidwBywFavObCiZc1BeAAcMtHJKUya92hPHO+at63JJPLygilZard8jw==", + "dev": true, "dependencies": { - "graceful-fs": "^4.2.4", - "tapable": "^2.2.0" + "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { - "node": ">=10.13.0" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/envinfo": { - "version": "7.8.1", - "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.8.1.tgz", - "integrity": "sha512-/o+BXHmB7ocbHEAs6F2EnG0ogybVVUdkRunTT2glZU9XAaGmhqskrvKwqXuDfNjEO0LZKWdejEEpnq8aM0tOaw==", + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.23.3.tgz", + "integrity": "sha512-pawnE0P9g10xgoP7yKr6CK63K2FMsTE+FZidZO/1PwRdzmAPVs+HS1mAURUsgaoxammTJvULUdIkEK0gOcU2tA==", "dev": true, - "bin": { - "envinfo": "dist/cli.js" + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { - "node": ">=4" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/es-module-lexer": { - "version": "0.9.3", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-0.9.3.tgz", - "integrity": "sha512-1HQ2M2sPtxwnvOvT1ZClHyQDiggdNjURWpY2we6aMKCQiUVxTmVs2UYPLIrD84sS+kMdUwfBSylbJPwNnBrnHQ==" - }, - "node_modules/escalade": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", - "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", - "engines": { - "node": ">=6" + "node_modules/@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-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==", + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^4.1.1" + "@babel/helper-plugin-utils": "^7.8.0" }, - "engines": { - "node": ">=8.0.0" + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "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==", + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.22.5.tgz", + "integrity": "sha512-gvyP4hZrgrs/wWMaocvxZ44Hw0b3W8Pe+cMxc8V1ULQ07oh8VNbIRaoD1LRZVTvD+0nieDKjfgKg89sD7rrKrg==", + "dev": true, "dependencies": { - "estraverse": "^5.2.0" + "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { - "node": ">=4.0" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-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==", - "engines": { - "node": ">=4.0" + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/estraverse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", - "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", - "engines": { - "node": ">=4.0" + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/events": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", - "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", - "engines": { - "node": ">=0.8.x" + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "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==" + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } }, - "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==" + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } }, - "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==", + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", "dev": true, - "engines": { - "node": ">= 4.9.1" + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/file-loader": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/file-loader/-/file-loader-6.2.0.tgz", - "integrity": "sha512-qo3glqyTa61Ytg4u73GultjHGjdRyig3tG6lPtyX/jOEJvHif9uB0/OCI2Kif6ctF3caQTW2G5gym21oAsI4pw==", + "node_modules/@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", + "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", + "dev": true, "dependencies": { - "loader-utils": "^2.0.0", - "schema-utils": "^3.0.0" + "@babel/helper-plugin-utils": "^7.14.5" }, "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" + "node": ">=6.9.0" }, "peerDependencies": { - "webpack": "^4.0.0 || ^5.0.0" + "@babel/core": "^7.0.0-0" } }, - "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/@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "dev": true, "dependencies": { - "to-regex-range": "^5.0.1" + "@babel/helper-plugin-utils": "^7.14.5" }, "engines": { - "node": ">=8" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.23.3.tgz", + "integrity": "sha512-9EiNjVJOMwCO+43TqoTrgQ8jMwcAd0sWyXi9RPfIsLTj4R2MADDDQXELhffaUx/uJv2AYcxBgPwH6j4TIA4ytQ==", "dev": true, "dependencies": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" + "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { - "node": ">=8" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "hasInstallScript": true, - "optional": true, - "os": [ - "darwin" - ], + "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": "^8.16.0 || ^10.6.0 || >=11.0.0" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" } }, - "node_modules/function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", - "dev": true - }, - "node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "node_modules/@babel/plugin-transform-arrow-functions": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.23.3.tgz", + "integrity": "sha512-NzQcQrzaQPkaEwoTm4Mhyl8jI1huEL/WWIEvudjTCMJ9aBZNpsJbMASx7EQECtQQPS/DcnFpo0FIh3LvEO9cxQ==", + "dev": true, "dependencies": { - "is-glob": "^4.0.1" + "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { - "node": ">= 6" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-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==" - }, - "node_modules/govuk-frontend": { - "version": "3.14.0", - "resolved": "https://registry.npmjs.org/govuk-frontend/-/govuk-frontend-3.14.0.tgz", - "integrity": "sha512-y7FTuihCSA8Hty+e9h0uPhCoNanCAN+CLioNFlPmlbeHXpbi09VMyxTcH+XfnMPY4Cp++7096v0rLwwdapTXnA==", + "node_modules/@babel/plugin-transform-async-generator-functions": { + "version": "7.23.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.23.7.tgz", + "integrity": "sha512-PdxEpL71bJp1byMG0va5gwQcXHxuEYC/BgI/e88mGTtohbZN28O5Yit0Plkkm/dBzCF/BxmbNcses1RH1T+urA==", + "dev": true, + "dependencies": { + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-remap-async-to-generator": "^7.22.20", + "@babel/plugin-syntax-async-generators": "^7.8.4" + }, "engines": { - "node": ">= 4.2.0" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/graceful-fs": { - "version": "4.2.10", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", - "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==" - }, - "node_modules/has": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", - "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "node_modules/@babel/plugin-transform-async-to-generator": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.23.3.tgz", + "integrity": "sha512-A7LFsKi4U4fomjqXJlZg/u0ft/n8/7n7lpffUP/ZULx/DtV9SGlNKZolHH6PE8Xl1ngCc0M11OaeZptXVkfKSw==", "dev": true, "dependencies": { - "function-bind": "^1.1.1" + "@babel/helper-module-imports": "^7.22.15", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-remap-async-to-generator": "^7.22.20" }, "engines": { - "node": ">= 0.4.0" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "node_modules/@babel/plugin-transform-block-scoped-functions": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.23.3.tgz", + "integrity": "sha512-vI+0sIaPIO6CNuM9Kk5VmXcMVRiOpDh7w2zZt9GXzmE/9KD70CUEVhvPR/etAeNK/FAEkhxQtXOzVF3EuRL41A==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, "engines": { - "node": ">=8" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/icss-utils": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz", - "integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==", + "node_modules/@babel/plugin-transform-block-scoping": { + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.23.4.tgz", + "integrity": "sha512-0QqbP6B6HOh7/8iNR4CQU2Th/bbRtBp4KS9vcaZd1fZ0wSh5Fyssg0UCIHwxh+ka+pNDREbVLQnHCMHKZfPwfw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, "engines": { - "node": "^10 || ^12 || >= 14" + "node": ">=6.9.0" }, "peerDependencies": { - "postcss": "^8.1.0" + "@babel/core": "^7.0.0-0" } }, - "node_modules/immutable": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.1.0.tgz", - "integrity": "sha512-oNkuqVTA8jqG1Q6c+UglTOD1xhC1BtjKI7XkCXRkZHrN5m18/XsnUp8Q89GkQO/z+0WjonSvl0FLhDYftp46nQ==" - }, - "node_modules/import-local": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.1.0.tgz", - "integrity": "sha512-ASB07uLtnDs1o6EHjKpX34BKYDSqnFerfTOJL2HvMqF70LnxpjkzDB8J44oT9pu4AMPkQwf8jl6szgvNd2tRIg==", + "node_modules/@babel/plugin-transform-class-properties": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.23.3.tgz", + "integrity": "sha512-uM+AN8yCIjDPccsKGlw271xjJtGii+xQIF/uMPS8H15L12jZTsLfF4o5vNO7d/oUguOyfdikHGc/yi9ge4SGIg==", "dev": true, "dependencies": { - "pkg-dir": "^4.2.0", - "resolve-cwd": "^3.0.0" - }, - "bin": { - "import-local-fixture": "fixtures/cli.js" + "@babel/helper-create-class-features-plugin": "^7.22.15", + "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { - "node": ">=8" + "node": ">=6.9.0" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/interpret": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/interpret/-/interpret-2.2.0.tgz", - "integrity": "sha512-Ju0Bz/cEia55xDwUWEa8+olFpCiQoypjnQySseKtmjNrnps3P+xfpUmGr90T7yjlVJmOtybRvPXhKMbHr+fWnw==", + "node_modules/@babel/plugin-transform-class-static-block": { + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.23.4.tgz", + "integrity": "sha512-nsWu/1M+ggti1SOALj3hfx5FXzAY06fwPJsUZD4/A5e1bWi46VUIWtD+kOX6/IdhXGsXBWllLFDSnqSCdUNydQ==", "dev": true, + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.22.15", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-class-static-block": "^7.14.5" + }, "engines": { - "node": ">= 0.10" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.12.0" } }, - "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==", + "node_modules/@babel/plugin-transform-classes": { + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.23.5.tgz", + "integrity": "sha512-jvOTR4nicqYC9yzOHIhXG5emiFEOpappSJAl73SDSEDcybD+Puuze8Tnpb9p9qEyYup24tq891gkaygIFvWDqg==", + "dev": true, "dependencies": { - "binary-extensions": "^2.0.0" + "@babel/helper-annotate-as-pure": "^7.22.5", + "@babel/helper-compilation-targets": "^7.22.15", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-function-name": "^7.23.0", + "@babel/helper-optimise-call-expression": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-replace-supers": "^7.22.20", + "@babel/helper-split-export-declaration": "^7.22.6", + "globals": "^11.1.0" }, "engines": { - "node": ">=8" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/is-core-module": { - "version": "2.11.0", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.11.0.tgz", - "integrity": "sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw==", + "node_modules/@babel/plugin-transform-computed-properties": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.23.3.tgz", + "integrity": "sha512-dTj83UVTLw/+nbiHqQSFdwO9CbTtwq1DsDqm3CUEtDrZNET5rT5E6bIdTlOftDTDLMYxvxHNEYO4B9SLl8SLZw==", "dev": true, "dependencies": { - "has": "^1.0.3" + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/template": "^7.22.15" }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "node_modules/@babel/plugin-transform-destructuring": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.23.3.tgz", + "integrity": "sha512-n225npDqjDIr967cMScVKHXJs7rout1q+tt50inyBCPkyZ8KxeI6d+GIbSBTT/w/9WdlWDOej3V9HE5Lgk57gw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, "engines": { - "node": ">=0.10.0" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "node_modules/@babel/plugin-transform-dotall-regex": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.23.3.tgz", + "integrity": "sha512-vgnFYDHAKzFaTVp+mneDsIEbnJ2Np/9ng9iviHw3P/KVcgONxpNULEW/51Z/BaFojG2GI2GwwXck5uV1+1NOYQ==", + "dev": true, "dependencies": { - "is-extglob": "^2.1.1" + "@babel/helper-create-regexp-features-plugin": "^7.22.15", + "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { - "node": ">=0.10.0" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "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==", + "node_modules/@babel/plugin-transform-duplicate-keys": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.23.3.tgz", + "integrity": "sha512-RrqQ+BQmU3Oyav3J+7/myfvRCq7Tbz+kKLLshUmMwNlDHExbGL7ARhajvoBJEvc+fCguPPu887N+3RRXBVKZUA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, "engines": { - "node": ">=0.12.0" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "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==", + "node_modules/@babel/plugin-transform-dynamic-import": { + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.23.4.tgz", + "integrity": "sha512-V6jIbLhdJK86MaLh4Jpghi8ho5fGzt3imHOBu/x0jlBaPYqDoWz4RDXjmMOfnh+JWNaQleEAByZLV0QzBT4YQQ==", "dev": true, "dependencies": { - "isobject": "^3.0.1" + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-dynamic-import": "^7.8.3" }, "engines": { - "node": ">=0.10.0" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true - }, - "node_modules/isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", + "node_modules/@babel/plugin-transform-exponentiation-operator": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.23.3.tgz", + "integrity": "sha512-5fhCsl1odX96u7ILKHBj4/Y8vipoqwsJMh4csSA8qFfxrZDEA4Ssku2DyNvMJSmZNOEBT750LfFPbtrnTP90BQ==", "dev": true, + "dependencies": { + "@babel/helper-builder-binary-assignment-operator-visitor": "^7.22.15", + "@babel/helper-plugin-utils": "^7.22.5" + }, "engines": { - "node": ">=0.10.0" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "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==", + "node_modules/@babel/plugin-transform-export-namespace-from": { + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.23.4.tgz", + "integrity": "sha512-GzuSBcKkx62dGzZI1WVgTWvkkz84FZO5TC5T8dl/Tht/rAla6Dg/Mz9Yhypg+ezVACf/rgDuQt3kbWEv7LdUDQ==", + "dev": true, "dependencies": { - "@types/node": "*", - "merge-stream": "^2.0.0", - "supports-color": "^8.0.0" + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-export-namespace-from": "^7.8.3" }, "engines": { - "node": ">= 10.13.0" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "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==" + "node_modules/@babel/plugin-transform-for-of": { + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.23.6.tgz", + "integrity": "sha512-aYH4ytZ0qSuBbpfhuofbg/e96oQ7U2w1Aw/UQmKT+1l39uEhUPoFS3fHevDc1G0OvewyDudfMKY1OulczHzWIw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } }, - "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==" + "node_modules/@babel/plugin-transform-function-name": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.23.3.tgz", + "integrity": "sha512-I1QXp1LxIvt8yLaib49dRW5Okt7Q4oaxao6tFVKS/anCdEOMtYwWVKoiOA1p34GOWIZjUK0E+zCp7+l1pfQyiw==", + "dev": true, + "dependencies": { + "@babel/helper-compilation-targets": "^7.22.15", + "@babel/helper-function-name": "^7.23.0", + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } }, - "node_modules/json5": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", - "bin": { - "json5": "lib/cli.js" + "node_modules/@babel/plugin-transform-json-strings": { + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.23.4.tgz", + "integrity": "sha512-81nTOqM1dMwZ/aRXQ59zVubN9wHGqk6UtqRK+/q+ciXmRy8fSolhGVvG09HHRGo4l6fr/c4ZhXUQH0uFW7PZbg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-json-strings": "^7.8.3" }, "engines": { - "node": ">=6" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "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==", + "node_modules/@babel/plugin-transform-literals": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.23.3.tgz", + "integrity": "sha512-wZ0PIXRxnwZvl9AYpqNUxpZ5BiTGrYt7kueGQ+N5FiQ7RCOD4cm8iShd6S6ggfVIWaJf2EMk8eRzAh52RfP4rQ==", "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, "engines": { - "node": ">=0.10.0" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/klona": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/klona/-/klona-2.0.5.tgz", - "integrity": "sha512-pJiBpiXMbt7dkzXe8Ghj/u4FfXOOa98fPW+bihOJ4SjnoijweJrNThJfd3ifXpXhREjpoF2mZVH1GfS9LV3kHQ==", + "node_modules/@babel/plugin-transform-logical-assignment-operators": { + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.23.4.tgz", + "integrity": "sha512-Mc/ALf1rmZTP4JKKEhUwiORU+vcfarFVLfcFiolKUo6sewoxSEgl36ak5t+4WamRsNr6nzjZXQjM35WsU+9vbg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4" + }, "engines": { - "node": ">= 8" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-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==", + "node_modules/@babel/plugin-transform-member-expression-literals": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.23.3.tgz", + "integrity": "sha512-sC3LdDBDi5x96LA+Ytekz2ZPk8i/Ck+DEuDbRAll5rknJ5XRTSaPKEYwomLcs1AA8wg9b3KjIQRsnApj+q51Ag==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, "engines": { - "node": ">=6.11.5" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/loader-utils": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", - "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", + "node_modules/@babel/plugin-transform-modules-amd": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.23.3.tgz", + "integrity": "sha512-vJYQGxeKM4t8hYCKVBlZX/gtIY2I7mRGFNcm85sgXGMTBcoV3QdVtdpbcWEbzbfUIUZKwvgFT82mRvaQIebZzw==", + "dev": true, "dependencies": { - "big.js": "^5.2.2", - "emojis-list": "^3.0.0", - "json5": "^2.1.2" + "@babel/helper-module-transforms": "^7.23.3", + "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { - "node": ">=8.9.0" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "node_modules/@babel/plugin-transform-modules-commonjs": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.23.3.tgz", + "integrity": "sha512-aVS0F65LKsdNOtcz6FRCpE4OgsP2OFnW46qNxNIX9h3wuzaNcSQsJysuMwqSibC98HPrf2vCgtxKNwS0DAlgcA==", "dev": true, "dependencies": { - "p-locate": "^4.1.0" + "@babel/helper-module-transforms": "^7.23.3", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-simple-access": "^7.22.5" }, "engines": { - "node": ">=8" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/lodash.assign": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/lodash.assign/-/lodash.assign-4.2.0.tgz", - "integrity": "sha512-hFuH8TY+Yji7Eja3mGiuAxBqLagejScbG8GbG0j6o9vzn0YL14My+ktnqtZgFTosKymC9/44wP6s7xyuLfnClw==" - }, - "node_modules/lodash.defaults": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", - "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==" - }, - "node_modules/lodash.foreach": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.foreach/-/lodash.foreach-4.5.0.tgz", - "integrity": "sha512-aEXTF4d+m05rVOAUG3z4vZZ4xVexLKZGF0lIxuHZ1Hplpk/3B6Z1+/ICICYRLm7c41Z2xiejbkCkJoTlypoXhQ==" - }, - "node_modules/lodash.frompairs": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/lodash.frompairs/-/lodash.frompairs-4.0.1.tgz", - "integrity": "sha512-dvqe2I+cO5MzXCMhUnfYFa9MD+/760yx2aTAN1lqEcEkf896TxgrX373igVdqSJj6tQd0jnSLE1UMuKufqqxFw==" - }, - "node_modules/lodash.get": { - "version": "4.4.2", - "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", - "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==" - }, - "node_modules/lodash.topairs": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/lodash.topairs/-/lodash.topairs-4.3.0.tgz", - "integrity": "sha512-qrRMbykBSEGdOgQLJJqVSdPWMD7Q+GJJ5jMRfQYb+LTLsw3tYVIabnCzRqTJb2WTo17PG5gNzXuFaZgYH/9SAQ==" - }, - "node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "node_modules/@babel/plugin-transform-modules-systemjs": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.23.3.tgz", + "integrity": "sha512-ZxyKGTkF9xT9YJuKQRo19ewf3pXpopuYQd8cDXqNzc3mUNbOME0RKMoZxviQk74hwzfQsEe66dE92MaZbdHKNQ==", + "dev": true, "dependencies": { - "yallist": "^4.0.0" + "@babel/helper-hoist-variables": "^7.22.5", + "@babel/helper-module-transforms": "^7.23.3", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-validator-identifier": "^7.22.20" }, "engines": { - "node": ">=10" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "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==" - }, - "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==", + "node_modules/@babel/plugin-transform-modules-umd": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.23.3.tgz", + "integrity": "sha512-zHsy9iXX2nIsCBFPud3jKn1IRPWg3Ing1qOZgeKV39m1ZgIdpJqvlWVeiHBZC6ITRG0MfskhYe9cLgntfSFPIg==", + "dev": true, + "dependencies": { + "@babel/helper-module-transforms": "^7.23.3", + "@babel/helper-plugin-utils": "^7.22.5" + }, "engines": { - "node": ">= 0.6" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "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==", + "node_modules/@babel/plugin-transform-named-capturing-groups-regex": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.22.5.tgz", + "integrity": "sha512-YgLLKmS3aUBhHaxp5hi1WJTgOUb/NCuDHzGT9z9WTt3YG+CPRhJs6nprbStx6DnWM4dh6gt7SU3sZodbZ08adQ==", + "dev": true, "dependencies": { - "mime-db": "1.52.0" + "@babel/helper-create-regexp-features-plugin": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { - "node": ">= 0.6" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" } }, - "node_modules/mini-css-extract-plugin": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-1.6.2.tgz", - "integrity": "sha512-WhDvO3SjGm40oV5y26GjMJYjd2UMqrLAGKy5YS2/3QKJy2F7jgynuHTir/tgUUOiNQu5saXHdc8reo7YuhhT4Q==", + "node_modules/@babel/plugin-transform-new-target": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.23.3.tgz", + "integrity": "sha512-YJ3xKqtJMAT5/TIZnpAR3I+K+WaDowYbN3xyxI8zxx/Gsypwf9B9h0VB+1Nh6ACAAPRS5NSRje0uVv5i79HYGQ==", + "dev": true, "dependencies": { - "loader-utils": "^2.0.0", - "schema-utils": "^3.0.0", - "webpack-sources": "^1.1.0" + "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" + "node": ">=6.9.0" }, "peerDependencies": { - "webpack": "^4.4.0 || ^5.0.0" + "@babel/core": "^7.0.0-0" } }, - "node_modules/moment": { - "version": "2.29.4", - "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz", - "integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==", + "node_modules/@babel/plugin-transform-nullish-coalescing-operator": { + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.23.4.tgz", + "integrity": "sha512-jHE9EVVqHKAQx+VePv5LLGHjmHSJR76vawFPTdlxR/LVJPfOEGxREQwQfjuZEOPTwG92X3LINSh3M40Rv4zpVA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3" + }, "engines": { - "node": "*" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/nanoid": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz", - "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "bin": { - "nanoid": "bin/nanoid.cjs" + "node_modules/@babel/plugin-transform-numeric-separator": { + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.23.4.tgz", + "integrity": "sha512-mps6auzgwjRrwKEZA05cOwuDc9FAzoyFS4ZsG/8F43bTLf/TgkJg7QXOrPO1JO599iA3qgK9MXdMGOEC8O1h6Q==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-numeric-separator": "^7.10.4" }, "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-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==" - }, - "node_modules/node-releases": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.6.tgz", - "integrity": "sha512-PiVXnNuFm5+iYkLBNeq5211hvO38y63T0i2KKh2KnUs3RpzJ+JtODFjkD8yjLwnDkTYF1eKXheUwdssR+NRZdg==" - }, - "node_modules/normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "node_modules/@babel/plugin-transform-object-rest-spread": { + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.23.4.tgz", + "integrity": "sha512-9x9K1YyeQVw0iOXJlIzwm8ltobIIv7j2iLyP2jIhEbqPRQ7ScNgwQufU2I0Gq11VjyG4gI4yMXt2VFags+1N3g==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.23.3", + "@babel/helper-compilation-targets": "^7.22.15", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-transform-parameters": "^7.23.3" + }, "engines": { - "node": ">=0.10.0" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "node_modules/@babel/plugin-transform-object-super": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.23.3.tgz", + "integrity": "sha512-BwQ8q0x2JG+3lxCVFohg+KbQM7plfpBwThdW9A6TMtWwLsbDA01Ek2Zb/AgDN39BiZsExm4qrXxjk+P1/fzGrA==", "dev": true, "dependencies": { - "p-try": "^2.0.0" + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-replace-supers": "^7.22.20" }, "engines": { - "node": ">=6" + "node": ">=6.9.0" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "node_modules/@babel/plugin-transform-optional-catch-binding": { + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.23.4.tgz", + "integrity": "sha512-XIq8t0rJPHf6Wvmbn9nFxU6ao4c7WhghTR5WyV8SrJfUFzyxhCm4nhC+iAp3HFhbAKLfYpgzhJ6t4XCtVwqO5A==", "dev": true, "dependencies": { - "p-limit": "^2.2.0" + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3" }, "engines": { - "node": ">=8" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "node_modules/@babel/plugin-transform-optional-chaining": { + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.23.4.tgz", + "integrity": "sha512-ZU8y5zWOfjM5vZ+asjgAPwDaBjJzgufjES89Rs4Lpq63O300R/kOz30WCLo6BxxX6QVEilwSlpClnG5cZaikTA==", "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", + "@babel/plugin-syntax-optional-chaining": "^7.8.3" + }, "engines": { - "node": ">=6" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "node_modules/@babel/plugin-transform-parameters": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.23.3.tgz", + "integrity": "sha512-09lMt6UsUb3/34BbECKVbVwrT9bO6lILWln237z7sLaWnMsTi7Yc9fhX5DLpkJzAGfaReXI22wP41SZmnAA3Vw==", "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, "engines": { - "node": ">=8" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "node_modules/@babel/plugin-transform-private-methods": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.23.3.tgz", + "integrity": "sha512-UzqRcRtWsDMTLrRWFvUBDwmw06tCQH9Rl1uAjfh6ijMSmGYQ+fpdB+cnqRC8EMh5tuuxSv0/TejGL+7vyj+50g==", "dev": true, + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.22.15", + "@babel/helper-plugin-utils": "^7.22.5" + }, "engines": { - "node": ">=8" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/path-parse": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true - }, - "node_modules/picocolors": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", - "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==" - }, - "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "node_modules/@babel/plugin-transform-private-property-in-object": { + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.23.4.tgz", + "integrity": "sha512-9G3K1YqTq3F4Vt88Djx1UZ79PDyj+yKRnUy7cZGSMe+a7jkwD259uKKuUzQlPkGam7R+8RJwh5z4xO27fA1o2A==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.22.5", + "@babel/helper-create-class-features-plugin": "^7.22.15", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5" + }, "engines": { - "node": ">=8.6" + "node": ">=6.9.0" }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/pkg-dir": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", - "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "node_modules/@babel/plugin-transform-property-literals": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.23.3.tgz", + "integrity": "sha512-jR3Jn3y7cZp4oEWPFAlRsSWjxKe4PZILGBSd4nis1TsC5qeSpb+nrtihJuDhNI7QHiVbUaiXa0X2RZY3/TI6Nw==", "dev": true, "dependencies": { - "find-up": "^4.0.0" + "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { - "node": ">=8" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/postcss": { - "version": "8.4.31", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", - "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], + "node_modules/@babel/plugin-transform-react-display-name": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.23.3.tgz", + "integrity": "sha512-GnvhtVfA2OAtzdX58FJxU19rhoGeQzyVndw3GgtdECQvQFXPEZIOVULHVZGAYmOgmqjXpVpfocAbSjh99V/Fqw==", + "dev": true, "dependencies": { - "nanoid": "^3.3.6", - "picocolors": "^1.0.0", - "source-map-js": "^1.0.2" + "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { - "node": "^10 || ^12 || >=14" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/postcss-modules-extract-imports": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.0.0.tgz", - "integrity": "sha512-bdHleFnP3kZ4NYDhuGlVK+CMrQ/pqUm8bx/oGL93K6gVwiclvX5x0n76fYMKuIGKzlABOy13zsvqjb0f92TEXw==", + "node_modules/@babel/plugin-transform-react-jsx": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.22.15.tgz", + "integrity": "sha512-oKckg2eZFa8771O/5vi7XeTvmM6+O9cxZu+kanTU7tD4sin5nO/G8jGJhq8Hvt2Z0kUoEDRayuZLaUlYl8QuGA==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.22.5", + "@babel/helper-module-imports": "^7.22.15", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-jsx": "^7.22.5", + "@babel/types": "^7.22.15" + }, "engines": { - "node": "^10 || ^12 || >= 14" + "node": ">=6.9.0" }, "peerDependencies": { - "postcss": "^8.1.0" + "@babel/core": "^7.0.0-0" } }, - "node_modules/postcss-modules-local-by-default": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.0.tgz", - "integrity": "sha512-sT7ihtmGSF9yhm6ggikHdV0hlziDTX7oFoXtuVWeDd3hHObNkcHRo9V3yg7vCAY7cONyxJC/XXCmmiHHcvX7bQ==", + "node_modules/@babel/plugin-transform-react-jsx-development": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.22.5.tgz", + "integrity": "sha512-bDhuzwWMuInwCYeDeMzyi7TaBgRQei6DqxhbyniL7/VG4RSS7HtSL2QbY4eESy1KJqlWt8g3xeEBGPuo+XqC8A==", + "dev": true, "dependencies": { - "icss-utils": "^5.0.0", - "postcss-selector-parser": "^6.0.2", - "postcss-value-parser": "^4.1.0" + "@babel/plugin-transform-react-jsx": "^7.22.5" }, "engines": { - "node": "^10 || ^12 || >= 14" + "node": ">=6.9.0" }, "peerDependencies": { - "postcss": "^8.1.0" + "@babel/core": "^7.0.0-0" } }, - "node_modules/postcss-modules-scope": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.0.0.tgz", - "integrity": "sha512-hncihwFA2yPath8oZ15PZqvWGkWf+XUfQgUGamS4LqoP1anQLOsOJw0vr7J7IwLpoY9fatA2qiGUGmuZL0Iqlg==", + "node_modules/@babel/plugin-transform-react-pure-annotations": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.23.3.tgz", + "integrity": "sha512-qMFdSS+TUhB7Q/3HVPnEdYJDQIk57jkntAwSuz9xfSE4n+3I+vHYCli3HoHawN1Z3RfCz/y1zXA/JXjG6cVImQ==", + "dev": true, "dependencies": { - "postcss-selector-parser": "^6.0.4" + "@babel/helper-annotate-as-pure": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { - "node": "^10 || ^12 || >= 14" + "node": ">=6.9.0" }, "peerDependencies": { - "postcss": "^8.1.0" + "@babel/core": "^7.0.0-0" } }, - "node_modules/postcss-modules-values": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz", - "integrity": "sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==", + "node_modules/@babel/plugin-transform-regenerator": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.23.3.tgz", + "integrity": "sha512-KP+75h0KghBMcVpuKisx3XTu9Ncut8Q8TuvGO4IhY+9D5DFEckQefOuIsB/gQ2tG71lCke4NMrtIPS8pOj18BQ==", + "dev": true, "dependencies": { - "icss-utils": "^5.0.0" + "@babel/helper-plugin-utils": "^7.22.5", + "regenerator-transform": "^0.15.2" }, "engines": { - "node": "^10 || ^12 || >= 14" + "node": ">=6.9.0" }, "peerDependencies": { - "postcss": "^8.1.0" + "@babel/core": "^7.0.0-0" } }, - "node_modules/postcss-selector-parser": { - "version": "6.0.11", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.11.tgz", - "integrity": "sha512-zbARubNdogI9j7WY4nQJBiNqQf3sLS3wCP4WfOidu+p28LofJqDH1tcXypGrcmMHhDk2t9wGhCsYe/+szLTy1g==", + "node_modules/@babel/plugin-transform-reserved-words": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.23.3.tgz", + "integrity": "sha512-QnNTazY54YqgGxwIexMZva9gqbPa15t/x9VS+0fsEFWplwVpXYZivtgl43Z1vMpc1bdPP2PP8siFeVcnFvA3Cg==", + "dev": true, "dependencies": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" + "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { - "node": ">=4" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/postcss-value-parser": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" - }, - "node_modules/preact": { - "version": "8.5.3", - "resolved": "https://registry.npmjs.org/preact/-/preact-8.5.3.tgz", - "integrity": "sha512-O3kKP+1YdgqHOFsZF2a9JVdtqD+RPzCQc3rP+Ualf7V6rmRDchZ9MJbiGTT7LuyqFKZqlHSOyO/oMFmI2lVTsw==", - "hasInstallScript": true - }, - "node_modules/punycode": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", - "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", + "node_modules/@babel/plugin-transform-shorthand-properties": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.23.3.tgz", + "integrity": "sha512-ED2fgqZLmexWiN+YNFX26fx4gh5qHDhn1O2gvEhreLW2iI63Sqm4llRLCXALKrCnbN4Jy0VcMQZl/SAzqug/jg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, "engines": { - "node": ">=6" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/randombytes": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", - "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "node_modules/@babel/plugin-transform-spread": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.23.3.tgz", + "integrity": "sha512-VvfVYlrlBVu+77xVTOAoxQ6mZbnIq5FM0aGBSFEcIh03qHf+zNqA4DC/3XMUozTg7bZV3e3mZQ0i13VB6v5yUg==", + "dev": true, "dependencies": { - "safe-buffer": "^5.1.0" + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "node_modules/@babel/plugin-transform-sticky-regex": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.23.3.tgz", + "integrity": "sha512-HZOyN9g+rtvnOU3Yh7kSxXrKbzgrm5X4GncPY1QOquu7epga5MxKHVpYu2hvQnry/H+JjckSYRb93iNfsioAGg==", + "dev": true, "dependencies": { - "picomatch": "^2.2.1" + "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { - "node": ">=8.10.0" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/rechoir": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.7.1.tgz", - "integrity": "sha512-/njmZ8s1wVeR6pjTZ+0nCnv8SpZNRMT2D1RLOJQESlYFDBvwpTA4KWJpZ+sBJ4+vhjILRcK7JIFdGCdxEAAitg==", + "node_modules/@babel/plugin-transform-template-literals": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.23.3.tgz", + "integrity": "sha512-Flok06AYNp7GV2oJPZZcP9vZdszev6vPBkHLwxwSpaIqx75wn6mUd3UFWsSsA0l8nXAKkyCmL/sR02m8RYGeHg==", "dev": true, "dependencies": { - "resolve": "^1.9.0" + "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { - "node": ">= 0.10" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/resolve": { - "version": "1.22.1", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz", - "integrity": "sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==", + "node_modules/@babel/plugin-transform-typeof-symbol": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.23.3.tgz", + "integrity": "sha512-4t15ViVnaFdrPC74be1gXBSMzXk3B4Us9lP7uLRQHTFpV5Dvt33pn+2MyyNxmN3VTTm3oTrZVMUmuw3oBnQ2oQ==", "dev": true, "dependencies": { - "is-core-module": "^2.9.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" + "@babel/helper-plugin-utils": "^7.22.5" }, - "bin": { - "resolve": "bin/resolve" + "engines": { + "node": ">=6.9.0" }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/resolve-cwd": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", - "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "node_modules/@babel/plugin-transform-unicode-escapes": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.23.3.tgz", + "integrity": "sha512-OMCUx/bU6ChE3r4+ZdylEqAjaQgHAgipgW8nsCfu5pGqDcFytVd91AwRvUJSBZDz0exPGgnjoqhgRYLRjFZc9Q==", "dev": true, "dependencies": { - "resolve-from": "^5.0.0" + "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { - "node": ">=8" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/resolve-from": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", - "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "node_modules/@babel/plugin-transform-unicode-property-regex": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.23.3.tgz", + "integrity": "sha512-KcLIm+pDZkWZQAFJ9pdfmh89EwVfmNovFBcXko8szpBeF8z68kWIPeKlmSOkT9BXJxs2C0uk+5LxoxIv62MROA==", "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.22.15", + "@babel/helper-plugin-utils": "^7.22.5" + }, "engines": { - "node": ">=8" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, - "node_modules/sass": { - "version": "1.56.1", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.56.1.tgz", - "integrity": "sha512-VpEyKpyBPCxE7qGDtOcdJ6fFbcpOM+Emu7uZLxVrkX8KVU/Dp5UF7WLvzqRuUhB6mqqQt1xffLoG+AndxTZrCQ==", + "node_modules/@babel/plugin-transform-unicode-regex": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.23.3.tgz", + "integrity": "sha512-wMHpNA4x2cIA32b/ci3AfwNgheiva2W0WUKWTK7vBHBhDKfPsc5cFGNWm69WBqpwd86u1qwZ9PWevKqm1A3yAw==", + "dev": true, "dependencies": { - "chokidar": ">=3.0.0 <4.0.0", - "immutable": "^4.0.0", - "source-map-js": ">=0.6.2 <2.0.0" - }, - "bin": { - "sass": "sass.js" + "@babel/helper-create-regexp-features-plugin": "^7.22.15", + "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { - "node": ">=12.0.0" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/sass-loader": { - "version": "12.6.0", - "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-12.6.0.tgz", - "integrity": "sha512-oLTaH0YCtX4cfnJZxKSLAyglED0naiYfNG1iXfU5w1LNZ+ukoA5DtyDIN5zmKVZwYNJP4KRc5Y3hkWga+7tYfA==", + "node_modules/@babel/plugin-transform-unicode-sets-regex": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.23.3.tgz", + "integrity": "sha512-W7lliA/v9bNR83Qc3q1ip9CQMZ09CcHDbHfbLRDNuAhn1Mvkr1ZNF7hPmztMQvtTGVLJ9m8IZqWsTkXOml8dbw==", + "dev": true, "dependencies": { - "klona": "^2.0.4", - "neo-async": "^2.6.2" + "@babel/helper-create-regexp-features-plugin": "^7.22.15", + "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { - "node": ">= 12.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" + "node": ">=6.9.0" }, "peerDependencies": { - "fibers": ">= 3.1.0", - "node-sass": "^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0", - "sass": "^1.3.0", - "sass-embedded": "*", - "webpack": "^5.0.0" - }, - "peerDependenciesMeta": { - "fibers": { - "optional": true - }, - "node-sass": { - "optional": true - }, - "sass": { - "optional": true - }, - "sass-embedded": { - "optional": true - } + "@babel/core": "^7.0.0" } }, - "node_modules/schema-utils": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.1.tgz", - "integrity": "sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw==", + "node_modules/@babel/preset-env": { + "version": "7.23.7", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.23.7.tgz", + "integrity": "sha512-SY27X/GtTz/L4UryMNJ6p4fH4nsgWbz84y9FE0bQeWJP6O5BhgVCt53CotQKHCOeXJel8VyhlhujhlltKms/CA==", + "dev": true, "dependencies": { - "@types/json-schema": "^7.0.8", - "ajv": "^6.12.5", - "ajv-keywords": "^3.5.2" + "@babel/compat-data": "^7.23.5", + "@babel/helper-compilation-targets": "^7.23.6", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-validator-option": "^7.23.5", + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.23.3", + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.23.3", + "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.23.7", + "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-dynamic-import": "^7.8.3", + "@babel/plugin-syntax-export-namespace-from": "^7.8.3", + "@babel/plugin-syntax-import-assertions": "^7.23.3", + "@babel/plugin-syntax-import-attributes": "^7.23.3", + "@babel/plugin-syntax-import-meta": "^7.10.4", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5", + "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", + "@babel/plugin-transform-arrow-functions": "^7.23.3", + "@babel/plugin-transform-async-generator-functions": "^7.23.7", + "@babel/plugin-transform-async-to-generator": "^7.23.3", + "@babel/plugin-transform-block-scoped-functions": "^7.23.3", + "@babel/plugin-transform-block-scoping": "^7.23.4", + "@babel/plugin-transform-class-properties": "^7.23.3", + "@babel/plugin-transform-class-static-block": "^7.23.4", + "@babel/plugin-transform-classes": "^7.23.5", + "@babel/plugin-transform-computed-properties": "^7.23.3", + "@babel/plugin-transform-destructuring": "^7.23.3", + "@babel/plugin-transform-dotall-regex": "^7.23.3", + "@babel/plugin-transform-duplicate-keys": "^7.23.3", + "@babel/plugin-transform-dynamic-import": "^7.23.4", + "@babel/plugin-transform-exponentiation-operator": "^7.23.3", + "@babel/plugin-transform-export-namespace-from": "^7.23.4", + "@babel/plugin-transform-for-of": "^7.23.6", + "@babel/plugin-transform-function-name": "^7.23.3", + "@babel/plugin-transform-json-strings": "^7.23.4", + "@babel/plugin-transform-literals": "^7.23.3", + "@babel/plugin-transform-logical-assignment-operators": "^7.23.4", + "@babel/plugin-transform-member-expression-literals": "^7.23.3", + "@babel/plugin-transform-modules-amd": "^7.23.3", + "@babel/plugin-transform-modules-commonjs": "^7.23.3", + "@babel/plugin-transform-modules-systemjs": "^7.23.3", + "@babel/plugin-transform-modules-umd": "^7.23.3", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.22.5", + "@babel/plugin-transform-new-target": "^7.23.3", + "@babel/plugin-transform-nullish-coalescing-operator": "^7.23.4", + "@babel/plugin-transform-numeric-separator": "^7.23.4", + "@babel/plugin-transform-object-rest-spread": "^7.23.4", + "@babel/plugin-transform-object-super": "^7.23.3", + "@babel/plugin-transform-optional-catch-binding": "^7.23.4", + "@babel/plugin-transform-optional-chaining": "^7.23.4", + "@babel/plugin-transform-parameters": "^7.23.3", + "@babel/plugin-transform-private-methods": "^7.23.3", + "@babel/plugin-transform-private-property-in-object": "^7.23.4", + "@babel/plugin-transform-property-literals": "^7.23.3", + "@babel/plugin-transform-regenerator": "^7.23.3", + "@babel/plugin-transform-reserved-words": "^7.23.3", + "@babel/plugin-transform-shorthand-properties": "^7.23.3", + "@babel/plugin-transform-spread": "^7.23.3", + "@babel/plugin-transform-sticky-regex": "^7.23.3", + "@babel/plugin-transform-template-literals": "^7.23.3", + "@babel/plugin-transform-typeof-symbol": "^7.23.3", + "@babel/plugin-transform-unicode-escapes": "^7.23.3", + "@babel/plugin-transform-unicode-property-regex": "^7.23.3", + "@babel/plugin-transform-unicode-regex": "^7.23.3", + "@babel/plugin-transform-unicode-sets-regex": "^7.23.3", + "@babel/preset-modules": "0.1.6-no-external-plugins", + "babel-plugin-polyfill-corejs2": "^0.4.7", + "babel-plugin-polyfill-corejs3": "^0.8.7", + "babel-plugin-polyfill-regenerator": "^0.5.4", + "core-js-compat": "^3.31.0", + "semver": "^6.3.1" }, "engines": { - "node": ">= 10.13.0" + "node": ">=6.9.0" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "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": { - "lru-cache": "^6.0.0" + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/types": "^7.4.4", + "esutils": "^2.0.2" }, - "bin": { - "semver": "bin/semver.js" + "peerDependencies": { + "@babel/core": "^7.0.0-0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/@babel/preset-react": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/preset-react/-/preset-react-7.23.3.tgz", + "integrity": "sha512-tbkHOS9axH6Ysf2OUEqoSZ6T3Fa2SrNH6WTWSPBboxKzdxNc9qOICeLXkNG0ZEwbQ1HY8liwOce4aN/Ceyuq6w==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-validator-option": "^7.22.15", + "@babel/plugin-transform-react-display-name": "^7.23.3", + "@babel/plugin-transform-react-jsx": "^7.22.15", + "@babel/plugin-transform-react-jsx-development": "^7.22.5", + "@babel/plugin-transform-react-pure-annotations": "^7.23.3" }, "engines": { - "node": ">=10" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/serialize-javascript": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz", - "integrity": "sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==", + "node_modules/@babel/regjsgen": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@babel/regjsgen/-/regjsgen-0.8.0.tgz", + "integrity": "sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA==", + "dev": true + }, + "node_modules/@babel/runtime": { + "version": "7.23.2", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.2.tgz", + "integrity": "sha512-mM8eg4yl5D6i3lu2QKPuPH4FArvJ8KhTofbE7jwMUv9KX5mBvwPAqnV3MlyBNqdp9RyRKP6Yck8TrfYrPvX3bg==", "dependencies": { - "randombytes": "^2.1.0" + "regenerator-runtime": "^0.14.0" + }, + "engines": { + "node": ">=6.9.0" } }, - "node_modules/shallow-clone": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", - "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==", - "dev": true, + "node_modules/@babel/template": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz", + "integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==", "dependencies": { - "kind-of": "^6.0.2" + "@babel/code-frame": "^7.22.13", + "@babel/parser": "^7.22.15", + "@babel/types": "^7.22.15" }, "engines": { - "node": ">=8" + "node": ">=6.9.0" } }, - "node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, + "node_modules/@babel/traverse": { + "version": "7.23.2", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.2.tgz", + "integrity": "sha512-azpe59SQ48qG6nu2CzcMLbxUudtN+dOM9kDbUqGq3HXUJRlo7i8fvPoxQUzYgLZ4cMVmuZgm8vvBpNeRhd6XSw==", "dependencies": { - "shebang-regex": "^3.0.0" + "@babel/code-frame": "^7.22.13", + "@babel/generator": "^7.23.0", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-function-name": "^7.23.0", + "@babel/helper-hoist-variables": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/parser": "^7.23.0", + "@babel/types": "^7.23.0", + "debug": "^4.1.0", + "globals": "^11.1.0" }, "engines": { - "node": ">=8" + "node": ">=6.9.0" } }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, + "node_modules/@babel/types": { + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.0.tgz", + "integrity": "sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg==", + "dependencies": { + "@babel/helper-string-parser": "^7.22.5", + "@babel/helper-validator-identifier": "^7.22.20", + "to-fast-properties": "^2.0.0" + }, "engines": { - "node": ">=8" + "node": ">=6.9.0" } }, - "node_modules/source-list-map": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.1.tgz", - "integrity": "sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw==" + "node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true }, - "node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "node_modules/@discoveryjs/json-ext": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz", + "integrity": "sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==", + "dev": true, "engines": { - "node": ">=0.10.0" + "node": ">=10.0.0" } }, - "node_modules/source-map-js": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", - "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", - "engines": { - "node": ">=0.10.0" + "node_modules/@emotion/is-prop-valid": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.2.1.tgz", + "integrity": "sha512-61Mf7Ufx4aDxx1xlDeOm8aFFigGHE4z+0sKCa+IHCeZKiyP9RLD0Mmx7m8b9/Cf37f7NAvQOOJAbQQGVr5uERw==", + "dependencies": { + "@emotion/memoize": "^0.8.1" } }, - "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==", + "node_modules/@emotion/memoize": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.8.1.tgz", + "integrity": "sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA==" + }, + "node_modules/@emotion/unitless": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.8.1.tgz", + "integrity": "sha512-KOEGMu6dmJZtpadb476IsZBclKvILjopjUii3V+7MnXIQCYh8W3NgNcgwo21n9LXZX6EDIKvqfjYxXebDwxKmQ==" + }, + "node_modules/@govuk-react/back-link": { + "version": "0.10.6", + "resolved": "https://registry.npmjs.org/@govuk-react/back-link/-/back-link-0.10.6.tgz", + "integrity": "sha512-68ZAp3jw4f57E8sle6ReHCXlQkjwkcvNF7Ai8ONbZ/Wlj5bCzywPWwKxj4BTr6Yr/KAyxXeSpEvAjY9AvyVoEQ==", "dependencies": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" + "@govuk-react/constants": "^0.10.6", + "@govuk-react/lib": "^0.10.6", + "govuk-colours": "^1.1.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "styled-components": ">=5.1" } }, - "node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "node_modules/@govuk-react/breadcrumbs": { + "version": "0.10.6", + "resolved": "https://registry.npmjs.org/@govuk-react/breadcrumbs/-/breadcrumbs-0.10.6.tgz", + "integrity": "sha512-XqidFXw4oeqpuTfQVfz9z1Uw4RaX10yg6cfAdhd8KNjqoxyQKf+SsrKrERKl3/x633m9R8JuHSRcoZrTnna1vg==", "dependencies": { - "ansi-regex": "^5.0.1" + "@govuk-react/constants": "^0.10.6", + "@govuk-react/lib": "^0.10.6", + "govuk-colours": "^1.1.0" }, - "engines": { - "node": ">=8" + "peerDependencies": { + "react": ">=16.8.0", + "styled-components": ">=5.1" } }, - "node_modules/strip-ansi/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "engines": { - "node": ">=8" + "node_modules/@govuk-react/button": { + "version": "0.10.6", + "resolved": "https://registry.npmjs.org/@govuk-react/button/-/button-0.10.6.tgz", + "integrity": "sha512-UB+wCBiz4Z/00HsSFzB5VzyhK9WASKvXKg0nmt1D9W1ovo9GI2JlX0Zy/+m3pS55ZVHmmrHa5Bi+eGMlUAvZAw==", + "dependencies": { + "@govuk-react/constants": "^0.10.6", + "@govuk-react/icons": "^0.10.6", + "@govuk-react/lib": "^0.10.6", + "govuk-colours": "^1.1.0", + "polished": "^4.1.2" + }, + "peerDependencies": { + "react": ">=16.8.0", + "styled-components": ">=5.1" } }, - "node_modules/style-loader": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-3.3.1.tgz", - "integrity": "sha512-GPcQ+LDJbrcxHORTRes6Jy2sfvK2kS6hpSfI/fXhPt+spVzxF6LJ1dHLN9zIGmVaaP044YKaIatFaufENRiDoQ==", - "engines": { - "node": ">= 12.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" + "node_modules/@govuk-react/caption": { + "version": "0.10.6", + "resolved": "https://registry.npmjs.org/@govuk-react/caption/-/caption-0.10.6.tgz", + "integrity": "sha512-RkWZ6SSs73iL5iUK5MmRZQ/lX3buIsYSM3q20Rvq5I5alfkutQUHMcpIPRvK2WpFZ0rjXjxVbyqMMovezXVdHQ==", + "dependencies": { + "@govuk-react/constants": "^0.10.6", + "@govuk-react/lib": "^0.10.6", + "govuk-colours": "^1.1.0" }, "peerDependencies": { - "webpack": "^5.0.0" + "react": ">=16.8.0", + "styled-components": ">=5.1" } }, - "node_modules/supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "node_modules/@govuk-react/checkbox": { + "version": "0.10.6", + "resolved": "https://registry.npmjs.org/@govuk-react/checkbox/-/checkbox-0.10.6.tgz", + "integrity": "sha512-j0IaysLp8HHIZoza/Ys/7fx32CtX1AqBiyVa8qyprAHzig6iR1pd8MvXefPN1H9U6J6r7HVYrdMxJfk/eMd+hQ==", "dependencies": { - "has-flag": "^4.0.0" + "@govuk-react/constants": "^0.10.6", + "@govuk-react/hint-text": "^0.10.6", + "@govuk-react/lib": "^0.10.6", + "govuk-colours": "^1.1.0" }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" + "peerDependencies": { + "react": ">=16.8.0", + "styled-components": ">=5.1" } }, - "node_modules/supports-preserve-symlinks-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "dev": true, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node_modules/@govuk-react/constants": { + "version": "0.10.6", + "resolved": "https://registry.npmjs.org/@govuk-react/constants/-/constants-0.10.6.tgz", + "integrity": "sha512-gXwMnkoWVihOecqzFLSsmov1imQvhsip2hTN49Jnh8mSPd86ltGrndFREQP8WluC/MnOt8dcuaHsuDbYAdtpuA==", + "dependencies": { + "govuk-colours": "^1.1.0" } }, - "node_modules/tapable": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", - "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", - "engines": { - "node": ">=6" + "node_modules/@govuk-react/date-field": { + "version": "0.10.6", + "resolved": "https://registry.npmjs.org/@govuk-react/date-field/-/date-field-0.10.6.tgz", + "integrity": "sha512-8sotTaYdlPT/Dj1in/eDb2g1GMdnrxaBo5XPo8vt2HpS1eBy3Fxts1gO5qjcmkgnXYppbQl0zkcxhHDEAIyAdw==", + "dependencies": { + "@govuk-react/constants": "^0.10.6", + "@govuk-react/error-text": "^0.10.6", + "@govuk-react/hint-text": "^0.10.6", + "@govuk-react/input": "^0.10.6", + "@govuk-react/label": "^0.10.6", + "@govuk-react/label-text": "^0.10.6", + "@govuk-react/lib": "^0.10.6", + "govuk-colours": "^1.1.0", + "multi-input-input": "0.0.3" + }, + "peerDependencies": { + "react": ">=16.8.0", + "styled-components": ">=5.1" } }, - "node_modules/terser": { - "version": "5.15.1", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.15.1.tgz", - "integrity": "sha512-K1faMUvpm/FBxjBXud0LWVAGxmvoPbZbfTCYbSgaaYQaIXI3/TdI7a7ZGA73Zrou6Q8Zmz3oeUTsp/dj+ag2Xw==", + "node_modules/@govuk-react/details": { + "version": "0.10.6", + "resolved": "https://registry.npmjs.org/@govuk-react/details/-/details-0.10.6.tgz", + "integrity": "sha512-p5D48HLGSMwmscaT7giQHujd2yQ+JFs6f/6Za7lbcAnLn6DMtaav8Tdc2cYXInC/qt7aeEcbKGWtg50GC6fZJw==", "dependencies": { - "@jridgewell/source-map": "^0.3.2", - "acorn": "^8.5.0", - "commander": "^2.20.0", - "source-map-support": "~0.5.20" + "@govuk-react/constants": "^0.10.6", + "@govuk-react/lib": "^0.10.6", + "govuk-colours": "^1.1.0", + "polished": "^4.1.2" }, - "bin": { - "terser": "bin/terser" - }, - "engines": { - "node": ">=10" + "peerDependencies": { + "react": ">=15", + "styled-components": ">=5.1" } }, - "node_modules/terser-webpack-plugin": { - "version": "5.3.6", - "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.6.tgz", - "integrity": "sha512-kfLFk+PoLUQIbLmB1+PZDMRSZS99Mp+/MHqDNmMA6tOItzRt+Npe3E+fsMs5mfcM0wCtrrdU387UnV+vnSffXQ==", + "node_modules/@govuk-react/document-footer-metadata": { + "version": "0.10.6", + "resolved": "https://registry.npmjs.org/@govuk-react/document-footer-metadata/-/document-footer-metadata-0.10.6.tgz", + "integrity": "sha512-tc0EMcdaWBmZFqHzxg/BEYBMqhBX3ulN46JAv/o6oqXrwoaaqdU6IlrwIULR0jHGxSroZ/muFuao4omp1YpopQ==", "dependencies": { - "@jridgewell/trace-mapping": "^0.3.14", - "jest-worker": "^27.4.5", - "schema-utils": "^3.1.1", - "serialize-javascript": "^6.0.0", - "terser": "^5.14.1" - }, - "engines": { - "node": ">= 10.13.0" + "@govuk-react/constants": "^0.10.6", + "@govuk-react/lib": "^0.10.6", + "@govuk-react/unordered-list": "^0.10.6" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" + "peerDependencies": { + "react": ">=16.8.0", + "styled-components": ">=5.1" + } + }, + "node_modules/@govuk-react/error-summary": { + "version": "0.10.6", + "resolved": "https://registry.npmjs.org/@govuk-react/error-summary/-/error-summary-0.10.6.tgz", + "integrity": "sha512-wBVYgdzGH0Bvq4ddFiKCo6I4+48H0igw1vEkDfj5Yyw8skvKXrCGbELkFAPYgE7A7vD9Rbkcs12YO3zcy+U6cQ==", + "dependencies": { + "@govuk-react/constants": "^0.10.6", + "@govuk-react/heading": "^0.10.6", + "@govuk-react/input-field": "^0.10.6", + "@govuk-react/lib": "^0.10.6", + "@govuk-react/link": "^0.10.6", + "@govuk-react/list-item": "^0.10.6", + "@govuk-react/paragraph": "^0.10.6", + "@govuk-react/text-area": "^0.10.6", + "@govuk-react/unordered-list": "^0.10.6", + "govuk-colours": "^1.1.0" }, "peerDependencies": { - "webpack": "^5.1.0" + "react": ">=16.8.0", + "styled-components": ">=5.1" + } + }, + "node_modules/@govuk-react/error-text": { + "version": "0.10.6", + "resolved": "https://registry.npmjs.org/@govuk-react/error-text/-/error-text-0.10.6.tgz", + "integrity": "sha512-nGA09zO4Km024uK2U0qemXJ0fWI0DNPGTiS07SXmXBjBW3hUJetO5jy7bgTA8Hz8EDgvPFA7s7fZ3xe6CaPFng==", + "dependencies": { + "@govuk-react/constants": "^0.10.6", + "@govuk-react/lib": "^0.10.6", + "govuk-colours": "^1.1.0" }, - "peerDependenciesMeta": { - "@swc/core": { - "optional": true - }, - "esbuild": { - "optional": true - }, - "uglify-js": { - "optional": true - } + "peerDependencies": { + "react": ">=16.8.0", + "styled-components": ">=5.1" } }, - "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==", + "node_modules/@govuk-react/fieldset": { + "version": "0.10.6", + "resolved": "https://registry.npmjs.org/@govuk-react/fieldset/-/fieldset-0.10.6.tgz", + "integrity": "sha512-RcALeKkwSKXSTE0UGKpAVT3Y/wgayozeqcznjMYqhj1NTf8mEtZEv3Nxa6PG9uNsaqiNxTLqPK8KuUyESK4naA==", "dependencies": { - "is-number": "^7.0.0" + "@govuk-react/constants": "^0.10.6", + "@govuk-react/lib": "^0.10.6", + "govuk-colours": "^1.1.0" }, - "engines": { - "node": ">=8.0" + "peerDependencies": { + "react": ">=16.8.0", + "styled-components": ">=5.1" } }, - "node_modules/update-browserslist-db": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.10.tgz", - "integrity": "sha512-OztqDenkfFkbSG+tRxBeAnCVPckDBcvibKd35yDONx6OU8N7sqgwc7rCbkJ/WcYtVRZ4ba68d6byhC21GFh7sQ==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - } - ], + "node_modules/@govuk-react/file-upload": { + "version": "0.10.6", + "resolved": "https://registry.npmjs.org/@govuk-react/file-upload/-/file-upload-0.10.6.tgz", + "integrity": "sha512-woyPfoG8JS4NInlLld3WIUO47QTJKX0i/hz6LNK5bSNH92D4JvATbA8SNm8rSqaGunjUZD/8UFO8WBT/MtZxRw==", "dependencies": { - "escalade": "^3.1.1", - "picocolors": "^1.0.0" + "@govuk-react/constants": "^0.10.6", + "@govuk-react/error-text": "^0.10.6", + "@govuk-react/hint-text": "^0.10.6", + "@govuk-react/label": "^0.10.6", + "@govuk-react/label-text": "^0.10.6" }, - "bin": { - "browserslist-lint": "cli.js" + "peerDependencies": { + "react": ">=16.8.0", + "styled-components": ">=5.1" + } + }, + "node_modules/@govuk-react/footer": { + "version": "0.10.6", + "resolved": "https://registry.npmjs.org/@govuk-react/footer/-/footer-0.10.6.tgz", + "integrity": "sha512-EB6qaJvaYKogilPADlF7/KLMQPLxonEVbOKEJCqboyavyClKt8FV+/2ZTi42nsDa6ObE2/+UxWpEe/MlgPTrfw==", + "dependencies": { + "@govuk-react/constants": "^0.10.6", + "@govuk-react/heading": "^0.10.6", + "@govuk-react/icons": "^0.10.6", + "@govuk-react/lib": "^0.10.6", + "@govuk-react/link": "^0.10.6", + "@govuk-react/visually-hidden": "^0.10.6", + "govuk-colours": "^1.1.0" }, "peerDependencies": { - "browserslist": ">= 4.21.0" + "react": ">=16.8.0", + "styled-components": ">=5.1" } }, - "node_modules/uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "node_modules/@govuk-react/form-group": { + "version": "0.10.6", + "resolved": "https://registry.npmjs.org/@govuk-react/form-group/-/form-group-0.10.6.tgz", + "integrity": "sha512-cx536tpKdxIIkGXIc0NcQRqxKSvhT0Ygy2F3/6JRE22tQ3J18JUlb48a2IqCLsY19Hu7cijcEfX5P1+23knJ0g==", "dependencies": { - "punycode": "^2.1.0" + "@govuk-react/constants": "^0.10.6", + "@govuk-react/lib": "^0.10.6", + "govuk-colours": "^1.1.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "styled-components": ">=5.1" } }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" + "node_modules/@govuk-react/global-style": { + "version": "0.10.6", + "resolved": "https://registry.npmjs.org/@govuk-react/global-style/-/global-style-0.10.6.tgz", + "integrity": "sha512-KvGtA58kms+poH6vc5JJDiNFWAabm8SkxpAIemI2QfjxKPJEroih0jQg3Yi+BqvOCrfvSFyd9fHSYFiiCX5e2Q==", + "peerDependencies": { + "react": ">=16.8.0", + "styled-components": ">=5.1" + } }, - "node_modules/watchpack": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz", - "integrity": "sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==", + "node_modules/@govuk-react/grid-col": { + "version": "0.10.6", + "resolved": "https://registry.npmjs.org/@govuk-react/grid-col/-/grid-col-0.10.6.tgz", + "integrity": "sha512-9hrpjxSG2Zda5vZumeX/lZpIMYM1Q5Xf1/GmhCU+Cforfc4F733f5w9hOpzr4S+FGOaXmri7dwcDLTjclyrOlw==", "dependencies": { - "glob-to-regexp": "^0.4.1", - "graceful-fs": "^4.1.2" + "@govuk-react/constants": "^0.10.6", + "@govuk-react/lib": "^0.10.6" }, - "engines": { - "node": ">=10.13.0" + "peerDependencies": { + "react": ">=16.8.0", + "styled-components": ">=5.1" } }, - "node_modules/webpack": { - "version": "5.76.0", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.76.0.tgz", - "integrity": "sha512-l5sOdYBDunyf72HW8dF23rFtWq/7Zgvt/9ftMof71E/yUb1YLOBmTgA2K4vQthB3kotMrSj609txVE0dnr2fjA==", + "node_modules/@govuk-react/grid-row": { + "version": "0.10.6", + "resolved": "https://registry.npmjs.org/@govuk-react/grid-row/-/grid-row-0.10.6.tgz", + "integrity": "sha512-2mn/Xunz7q443rfwwyScKI4GcqafpebhSKRD03QcAij6Ys2aIuJ9MU85OPSF3qG5t2IZoN3sUNIx0l0AXUoAOQ==", "dependencies": { - "@types/eslint-scope": "^3.7.3", - "@types/estree": "^0.0.51", - "@webassemblyjs/ast": "1.11.1", - "@webassemblyjs/wasm-edit": "1.11.1", - "@webassemblyjs/wasm-parser": "1.11.1", - "acorn": "^8.7.1", - "acorn-import-assertions": "^1.7.6", - "browserslist": "^4.14.5", - "chrome-trace-event": "^1.0.2", - "enhanced-resolve": "^5.10.0", - "es-module-lexer": "^0.9.0", - "eslint-scope": "5.1.1", - "events": "^3.2.0", - "glob-to-regexp": "^0.4.1", - "graceful-fs": "^4.2.9", - "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.1.0", - "tapable": "^2.1.1", - "terser-webpack-plugin": "^5.1.3", - "watchpack": "^2.4.0", - "webpack-sources": "^3.2.3" + "@govuk-react/constants": "^0.10.6", + "@govuk-react/lib": "^0.10.6" }, - "bin": { - "webpack": "bin/webpack.js" - }, - "engines": { - "node": ">=10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependenciesMeta": { - "webpack-cli": { - "optional": true - } + "peerDependencies": { + "react": ">=16.8.0", + "styled-components": ">=5.1" } }, - "node_modules/webpack-bundle-tracker": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/webpack-bundle-tracker/-/webpack-bundle-tracker-1.7.0.tgz", - "integrity": "sha512-CwdFpeLcc4uBurgmtszCHW6ISJ5RN70jvGWnvUG/7LQS1gmv2g6IdYw9A8DvT4rydHzWnRFwqVsx1hN1IebkQA==", + "node_modules/@govuk-react/heading": { + "version": "0.10.6", + "resolved": "https://registry.npmjs.org/@govuk-react/heading/-/heading-0.10.6.tgz", + "integrity": "sha512-x/79I3b9xyicmIGWOMO5jatWxhAIqOGpdYPEAzwPWZgpZQ/AtmQMM4/09o3voQNzvc4j8UwX24pYTX/C1/yPXg==", "dependencies": { - "lodash.assign": "^4.2.0", - "lodash.defaults": "^4.2.0", - "lodash.foreach": "^4.5.0", - "lodash.frompairs": "^4.0.1", - "lodash.get": "^4.4.2", - "lodash.topairs": "^4.3.0", - "strip-ansi": "^6.0.0" + "@govuk-react/constants": "^0.10.6", + "@govuk-react/lib": "^0.10.6" + }, + "peerDependencies": { + "react": ">=16.8.0", + "styled-components": ">=5.1" } }, - "node_modules/webpack-cli": { - "version": "4.10.0", - "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-4.10.0.tgz", - "integrity": "sha512-NLhDfH/h4O6UOy+0LSso42xvYypClINuMNBVVzX4vX98TmTaTUxwRbXdhucbFMd2qLaCTcLq/PdYrvi8onw90w==", - "dev": true, + "node_modules/@govuk-react/hint-text": { + "version": "0.10.6", + "resolved": "https://registry.npmjs.org/@govuk-react/hint-text/-/hint-text-0.10.6.tgz", + "integrity": "sha512-IXXDsqp13x8sdT4b9A1RcZu2gI0Lej8IshUEnOSXY1oV7aUNUlqcFkIfbvgQg9P6BFJbdVC4Ure4SlFxXrnbjg==", "dependencies": { - "@discoveryjs/json-ext": "^0.5.0", - "@webpack-cli/configtest": "^1.2.0", - "@webpack-cli/info": "^1.5.0", - "@webpack-cli/serve": "^1.7.0", - "colorette": "^2.0.14", - "commander": "^7.0.0", - "cross-spawn": "^7.0.3", - "fastest-levenshtein": "^1.0.12", - "import-local": "^3.0.2", - "interpret": "^2.2.0", - "rechoir": "^0.7.0", - "webpack-merge": "^5.7.3" + "@govuk-react/constants": "^0.10.6", + "@govuk-react/lib": "^0.10.6", + "govuk-colours": "^1.1.0" }, - "bin": { - "webpack-cli": "bin/cli.js" + "peerDependencies": { + "react": ">=16.8.0", + "styled-components": ">=5.1" + } + }, + "node_modules/@govuk-react/icon-crown": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/@govuk-react/icon-crown/-/icon-crown-0.0.8.tgz", + "integrity": "sha512-Jz/KHDwPfEj2t8owdtEigjRSzaBxjNTUPXtljAdoaACRJ/RUXTsudAZOGcvpndOiT1zd7Z/l+B5vHsvqPNfHMQ==", + "peerDependencies": { + "react": ">=16.2.0" + } + }, + "node_modules/@govuk-react/icons": { + "version": "0.10.6", + "resolved": "https://registry.npmjs.org/@govuk-react/icons/-/icons-0.10.6.tgz", + "integrity": "sha512-7pY5EG0ot/3wttHQaJjbji7k44woNsa9vI1bvorDulEpv9DzXs2O4fx6EnVQHoDFf40CLoP1yamQwVbsKdhz+A==", + "dependencies": { + "@govuk-react/constants": "^0.10.6", + "govuk-colours": "^1.1.0" }, - "engines": { - "node": ">=10.13.0" + "peerDependencies": { + "react": ">=16.8.0", + "styled-components": ">=5.1" + } + }, + "node_modules/@govuk-react/input": { + "version": "0.10.6", + "resolved": "https://registry.npmjs.org/@govuk-react/input/-/input-0.10.6.tgz", + "integrity": "sha512-VqHSv2zaeYQRyn7H25ySr2Wy1OhdprHwPA2rCnIi1pxa6qCXMnTbMGjibgJosI6XXLhHxx9gXovoKhg0jMi2WA==", + "dependencies": { + "@govuk-react/constants": "^0.10.6", + "@govuk-react/lib": "^0.10.6", + "govuk-colours": "^1.1.0" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" + "peerDependencies": { + "react": ">=16.8.0", + "styled-components": ">=5.1" + } + }, + "node_modules/@govuk-react/input-field": { + "version": "0.10.6", + "resolved": "https://registry.npmjs.org/@govuk-react/input-field/-/input-field-0.10.6.tgz", + "integrity": "sha512-FUXEbLhYCbUMHs9A8BFbEt/knl1wDxAw7t1D2LwJO8dL5IPoxac8PRCWq14T9r5XyGnPemEgJgVdW0Hc5mulAQ==", + "dependencies": { + "@govuk-react/constants": "^0.10.6", + "@govuk-react/error-text": "^0.10.6", + "@govuk-react/hint-text": "^0.10.6", + "@govuk-react/input": "^0.10.6", + "@govuk-react/label": "^0.10.6", + "@govuk-react/label-text": "^0.10.6" }, "peerDependencies": { - "webpack": "4.x.x || 5.x.x" + "react": ">=16.8.0", + "styled-components": ">=5.1" + } + }, + "node_modules/@govuk-react/inset-text": { + "version": "0.10.6", + "resolved": "https://registry.npmjs.org/@govuk-react/inset-text/-/inset-text-0.10.6.tgz", + "integrity": "sha512-rdcrcW+qpwbYv6tTvQOlFSRir2CODZJ8q13zVeZYzroUik8czZsuhgmU3JhaAVnfxPrlVmsX3DuFvnvnMUy3Sg==", + "dependencies": { + "@govuk-react/constants": "^0.10.6", + "@govuk-react/lib": "^0.10.6", + "govuk-colours": "^1.1.0" }, - "peerDependenciesMeta": { - "@webpack-cli/generators": { - "optional": true - }, - "@webpack-cli/migrate": { - "optional": true - }, - "webpack-bundle-analyzer": { - "optional": true - }, - "webpack-dev-server": { - "optional": true - } + "peerDependencies": { + "react": ">=16.8.0", + "styled-components": ">=5.1" } }, - "node_modules/webpack-cli/node_modules/commander": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", - "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", - "dev": true, - "engines": { - "node": ">= 10" + "node_modules/@govuk-react/label": { + "version": "0.10.6", + "resolved": "https://registry.npmjs.org/@govuk-react/label/-/label-0.10.6.tgz", + "integrity": "sha512-ia9qjQ0XLiB8BRNTsUN+v7XVrICA6e4+wNVcD0903B/SIn9uC26CMQADQ4LtTfUNZBEqxckrADdgg10nf/0roQ==", + "dependencies": { + "@govuk-react/constants": "^0.10.6", + "@govuk-react/lib": "^0.10.6", + "govuk-colours": "^1.1.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "styled-components": ">=5.1" } }, - "node_modules/webpack-merge": { - "version": "5.8.0", - "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.8.0.tgz", - "integrity": "sha512-/SaI7xY0831XwP6kzuwhKWVKDP9t1QY1h65lAFLbZqMPIuYcD9QAW4u9STIbU9kaJbPBB/geU/gLr1wDjOhQ+Q==", - "dev": true, + "node_modules/@govuk-react/label-text": { + "version": "0.10.6", + "resolved": "https://registry.npmjs.org/@govuk-react/label-text/-/label-text-0.10.6.tgz", + "integrity": "sha512-EWHTgmsOKY8aYgU3/2O+kRKRMvQEAiuot8jFDnVdgLw9J19S7Pi75owluCFimtc4bz70c7oDlc9Lp+gNhWsP3Q==", "dependencies": { - "clone-deep": "^4.0.1", - "wildcard": "^2.0.0" + "@govuk-react/lib": "^0.10.6" }, - "engines": { - "node": ">=10.0.0" + "peerDependencies": { + "react": ">=16.8.0", + "styled-components": ">=5.1" } }, - "node_modules/webpack-sources": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-1.4.3.tgz", - "integrity": "sha512-lgTS3Xhv1lCOKo7SA5TjKXMjpSM4sBjNV5+q2bqesbSPs5FjGmU6jjtBSkX9b4qW87vDIsCIlUPOEhbZrMdjeQ==", + "node_modules/@govuk-react/lead-paragraph": { + "version": "0.10.6", + "resolved": "https://registry.npmjs.org/@govuk-react/lead-paragraph/-/lead-paragraph-0.10.6.tgz", + "integrity": "sha512-+r8DIc3pBpIRMM4Ms6DnO/CdmPdhskenjogC/RsTYJsXr0PjfWnF0mLU5kBOVQhsv82Ycx12b9stmbKPui65rA==", "dependencies": { - "source-list-map": "^2.0.0", - "source-map": "~0.6.1" + "@govuk-react/lib": "^0.10.6" + }, + "peerDependencies": { + "react": ">=16.8.0", + "styled-components": ">=5.1" } }, - "node_modules/webpack/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==", - "engines": { - "node": ">=10.13.0" + "node_modules/@govuk-react/lib": { + "version": "0.10.6", + "resolved": "https://registry.npmjs.org/@govuk-react/lib/-/lib-0.10.6.tgz", + "integrity": "sha512-nC0dBnVpcEccZUnNaWt3DkLz4Q/tU7NypAZi3A4f6QJYrBHTpOZEDj7DCSi7ZXPMOiMuR+CcWhBqUtYNkvfcFw==", + "dependencies": { + "@govuk-react/constants": "^0.10.6", + "govuk-colours": "^1.1.0" } }, - "node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, + "node_modules/@govuk-react/link": { + "version": "0.10.6", + "resolved": "https://registry.npmjs.org/@govuk-react/link/-/link-0.10.6.tgz", + "integrity": "sha512-c6K3/surYuIdxjkVmnw/47hRkpuhzIgzXlzG36XsnhzXpdrOn5mK8HmZag7Pehib6HrEXzrMFSzQ5hntRwZ75Q==", "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" + "@govuk-react/lib": "^0.10.6", + "govuk-colours": "^1.1.0" }, - "engines": { - "node": ">= 8" + "peerDependencies": { + "react": ">=16.8.0", + "styled-components": ">=5.1" } }, - "node_modules/wildcard": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.0.tgz", - "integrity": "sha512-JcKqAHLPxcdb9KM49dufGXn2x3ssnfjbcaQdLlfZsL9rH9wgDQjUtDxbo8NE0F6SFvydeu1VhZe7hZuHsB2/pw==", - "dev": true - }, - "node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" - } - }, - "dependencies": { - "@discoveryjs/json-ext": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz", - "integrity": "sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==", - "dev": true - }, - "@jridgewell/gen-mapping": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz", - "integrity": "sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==", - "requires": { - "@jridgewell/set-array": "^1.0.1", - "@jridgewell/sourcemap-codec": "^1.4.10", - "@jridgewell/trace-mapping": "^0.3.9" + "node_modules/@govuk-react/list-item": { + "version": "0.10.6", + "resolved": "https://registry.npmjs.org/@govuk-react/list-item/-/list-item-0.10.6.tgz", + "integrity": "sha512-fvqHv6Z2HGwoQAMdBePThi0M4sL95PfZn6y2PQ1wn6t02uDD2XW4aUon/NAof9YkO8nD6U9+9wvb73vAT54vmA==", + "dependencies": { + "@govuk-react/constants": "^0.10.6", + "@govuk-react/lib": "^0.10.6" + }, + "peerDependencies": { + "react": ">=16.8.0", + "styled-components": ">=5.1" } }, - "@jridgewell/resolve-uri": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", - "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==" - }, - "@jridgewell/set-array": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", - "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==" - }, - "@jridgewell/source-map": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.2.tgz", - "integrity": "sha512-m7O9o2uR8k2ObDysZYzdfhb08VuEml5oWGiosa1VdaPZ/A6QyPkAJuwN0Q1lhULOf6B7MtQmHENS743hWtCrgw==", - "requires": { - "@jridgewell/gen-mapping": "^0.3.0", - "@jridgewell/trace-mapping": "^0.3.9" + "node_modules/@govuk-react/loading-box": { + "version": "0.10.6", + "resolved": "https://registry.npmjs.org/@govuk-react/loading-box/-/loading-box-0.10.6.tgz", + "integrity": "sha512-I1iqTINfgc2VZxdtdTvPruPgEr7mN+iZEb6qxeOjSSIXoLWQjGkYE0EV4DNF24kLAeuAneY9uszIURKqDcALnw==", + "dependencies": { + "@govuk-react/constants": "^0.10.6", + "@govuk-react/icons": "^0.10.6", + "govuk-colours": "^1.1.0", + "hex-rgb": "^4.0.0", + "react-transition-group": "^4.4.5" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0", + "styled-components": ">=5.1" } }, - "@jridgewell/sourcemap-codec": { - "version": "1.4.14", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", - "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==" - }, - "@jridgewell/trace-mapping": { - "version": "0.3.17", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.17.tgz", - "integrity": "sha512-MCNzAp77qzKca9+W/+I0+sEpaUnZoeasnghNeVc41VZCEKaCH73Vq3BZZ/SzWIgrqE4H4ceI+p+b6C0mHf9T4g==", - "requires": { - "@jridgewell/resolve-uri": "3.1.0", - "@jridgewell/sourcemap-codec": "1.4.14" + "node_modules/@govuk-react/main": { + "version": "0.10.6", + "resolved": "https://registry.npmjs.org/@govuk-react/main/-/main-0.10.6.tgz", + "integrity": "sha512-ZgH7SwdddE7gGZ9QhPBxRcWtow5YDb2HQu5XibH6FKZWaQTaSxbHDnwUvTRe+1LFLp9xGhv70jnyyzW5b6NoAA==", + "dependencies": { + "@govuk-react/constants": "^0.10.6" + }, + "peerDependencies": { + "react": ">=16.8.0", + "styled-components": ">=5.1" } }, - "@types/eslint": { - "version": "8.4.10", - "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.4.10.tgz", - "integrity": "sha512-Sl/HOqN8NKPmhWo2VBEPm0nvHnu2LL3v9vKo8MEq0EtbJ4eVzGPl41VNPvn5E1i5poMk4/XD8UriLHpJvEP/Nw==", - "requires": { - "@types/estree": "*", - "@types/json-schema": "*" + "node_modules/@govuk-react/multi-choice": { + "version": "0.10.6", + "resolved": "https://registry.npmjs.org/@govuk-react/multi-choice/-/multi-choice-0.10.6.tgz", + "integrity": "sha512-y3whM8MSmuyIqIAPRjkWyGuM17RUlwaY1s9xbi1uGYxZ+L/LcPKOqEc3D2GusIRS63gf1WAa84LoIuVYSg9CjA==", + "dependencies": { + "@govuk-react/constants": "^0.10.6", + "@govuk-react/error-text": "^0.10.6", + "@govuk-react/hint-text": "^0.10.6", + "@govuk-react/label-text": "^0.10.6", + "@govuk-react/lib": "^0.10.6", + "govuk-colours": "^1.1.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "styled-components": ">=5.1" } }, - "@types/eslint-scope": { - "version": "3.7.4", - "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.4.tgz", - "integrity": "sha512-9K4zoImiZc3HlIp6AVUDE4CWYx22a+lhSZMYNpbjW04+YF0KWj4pJXnEMjdnFTiQibFFmElcsasJXDbdI/EPhA==", - "requires": { - "@types/eslint": "*", - "@types/estree": "*" + "node_modules/@govuk-react/ordered-list": { + "version": "0.10.6", + "resolved": "https://registry.npmjs.org/@govuk-react/ordered-list/-/ordered-list-0.10.6.tgz", + "integrity": "sha512-+1f2ZoJJhKKZ/4viQoXku0DGJyN96REFbmSBZjclOhsrExTJ2NPVBwtT4mLicjlquND7MDV3t8gyKYQ5OHmBiw==", + "dependencies": { + "@govuk-react/constants": "^0.10.6", + "@govuk-react/lib": "^0.10.6", + "@govuk-react/list-item": "^0.10.6" + }, + "peerDependencies": { + "react": ">=16.8.0", + "styled-components": ">=5.1" } }, - "@types/estree": { - "version": "0.0.51", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.51.tgz", - "integrity": "sha512-CuPgU6f3eT/XgKKPqKd/gLZV1Xmvf1a2R5POBOGQa6uv82xpls89HU5zKeVoyR8XzHd1RGNOlQlvUe3CFkjWNQ==" - }, - "@types/json-schema": { - "version": "7.0.11", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz", - "integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==" - }, - "@types/node": { - "version": "18.11.9", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.9.tgz", - "integrity": "sha512-CRpX21/kGdzjOpFsZSkcrXMGIBWMGNIHXXBVFSH+ggkftxg+XYP20TESbh+zFvFj3EQOl5byk0HTRn1IL6hbqg==" - }, - "@webassemblyjs/ast": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.1.tgz", - "integrity": "sha512-ukBh14qFLjxTQNTXocdyksN5QdM28S1CxHt2rdskFyL+xFV7VremuBLVbmCePj+URalXBENx/9Lm7lnhihtCSw==", - "requires": { - "@webassemblyjs/helper-numbers": "1.11.1", - "@webassemblyjs/helper-wasm-bytecode": "1.11.1" - } - }, - "@webassemblyjs/floating-point-hex-parser": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.1.tgz", - "integrity": "sha512-iGRfyc5Bq+NnNuX8b5hwBrRjzf0ocrJPI6GWFodBFzmFnyvrQ83SHKhmilCU/8Jv67i4GJZBMhEzltxzcNagtQ==" - }, - "@webassemblyjs/helper-api-error": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.1.tgz", - "integrity": "sha512-RlhS8CBCXfRUR/cwo2ho9bkheSXG0+NwooXcc3PAILALf2QLdFyj7KGsKRbVc95hZnhnERon4kW/D3SZpp6Tcg==" - }, - "@webassemblyjs/helper-buffer": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.1.tgz", - "integrity": "sha512-gwikF65aDNeeXa8JxXa2BAk+REjSyhrNC9ZwdT0f8jc4dQQeDQ7G4m0f2QCLPJiMTTO6wfDmRmj/pW0PsUvIcA==" - }, - "@webassemblyjs/helper-numbers": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.1.tgz", - "integrity": "sha512-vDkbxiB8zfnPdNK9Rajcey5C0w+QJugEglN0of+kmO8l7lDb77AnlKYQF7aarZuCrv+l0UvqL+68gSDr3k9LPQ==", - "requires": { - "@webassemblyjs/floating-point-hex-parser": "1.11.1", - "@webassemblyjs/helper-api-error": "1.11.1", - "@xtuc/long": "4.2.2" + "node_modules/@govuk-react/page": { + "version": "0.10.6", + "resolved": "https://registry.npmjs.org/@govuk-react/page/-/page-0.10.6.tgz", + "integrity": "sha512-tgLbbNuAzNniAUqfD4Iy7oQYuI3BgMCcsXld1ClOYCnJSZfLsQx+qwxfFhKaQaVX1I9oDeEGLrv7FYAkM+MH/A==", + "dependencies": { + "@govuk-react/constants": "^0.10.6", + "@govuk-react/lib": "^0.10.6", + "@govuk-react/skip-link": "^0.10.6", + "@govuk-react/top-nav": "^0.10.6" + }, + "peerDependencies": { + "react": ">=16.8.0", + "styled-components": ">=5.1" } }, - "@webassemblyjs/helper-wasm-bytecode": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.1.tgz", - "integrity": "sha512-PvpoOGiJwXeTrSf/qfudJhwlvDQxFgelbMqtq52WWiXC6Xgg1IREdngmPN3bs4RoO83PnL/nFrxucXj1+BX62Q==" - }, - "@webassemblyjs/helper-wasm-section": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.1.tgz", - "integrity": "sha512-10P9No29rYX1j7F3EVPX3JvGPQPae+AomuSTPiF9eBQeChHI6iqjMIwR9JmOJXwpnn/oVGDk7I5IlskuMwU/pg==", - "requires": { - "@webassemblyjs/ast": "1.11.1", - "@webassemblyjs/helper-buffer": "1.11.1", - "@webassemblyjs/helper-wasm-bytecode": "1.11.1", - "@webassemblyjs/wasm-gen": "1.11.1" + "node_modules/@govuk-react/pagination": { + "version": "0.10.6", + "resolved": "https://registry.npmjs.org/@govuk-react/pagination/-/pagination-0.10.6.tgz", + "integrity": "sha512-zynjmXGXbGLxD5/XtyJy6FTuWhDAFtMKXYdtZOHoMvnflZnQEy6ce5u7vSj3uKxGL2k/xPGgkXMRQQCU/k4S8A==", + "dependencies": { + "@govuk-react/constants": "^0.10.6", + "@govuk-react/icons": "^0.10.6", + "@govuk-react/lib": "^0.10.6", + "govuk-colours": "^1.1.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "styled-components": ">=5.1" } }, - "@webassemblyjs/ieee754": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.11.1.tgz", - "integrity": "sha512-hJ87QIPtAMKbFq6CGTkZYJivEwZDbQUgYd3qKSadTNOhVY7p+gfP6Sr0lLRVTaG1JjFj+r3YchoqRYxNH3M0GQ==", - "requires": { - "@xtuc/ieee754": "^1.2.0" + "node_modules/@govuk-react/panel": { + "version": "0.10.6", + "resolved": "https://registry.npmjs.org/@govuk-react/panel/-/panel-0.10.6.tgz", + "integrity": "sha512-IzN4WhU+QI1MrS3vF0p0sVQ/sBxkBsnbO3IoCONjDKlfg5/LYWW60LpR6VyAN9UVUXKo3MaOm0iK+Kz0vXOSGQ==", + "dependencies": { + "@govuk-react/constants": "^0.10.6", + "@govuk-react/lib": "^0.10.6", + "govuk-colours": "^1.1.0", + "polished": "^4.1.2" + }, + "peerDependencies": { + "react": ">=16.8.0", + "styled-components": ">=5.1" } }, - "@webassemblyjs/leb128": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.11.1.tgz", - "integrity": "sha512-BJ2P0hNZ0u+Th1YZXJpzW6miwqQUGcIHT1G/sf72gLVD9DZ5AdYTqPNbHZh6K1M5VmKvFXwGSWZADz+qBWxeRw==", - "requires": { - "@xtuc/long": "4.2.2" + "node_modules/@govuk-react/paragraph": { + "version": "0.10.6", + "resolved": "https://registry.npmjs.org/@govuk-react/paragraph/-/paragraph-0.10.6.tgz", + "integrity": "sha512-kvoNWsAqIg1VvysVpO1jZbZMelTLYEja7qtnXdCXpswtZQwqyEmccnkmmDU8Cc4+t5chDRyYOU/FOCBy449CfQ==", + "dependencies": { + "@govuk-react/lib": "^0.10.6", + "@govuk-react/link": "^0.10.6", + "react-markdown": "^5.0.3" + }, + "peerDependencies": { + "react": ">=16.8.0", + "styled-components": ">=5.1" } }, - "@webassemblyjs/utf8": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.11.1.tgz", - "integrity": "sha512-9kqcxAEdMhiwQkHpkNiorZzqpGrodQQ2IGrHHxCy+Ozng0ofyMA0lTqiLkVs1uzTRejX+/O0EOT7KxqVPuXosQ==" - }, - "@webassemblyjs/wasm-edit": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.1.tgz", - "integrity": "sha512-g+RsupUC1aTHfR8CDgnsVRVZFJqdkFHpsHMfJuWQzWU3tvnLC07UqHICfP+4XyL2tnr1amvl1Sdp06TnYCmVkA==", - "requires": { - "@webassemblyjs/ast": "1.11.1", - "@webassemblyjs/helper-buffer": "1.11.1", - "@webassemblyjs/helper-wasm-bytecode": "1.11.1", - "@webassemblyjs/helper-wasm-section": "1.11.1", - "@webassemblyjs/wasm-gen": "1.11.1", - "@webassemblyjs/wasm-opt": "1.11.1", - "@webassemblyjs/wasm-parser": "1.11.1", - "@webassemblyjs/wast-printer": "1.11.1" - } - }, - "@webassemblyjs/wasm-gen": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.1.tgz", - "integrity": "sha512-F7QqKXwwNlMmsulj6+O7r4mmtAlCWfO/0HdgOxSklZfQcDu0TpLiD1mRt/zF25Bk59FIjEuGAIyn5ei4yMfLhA==", - "requires": { - "@webassemblyjs/ast": "1.11.1", - "@webassemblyjs/helper-wasm-bytecode": "1.11.1", - "@webassemblyjs/ieee754": "1.11.1", - "@webassemblyjs/leb128": "1.11.1", - "@webassemblyjs/utf8": "1.11.1" - } - }, - "@webassemblyjs/wasm-opt": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.1.tgz", - "integrity": "sha512-VqnkNqnZlU5EB64pp1l7hdm3hmQw7Vgqa0KF/KCNO9sIpI6Fk6brDEiX+iCOYrvMuBWDws0NkTOxYEb85XQHHw==", - "requires": { - "@webassemblyjs/ast": "1.11.1", - "@webassemblyjs/helper-buffer": "1.11.1", - "@webassemblyjs/wasm-gen": "1.11.1", - "@webassemblyjs/wasm-parser": "1.11.1" - } - }, - "@webassemblyjs/wasm-parser": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.1.tgz", - "integrity": "sha512-rrBujw+dJu32gYB7/Lup6UhdkPx9S9SnobZzRVL7VcBH9Bt9bCBLEuX/YXOOtBsOZ4NQrRykKhffRWHvigQvOA==", - "requires": { - "@webassemblyjs/ast": "1.11.1", - "@webassemblyjs/helper-api-error": "1.11.1", - "@webassemblyjs/helper-wasm-bytecode": "1.11.1", - "@webassemblyjs/ieee754": "1.11.1", - "@webassemblyjs/leb128": "1.11.1", - "@webassemblyjs/utf8": "1.11.1" - } - }, - "@webassemblyjs/wast-printer": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.11.1.tgz", - "integrity": "sha512-IQboUWM4eKzWW+N/jij2sRatKMh99QEelo3Eb2q0qXkvPRISAj8Qxtmw5itwqK+TTkBuUIE45AxYPToqPtL5gg==", - "requires": { - "@webassemblyjs/ast": "1.11.1", - "@xtuc/long": "4.2.2" + "node_modules/@govuk-react/phase-banner": { + "version": "0.10.6", + "resolved": "https://registry.npmjs.org/@govuk-react/phase-banner/-/phase-banner-0.10.6.tgz", + "integrity": "sha512-TX5OMb1ORlpzQZHwoP+diuLutH3orpmlxRjuGxLrp5psVDLu/ET9EHz6rqPLETCiml1DbRE0+XB2/mVDET4GCA==", + "dependencies": { + "@govuk-react/constants": "^0.10.6", + "@govuk-react/lib": "^0.10.6", + "@govuk-react/tag": "^0.10.6", + "govuk-colours": "^1.1.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "styled-components": ">=5.1" } }, - "@webpack-cli/configtest": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@webpack-cli/configtest/-/configtest-1.2.0.tgz", - "integrity": "sha512-4FB8Tj6xyVkyqjj1OaTqCjXYULB9FMkqQ8yGrZjRDrYh0nOE+7Lhs45WioWQQMV+ceFlE368Ukhe6xdvJM9Egg==", - "dev": true, - "requires": {} - }, - "@webpack-cli/info": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@webpack-cli/info/-/info-1.5.0.tgz", - "integrity": "sha512-e8tSXZpw2hPl2uMJY6fsMswaok5FdlGNRTktvFk2sD8RjH0hE2+XistawJx1vmKteh4NmGmNUrp+Tb2w+udPcQ==", - "dev": true, - "requires": { - "envinfo": "^7.7.3" + "node_modules/@govuk-react/radio": { + "version": "0.10.6", + "resolved": "https://registry.npmjs.org/@govuk-react/radio/-/radio-0.10.6.tgz", + "integrity": "sha512-oPkZSNNzK/T9veD9Rk0yVN5XMmMfLzGOMrzGtugszt6pyXHHgEtaeQZJM/nSR0ZzAMXyk6YM8wlGrqXfT18Kug==", + "dependencies": { + "@govuk-react/constants": "^0.10.6", + "@govuk-react/hint-text": "^0.10.6", + "@govuk-react/lib": "^0.10.6", + "govuk-colours": "^1.1.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "styled-components": ">=5.1" } }, - "@webpack-cli/serve": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/@webpack-cli/serve/-/serve-1.7.0.tgz", - "integrity": "sha512-oxnCNGj88fL+xzV+dacXs44HcDwf1ovs3AuEzvP7mqXw7fQntqIhQ1BRmynh4qEKQSSSRSWVyXRjmTbZIX9V2Q==", - "dev": true, - "requires": {} - }, - "@xtuc/ieee754": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", - "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==" - }, - "@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==" + "node_modules/@govuk-react/related-items": { + "version": "0.10.6", + "resolved": "https://registry.npmjs.org/@govuk-react/related-items/-/related-items-0.10.6.tgz", + "integrity": "sha512-ORfFWgFqiIBxmDAxXOWSNE/L5ij8sUJwysyQR4/vxYCOKsMpBg6VOU8sl8BaKa2RJTWqgvC/e6m4MSJsm9Jkag==", + "dependencies": { + "@govuk-react/constants": "^0.10.6", + "@govuk-react/lib": "^0.10.6", + "govuk-colours": "^1.1.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "styled-components": ">=5.1" + } }, - "accessible-autocomplete": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/accessible-autocomplete/-/accessible-autocomplete-2.0.4.tgz", - "integrity": "sha512-2p0txrSpvs5wXFUeQJHMheDPTZVSEmiUHWlEPb7vJnv2Dd1xPfoLnBQQMfNbTSit2pL/9sSQYESuD2Yyohd4Yw==", - "requires": { - "preact": "^8.3.1" + "node_modules/@govuk-react/search-box": { + "version": "0.10.6", + "resolved": "https://registry.npmjs.org/@govuk-react/search-box/-/search-box-0.10.6.tgz", + "integrity": "sha512-L6C18OMRANfWx6mtrU8m3ycU0UFe3kENd2LD2n4lapHC9IlS8N89drc7dXCdKoyRG/s9LUjBxzETbHUasaUMLQ==", + "dependencies": { + "@govuk-react/constants": "^0.10.6", + "@govuk-react/icons": "^0.10.6", + "@govuk-react/lib": "^0.10.6", + "govuk-colours": "^1.1.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "styled-components": ">=5.1" } }, - "acorn": { - "version": "8.8.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.1.tgz", - "integrity": "sha512-7zFpHzhnqYKrkYdUjF1HI1bzd0VygEGX8lFk4k5zVMqHEoES+P+7TKI+EvLO9WVMJ8eekdO0aDEK044xTXwPPA==" + "node_modules/@govuk-react/section-break": { + "version": "0.10.6", + "resolved": "https://registry.npmjs.org/@govuk-react/section-break/-/section-break-0.10.6.tgz", + "integrity": "sha512-zq8S6lkqwIocfb+l/3WTesLGH3oM8EFI6srTJ5GwwIrBiVWOpeTvNXI0ckjM1C/QI1sah3JP9K2XvKdeCWbi4w==", + "dependencies": { + "@govuk-react/lib": "^0.10.6", + "govuk-colours": "^1.1.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "styled-components": ">=5.1" + } }, - "acorn-import-assertions": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.8.0.tgz", - "integrity": "sha512-m7VZ3jwz4eK6A4Vtt8Ew1/mNbP24u0FhdyfA7fSvnJR6LMdfOYnmuIrrJAgrYfYJ10F/otaHTtrtrtmHdMNzEw==", - "requires": {} + "node_modules/@govuk-react/select": { + "version": "0.10.6", + "resolved": "https://registry.npmjs.org/@govuk-react/select/-/select-0.10.6.tgz", + "integrity": "sha512-k+1DQzyAX5FxyTAggoCplqYfI/bcDtCIDMFLZO01Yreo/fRGq7aPv7y1RHDL0744MZix0BFA9loyuJDAe8dbUw==", + "dependencies": { + "@govuk-react/constants": "^0.10.6", + "@govuk-react/error-text": "^0.10.6", + "@govuk-react/hint-text": "^0.10.6", + "@govuk-react/label": "^0.10.6", + "@govuk-react/label-text": "^0.10.6", + "@govuk-react/lib": "^0.10.6", + "govuk-colours": "^1.1.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "styled-components": ">=5.1" + } }, - "ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "requires": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" + "node_modules/@govuk-react/skip-link": { + "version": "0.10.6", + "resolved": "https://registry.npmjs.org/@govuk-react/skip-link/-/skip-link-0.10.6.tgz", + "integrity": "sha512-K+upaClQMiB8BD7ZOBqB8FXAP+epoSjnImRcEus62S2pxCU4MGHhWUBvMJ7nlj8Czap0S454jOs1TMzJa41Eyw==", + "dependencies": { + "@govuk-react/constants": "^0.10.6", + "@govuk-react/lib": "^0.10.6", + "govuk-colours": "^1.1.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "styled-components": ">=5.1" } }, - "ajv-keywords": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", - "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", - "requires": {} + "node_modules/@govuk-react/table": { + "version": "0.10.6", + "resolved": "https://registry.npmjs.org/@govuk-react/table/-/table-0.10.6.tgz", + "integrity": "sha512-mQ/5WA3/S+mI7Buu2JPGZK6pbuyx4XX2jcm/6q+1drQDf0DLCn5QkNKFKPE1lqkt66uFxby7cw8JZnClFXsVJw==", + "dependencies": { + "@govuk-react/constants": "^0.10.6", + "@govuk-react/lib": "^0.10.6", + "govuk-colours": "^1.1.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "styled-components": ">=5.1" + } }, - "ansi-regex": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", - "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==" + "node_modules/@govuk-react/tabs": { + "version": "0.10.6", + "resolved": "https://registry.npmjs.org/@govuk-react/tabs/-/tabs-0.10.6.tgz", + "integrity": "sha512-cdtWN6dk3q+iGpRZthLzBkMGc0ysh7R9JTrsR3jU3l55/DqgK+z4wy4stmqjJsaT9C6yj0lYydEDofSelHtqIg==", + "dependencies": { + "@govuk-react/constants": "^0.10.6", + "@govuk-react/lib": "^0.10.6", + "govuk-colours": "^1.1.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "styled-components": ">=5.1" + } }, - "anymatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz", - "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==", - "requires": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" + "node_modules/@govuk-react/tag": { + "version": "0.10.6", + "resolved": "https://registry.npmjs.org/@govuk-react/tag/-/tag-0.10.6.tgz", + "integrity": "sha512-OMT31Tlc01IfLZOI+1qHlaNyatUN5ytEUrrTTeFbBdIYFKKOtAV9qvT7gEpr0eTmFyJ79hmcqtsgtlf3hJEgJA==", + "dependencies": { + "@govuk-react/lib": "^0.10.6", + "govuk-colours": "^1.1.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "styled-components": ">=5.1" } }, - "big.js": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", - "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==" + "node_modules/@govuk-react/text-area": { + "version": "0.10.6", + "resolved": "https://registry.npmjs.org/@govuk-react/text-area/-/text-area-0.10.6.tgz", + "integrity": "sha512-Z0S/w7A3WRqZ3LmCLxV6UXyysJoec4PkdNl6G5bGpIPRS1/7LA8ZDmeUq/OD3vKsEJQI59oYHbh8yylX8zMuzg==", + "dependencies": { + "@govuk-react/constants": "^0.10.6", + "@govuk-react/error-text": "^0.10.6", + "@govuk-react/hint-text": "^0.10.6", + "@govuk-react/label": "^0.10.6", + "@govuk-react/label-text": "^0.10.6", + "govuk-colours": "^1.1.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "styled-components": ">=5.1" + } }, - "binary-extensions": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", - "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==" + "node_modules/@govuk-react/top-nav": { + "version": "0.10.6", + "resolved": "https://registry.npmjs.org/@govuk-react/top-nav/-/top-nav-0.10.6.tgz", + "integrity": "sha512-mjzf9NZbB/6x4hoKrwuhk9KplpktGq3q3wrFGWxGBXPk9s6tT/OIiT/Bxz0vy0Om03xz/7F7GgMx3mzFUTjG2Q==", + "dependencies": { + "@govuk-react/button": "^0.10.6", + "@govuk-react/constants": "^0.10.6", + "@govuk-react/icon-crown": "0.0.8", + "@govuk-react/icons": "^0.10.6", + "@govuk-react/lib": "^0.10.6", + "@govuk-react/search-box": "^0.10.6", + "govuk-colours": "^1.1.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "styled-components": ">=5.1" + } }, - "braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", - "requires": { - "fill-range": "^7.0.1" + "node_modules/@govuk-react/unordered-list": { + "version": "0.10.6", + "resolved": "https://registry.npmjs.org/@govuk-react/unordered-list/-/unordered-list-0.10.6.tgz", + "integrity": "sha512-1saN2SBnpXGrWYEtdjwbxtJtJdKT9iidiiFmWlK3BWtii2gF91LCU47TCoTbNRLjKabXTLbxOd/Gu6UDCk1h3Q==", + "dependencies": { + "@govuk-react/ordered-list": "^0.10.6" + }, + "peerDependencies": { + "react": ">=16.8.0", + "styled-components": ">=5.1" } }, - "browserslist": { - "version": "4.21.4", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.4.tgz", - "integrity": "sha512-CBHJJdDmgjl3daYjN5Cp5kbTf1mUhZoS+beLklHIvkOWscs83YAhLlF3Wsh/lciQYAcbBJgTOD44VtG31ZM4Hw==", - "requires": { - "caniuse-lite": "^1.0.30001400", - "electron-to-chromium": "^1.4.251", - "node-releases": "^2.0.6", - "update-browserslist-db": "^1.0.9" + "node_modules/@govuk-react/visually-hidden": { + "version": "0.10.6", + "resolved": "https://registry.npmjs.org/@govuk-react/visually-hidden/-/visually-hidden-0.10.6.tgz", + "integrity": "sha512-fsI/ZaNzTbKy8CYiBbGEoYpU9F0/Vj2+qf4+pohjwprfmfbPwp239PHanqmIjVYpJ/5vjEmBF2yajt9Rg8p4hw==", + "dependencies": { + "@govuk-react/lib": "^0.10.6", + "govuk-colours": "^1.1.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "styled-components": ">=5.1" } }, - "buffer-from": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", - "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==" + "node_modules/@govuk-react/warning-text": { + "version": "0.10.6", + "resolved": "https://registry.npmjs.org/@govuk-react/warning-text/-/warning-text-0.10.6.tgz", + "integrity": "sha512-j17041WKRtN9JmPNo91D3T6sn0a7esJ6R/aNN7GlhJIP6lC3IwDvzyNlahGXY7SugNZDUueLw2+6tOzcPyr9dA==", + "dependencies": { + "@govuk-react/constants": "^0.10.6", + "@govuk-react/icons": "^0.10.6", + "@govuk-react/lib": "^0.10.6", + "govuk-colours": "^1.1.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "styled-components": ">=5.1" + } }, - "caniuse-lite": { - "version": "1.0.30001434", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001434.tgz", - "integrity": "sha512-aOBHrLmTQw//WFa2rcF1If9fa3ypkC1wzqqiKHgfdrXTWcU8C4gKVZT77eQAPWN1APys3+uQ0Df07rKauXGEYA==" + "node_modules/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dev": true, + "dependencies": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } }, - "chart.js": { - "version": "3.9.1", - "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-3.9.1.tgz", - "integrity": "sha512-Ro2JbLmvg83gXF5F4sniaQ+lTbSv18E+TIf2cOeiH1Iqd2PGFOtem+DUufMZsCJwFE7ywPOpfXFBwRTGq7dh6w==" + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "engines": { + "node": ">=8" + } }, - "chartjs-adapter-moment": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/chartjs-adapter-moment/-/chartjs-adapter-moment-1.0.0.tgz", - "integrity": "sha512-PqlerEvQcc5hZLQ/NQWgBxgVQ4TRdvkW3c/t+SUEQSj78ia3hgLkf2VZ2yGJtltNbEEFyYGm+cA6XXevodYvWA==", - "requires": {} + "node_modules/@jest/console": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.7.0.tgz", + "integrity": "sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } }, - "chokidar": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", - "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", - "requires": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "fsevents": "~2.3.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" + "node_modules/@jest/console/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" } }, - "chrome-trace-event": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz", - "integrity": "sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg==" + "node_modules/@jest/console/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" + } }, - "clone-deep": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", - "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", + "node_modules/@jest/console/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, - "requires": { - "is-plain-object": "^2.0.4", - "kind-of": "^6.0.2", - "shallow-clone": "^3.0.0" + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" } }, - "colorette": { - "version": "2.0.19", - "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.19.tgz", - "integrity": "sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ==", + "node_modules/@jest/console/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 }, - "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==" + "node_modules/@jest/console/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } }, - "cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "node_modules/@jest/console/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, - "requires": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" } }, - "css-loader": { - "version": "5.2.7", - "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-5.2.7.tgz", - "integrity": "sha512-Q7mOvpBNBG7YrVGMxRxcBJZFL75o+cH2abNASdibkj/fffYD8qWbInZrD0S9ccI6vZclF3DsHE7njGlLtaHbhg==", - "requires": { - "icss-utils": "^5.1.0", - "loader-utils": "^2.0.0", - "postcss": "^8.2.15", - "postcss-modules-extract-imports": "^3.0.0", - "postcss-modules-local-by-default": "^4.0.0", - "postcss-modules-scope": "^3.0.0", - "postcss-modules-values": "^4.0.0", - "postcss-value-parser": "^4.1.0", - "schema-utils": "^3.0.0", - "semver": "^7.3.5" + "node_modules/@jest/core": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.7.0.tgz", + "integrity": "sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==", + "dev": true, + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/reporters": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-changed-files": "^29.7.0", + "jest-config": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-resolve-dependencies": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "jest-watcher": "^29.7.0", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } } }, - "cssesc": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", - "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==" - }, - "electron-to-chromium": { - "version": "1.4.284", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.284.tgz", - "integrity": "sha512-M8WEXFuKXMYMVr45fo8mq0wUrrJHheiKZf6BArTKk9ZBYCKJEOU5H8cdWgDT+qCVZf7Na4lVUaZsA+h6uA9+PA==" + "node_modules/@jest/core/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" + } }, - "emojis-list": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", - "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==" + "node_modules/@jest/core/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" + } }, - "enhanced-resolve": { - "version": "5.10.0", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.10.0.tgz", - "integrity": "sha512-T0yTFjdpldGY8PmuXXR0PyQ1ufZpEGiHVrp7zHKB7jdR4qlmZHhONVM5AQOAWXuF/w3dnHbEQVrNptJgt7F+cQ==", - "requires": { - "graceful-fs": "^4.2.4", - "tapable": "^2.2.0" + "node_modules/@jest/core/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" } }, - "envinfo": { - "version": "7.8.1", - "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.8.1.tgz", - "integrity": "sha512-/o+BXHmB7ocbHEAs6F2EnG0ogybVVUdkRunTT2glZU9XAaGmhqskrvKwqXuDfNjEO0LZKWdejEEpnq8aM0tOaw==", + "node_modules/@jest/core/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 }, - "es-module-lexer": { - "version": "0.9.3", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-0.9.3.tgz", - "integrity": "sha512-1HQ2M2sPtxwnvOvT1ZClHyQDiggdNjURWpY2we6aMKCQiUVxTmVs2UYPLIrD84sS+kMdUwfBSylbJPwNnBrnHQ==" - }, - "escalade": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", - "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==" - }, - "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==", - "requires": { - "esrecurse": "^4.3.0", - "estraverse": "^4.1.1" + "node_modules/@jest/core/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" } }, - "esrecurse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", - "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", - "requires": { - "estraverse": "^5.2.0" - }, + "node_modules/@jest/core/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": { - "estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==" - } + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" } }, - "estraverse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", - "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==" - }, - "events": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", - "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==" - }, - "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==" - }, - "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==" - }, - "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 - }, - "file-loader": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/file-loader/-/file-loader-6.2.0.tgz", - "integrity": "sha512-qo3glqyTa61Ytg4u73GultjHGjdRyig3tG6lPtyX/jOEJvHif9uB0/OCI2Kif6ctF3caQTW2G5gym21oAsI4pw==", - "requires": { - "loader-utils": "^2.0.0", - "schema-utils": "^3.0.0" + "node_modules/@jest/environment": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz", + "integrity": "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==", + "dev": true, + "dependencies": { + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "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==", - "requires": { - "to-regex-range": "^5.0.1" + "node_modules/@jest/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==", + "dev": true, + "dependencies": { + "expect": "^29.7.0", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "node_modules/@jest/expect-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz", + "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", "dev": true, - "requires": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" + "dependencies": { + "jest-get-type": "^29.6.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "optional": true - }, - "function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", - "dev": true + "node_modules/@jest/fake-timers": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.7.0.tgz", + "integrity": "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@sinonjs/fake-timers": "^10.0.2", + "@types/node": "*", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } }, - "glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "requires": { - "is-glob": "^4.0.1" + "node_modules/@jest/globals": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.7.0.tgz", + "integrity": "sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==", + "dev": true, + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/types": "^29.6.3", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "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==" + "node_modules/@jest/reporters": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.7.0.tgz", + "integrity": "sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==", + "dev": true, + "dependencies": { + "@bcoe/v8-coverage": "^0.2.3", + "@jest/console": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "@types/node": "*", + "chalk": "^4.0.0", + "collect-v8-coverage": "^1.0.0", + "exit": "^0.1.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^6.0.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.0", + "istanbul-reports": "^3.1.3", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "slash": "^3.0.0", + "string-length": "^4.0.1", + "strip-ansi": "^6.0.0", + "v8-to-istanbul": "^9.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } }, - "govuk-frontend": { - "version": "3.14.0", - "resolved": "https://registry.npmjs.org/govuk-frontend/-/govuk-frontend-3.14.0.tgz", - "integrity": "sha512-y7FTuihCSA8Hty+e9h0uPhCoNanCAN+CLioNFlPmlbeHXpbi09VMyxTcH+XfnMPY4Cp++7096v0rLwwdapTXnA==" + "node_modules/@jest/reporters/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" + } }, - "graceful-fs": { - "version": "4.2.10", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", - "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==" + "node_modules/@jest/reporters/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" + } }, - "has": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", - "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "node_modules/@jest/reporters/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, - "requires": { - "function-bind": "^1.1.1" + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" } }, - "has-flag": { + "node_modules/@jest/reporters/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/@jest/reporters/node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" - }, - "icss-utils": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz", - "integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==", - "requires": {} - }, - "immutable": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.1.0.tgz", - "integrity": "sha512-oNkuqVTA8jqG1Q6c+UglTOD1xhC1BtjKI7XkCXRkZHrN5m18/XsnUp8Q89GkQO/z+0WjonSvl0FLhDYftp46nQ==" - }, - "import-local": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.1.0.tgz", - "integrity": "sha512-ASB07uLtnDs1o6EHjKpX34BKYDSqnFerfTOJL2HvMqF70LnxpjkzDB8J44oT9pu4AMPkQwf8jl6szgvNd2tRIg==", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true, - "requires": { - "pkg-dir": "^4.2.0", - "resolve-cwd": "^3.0.0" + "engines": { + "node": ">=8" } }, - "interpret": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/interpret/-/interpret-2.2.0.tgz", - "integrity": "sha512-Ju0Bz/cEia55xDwUWEa8+olFpCiQoypjnQySseKtmjNrnps3P+xfpUmGr90T7yjlVJmOtybRvPXhKMbHr+fWnw==", - "dev": true + "node_modules/@jest/reporters/node_modules/jest-worker": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", + "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", + "dev": true, + "dependencies": { + "@types/node": "*", + "jest-util": "^29.7.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } }, - "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==", - "requires": { - "binary-extensions": "^2.0.0" + "node_modules/@jest/reporters/node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" } }, - "is-core-module": { - "version": "2.11.0", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.11.0.tgz", - "integrity": "sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw==", + "node_modules/@jest/reporters/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, - "requires": { - "has": "^1.0.3" + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" } }, - "is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==" + "node_modules/@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } }, - "is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "requires": { - "is-extglob": "^2.1.1" + "node_modules/@jest/source-map": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.6.3.tgz", + "integrity": "sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.18", + "callsites": "^3.0.0", + "graceful-fs": "^4.2.9" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==" + "node_modules/@jest/test-result": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.7.0.tgz", + "integrity": "sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==", + "dev": true, + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "collect-v8-coverage": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } }, - "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==", + "node_modules/@jest/test-sequencer": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.7.0.tgz", + "integrity": "sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==", "dev": true, - "requires": { - "isobject": "^3.0.1" + "dependencies": { + "@jest/test-result": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true + "node_modules/@jest/transform": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", + "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", + "dev": true, + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "babel-plugin-istanbul": "^6.1.1", + "chalk": "^4.0.0", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "micromatch": "^4.0.4", + "pirates": "^4.0.4", + "slash": "^3.0.0", + "write-file-atomic": "^4.0.2" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } }, - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", + "node_modules/@jest/transform/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/@jest/transform/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/@jest/transform/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/@jest/transform/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 }, - "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==", - "requires": { + "node_modules/@jest/transform/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/transform/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/@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", "@types/node": "*", - "merge-stream": "^2.0.0", - "supports-color": "^8.0.0" + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "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==" + "node_modules/@jest/types/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" + } }, - "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==" + "node_modules/@jest/types/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" + } }, - "json5": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==" + "node_modules/@jest/types/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" + } }, - "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==", + "node_modules/@jest/types/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 }, - "klona": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/klona/-/klona-2.0.5.tgz", - "integrity": "sha512-pJiBpiXMbt7dkzXe8Ghj/u4FfXOOa98fPW+bihOJ4SjnoijweJrNThJfd3ifXpXhREjpoF2mZVH1GfS9LV3kHQ==" + "node_modules/@jest/types/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/types/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/@jridgewell/gen-mapping": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", + "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", + "dependencies": { + "@jridgewell/set-array": "^1.0.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.9" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz", + "integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", + "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.5.tgz", + "integrity": "sha512-UTYAUj/wviwdsMfzoSJspJxbkH5o1snzwX0//0ENX1u/55kkZZkcTZP6u9bwKGkv+dkk9at4m1Cpt0uY80kcpQ==", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.0", + "@jridgewell/trace-mapping": "^0.3.9" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.15", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.20", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.20.tgz", + "integrity": "sha512-R8LcPeWZol2zR8mmH3JeKQ6QRCFb7XgUhV9ZlGhHLGyg4wpPiPZNQOOWhFZhxKw8u//yTbNGI42Bx/3paXEQ+Q==", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@sinclair/typebox": { + "version": "0.27.8", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", + "dev": true + }, + "node_modules/@sinonjs/commons": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.0.tgz", + "integrity": "sha512-jXBtWAF4vmdNmZgD5FoKsVLv3rPgDnLgPbU84LIJ3otV44vJlDRokVng5v8NFJdCf/da9legHcKaRuZs4L7faA==", + "dev": true, + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", + "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^3.0.0" + } + }, + "node_modules/@testing-library/dom": { + "version": "9.3.4", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-9.3.4.tgz", + "integrity": "sha512-FlS4ZWlp97iiNWig0Muq8p+3rVDjRiYE+YKGbAqXOu9nwJFFOdL00kFpz42M+4huzYi86vAK1sOOfyOG45muIQ==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.1.3", + "chalk": "^4.1.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "pretty-format": "^27.0.2" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@testing-library/dom/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } }, - "loader-runner": { + "node_modules/@testing-library/dom/node_modules/ansi-styles": { "version": "4.3.0", - "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz", - "integrity": "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==" + "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/@testing-library/dom/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/@testing-library/dom/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/@testing-library/dom/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/@testing-library/dom/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@testing-library/dom/node_modules/pretty-format": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/@testing-library/dom/node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@testing-library/dom/node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "dev": true + }, + "node_modules/@testing-library/dom/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/@testing-library/jest-dom": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.2.1.tgz", + "integrity": "sha512-Nuy/uFFDe9h/2jwoUuMKgoxvgkUv4S9jI9bARj6dGUKJ3euRhg8JFi5sciYbrayoxkadEOZednRT9+vo6LvvxQ==", + "dev": true, + "dependencies": { + "@adobe/css-tools": "^4.3.2", + "@babel/runtime": "^7.9.2", + "aria-query": "^5.0.0", + "chalk": "^3.0.0", + "css.escape": "^1.5.1", + "dom-accessibility-api": "^0.6.3", + "lodash": "^4.17.15", + "redent": "^3.0.0" + }, + "engines": { + "node": ">=14", + "npm": ">=6", + "yarn": ">=1" + }, + "peerDependencies": { + "@jest/globals": ">= 28", + "@types/bun": "latest", + "@types/jest": ">= 28", + "jest": ">= 28", + "vitest": ">= 0.32" + }, + "peerDependenciesMeta": { + "@jest/globals": { + "optional": true + }, + "@types/bun": { + "optional": true + }, + "@types/jest": { + "optional": true + }, + "jest": { + "optional": true + }, + "vitest": { + "optional": true + } + } + }, + "node_modules/@testing-library/jest-dom/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/@testing-library/jest-dom/node_modules/chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@testing-library/jest-dom/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/@testing-library/jest-dom/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/@testing-library/jest-dom/node_modules/dom-accessibility-api": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", + "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==", + "dev": true + }, + "node_modules/@testing-library/jest-dom/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@testing-library/jest-dom/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/@testing-library/react": { + "version": "14.1.2", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-14.1.2.tgz", + "integrity": "sha512-z4p7DVBTPjKM5qDZ0t5ZjzkpSNb+fZy1u6bzO7kk8oeGagpPCAtgh4cx1syrfp7a+QWkM021jGqjJaxJJnXAZg==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.12.5", + "@testing-library/dom": "^9.0.0", + "@types/react-dom": "^18.0.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "react": "^18.0.0", + "react-dom": "^18.0.0" + } + }, + "node_modules/@tootallnate/once": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", + "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==", + "dev": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/@types/aria-query": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", + "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", + "dev": true + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.6.8", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.8.tgz", + "integrity": "sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==", + "dev": true, + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.5.tgz", + "integrity": "sha512-WXCyOcRtH37HAUkpXhUduaxdm82b4GSlyTqajXviN4EfiuPgNYR109xMCKvpl6zPIpua0DGlMEDCq+g8EdoheQ==", + "dev": true, + "dependencies": { + "@babel/types": "^7.20.7" + } + }, + "node_modules/@types/eslint": { + "version": "8.44.6", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.44.6.tgz", + "integrity": "sha512-P6bY56TVmX8y9J87jHNgQh43h6VVU+6H7oN7hgvivV81K2XY8qJZ5vqPy/HdUoVIelii2kChYVzQanlswPWVFw==", + "dependencies": { + "@types/estree": "*", + "@types/json-schema": "*" + } + }, + "node_modules/@types/eslint-scope": { + "version": "3.7.6", + "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.6.tgz", + "integrity": "sha512-zfM4ipmxVKWdxtDaJ3MP3pBurDXOCoyjvlpE3u6Qzrmw4BPbfm4/ambIeTk/r/J0iq/+2/xp0Fmt+gFvXJY2PQ==", + "dependencies": { + "@types/eslint": "*", + "@types/estree": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.4.tgz", + "integrity": "sha512-2JwWnHK9H+wUZNorf2Zr6ves96WHoWDJIftkcxPKsS7Djta6Zu519LarhRNljPXkpsZR2ZMwNCPeW7omW07BJw==" + }, + "node_modules/@types/graceful-fs": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", + "integrity": "sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/hoist-non-react-statics": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.4.tgz", + "integrity": "sha512-ZchYkbieA+7tnxwX/SCBySx9WwvWR8TaP5tb2jRAzwvLb/rWchGw3v0w3pqUbUvj0GCwW2Xz/AVPSk6kUGctXQ==", + "dependencies": { + "@types/react": "*", + "hoist-non-react-statics": "^3.3.0" + } + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "dev": true + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", + "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", + "dev": true, + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", + "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", + "dev": true, + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/jsdom": { + "version": "20.0.1", + "resolved": "https://registry.npmjs.org/@types/jsdom/-/jsdom-20.0.1.tgz", + "integrity": "sha512-d0r18sZPmMQr1eG35u12FZfhIXNrnsPU/g5wvRKCUf/tOGilKKwYMYGqh33BNR6ba+2gkHw1EUiHoN3mn7E5IQ==", + "dev": true, + "dependencies": { + "@types/node": "*", + "@types/tough-cookie": "*", + "parse5": "^7.0.0" + } + }, + "node_modules/@types/json-schema": { + "version": "7.0.14", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.14.tgz", + "integrity": "sha512-U3PUjAudAdJBeC2pgN8uTIKgxrb4nlDF3SF0++EldXQvQBGkpFZMSnwQiIoDU77tv45VgNkl/L4ouD+rEomujw==" + }, + "node_modules/@types/mdast": { + "version": "3.0.14", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.14.tgz", + "integrity": "sha512-gVZ04PGgw1qLZKsnWnyFv4ORnaJ+DXLdHTVSFbU8yX6xZ34Bjg4Q32yPkmveUP1yItXReKfB0Aknlh/3zxTKAw==", + "dependencies": { + "@types/unist": "^2" + } + }, + "node_modules/@types/node": { + "version": "20.8.10", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.8.10.tgz", + "integrity": "sha512-TlgT8JntpcbmKUFzjhsyhGfP2fsiz1Mv56im6enJ905xG1DAYesxJaeSbGqQmAw8OWPdhyJGhGSQGKRNJ45u9w==", + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/@types/prop-types": { + "version": "15.7.9", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.9.tgz", + "integrity": "sha512-n1yyPsugYNSmHgxDFjicaI2+gCNjsBck8UX9kuofAKlc0h1bL+20oSF72KeNaW2DUlesbEVCFgyV2dPGTiY42g==" + }, + "node_modules/@types/react": { + "version": "18.2.33", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.33.tgz", + "integrity": "sha512-v+I7S+hu3PIBoVkKGpSYYpiBT1ijqEzWpzQD62/jm4K74hPpSP7FF9BnKG6+fg2+62weJYkkBWDJlZt5JO/9hg==", + "dependencies": { + "@types/prop-types": "*", + "@types/scheduler": "*", + "csstype": "^3.0.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.2.18", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.2.18.tgz", + "integrity": "sha512-TJxDm6OfAX2KJWJdMEVTwWke5Sc/E/RlnPGvGfS0W7+6ocy2xhDVQVh/KvC2Uf7kACs+gDytdusDSdWfWkaNzw==", + "dev": true, + "dependencies": { + "@types/react": "*" + } + }, + "node_modules/@types/scheduler": { + "version": "0.16.5", + "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.5.tgz", + "integrity": "sha512-s/FPdYRmZR8SjLWGMCuax7r3qCWQw9QKHzXVukAuuIJkXkDRwp+Pu5LMIVFi0Fxbav35WURicYr8u1QsoybnQw==" + }, + "node_modules/@types/stack-utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", + "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", + "dev": true + }, + "node_modules/@types/styled-components": { + "version": "5.1.29", + "resolved": "https://registry.npmjs.org/@types/styled-components/-/styled-components-5.1.29.tgz", + "integrity": "sha512-5h/ah9PAblggQ6Laa4peplT4iY5ddA8qM1LMD4HzwToUWs3hftfy0fayeRgbtH1JZUdw5CCaowmz7Lnb8SjIxQ==", + "dependencies": { + "@types/hoist-non-react-statics": "*", + "@types/react": "*", + "csstype": "^3.0.2" + } + }, + "node_modules/@types/stylis": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@types/stylis/-/stylis-4.2.2.tgz", + "integrity": "sha512-Rm17MsTpQQP5Jq4BF7CdrxJsDufoiL/q5IbJZYZmOZAJALyijgF7BzLgobXUqraNcQdqFYLYGeglDp6QzaxPpg==" + }, + "node_modules/@types/tough-cookie": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz", + "integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==", + "dev": true + }, + "node_modules/@types/unist": { + "version": "2.0.9", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.9.tgz", + "integrity": "sha512-zC0iXxAv1C1ERURduJueYzkzZ2zaGyc+P2c95hgkikHPr3z8EdUZOlgEQ5X0DRmwDZn+hekycQnoeiiRVrmilQ==" + }, + "node_modules/@types/yargs": { + "version": "17.0.32", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz", + "integrity": "sha512-xQ67Yc/laOG5uMfX/093MRlGGCIBzZMarVa+gfNKJxWAIgykYpVGkBdbqEzGDDfCrVUj6Hiff4mTZ5BA6TmAog==", + "dev": true, + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "dev": true + }, + "node_modules/@webassemblyjs/ast": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.6.tgz", + "integrity": "sha512-IN1xI7PwOvLPgjcf180gC1bqn3q/QaOCwYUahIOhbYUu8KA/3tw2RT/T0Gidi1l7Hhj5D/INhJxiICObqpMu4Q==", + "dependencies": { + "@webassemblyjs/helper-numbers": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6" + } + }, + "node_modules/@webassemblyjs/floating-point-hex-parser": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.6.tgz", + "integrity": "sha512-ejAj9hfRJ2XMsNHk/v6Fu2dGS+i4UaXBXGemOfQ/JfQ6mdQg/WXtwleQRLLS4OvfDhv8rYnVwH27YJLMyYsxhw==" + }, + "node_modules/@webassemblyjs/helper-api-error": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.6.tgz", + "integrity": "sha512-o0YkoP4pVu4rN8aTJgAyj9hC2Sv5UlkzCHhxqWj8butaLvnpdc2jOwh4ewE6CX0txSfLn/UYaV/pheS2Txg//Q==" + }, + "node_modules/@webassemblyjs/helper-buffer": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.6.tgz", + "integrity": "sha512-z3nFzdcp1mb8nEOFFk8DrYLpHvhKC3grJD2ardfKOzmbmJvEf/tPIqCY+sNcwZIY8ZD7IkB2l7/pqhUhqm7hLA==" + }, + "node_modules/@webassemblyjs/helper-numbers": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.6.tgz", + "integrity": "sha512-vUIhZ8LZoIWHBohiEObxVm6hwP034jwmc9kuq5GdHZH0wiLVLIPcMCdpJzG4C11cHoQ25TFIQj9kaVADVX7N3g==", + "dependencies": { + "@webassemblyjs/floating-point-hex-parser": "1.11.6", + "@webassemblyjs/helper-api-error": "1.11.6", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/helper-wasm-bytecode": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.6.tgz", + "integrity": "sha512-sFFHKwcmBprO9e7Icf0+gddyWYDViL8bpPjJJl0WHxCdETktXdmtWLGVzoHbqUcY4Be1LkNfwTmXOJUFZYSJdA==" + }, + "node_modules/@webassemblyjs/helper-wasm-section": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.6.tgz", + "integrity": "sha512-LPpZbSOwTpEC2cgn4hTydySy1Ke+XEu+ETXuoyvuyezHO3Kjdu90KK95Sh9xTbmjrCsUwvWwCOQQNta37VrS9g==", + "dependencies": { + "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/helper-buffer": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6", + "@webassemblyjs/wasm-gen": "1.11.6" + } + }, + "node_modules/@webassemblyjs/ieee754": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.11.6.tgz", + "integrity": "sha512-LM4p2csPNvbij6U1f19v6WR56QZ8JcHg3QIJTlSwzFcmx6WSORicYj6I63f9yU1kEUtrpG+kjkiIAkevHpDXrg==", + "dependencies": { + "@xtuc/ieee754": "^1.2.0" + } + }, + "node_modules/@webassemblyjs/leb128": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.11.6.tgz", + "integrity": "sha512-m7a0FhE67DQXgouf1tbN5XQcdWoNgaAuoULHIfGFIEVKA6tu/edls6XnIlkmS6FrXAquJRPni3ZZKjw6FSPjPQ==", + "dependencies": { + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/utf8": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.11.6.tgz", + "integrity": "sha512-vtXf2wTQ3+up9Zsg8sa2yWiQpzSsMyXj0qViVP6xKGCUT8p8YJ6HqI7l5eCnWx1T/FYdsv07HQs2wTFbbof/RA==" + }, + "node_modules/@webassemblyjs/wasm-edit": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.6.tgz", + "integrity": "sha512-Ybn2I6fnfIGuCR+Faaz7YcvtBKxvoLV3Lebn1tM4o/IAJzmi9AWYIPWpyBfU8cC+JxAO57bk4+zdsTjJR+VTOw==", + "dependencies": { + "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/helper-buffer": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6", + "@webassemblyjs/helper-wasm-section": "1.11.6", + "@webassemblyjs/wasm-gen": "1.11.6", + "@webassemblyjs/wasm-opt": "1.11.6", + "@webassemblyjs/wasm-parser": "1.11.6", + "@webassemblyjs/wast-printer": "1.11.6" + } + }, + "node_modules/@webassemblyjs/wasm-gen": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.6.tgz", + "integrity": "sha512-3XOqkZP/y6B4F0PBAXvI1/bky7GryoogUtfwExeP/v7Nzwo1QLcq5oQmpKlftZLbT+ERUOAZVQjuNVak6UXjPA==", + "dependencies": { + "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6", + "@webassemblyjs/ieee754": "1.11.6", + "@webassemblyjs/leb128": "1.11.6", + "@webassemblyjs/utf8": "1.11.6" + } + }, + "node_modules/@webassemblyjs/wasm-opt": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.6.tgz", + "integrity": "sha512-cOrKuLRE7PCe6AsOVl7WasYf3wbSo4CeOk6PkrjS7g57MFfVUF9u6ysQBBODX0LdgSvQqRiGz3CXvIDKcPNy4g==", + "dependencies": { + "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/helper-buffer": "1.11.6", + "@webassemblyjs/wasm-gen": "1.11.6", + "@webassemblyjs/wasm-parser": "1.11.6" + } + }, + "node_modules/@webassemblyjs/wasm-parser": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.6.tgz", + "integrity": "sha512-6ZwPeGzMJM3Dqp3hCsLgESxBGtT/OeCvCZ4TA1JUPYgmhAx38tTPR9JaKy0S5H3evQpO/h2uWs2j6Yc/fjkpTQ==", + "dependencies": { + "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/helper-api-error": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6", + "@webassemblyjs/ieee754": "1.11.6", + "@webassemblyjs/leb128": "1.11.6", + "@webassemblyjs/utf8": "1.11.6" + } + }, + "node_modules/@webassemblyjs/wast-printer": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.11.6.tgz", + "integrity": "sha512-JM7AhRcE+yW2GWYaKeHL5vt4xqee5N2WcezptmgyhNS+ScggqcT1OtXykhAb13Sn5Yas0j2uv9tHgrjwvzAP4A==", + "dependencies": { + "@webassemblyjs/ast": "1.11.6", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webpack-cli/configtest": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@webpack-cli/configtest/-/configtest-1.2.0.tgz", + "integrity": "sha512-4FB8Tj6xyVkyqjj1OaTqCjXYULB9FMkqQ8yGrZjRDrYh0nOE+7Lhs45WioWQQMV+ceFlE368Ukhe6xdvJM9Egg==", + "dev": true, + "peerDependencies": { + "webpack": "4.x.x || 5.x.x", + "webpack-cli": "4.x.x" + } + }, + "node_modules/@webpack-cli/info": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@webpack-cli/info/-/info-1.5.0.tgz", + "integrity": "sha512-e8tSXZpw2hPl2uMJY6fsMswaok5FdlGNRTktvFk2sD8RjH0hE2+XistawJx1vmKteh4NmGmNUrp+Tb2w+udPcQ==", + "dev": true, + "dependencies": { + "envinfo": "^7.7.3" + }, + "peerDependencies": { + "webpack-cli": "4.x.x" + } + }, + "node_modules/@webpack-cli/serve": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@webpack-cli/serve/-/serve-1.7.0.tgz", + "integrity": "sha512-oxnCNGj88fL+xzV+dacXs44HcDwf1ovs3AuEzvP7mqXw7fQntqIhQ1BRmynh4qEKQSSSRSWVyXRjmTbZIX9V2Q==", + "dev": true, + "peerDependencies": { + "webpack-cli": "4.x.x" + }, + "peerDependenciesMeta": { + "webpack-dev-server": { + "optional": true + } + } + }, + "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==" + }, + "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==" + }, + "node_modules/abab": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz", + "integrity": "sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==", + "deprecated": "Use your platform's native atob() and btoa() methods instead", + "dev": true + }, + "node_modules/accessible-autocomplete": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/accessible-autocomplete/-/accessible-autocomplete-2.0.4.tgz", + "integrity": "sha512-2p0txrSpvs5wXFUeQJHMheDPTZVSEmiUHWlEPb7vJnv2Dd1xPfoLnBQQMfNbTSit2pL/9sSQYESuD2Yyohd4Yw==", + "dependencies": { + "preact": "^8.3.1" + } + }, + "node_modules/acorn": { + "version": "8.11.2", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.2.tgz", + "integrity": "sha512-nc0Axzp/0FILLEVsm4fNwLCwMttvhEI263QtVPQcbpfZZ3ts0hLsZGOpE6czNlid7CJ9MlyH8reXkpsf3YUY4w==", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-globals": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-7.0.1.tgz", + "integrity": "sha512-umOSDSDrfHbTNPuNpC2NSnnA3LUrqpevPb4T9jRx4MagXNS0rs+gwiTcAvqCRmsD6utzsrzNt+ebm00SNWiC3Q==", + "dev": true, + "dependencies": { + "acorn": "^8.1.0", + "acorn-walk": "^8.0.2" + } + }, + "node_modules/acorn-import-assertions": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.9.0.tgz", + "integrity": "sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA==", + "peerDependencies": { + "acorn": "^8" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.2.tgz", + "integrity": "sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "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/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ajv-formats/node_modules/ajv": { + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", + "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats/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==" + }, + "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==", + "peerDependencies": { + "ajv": "^6.9.1" + } + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "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==", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/aria-query": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.1.3.tgz", + "integrity": "sha512-R5iJ5lkuHybztUfuOAznmboyjWq8O6sqNqtK7CLOqdydi54VNbORp49mb14KbWgG1QD3JFO9hJdZ+y4KutfdOQ==", + "dev": true, + "dependencies": { + "deep-equal": "^2.0.5" + } + }, + "node_modules/array-buffer-byte-length": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.0.tgz", + "integrity": "sha512-LPuwb2P+NrQw3XhxGc36+XSvuBPopovXYTR9Ew++Du9Yb/bx5AzBfrIsBoj0EZUifjQU+sHL21sseZ3jerWO/A==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "is-array-buffer": "^3.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true + }, + "node_modules/available-typed-arrays": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz", + "integrity": "sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/babel-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", + "integrity": "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==", + "dev": true, + "dependencies": { + "@jest/transform": "^29.7.0", + "@types/babel__core": "^7.1.14", + "babel-plugin-istanbul": "^6.1.1", + "babel-preset-jest": "^29.6.3", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.8.0" + } + }, + "node_modules/babel-jest/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/babel-jest/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/babel-jest/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/babel-jest/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/babel-jest/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-jest/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/babel-loader": { + "version": "9.1.3", + "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-9.1.3.tgz", + "integrity": "sha512-xG3ST4DglodGf8qSwv0MdeWLhrDsw/32QMdTO5T1ZIp9gQur0HkCyFs7Awskr10JKXFXwpAhiCuYX5oGXnRGbw==", + "dependencies": { + "find-cache-dir": "^4.0.0", + "schema-utils": "^4.0.0" + }, + "engines": { + "node": ">= 14.15.0" + }, + "peerDependencies": { + "@babel/core": "^7.12.0", + "webpack": ">=5" + } + }, + "node_modules/babel-loader/node_modules/ajv": { + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", + "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/babel-loader/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==", + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/babel-loader/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==" + }, + "node_modules/babel-loader/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==", + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/babel-plugin-istanbul": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", + "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-instrument": "^5.0.4", + "test-exclude": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-istanbul/node_modules/istanbul-lib-instrument": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", + "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", + "dev": true, + "dependencies": { + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-jest-hoist": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz", + "integrity": "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==", + "dev": true, + "dependencies": { + "@babel/template": "^7.3.3", + "@babel/types": "^7.3.3", + "@types/babel__core": "^7.1.14", + "@types/babel__traverse": "^7.0.6" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/babel-plugin-polyfill-corejs2": { + "version": "0.4.7", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.7.tgz", + "integrity": "sha512-LidDk/tEGDfuHW2DWh/Hgo4rmnw3cduK6ZkOI1NPFceSK3n/yAGeOsNT7FLnSGHkXj3RHGSEVkN3FsCTY6w2CQ==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.22.6", + "@babel/helper-define-polyfill-provider": "^0.4.4", + "semver": "^6.3.1" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/babel-plugin-polyfill-corejs3": { + "version": "0.8.7", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.8.7.tgz", + "integrity": "sha512-KyDvZYxAzkC0Aj2dAPyDzi2Ym15e5JKZSK+maI7NAwSqofvuFglbSsxE7wUOvTg9oFVnHMzVzBKcqEb4PJgtOA==", + "dev": true, + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.4.4", + "core-js-compat": "^3.33.1" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/babel-plugin-polyfill-regenerator": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.5.4.tgz", + "integrity": "sha512-S/x2iOCvDaCASLYsOOgWOq4bCfKYVqvO/uxjkaYyZ3rVsVE3CeAI/c84NpyuBBymEgNvHgjEot3a9/Z/kXvqsg==", + "dev": true, + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.4.4" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/babel-preset-current-node-syntax": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.0.1.tgz", + "integrity": "sha512-M7LQ0bxarkxQoN+vz5aJPsLBn77n8QgTFmo8WK0/44auK2xlCXrYcUxHFxgU7qW5Yzw/CjmLRK2uJzaCd7LvqQ==", + "dev": true, + "dependencies": { + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.8.3", + "@babel/plugin-syntax-import-meta": "^7.8.3", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.8.3", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.8.3", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-top-level-await": "^7.8.3" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/babel-preset-jest": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz", + "integrity": "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==", + "dev": true, + "dependencies": { + "babel-plugin-jest-hoist": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/bail": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/bail/-/bail-1.0.5.tgz", + "integrity": "sha512-xFbRxM1tahm08yHBP16MMjVUAvDaBMD38zsM9EMAUN61omwLmKlOpB/Zku5QkjZ8TZ4vn53pj+t518cH0S03RQ==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "node_modules/big.js": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", + "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", + "engines": { + "node": "*" + } + }, + "node_modules/binary-extensions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "engines": { + "node": ">=8" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "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/browserslist": { + "version": "4.22.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.22.2.tgz", + "integrity": "sha512-0UgcrvQmBDvZHFGdYUehrCNIazki7/lUP3kkoi/r3YB2amZbFM9J43ZRkJTXBUZK4gmx56+Sqk9+Vs9mwZx9+A==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "caniuse-lite": "^1.0.30001565", + "electron-to-chromium": "^1.4.601", + "node-releases": "^2.0.14", + "update-browserslist-db": "^1.0.13" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/bser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", + "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", + "dev": true, + "dependencies": { + "node-int64": "^0.4.0" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==" + }, + "node_modules/call-bind": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz", + "integrity": "sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.1", + "set-function-length": "^1.1.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/camelize": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/camelize/-/camelize-1.0.1.tgz", + "integrity": "sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001576", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001576.tgz", + "integrity": "sha512-ff5BdakGe2P3SQsMsiqmt1Lc8221NR1VzHj5jXN5vBny9A6fpze94HiVV/n7XRosOlsShJcvMv5mdnpjOGCEgg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ] + }, + "node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/char-regex": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", + "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/character-entities": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-1.2.4.tgz", + "integrity": "sha512-iBMyeEHxfVnIakwOuDXpVkc54HijNgCyQB2w0VfGQThle6NXn50zU6V/u+LDhxHcDUPojn6Kpga3PTAD8W1bQw==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-legacy": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-1.1.4.tgz", + "integrity": "sha512-3Xnr+7ZFS1uxeiUDvV02wQ+QDbc55o97tIV5zHScSPJpcLm/r0DFPcoY3tYRp+VZukxuMeKgXYmsXQHO05zQeA==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-reference-invalid": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-1.1.4.tgz", + "integrity": "sha512-mKKUkUbhPpQlCOfIuZkvSEgktjPFIsZKRRbC6KWVEMvlzblj3i3asQv5ODsrwt0N3pHAEvjP8KTQPHkp0+6jOg==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/chart.js": { + "version": "3.9.1", + "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-3.9.1.tgz", + "integrity": "sha512-Ro2JbLmvg83gXF5F4sniaQ+lTbSv18E+TIf2cOeiH1Iqd2PGFOtem+DUufMZsCJwFE7ywPOpfXFBwRTGq7dh6w==" + }, + "node_modules/chartjs-adapter-moment": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/chartjs-adapter-moment/-/chartjs-adapter-moment-1.0.1.tgz", + "integrity": "sha512-Uz+nTX/GxocuqXpGylxK19YG4R3OSVf8326D+HwSTsNw1LgzyIGRo+Qujwro1wy6X+soNSnfj5t2vZ+r6EaDmA==", + "peerDependencies": { + "chart.js": ">=3.0.0", + "moment": "^2.10.2" + } + }, + "node_modules/chokidar": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", + "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ], + "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" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chrome-trace-event": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz", + "integrity": "sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg==", + "engines": { + "node": ">=6.0" + } + }, + "node_modules/ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "engines": { + "node": ">=8" + } + }, + "node_modules/cjs-module-lexer": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.2.3.tgz", + "integrity": "sha512-0TNiGstbQmCFwt4akjjBg5pLRTSyj/PkWQ1ZoO2zntmg9yLqSRxwEa4iCfQLGjqhiqBfOJa7W/E8wfGrTDmlZQ==", + "dev": true + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/clone-deep": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", + "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", + "dev": true, + "dependencies": { + "is-plain-object": "^2.0.4", + "kind-of": "^6.0.2", + "shallow-clone": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", + "dev": true, + "engines": { + "iojs": ">= 1.0.0", + "node": ">= 0.12.0" + } + }, + "node_modules/collect-v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.2.tgz", + "integrity": "sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==", + "dev": true + }, + "node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" + }, + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "dev": true + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "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==" + }, + "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==" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "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==" + }, + "node_modules/core-js-compat": { + "version": "3.35.0", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.35.0.tgz", + "integrity": "sha512-5blwFAddknKeNgsjBzilkdQ0+YK8L1PfqPYq40NOYMYFSS38qj+hpTcLLWwpIwA2A5bje/x5jmVn2tzUMg9IVw==", + "dev": true, + "dependencies": { + "browserslist": "^4.22.2" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, + "node_modules/create-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", + "integrity": "sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "prompts": "^2.0.1" + }, + "bin": { + "create-jest": "bin/create-jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/create-jest/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/create-jest/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/create-jest/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/create-jest/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/create-jest/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/create-jest/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/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/css-color-keywords": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz", + "integrity": "sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg==", + "engines": { + "node": ">=4" + } + }, + "node_modules/css-loader": { + "version": "5.2.7", + "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-5.2.7.tgz", + "integrity": "sha512-Q7mOvpBNBG7YrVGMxRxcBJZFL75o+cH2abNASdibkj/fffYD8qWbInZrD0S9ccI6vZclF3DsHE7njGlLtaHbhg==", + "dependencies": { + "icss-utils": "^5.1.0", + "loader-utils": "^2.0.0", + "postcss": "^8.2.15", + "postcss-modules-extract-imports": "^3.0.0", + "postcss-modules-local-by-default": "^4.0.0", + "postcss-modules-scope": "^3.0.0", + "postcss-modules-values": "^4.0.0", + "postcss-value-parser": "^4.1.0", + "schema-utils": "^3.0.0", + "semver": "^7.3.5" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^4.27.0 || ^5.0.0" + } + }, + "node_modules/css-loader/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/css-loader/node_modules/semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/css-loader/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + }, + "node_modules/css-to-react-native": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/css-to-react-native/-/css-to-react-native-3.2.0.tgz", + "integrity": "sha512-e8RKaLXMOFii+02mOlqwjbD00KSEKqblnpO9e++1aXS1fPQOpS1YoqdVHBqPjHNoxeF2mimzVqawm2KCbEdtHQ==", + "dependencies": { + "camelize": "^1.0.0", + "css-color-keywords": "^1.0.0", + "postcss-value-parser": "^4.0.2" + } + }, + "node_modules/css.escape": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", + "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", + "dev": true + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/cssom": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.5.0.tgz", + "integrity": "sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw==", + "dev": true + }, + "node_modules/csstype": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz", + "integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==" + }, + "node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decimal.js": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.4.3.tgz", + "integrity": "sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==", + "dev": true + }, + "node_modules/dedent": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.5.1.tgz", + "integrity": "sha512-+LxW+KLWxu3HW3M2w2ympwtqPrqYRzU8fqi6Fhd18fBALe15blJPI/I4+UHveMVG6lJqB4JNd4UG0S5cnVHwIg==", + "dev": true, + "peerDependencies": { + "babel-plugin-macros": "^3.1.0" + }, + "peerDependenciesMeta": { + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/deep-equal": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-2.2.3.tgz", + "integrity": "sha512-ZIwpnevOurS8bpT4192sqAowWM76JDKSHYzMLty3BZGSswgq6pBaH3DhCSW5xVAZICZyKdOBPjwww5wfgT/6PA==", + "dev": true, + "dependencies": { + "array-buffer-byte-length": "^1.0.0", + "call-bind": "^1.0.5", + "es-get-iterator": "^1.1.3", + "get-intrinsic": "^1.2.2", + "is-arguments": "^1.1.1", + "is-array-buffer": "^3.0.2", + "is-date-object": "^1.0.5", + "is-regex": "^1.1.4", + "is-shared-array-buffer": "^1.0.2", + "isarray": "^2.0.5", + "object-is": "^1.1.5", + "object-keys": "^1.1.1", + "object.assign": "^4.1.4", + "regexp.prototype.flags": "^1.5.1", + "side-channel": "^1.0.4", + "which-boxed-primitive": "^1.0.2", + "which-collection": "^1.0.1", + "which-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/define-data-property": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.1.tgz", + "integrity": "sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.2.1", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/detect-newline": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", + "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "dev": true, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/dom-accessibility-api": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", + "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", + "dev": true + }, + "node_modules/dom-helpers": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", + "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", + "dependencies": { + "@babel/runtime": "^7.8.7", + "csstype": "^3.0.2" + } + }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ] + }, + "node_modules/domexception": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/domexception/-/domexception-4.0.0.tgz", + "integrity": "sha512-A2is4PLG+eeSfoTMA95/s4pvAoSo2mKtiM5jlHkAVewmiO8ISFTFKZjH7UAM1Atli/OT/7JHOrJRJiMKUZKYBw==", + "deprecated": "Use your platform's native DOMException instead", + "dev": true, + "dependencies": { + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz", + "integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.4.623", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.623.tgz", + "integrity": "sha512-lKoz10iCYlP1WtRYdh5MvocQPWVRoI7ysp6qf18bmeBgR8abE6+I2CsfyNKztRDZvhdWc+krKT6wS7Neg8sw3A==" + }, + "node_modules/emittery": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", + "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sindresorhus/emittery?sponsor=1" + } + }, + "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/emojis-list": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", + "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==", + "engines": { + "node": ">= 4" + } + }, + "node_modules/enhanced-resolve": { + "version": "5.15.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.15.0.tgz", + "integrity": "sha512-LXYT42KJ7lpIKECr2mAXIaMldcNCh/7E0KBKOu4KSfkHmP+mZmSs+8V5gBAqisWBy0OO4W5Oyys0GO1Y8KtdKg==", + "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", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/envinfo": { + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.11.0.tgz", + "integrity": "sha512-G9/6xF1FPbIw0TtalAMaVPpiq2aDEuKLXM314jPVAO9r2fo2a4BLqMNkmRS7O/xPPZ+COAhGIz3ETvHEV3eUcg==", + "dev": true, + "bin": { + "envinfo": "dist/cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/es-get-iterator": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/es-get-iterator/-/es-get-iterator-1.1.3.tgz", + "integrity": "sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.3", + "has-symbols": "^1.0.3", + "is-arguments": "^1.1.1", + "is-map": "^2.0.2", + "is-set": "^2.0.2", + "is-string": "^1.0.7", + "isarray": "^2.0.5", + "stop-iteration-iterator": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-module-lexer": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.3.1.tgz", + "integrity": "sha512-JUFAyicQV9mXc3YRxPnDlrfBKpqt6hUYzz9/boprUJHs4e4KVr3XwOF70doO6gwXUor6EWZJAyWAfKki84t20Q==" + }, + "node_modules/escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/escodegen": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", + "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", + "dev": true, + "dependencies": { + "esprima": "^4.0.1", + "estraverse": "^5.2.0", + "esutils": "^2.0.2" + }, + "bin": { + "escodegen": "bin/escodegen.js", + "esgenerate": "bin/esgenerate.js" + }, + "engines": { + "node": ">=6.0" + }, + "optionalDependencies": { + "source-map": "~0.6.1" + } + }, + "node_modules/escodegen/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/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==", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "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==", + "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==", + "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==", + "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/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/exit": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", + "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", + "dev": true, + "dependencies": { + "@jest/expect-utils": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" + }, + "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==" + }, + "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==" + }, + "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/fb-watchman": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", + "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", + "dev": true, + "dependencies": { + "bser": "2.1.1" + } + }, + "node_modules/file-loader": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/file-loader/-/file-loader-6.2.0.tgz", + "integrity": "sha512-qo3glqyTa61Ytg4u73GultjHGjdRyig3tG6lPtyX/jOEJvHif9uB0/OCI2Kif6ctF3caQTW2G5gym21oAsI4pw==", + "dependencies": { + "loader-utils": "^2.0.0", + "schema-utils": "^3.0.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^4.0.0 || ^5.0.0" + } + }, + "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/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==", + "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==", + "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==", + "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==", + "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==", + "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==", + "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==", + "dependencies": { + "find-up": "^6.3.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/flat": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", + "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", + "dev": true, + "bin": { + "flat": "cli.js" + } + }, + "node_modules/for-each": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", + "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", + "dev": true, + "dependencies": { + "is-callable": "^1.1.3" + } + }, + "node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dev": true, + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true + }, + "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/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "dev": true, + "funding": { + "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==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "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.2", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.2.tgz", + "integrity": "sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.2", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "dev": true, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "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==" + }, + "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==", + "engines": { + "node": ">=4" + } + }, + "node_modules/gopd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.1.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/govuk-colours": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/govuk-colours/-/govuk-colours-1.1.0.tgz", + "integrity": "sha512-EcwnP9PsWubmTcsJtLF8w0D5b69j43LwYtSqGyPtO3903J0bbwB2t5ujWZ9UhsbLamuUmigBkNpLItU3/KuFqw==" + }, + "node_modules/govuk-frontend": { + "version": "3.14.0", + "resolved": "https://registry.npmjs.org/govuk-frontend/-/govuk-frontend-3.14.0.tgz", + "integrity": "sha512-y7FTuihCSA8Hty+e9h0uPhCoNanCAN+CLioNFlPmlbeHXpbi09VMyxTcH+XfnMPY4Cp++7096v0rLwwdapTXnA==", + "engines": { + "node": ">= 4.2.0" + } + }, + "node_modules/govuk-react": { + "version": "0.10.6", + "resolved": "https://registry.npmjs.org/govuk-react/-/govuk-react-0.10.6.tgz", + "integrity": "sha512-a10BEPsp5qXtwEfqeWUzljGtNdExODU3KFkZg5BQ0uR5d31lKCmowIaLZ/+OvPw3xJ+KzNhJxAZZme4Cp/+eeQ==", + "dependencies": { + "@govuk-react/back-link": "^0.10.6", + "@govuk-react/breadcrumbs": "^0.10.6", + "@govuk-react/button": "^0.10.6", + "@govuk-react/caption": "^0.10.6", + "@govuk-react/checkbox": "^0.10.6", + "@govuk-react/constants": "^0.10.6", + "@govuk-react/date-field": "^0.10.6", + "@govuk-react/details": "^0.10.6", + "@govuk-react/document-footer-metadata": "^0.10.6", + "@govuk-react/error-summary": "^0.10.6", + "@govuk-react/error-text": "^0.10.6", + "@govuk-react/fieldset": "^0.10.6", + "@govuk-react/file-upload": "^0.10.6", + "@govuk-react/footer": "^0.10.6", + "@govuk-react/form-group": "^0.10.6", + "@govuk-react/global-style": "^0.10.6", + "@govuk-react/grid-col": "^0.10.6", + "@govuk-react/grid-row": "^0.10.6", + "@govuk-react/heading": "^0.10.6", + "@govuk-react/hint-text": "^0.10.6", + "@govuk-react/icons": "^0.10.6", + "@govuk-react/input": "^0.10.6", + "@govuk-react/input-field": "^0.10.6", + "@govuk-react/inset-text": "^0.10.6", + "@govuk-react/label": "^0.10.6", + "@govuk-react/label-text": "^0.10.6", + "@govuk-react/lead-paragraph": "^0.10.6", + "@govuk-react/link": "^0.10.6", + "@govuk-react/list-item": "^0.10.6", + "@govuk-react/loading-box": "^0.10.6", + "@govuk-react/main": "^0.10.6", + "@govuk-react/multi-choice": "^0.10.6", + "@govuk-react/ordered-list": "^0.10.6", + "@govuk-react/page": "^0.10.6", + "@govuk-react/pagination": "^0.10.6", + "@govuk-react/panel": "^0.10.6", + "@govuk-react/paragraph": "^0.10.6", + "@govuk-react/phase-banner": "^0.10.6", + "@govuk-react/radio": "^0.10.6", + "@govuk-react/related-items": "^0.10.6", + "@govuk-react/search-box": "^0.10.6", + "@govuk-react/section-break": "^0.10.6", + "@govuk-react/select": "^0.10.6", + "@govuk-react/skip-link": "^0.10.6", + "@govuk-react/table": "^0.10.6", + "@govuk-react/tabs": "^0.10.6", + "@govuk-react/tag": "^0.10.6", + "@govuk-react/text-area": "^0.10.6", + "@govuk-react/top-nav": "^0.10.6", + "@govuk-react/unordered-list": "^0.10.6", + "@govuk-react/visually-hidden": "^0.10.6", + "@govuk-react/warning-text": "^0.10.6", + "govuk-colours": "^1.1.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0", + "styled-components": ">=5.1" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" + }, + "node_modules/has-bigints": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", + "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "engines": { + "node": ">=4" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.1.tgz", + "integrity": "sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.2.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", + "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", + "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", + "dev": true, + "dependencies": { + "has-symbols": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz", + "integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hex-rgb": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/hex-rgb/-/hex-rgb-4.3.0.tgz", + "integrity": "sha512-Ox1pJVrDCyGHMG9CFg1tmrRUMRPRsAWYc/PinY0XzJU4K7y7vjNoLKIQ7BR5UJMCxNN8EM1MNDmHWA/B3aZUuw==", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/hoist-non-react-statics": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", + "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "dependencies": { + "react-is": "^16.7.0" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true + }, + "node_modules/html-to-react": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/html-to-react/-/html-to-react-1.7.0.tgz", + "integrity": "sha512-b5HTNaTGyOj5GGIMiWVr1k57egAZ/vGy0GGefnCQ1VW5hu9+eku8AXHtf2/DeD95cj/FKBKYa1J7SWBOX41yUQ==", + "dependencies": { + "domhandler": "^5.0", + "htmlparser2": "^9.0", + "lodash.camelcase": "^4.3.0" + }, + "peerDependencies": { + "react": "^0.13.0 || ^0.14.0 || >=15" + } + }, + "node_modules/htmlparser2": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-9.0.0.tgz", + "integrity": "sha512-uxbSI98wmFT/G4P2zXx4OVx04qWUmyFPrD2/CNepa2Zo3GPNaCaaxElDgwUrwYWkK1nr9fft0Ya8dws8coDLLQ==", + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.1.0", + "entities": "^4.5.0" + } + }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "engines": { + "node": ">=10.17.0" + } + }, + "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/icss-utils": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz", + "integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==", + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/immutable": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.4.tgz", + "integrity": "sha512-fsXeu4J4i6WNWSikpI88v/PcVflZz+6kMhUfIwc5SY+poQRPnaf5V7qds6SUyUN3cVxEzuCab7QIoLOQ+DQ1wA==" + }, + "node_modules/import-local": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.1.0.tgz", + "integrity": "sha512-ASB07uLtnDs1o6EHjKpX34BKYDSqnFerfTOJL2HvMqF70LnxpjkzDB8J44oT9pu4AMPkQwf8jl6szgvNd2tRIg==", + "dev": true, + "dependencies": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "dev": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "node_modules/internal-slot": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.6.tgz", + "integrity": "sha512-Xj6dv+PsbtwyPpEflsejS+oIZxmMlV44zAhG479uYu89MsjcYOhCFnNyKrkJrihbsiasQyY0afoCl/9BLR65bg==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.2.2", + "hasown": "^2.0.0", + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/interpret": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-2.2.0.tgz", + "integrity": "sha512-Ju0Bz/cEia55xDwUWEa8+olFpCiQoypjnQySseKtmjNrnps3P+xfpUmGr90T7yjlVJmOtybRvPXhKMbHr+fWnw==", + "dev": true, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-alphabetical": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-1.0.4.tgz", + "integrity": "sha512-DwzsA04LQ10FHTZuL0/grVDk4rFoVH1pjAToYwBrHSxcrBIGQuXrQMtD5U1b0U2XVgKZCTLLP8u2Qxqhy3l2Vg==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-alphanumerical": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-1.0.4.tgz", + "integrity": "sha512-UzoZUr+XfVz3t3v4KyGEniVL9BDRoQtY7tOyrRybkVNjDFWyo1yhXNGrrBTQxp3ib9BLAWs7k2YKBQsFRkZG9A==", + "dependencies": { + "is-alphabetical": "^1.0.0", + "is-decimal": "^1.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-arguments": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz", + "integrity": "sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-array-buffer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.2.tgz", + "integrity": "sha512-y+FyyR/w8vfIRq4eQcM1EYgSTnmHXPqaF+IgzgraytCFq5Xh8lllDVmAZolPJiZttZLeFSINPYMaEJ7/vWUa1w==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.2.0", + "is-typed-array": "^1.1.10" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true + }, + "node_modules/is-bigint": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", + "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==", + "dev": true, + "dependencies": { + "has-bigints": "^1.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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/is-boolean-object": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", + "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-buffer": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz", + "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "engines": { + "node": ">=4" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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==", + "dev": true, + "dependencies": { + "hasown": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", + "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", + "dev": true, + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-decimal": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-1.0.4.tgz", + "integrity": "sha512-RGdriMmQQvZ2aqaQq3awNA6dCGtKpiDFcOzrTWrDAT2MiWrKQVPmxLGHl7Y2nNu6led0kEyoX0enY0qXYsv9zw==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "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, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-generator-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", + "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-hexadecimal": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-1.0.4.tgz", + "integrity": "sha512-gyPJuv83bHMpocVYoqof5VDiZveEoGoFL8m3BXNb2VW8Xs+rz9kqO8LOQ5DH6EsuvilT1ApazU0pyl+ytbPtlw==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-map": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.2.tgz", + "integrity": "sha512-cOZFQQozTha1f4MxLFzlgKYPTyj26picdZTx82hbc/Xf4K/tZOOXSCkMvU4pKioRXGDLJRn0GM7Upe7kR721yg==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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/is-number-object": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz", + "integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==", + "dev": true, + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-plain-obj": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", + "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", + "engines": { + "node": ">=8" + } + }, + "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/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true + }, + "node_modules/is-regex": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", + "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-set": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.2.tgz", + "integrity": "sha512-+2cnTEZeY5z/iXGbLhPrOAaK/Mau5k5eXq9j14CpRTftq0pAJu2MwVRSZhyZWBzx3o6X795Lz6Bpb6R0GKf37g==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-shared-array-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz", + "integrity": "sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-string": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", + "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", + "dev": true, + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-symbol": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", + "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", + "dev": true, + "dependencies": { + "has-symbols": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.12.tgz", + "integrity": "sha512-Z14TF2JNG8Lss5/HMqt0//T9JeHXttXy5pH/DBU4vi98ozO2btxzq9MwYDZYnKwU8nRsz/+GVFVRDq3DkVuSPg==", + "dev": true, + "dependencies": { + "which-typed-array": "^1.1.11" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakmap": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.1.tgz", + "integrity": "sha512-NSBR4kH5oVj1Uwvv970ruUkCV7O1mzgVFO4/rev2cLRda9Tm9HrL70ZPut4rOHgY0FNrUu9BCbXA2sdQ+x0chA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakset": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.2.tgz", + "integrity": "sha512-t2yVvttHkQktwnNNmBQ98AhENLdPUTDTE21uPqAQ0ARwQfGeQKRVS0NNurH7bTf7RrvcVn1OOge45CnBeHCSmg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "node_modules/isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.1.tgz", + "integrity": "sha512-EAMEJBsYuyyztxMxW3g7ugGPkrZsV57v0Hmv3mm1uQsmB+QnZuepg731CRaIgeUVSdmsTngOkSnauNF8p7FIhA==", + "dev": true, + "dependencies": { + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-instrument/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-instrument/node_modules/semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-instrument/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report/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/istanbul-lib-source-maps": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", + "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", + "dev": true, + "dependencies": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.6.tgz", + "integrity": "sha512-TLgnMkKg3iTDsQ9PbPTdpfAK2DzjF9mqUG7RMgcQl8oFjad8ob4laGxv5XV5U9MAfx8D6tSJiUyuAwzLicaxlg==", + "dev": true, + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", + "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", + "dev": true, + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/types": "^29.6.3", + "import-local": "^3.0.2", + "jest-cli": "^29.7.0" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-changed-files": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.7.0.tgz", + "integrity": "sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==", + "dev": true, + "dependencies": { + "execa": "^5.0.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-changed-files/node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-changed-files/node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-circus": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.7.0.tgz", + "integrity": "sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==", + "dev": true, + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "co": "^4.6.0", + "dedent": "^1.0.0", + "is-generator-fn": "^2.0.0", + "jest-each": "^29.7.0", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0", + "pretty-format": "^29.7.0", + "pure-rand": "^6.0.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-circus/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/jest-circus/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/jest-circus/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/jest-circus/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/jest-circus/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-circus/node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-circus/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/jest-circus/node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-cli": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.7.0.tgz", + "integrity": "sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==", + "dev": true, + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "create-jest": "^29.7.0", + "exit": "^0.1.2", + "import-local": "^3.0.2", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "yargs": "^17.3.1" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-cli/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/jest-cli/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/jest-cli/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/jest-cli/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/jest-cli/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-cli/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/jest-config": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.7.0.tgz", + "integrity": "sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==", + "dev": true, + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/test-sequencer": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-jest": "^29.7.0", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "deepmerge": "^4.2.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-circus": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "micromatch": "^4.0.4", + "parse-json": "^5.2.0", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@types/node": "*", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/jest-config/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/jest-config/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/jest-config/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/jest-config/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/jest-config/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-config/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/jest-diff": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", + "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", + "dev": true, + "dependencies": { + "chalk": "^4.0.0", + "diff-sequences": "^29.6.3", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-diff/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/jest-diff/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/jest-diff/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/jest-diff/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/jest-diff/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-diff/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/jest-docblock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.7.0.tgz", + "integrity": "sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==", + "dev": true, + "dependencies": { + "detect-newline": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-each": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.7.0.tgz", + "integrity": "sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "jest-util": "^29.7.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-each/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/jest-each/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/jest-each/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/jest-each/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/jest-each/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-each/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/jest-environment-jsdom": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-29.7.0.tgz", + "integrity": "sha512-k9iQbsf9OyOfdzWH8HDmrRT0gSIcX+FLNW7IQq94tFX0gynPwqDTW0Ho6iMVNjGz/nb+l/vW3dWM2bbLLpkbXA==", + "dev": true, + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/jsdom": "^20.0.0", + "@types/node": "*", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0", + "jsdom": "^20.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "canvas": "^2.5.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jest-environment-jsdom/node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dev": true, + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/jest-environment-jsdom/node_modules/cssstyle": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-2.3.0.tgz", + "integrity": "sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A==", + "dev": true, + "dependencies": { + "cssom": "~0.3.6" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-environment-jsdom/node_modules/cssstyle/node_modules/cssom": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz", + "integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==", + "dev": true + }, + "node_modules/jest-environment-jsdom/node_modules/data-urls": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-3.0.2.tgz", + "integrity": "sha512-Jy/tj3ldjZJo63sVAvg6LHt2mHvl4V6AgRAmNDtLdm7faqtsx+aJG42rsyCo9JCoRVKwPFzKlIPx3DIibwSIaQ==", + "dev": true, + "dependencies": { + "abab": "^2.0.6", + "whatwg-mimetype": "^3.0.0", + "whatwg-url": "^11.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/jest-environment-jsdom/node_modules/html-encoding-sniffer": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz", + "integrity": "sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==", + "dev": true, + "dependencies": { + "whatwg-encoding": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/jest-environment-jsdom/node_modules/http-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", + "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", + "dev": true, + "dependencies": { + "@tootallnate/once": "2", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/jest-environment-jsdom/node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "dev": true, + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/jest-environment-jsdom/node_modules/jsdom": { + "version": "20.0.3", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-20.0.3.tgz", + "integrity": "sha512-SYhBvTh89tTfCD/CRdSOm13mOBa42iTaTyfyEWBdKcGdPxPtLFBXuHR8XHb33YNYaP+lLbmSvBTsnoesCNJEsQ==", + "dev": true, + "dependencies": { + "abab": "^2.0.6", + "acorn": "^8.8.1", + "acorn-globals": "^7.0.0", + "cssom": "^0.5.0", + "cssstyle": "^2.3.0", + "data-urls": "^3.0.2", + "decimal.js": "^10.4.2", + "domexception": "^4.0.0", + "escodegen": "^2.0.0", + "form-data": "^4.0.0", + "html-encoding-sniffer": "^3.0.0", + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.1", + "is-potential-custom-element-name": "^1.0.1", + "nwsapi": "^2.2.2", + "parse5": "^7.1.1", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^4.1.2", + "w3c-xmlserializer": "^4.0.0", + "webidl-conversions": "^7.0.0", + "whatwg-encoding": "^2.0.0", + "whatwg-mimetype": "^3.0.0", + "whatwg-url": "^11.0.0", + "ws": "^8.11.0", + "xml-name-validator": "^4.0.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "canvas": "^2.5.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jest-environment-jsdom/node_modules/tr46": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-3.0.0.tgz", + "integrity": "sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==", + "dev": true, + "dependencies": { + "punycode": "^2.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/jest-environment-jsdom/node_modules/w3c-xmlserializer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-4.0.0.tgz", + "integrity": "sha512-d+BFHzbiCx6zGfz0HyQ6Rg69w9k19nviJspaj4yNscGjrHu94sVP+aRm75yEbCh+r2/yR+7q6hux9LVtbuTGBw==", + "dev": true, + "dependencies": { + "xml-name-validator": "^4.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/jest-environment-jsdom/node_modules/whatwg-encoding": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz", + "integrity": "sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==", + "dev": true, + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/jest-environment-jsdom/node_modules/whatwg-mimetype": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz", + "integrity": "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/jest-environment-jsdom/node_modules/whatwg-url": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-11.0.0.tgz", + "integrity": "sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==", + "dev": true, + "dependencies": { + "tr46": "^3.0.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/jest-environment-jsdom/node_modules/xml-name-validator": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz", + "integrity": "sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/jest-environment-node": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz", + "integrity": "sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==", + "dev": true, + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "dev": true, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-haste-map": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz", + "integrity": "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@types/graceful-fs": "^4.1.3", + "@types/node": "*", + "anymatch": "^3.0.3", + "fb-watchman": "^2.0.0", + "graceful-fs": "^4.2.9", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "micromatch": "^4.0.4", + "walker": "^1.0.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "optionalDependencies": { + "fsevents": "^2.3.2" + } + }, + "node_modules/jest-haste-map/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-haste-map/node_modules/jest-worker": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", + "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", + "dev": true, + "dependencies": { + "@types/node": "*", + "jest-util": "^29.7.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-haste-map/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/jest-leak-detector": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz", + "integrity": "sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==", + "dev": true, + "dependencies": { + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-matcher-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", + "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", + "dev": true, + "dependencies": { + "chalk": "^4.0.0", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-matcher-utils/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/jest-matcher-utils/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/jest-matcher-utils/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/jest-matcher-utils/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/jest-matcher-utils/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-matcher-utils/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/jest-message-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", + "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.6.3", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-message-util/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/jest-message-util/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/jest-message-util/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/jest-message-util/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/jest-message-util/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-message-util/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/jest-mock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz", + "integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-pnp-resolver": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", + "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", + "dev": true, + "engines": { + "node": ">=6" + }, + "peerDependencies": { + "jest-resolve": "*" + }, + "peerDependenciesMeta": { + "jest-resolve": { + "optional": true + } + } + }, + "node_modules/jest-regex-util": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", + "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", + "dev": true, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.7.0.tgz", + "integrity": "sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==", + "dev": true, + "dependencies": { + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-pnp-resolver": "^1.2.2", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "resolve": "^1.20.0", + "resolve.exports": "^2.0.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve-dependencies": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.7.0.tgz", + "integrity": "sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==", + "dev": true, + "dependencies": { + "jest-regex-util": "^29.6.3", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve/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/jest-resolve/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/jest-resolve/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/jest-resolve/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/jest-resolve/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-resolve/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/jest-runner": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.7.0.tgz", + "integrity": "sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==", + "dev": true, + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/environment": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "graceful-fs": "^4.2.9", + "jest-docblock": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-leak-detector": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-resolve": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-util": "^29.7.0", + "jest-watcher": "^29.7.0", + "jest-worker": "^29.7.0", + "p-limit": "^3.1.0", + "source-map-support": "0.5.13" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runner/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/jest-runner/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/jest-runner/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/jest-runner/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/jest-runner/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-runner/node_modules/jest-worker": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", + "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", + "dev": true, + "dependencies": { + "@types/node": "*", + "jest-util": "^29.7.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runner/node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/jest-runner/node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-runner/node_modules/source-map-support": { + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", + "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", + "dev": true, + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/jest-runner/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/jest-runner/node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-runtime": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.7.0.tgz", + "integrity": "sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==", + "dev": true, + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/globals": "^29.7.0", + "@jest/source-map": "^29.6.3", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "cjs-module-lexer": "^1.0.0", + "collect-v8-coverage": "^1.0.0", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0", + "strip-bom": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runtime/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/jest-runtime/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/jest-runtime/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/jest-runtime/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/jest-runtime/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-runtime/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/jest-snapshot": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.7.0.tgz", + "integrity": "sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==", + "dev": true, + "dependencies": { + "@babel/core": "^7.11.6", + "@babel/generator": "^7.7.2", + "@babel/plugin-syntax-jsx": "^7.7.2", + "@babel/plugin-syntax-typescript": "^7.7.2", + "@babel/types": "^7.3.3", + "@jest/expect-utils": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0", + "chalk": "^4.0.0", + "expect": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "natural-compare": "^1.4.0", + "pretty-format": "^29.7.0", + "semver": "^7.5.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot/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/jest-snapshot/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/jest-snapshot/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/jest-snapshot/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/jest-snapshot/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-snapshot/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jest-snapshot/node_modules/semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jest-snapshot/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/jest-snapshot/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-util/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/jest-util/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/jest-util/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/jest-util/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/jest-util/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-util/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/jest-validate": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.7.0.tgz", + "integrity": "sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "camelcase": "^6.2.0", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "leven": "^3.1.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate/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/jest-validate/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-validate/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/jest-validate/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/jest-validate/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/jest-validate/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-validate/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/jest-watcher": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.7.0.tgz", + "integrity": "sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==", + "dev": true, + "dependencies": { + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "jest-util": "^29.7.0", + "string-length": "^4.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-watcher/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/jest-watcher/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/jest-watcher/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/jest-watcher/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/jest-watcher/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-watcher/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/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==", + "dependencies": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/jest-worker/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" + }, + "node_modules/js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dev": true, + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=4" + } + }, + "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==" + }, + "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==" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "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/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/klona": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/klona/-/klona-2.0.6.tgz", + "integrity": "sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA==", + "engines": { + "node": ">= 8" + } + }, + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true + }, + "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==", + "engines": { + "node": ">=6.11.5" + } + }, + "node_modules/loader-utils": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", + "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", + "dependencies": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^2.1.2" + }, + "engines": { + "node": ">=8.9.0" + } + }, + "node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true + }, + "node_modules/lodash.assign": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.assign/-/lodash.assign-4.2.0.tgz", + "integrity": "sha512-hFuH8TY+Yji7Eja3mGiuAxBqLagejScbG8GbG0j6o9vzn0YL14My+ktnqtZgFTosKymC9/44wP6s7xyuLfnClw==" + }, + "node_modules/lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==" + }, + "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.defaults": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", + "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==" + }, + "node_modules/lodash.foreach": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.foreach/-/lodash.foreach-4.5.0.tgz", + "integrity": "sha512-aEXTF4d+m05rVOAUG3z4vZZ4xVexLKZGF0lIxuHZ1Hplpk/3B6Z1+/ICICYRLm7c41Z2xiejbkCkJoTlypoXhQ==" + }, + "node_modules/lodash.frompairs": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.frompairs/-/lodash.frompairs-4.0.1.tgz", + "integrity": "sha512-dvqe2I+cO5MzXCMhUnfYFa9MD+/760yx2aTAN1lqEcEkf896TxgrX373igVdqSJj6tQd0jnSLE1UMuKufqqxFw==" + }, + "node_modules/lodash.get": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", + "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==" + }, + "node_modules/lodash.topairs": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.topairs/-/lodash.topairs-4.3.0.tgz", + "integrity": "sha512-qrRMbykBSEGdOgQLJJqVSdPWMD7Q+GJJ5jMRfQYb+LTLsw3tYVIabnCzRqTJb2WTo17PG5gNzXuFaZgYH/9SAQ==" + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "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==", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/lz-string": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", + "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", + "dev": true, + "bin": { + "lz-string": "bin/bin.js" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/make-dir/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/makeerror": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", + "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", + "dev": true, + "dependencies": { + "tmpl": "1.0.5" + } + }, + "node_modules/mdast-add-list-metadata": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mdast-add-list-metadata/-/mdast-add-list-metadata-1.0.1.tgz", + "integrity": "sha512-fB/VP4MJ0LaRsog7hGPxgOrSL3gE/2uEdZyDuSEnKCv/8IkYHiDkIQSbChiJoHyxZZXZ9bzckyRk+vNxFzh8rA==", + "dependencies": { + "unist-util-visit-parents": "1.1.2" + } + }, + "node_modules/mdast-util-from-markdown": { + "version": "0.8.5", + "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-0.8.5.tgz", + "integrity": "sha512-2hkTXtYYnr+NubD/g6KGBS/0mFmBcifAsI0yIWRiRo0PjVs6SSOSOdtzbp6kSGnShDN6G5aWZpKQ2lWRy27mWQ==", + "dependencies": { + "@types/mdast": "^3.0.0", + "mdast-util-to-string": "^2.0.0", + "micromark": "~2.11.0", + "parse-entities": "^2.0.0", + "unist-util-stringify-position": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-string": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-2.0.0.tgz", + "integrity": "sha512-AW4DRS3QbBayY/jJmD8437V1Gombjf8RSOUCMFBuo5iHi58AGEgVCKQ+ezHkZZDpAQS75hcBMpLqjpJTjtUL7w==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "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==" + }, + "node_modules/micromark": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-2.11.4.tgz", + "integrity": "sha512-+WoovN/ppKolQOFIAajxi7Lu9kInbPxFuTBVEavFcL8eAfVstoc5MocPmqBeAdBOJV00uaVjegzH4+MA0DN/uA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "debug": "^4.0.0", + "parse-entities": "^2.0.0" + } + }, + "node_modules/micromatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", + "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "dev": true, + "dependencies": { + "braces": "^3.0.2", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "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==", + "engines": { + "node": ">= 0.6" + } + }, + "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==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/mini-css-extract-plugin": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-1.6.2.tgz", + "integrity": "sha512-WhDvO3SjGm40oV5y26GjMJYjd2UMqrLAGKy5YS2/3QKJy2F7jgynuHTir/tgUUOiNQu5saXHdc8reo7YuhhT4Q==", + "dependencies": { + "loader-utils": "^2.0.0", + "schema-utils": "^3.0.0", + "webpack-sources": "^1.1.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^4.4.0 || ^5.0.0" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/moment": { + "version": "2.29.4", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz", + "integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==", + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/multi-input-input": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/multi-input-input/-/multi-input-input-0.0.3.tgz", + "integrity": "sha512-jzpCxDqvyi7eqdgPykDlv11y1CWOS02T+oaSrLlzVeG3mSrQbNi75/QWsWkrgwxvWWvAkq+e0Pk59uN1YaYGLg==" + }, + "node_modules/nanoid": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz", + "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true + }, + "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==" + }, + "node_modules/node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", + "dev": true + }, + "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==" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/nwsapi": { + "version": "2.2.7", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.7.tgz", + "integrity": "sha512-ub5E4+FBPKwAZx0UwIQOjYWGHTEq5sPqHQNRN8Z9e4A7u3Tj1weLJsL59yH9vmvqEtBHaOmT6cYQKIZOxp35FQ==", + "dev": true + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", + "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-is": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.5.tgz", + "integrity": "sha512-3cyDsyHgtmi7I7DfSSI2LDp6SK2lwvtbg0p0R1e0RvTqF5ceGx+K2dfSjm1bKDMVCFEDAQvy+o8c6a7VujOddw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.5.tgz", + "integrity": "sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.5", + "define-properties": "^1.2.1", + "has-symbols": "^1.0.3", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-entities": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-2.0.0.tgz", + "integrity": "sha512-kkywGpCcRYhqQIchaWqZ875wzpS/bMKhz5HnN3p7wveJTkTtyAB/AlnS0f8DFSqYW1T82t6yEAkEcB+A1I3MbQ==", + "dependencies": { + "character-entities": "^1.0.0", + "character-entities-legacy": "^1.0.0", + "character-reference-invalid": "^1.0.0", + "is-alphanumerical": "^1.0.0", + "is-decimal": "^1.0.0", + "is-hexadecimal": "^1.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parse5": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz", + "integrity": "sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==", + "dev": true, + "dependencies": { + "entities": "^4.4.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "node_modules/picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pirates": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", + "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/polished": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/polished/-/polished-4.2.2.tgz", + "integrity": "sha512-Sz2Lkdxz6F2Pgnpi9U5Ng/WdWAUZxmHrNPoVlm3aAemxoy2Qy7LGjQg4uf8qKelDAUW94F4np3iH2YPf2qefcQ==", + "dependencies": { + "@babel/runtime": "^7.17.8" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/postcss": { + "version": "8.4.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "nanoid": "^3.3.6", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-modules-extract-imports": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.0.0.tgz", + "integrity": "sha512-bdHleFnP3kZ4NYDhuGlVK+CMrQ/pqUm8bx/oGL93K6gVwiclvX5x0n76fYMKuIGKzlABOy13zsvqjb0f92TEXw==", + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-local-by-default": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.3.tgz", + "integrity": "sha512-2/u2zraspoACtrbFRnTijMiQtb4GW4BvatjaG/bCjYQo8kLTdevCUlwuBHx2sCnSyrI3x3qj4ZK1j5LQBgzmwA==", + "dependencies": { + "icss-utils": "^5.0.0", + "postcss-selector-parser": "^6.0.2", + "postcss-value-parser": "^4.1.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-scope": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.0.0.tgz", + "integrity": "sha512-hncihwFA2yPath8oZ15PZqvWGkWf+XUfQgUGamS4LqoP1anQLOsOJw0vr7J7IwLpoY9fatA2qiGUGmuZL0Iqlg==", + "dependencies": { + "postcss-selector-parser": "^6.0.4" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-values": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz", + "integrity": "sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==", + "dependencies": { + "icss-utils": "^5.0.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.0.13", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.13.tgz", + "integrity": "sha512-EaV1Gl4mUEV4ddhDnv/xtj7sxwrwxdetHdWUGnT4VJQf+4d05v6lHYZr8N573k5Z0BViss7BDhfWtKS3+sfAqQ==", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" + }, + "node_modules/preact": { + "version": "8.5.3", + "resolved": "https://registry.npmjs.org/preact/-/preact-8.5.3.tgz", + "integrity": "sha512-O3kKP+1YdgqHOFsZF2a9JVdtqD+RPzCQc3rP+Ualf7V6rmRDchZ9MJbiGTT7LuyqFKZqlHSOyO/oMFmI2lVTsw==", + "hasInstallScript": true + }, + "node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/pretty-format/node_modules/react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", + "dev": true + }, + "node_modules/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "dev": true, + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/psl": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", + "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==", + "dev": true + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "engines": { + "node": ">=6" + } + }, + "node_modules/pure-rand": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.0.4.tgz", + "integrity": "sha512-LA0Y9kxMYv47GIPJy6MI84fqTd2HmYZI83W/kM/SkKfDlajnZYfmXFTxkbY+xSBPkLJxltMa9hIkmdc29eguMA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ] + }, + "node_modules/querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", + "dev": true + }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/react": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", + "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", + "integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.0" + }, + "peerDependencies": { + "react": "^18.2.0" + } + }, + "node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" + }, + "node_modules/react-markdown": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-5.0.3.tgz", + "integrity": "sha512-jDWOc1AvWn0WahpjW6NK64mtx6cwjM4iSsLHJPNBqoAgGOVoIdJMqaKX4++plhOtdd4JksdqzlDibgPx6B/M2w==", + "dependencies": { + "@types/mdast": "^3.0.3", + "@types/unist": "^2.0.3", + "html-to-react": "^1.3.4", + "mdast-add-list-metadata": "1.0.1", + "prop-types": "^15.7.2", + "react-is": "^16.8.6", + "remark-parse": "^9.0.0", + "unified": "^9.0.0", + "unist-util-visit": "^2.0.0", + "xtend": "^4.0.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + }, + "peerDependencies": { + "@types/react": ">=16", + "react": ">=16" + } + }, + "node_modules/react-shallow-renderer": { + "version": "16.15.0", + "resolved": "https://registry.npmjs.org/react-shallow-renderer/-/react-shallow-renderer-16.15.0.tgz", + "integrity": "sha512-oScf2FqQ9LFVQgA73vr86xl2NaOIX73rh+YFqcOp68CWj56tSfgtGKrEbyhCj0rSijyG9M1CYprTh39fBi5hzA==", + "dev": true, + "dependencies": { + "object-assign": "^4.1.1", + "react-is": "^16.12.0 || ^17.0.0 || ^18.0.0" + }, + "peerDependencies": { + "react": "^16.0.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/react-test-renderer": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-test-renderer/-/react-test-renderer-18.2.0.tgz", + "integrity": "sha512-JWD+aQ0lh2gvh4NM3bBM42Kx+XybOxCpgYK7F8ugAlpaTSnWsX+39Z4XkOykGZAHrjwwTZT3x3KxswVWxHPUqA==", + "dev": true, + "dependencies": { + "react-is": "^18.2.0", + "react-shallow-renderer": "^16.15.0", + "scheduler": "^0.23.0" + }, + "peerDependencies": { + "react": "^18.2.0" + } + }, + "node_modules/react-test-renderer/node_modules/react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", + "dev": true + }, + "node_modules/react-transition-group": { + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", + "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==", + "dependencies": { + "@babel/runtime": "^7.5.5", + "dom-helpers": "^5.0.1", + "loose-envify": "^1.4.0", + "prop-types": "^15.6.2" + }, + "peerDependencies": { + "react": ">=16.6.0", + "react-dom": ">=16.6.0" + } + }, + "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/rechoir": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.7.1.tgz", + "integrity": "sha512-/njmZ8s1wVeR6pjTZ+0nCnv8SpZNRMT2D1RLOJQESlYFDBvwpTA4KWJpZ+sBJ4+vhjILRcK7JIFdGCdxEAAitg==", + "dev": true, + "dependencies": { + "resolve": "^1.9.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/redent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "dev": true, + "dependencies": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "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.1.1", + "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.1.1.tgz", + "integrity": "sha512-X007RyZLsCJVVrjgEFVpLUTZwyOZk3oiL75ZcuYjlIWd6rNJtOjkBwQc5AsRrpbKVkxN6sklw/k/9m2jJYOf8Q==", + "dev": true, + "dependencies": { + "regenerate": "^1.4.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/regenerator-runtime": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz", + "integrity": "sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA==" + }, + "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/regexp.prototype.flags": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.1.tgz", + "integrity": "sha512-sy6TXMN+hnP/wMy+ISxg3krXx7BAtWVO4UouuCN/ziM9UEne0euamVNafDfvC83bRNr95y0V5iijeDQFUNpvrg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "set-function-name": "^2.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/regexpu-core": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-5.3.2.tgz", + "integrity": "sha512-RAM5FlZz+Lhmo7db9L298p2vHP5ZywrVXmVXpmAD9GuL5MPH6t9ROw1iA/wfHkQ76Qe7AaPF0nGuim96/IrQMQ==", + "dev": true, + "dependencies": { + "@babel/regjsgen": "^0.8.0", + "regenerate": "^1.4.2", + "regenerate-unicode-properties": "^10.1.0", + "regjsparser": "^0.9.1", + "unicode-match-property-ecmascript": "^2.0.0", + "unicode-match-property-value-ecmascript": "^2.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/regjsparser": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.9.1.tgz", + "integrity": "sha512-dQUtn90WanSNl+7mQKcXAgZxvUe7Z0SqXlgzv0za4LwiUhyzBC58yQO3liFoUgu8GiJVInAhJjkj1N0EtQ5nkQ==", + "dev": true, + "dependencies": { + "jsesc": "~0.5.0" + }, + "bin": { + "regjsparser": "bin/parser" + } + }, + "node_modules/regjsparser/node_modules/jsesc": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", + "integrity": "sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA==", + "dev": true, + "bin": { + "jsesc": "bin/jsesc" + } + }, + "node_modules/remark-parse": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-9.0.0.tgz", + "integrity": "sha512-geKatMwSzEXKHuzBNU1z676sGcDcFoChMK38TgdHJNAYfFtsfHDQG7MoJAjs6sgYMqyLduCYWDIWZIxiPeafEw==", + "dependencies": { + "mdast-util-from-markdown": "^0.8.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "engines": { + "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==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "dev": true + }, + "node_modules/resolve": { + "version": "1.22.8", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", + "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "dev": true, + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve.exports": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.2.tgz", + "integrity": "sha512-X2UW6Nw3n/aMgDVy+0rSqgHlv39WZAlZrXCdnbyEiKm17DSqHX4MmQMaST3FbeWR5FTuRcUwYAziZajji0Y7mg==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "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.69.5", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.69.5.tgz", + "integrity": "sha512-qg2+UCJibLr2LCVOt3OlPhr/dqVHWOa9XtZf2OjbLs/T4VPSJ00udtgJxH3neXZm+QqX8B+3cU7RaLqp1iVfcQ==", + "dependencies": { + "chokidar": ">=3.0.0 <4.0.0", + "immutable": "^4.0.0", + "source-map-js": ">=0.6.2 <2.0.0" + }, + "bin": { + "sass": "sass.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-loader": { + "version": "12.6.0", + "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-12.6.0.tgz", + "integrity": "sha512-oLTaH0YCtX4cfnJZxKSLAyglED0naiYfNG1iXfU5w1LNZ+ukoA5DtyDIN5zmKVZwYNJP4KRc5Y3hkWga+7tYfA==", + "dependencies": { + "klona": "^2.0.4", + "neo-async": "^2.6.2" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "fibers": ">= 3.1.0", + "node-sass": "^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0", + "sass": "^1.3.0", + "sass-embedded": "*", + "webpack": "^5.0.0" + }, + "peerDependenciesMeta": { + "fibers": { + "optional": true + }, + "node-sass": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + } + } + }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, + "node_modules/scheduler": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz", + "integrity": "sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "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==", + "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/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/serialize-javascript": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.1.tgz", + "integrity": "sha512-owoXEFjWRllis8/M1Q+Cw5k8ZH40e3zhp/ovX+Xr/vi1qj6QesbyXXViFbpNvWvPNAD62SutwEXavefrLJWj7w==", + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "node_modules/set-function-length": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.0.tgz", + "integrity": "sha512-4DBHDoyHlM1IRPGYcoxexgh67y4ueR53FKV1yyxwFMY7aCqcN/38M1+SwZ/qJQ8iLv7+ck385ot4CcisOAPT9w==", + "dev": true, + "dependencies": { + "define-data-property": "^1.1.1", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.2", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + } }, - "loader-utils": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", - "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", - "requires": { - "big.js": "^5.2.2", - "emojis-list": "^3.0.0", - "json5": "^2.1.2" + "node_modules/set-function-name": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.1.tgz", + "integrity": "sha512-tMNCiqYVkXIZgc2Hnoy2IvC/f8ezc5koaRFkCjrpWzGpCd3qbZXPzVy9MAZzK1ch/X0jvSkojys3oqJN0qCmdA==", + "dev": true, + "dependencies": { + "define-data-property": "^1.0.1", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" } }, - "locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "node_modules/shallow-clone": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", + "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==", "dev": true, - "requires": { - "p-locate": "^4.1.0" + "dependencies": { + "kind-of": "^6.0.2" + }, + "engines": { + "node": ">=8" } }, - "lodash.assign": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/lodash.assign/-/lodash.assign-4.2.0.tgz", - "integrity": "sha512-hFuH8TY+Yji7Eja3mGiuAxBqLagejScbG8GbG0j6o9vzn0YL14My+ktnqtZgFTosKymC9/44wP6s7xyuLfnClw==" + "node_modules/shallowequal": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz", + "integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==" }, - "lodash.defaults": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", - "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==" + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } }, - "lodash.foreach": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.foreach/-/lodash.foreach-4.5.0.tgz", - "integrity": "sha512-aEXTF4d+m05rVOAUG3z4vZZ4xVexLKZGF0lIxuHZ1Hplpk/3B6Z1+/ICICYRLm7c41Z2xiejbkCkJoTlypoXhQ==" + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "engines": { + "node": ">=8" + } }, - "lodash.frompairs": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/lodash.frompairs/-/lodash.frompairs-4.0.1.tgz", - "integrity": "sha512-dvqe2I+cO5MzXCMhUnfYFa9MD+/760yx2aTAN1lqEcEkf896TxgrX373igVdqSJj6tQd0jnSLE1UMuKufqqxFw==" + "node_modules/side-channel": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", + "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.0", + "get-intrinsic": "^1.0.2", + "object-inspect": "^1.9.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, - "lodash.get": { - "version": "4.4.2", - "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", - "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==" + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true }, - "lodash.topairs": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/lodash.topairs/-/lodash.topairs-4.3.0.tgz", - "integrity": "sha512-qrRMbykBSEGdOgQLJJqVSdPWMD7Q+GJJ5jMRfQYb+LTLsw3tYVIabnCzRqTJb2WTo17PG5gNzXuFaZgYH/9SAQ==" + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "dev": true }, - "lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "requires": { - "yallist": "^4.0.0" + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "engines": { + "node": ">=8" } }, - "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==" - }, - "mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==" + "node_modules/source-list-map": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.1.tgz", + "integrity": "sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw==" }, - "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==", - "requires": { - "mime-db": "1.52.0" + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "engines": { + "node": ">=0.10.0" } }, - "mini-css-extract-plugin": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-1.6.2.tgz", - "integrity": "sha512-WhDvO3SjGm40oV5y26GjMJYjd2UMqrLAGKy5YS2/3QKJy2F7jgynuHTir/tgUUOiNQu5saXHdc8reo7YuhhT4Q==", - "requires": { - "loader-utils": "^2.0.0", - "schema-utils": "^3.0.0", - "webpack-sources": "^1.1.0" + "node_modules/source-map-js": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", + "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", + "engines": { + "node": ">=0.10.0" } }, - "moment": { - "version": "2.29.4", - "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz", - "integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==" - }, - "nanoid": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz", - "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==" + "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==", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } }, - "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==" + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true }, - "node-releases": { + "node_modules/stack-utils": { "version": "2.0.6", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.6.tgz", - "integrity": "sha512-PiVXnNuFm5+iYkLBNeq5211hvO38y63T0i2KKh2KnUs3RpzJ+JtODFjkD8yjLwnDkTYF1eKXheUwdssR+NRZdg==" - }, - "normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==" - }, - "p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", "dev": true, - "requires": { - "p-try": "^2.0.0" + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" } }, - "p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "node_modules/stack-utils/node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", "dev": true, - "requires": { - "p-limit": "^2.2.0" + "engines": { + "node": ">=8" } }, - "p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "dev": true - }, - "path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true - }, - "path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true - }, - "path-parse": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true - }, - "picocolors": { + "node_modules/stop-iteration-iterator": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", - "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==" + "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.0.0.tgz", + "integrity": "sha512-iCGQj+0l0HOdZ2AEeBADlsRC+vsnDsZsbdSiH1yNSjcfKM7fdpCMfqAL/dwF5BLiw/XhRft/Wax6zQbhq2BcjQ==", + "dev": true, + "dependencies": { + "internal-slot": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + } }, - "picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==" + "node_modules/string-length": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", + "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", + "dev": true, + "dependencies": { + "char-regex": "^1.0.2", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + } }, - "pkg-dir": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", - "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "node_modules/string-width": { + "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, - "requires": { - "find-up": "^4.0.0" + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" } }, - "postcss": { - "version": "8.4.31", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", - "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", - "requires": { - "nanoid": "^3.3.6", - "picocolors": "^1.0.0", - "source-map-js": "^1.0.2" + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" } }, - "postcss-modules-extract-imports": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.0.0.tgz", - "integrity": "sha512-bdHleFnP3kZ4NYDhuGlVK+CMrQ/pqUm8bx/oGL93K6gVwiclvX5x0n76fYMKuIGKzlABOy13zsvqjb0f92TEXw==", - "requires": {} + "node_modules/strip-ansi/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "engines": { + "node": ">=8" + } }, - "postcss-modules-local-by-default": { + "node_modules/strip-bom": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.0.tgz", - "integrity": "sha512-sT7ihtmGSF9yhm6ggikHdV0hlziDTX7oFoXtuVWeDd3hHObNkcHRo9V3yg7vCAY7cONyxJC/XXCmmiHHcvX7bQ==", - "requires": { - "icss-utils": "^5.0.0", - "postcss-selector-parser": "^6.0.2", - "postcss-value-parser": "^4.1.0" + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "dev": true, + "engines": { + "node": ">=8" } }, - "postcss-modules-scope": { + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/strip-indent": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.0.0.tgz", - "integrity": "sha512-hncihwFA2yPath8oZ15PZqvWGkWf+XUfQgUGamS4LqoP1anQLOsOJw0vr7J7IwLpoY9fatA2qiGUGmuZL0Iqlg==", - "requires": { - "postcss-selector-parser": "^6.0.4" + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, + "dependencies": { + "min-indent": "^1.0.0" + }, + "engines": { + "node": ">=8" } }, - "postcss-modules-values": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz", - "integrity": "sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==", - "requires": { - "icss-utils": "^5.0.0" + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/style-loader": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-3.3.3.tgz", + "integrity": "sha512-53BiGLXAcll9maCYtZi2RCQZKa8NQQai5C4horqKyRmHj9H7QmcUyucrH+4KW/gBQbXM2AsB0axoEcFZPlfPcw==", + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.0.0" } }, - "postcss-selector-parser": { - "version": "6.0.11", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.11.tgz", - "integrity": "sha512-zbARubNdogI9j7WY4nQJBiNqQf3sLS3wCP4WfOidu+p28LofJqDH1tcXypGrcmMHhDk2t9wGhCsYe/+szLTy1g==", - "requires": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" + "node_modules/styled-components": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/styled-components/-/styled-components-6.1.0.tgz", + "integrity": "sha512-VWNfYYBuXzuLS/QYEeoPgMErP26WL+dX9//rEh80B2mmlS1yRxRxuL5eax4m6ybYEUoHWlTy2XOU32767mlMkg==", + "dependencies": { + "@emotion/is-prop-valid": "^1.2.1", + "@emotion/unitless": "^0.8.0", + "@types/stylis": "^4.0.2", + "css-to-react-native": "^3.2.0", + "csstype": "^3.1.2", + "postcss": "^8.4.31", + "shallowequal": "^1.1.0", + "stylis": "^4.3.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/styled-components" + }, + "peerDependencies": { + "react": ">= 16.8.0", + "react-dom": ">= 16.8.0" } }, - "postcss-value-parser": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" - }, - "preact": { - "version": "8.5.3", - "resolved": "https://registry.npmjs.org/preact/-/preact-8.5.3.tgz", - "integrity": "sha512-O3kKP+1YdgqHOFsZF2a9JVdtqD+RPzCQc3rP+Ualf7V6rmRDchZ9MJbiGTT7LuyqFKZqlHSOyO/oMFmI2lVTsw==" + "node_modules/stylis": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.0.tgz", + "integrity": "sha512-E87pIogpwUsUwXw7dNyU4QDjdgVMy52m+XEOPEKUn161cCzWjjhPSQhByfd1CcNvrOLnXQ6OnnZDwnJrz/Z4YQ==" }, - "punycode": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", - "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" + "node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } }, - "randombytes": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", - "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", - "requires": { - "safe-buffer": "^5.1.0" + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "requires": { - "picomatch": "^2.2.1" + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true + }, + "node_modules/tapable": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", + "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", + "engines": { + "node": ">=6" } }, - "rechoir": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.7.1.tgz", - "integrity": "sha512-/njmZ8s1wVeR6pjTZ+0nCnv8SpZNRMT2D1RLOJQESlYFDBvwpTA4KWJpZ+sBJ4+vhjILRcK7JIFdGCdxEAAitg==", - "dev": true, - "requires": { - "resolve": "^1.9.0" + "node_modules/terser": { + "version": "5.24.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.24.0.tgz", + "integrity": "sha512-ZpGR4Hy3+wBEzVEnHvstMvqpD/nABNelQn/z2r0fjVWGQsN3bpOLzQlqDxmb4CDZnXq5lpjnQ+mHQLAOpfM5iw==", + "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" } }, - "resolve": { - "version": "1.22.1", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz", - "integrity": "sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==", - "dev": true, - "requires": { - "is-core-module": "^2.9.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" + "node_modules/terser-webpack-plugin": { + "version": "5.3.9", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.9.tgz", + "integrity": "sha512-ZuXsqE07EcggTWQjXUj+Aot/OMcD0bMKGgF63f7UxYcu5/AJF53aIpK1YoP5xR9l6s/Hy2b+t1AM0bLNPRuhwA==", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.17", + "jest-worker": "^27.4.5", + "schema-utils": "^3.1.1", + "serialize-javascript": "^6.0.1", + "terser": "^5.16.8" + }, + "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 + } } }, - "resolve-cwd": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", - "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", "dev": true, - "requires": { - "resolve-from": "^5.0.0" + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" } }, - "resolve-from": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", - "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "node_modules/tmpl": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", + "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", "dev": true }, - "safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" - }, - "sass": { - "version": "1.56.1", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.56.1.tgz", - "integrity": "sha512-VpEyKpyBPCxE7qGDtOcdJ6fFbcpOM+Emu7uZLxVrkX8KVU/Dp5UF7WLvzqRuUhB6mqqQt1xffLoG+AndxTZrCQ==", - "requires": { - "chokidar": ">=3.0.0 <4.0.0", - "immutable": "^4.0.0", - "source-map-js": ">=0.6.2 <2.0.0" + "node_modules/to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", + "engines": { + "node": ">=4" } }, - "sass-loader": { - "version": "12.6.0", - "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-12.6.0.tgz", - "integrity": "sha512-oLTaH0YCtX4cfnJZxKSLAyglED0naiYfNG1iXfU5w1LNZ+ukoA5DtyDIN5zmKVZwYNJP4KRc5Y3hkWga+7tYfA==", - "requires": { - "klona": "^2.0.4", - "neo-async": "^2.6.2" + "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" } }, - "schema-utils": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.1.tgz", - "integrity": "sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw==", - "requires": { - "@types/json-schema": "^7.0.8", - "ajv": "^6.12.5", - "ajv-keywords": "^3.5.2" + "node_modules/tough-cookie": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.3.tgz", + "integrity": "sha512-aX/y5pVRkfRnfmuX+OdbSdXvPe6ieKX/G2s7e98f4poJHnqH3281gDPm/metm6E/WRamfx7WC4HUqkWHfQHprw==", + "dev": true, + "dependencies": { + "psl": "^1.1.33", + "punycode": "^2.1.1", + "universalify": "^0.2.0", + "url-parse": "^1.5.3" + }, + "engines": { + "node": ">=6" } }, - "semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", - "requires": { - "lru-cache": "^6.0.0" + "node_modules/trough": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/trough/-/trough-1.0.5.tgz", + "integrity": "sha512-rvuRbTarPXmMb79SmzEp8aqXNKcK+y0XaB298IXueQ8I2PsrATcPBCSPyK/dDNa2iWOhKlfNnOjdAOTBU/nkFA==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" } }, - "serialize-javascript": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz", - "integrity": "sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==", - "requires": { - "randombytes": "^2.1.0" - } + "node_modules/tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" }, - "shallow-clone": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", - "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==", + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", "dev": true, - "requires": { - "kind-of": "^6.0.2" + "engines": { + "node": ">=4" } }, - "shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", "dev": true, - "requires": { - "shebang-regex": "^3.0.0" + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true + "node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" }, - "source-list-map": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.1.tgz", - "integrity": "sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw==" + "node_modules/unicode-canonical-property-names-ecmascript": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz", + "integrity": "sha512-yY5PpDlfVIU5+y/BSCxAJRBIS1Zc2dDG3Ujq+sR0U+JjUevW2JhocOF+soROYDSaAezOzOKuyyixhD6mBknSmQ==", + "dev": true, + "engines": { + "node": ">=4" + } }, - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" + "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" + } }, - "source-map-js": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", - "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==" + "node_modules/unicode-match-property-value-ecmascript": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.1.0.tgz", + "integrity": "sha512-qxkjQt6qjg/mYscYMC0XKRn3Rh0wFPlfxB0xkt9CfyTvpX1Ra0+rAmdX2QyAobptSEvuy4RtpPRui6XkV+8wjA==", + "dev": true, + "engines": { + "node": ">=4" + } }, - "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==", - "requires": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" + "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" } }, - "strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "requires": { - "ansi-regex": "^5.0.1" - }, + "node_modules/unified": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/unified/-/unified-9.2.2.tgz", + "integrity": "sha512-Sg7j110mtefBD+qunSLO1lqOEKdrwBFBrR6Qd8f4uwkhWNlbkaqwHse6e7QvD3AP/MNoJdEDLaf8OxYyoWgorQ==", "dependencies": { - "ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==" - } + "bail": "^1.0.0", + "extend": "^3.0.0", + "is-buffer": "^2.0.0", + "is-plain-obj": "^2.0.0", + "trough": "^1.0.0", + "vfile": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "style-loader": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-3.3.1.tgz", - "integrity": "sha512-GPcQ+LDJbrcxHORTRes6Jy2sfvK2kS6hpSfI/fXhPt+spVzxF6LJ1dHLN9zIGmVaaP044YKaIatFaufENRiDoQ==", - "requires": {} - }, - "supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", - "requires": { - "has-flag": "^4.0.0" + "node_modules/unist-util-is": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-4.1.0.tgz", + "integrity": "sha512-ZOQSsnce92GrxSqlnEEseX0gi7GH9zTJZ0p9dtu87WRb/37mMPO2Ilx1s/t9vBHrFhbgweUwb+t7cIn5dxPhZg==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "supports-preserve-symlinks-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "dev": true + "node_modules/unist-util-stringify-position": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-2.0.3.tgz", + "integrity": "sha512-3faScn5I+hy9VleOq/qNbAd6pAx7iH5jYBMS9I1HgQVijz/4mv5Bvw5iw1sC/90CODiKo81G/ps8AJrISn687g==", + "dependencies": { + "@types/unist": "^2.0.2" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } }, - "tapable": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", - "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==" - }, - "terser": { - "version": "5.15.1", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.15.1.tgz", - "integrity": "sha512-K1faMUvpm/FBxjBXud0LWVAGxmvoPbZbfTCYbSgaaYQaIXI3/TdI7a7ZGA73Zrou6Q8Zmz3oeUTsp/dj+ag2Xw==", - "requires": { - "@jridgewell/source-map": "^0.3.2", - "acorn": "^8.5.0", - "commander": "^2.20.0", - "source-map-support": "~0.5.20" + "node_modules/unist-util-visit": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-2.0.3.tgz", + "integrity": "sha512-iJ4/RczbJMkD0712mGktuGpm/U4By4FfDonL7N/9tATGIF4imikjOuagyMY53tnZq3NP6BcmlrHhEKAfGWjh7Q==", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-is": "^4.0.0", + "unist-util-visit-parents": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "terser-webpack-plugin": { - "version": "5.3.6", - "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.6.tgz", - "integrity": "sha512-kfLFk+PoLUQIbLmB1+PZDMRSZS99Mp+/MHqDNmMA6tOItzRt+Npe3E+fsMs5mfcM0wCtrrdU387UnV+vnSffXQ==", - "requires": { - "@jridgewell/trace-mapping": "^0.3.14", - "jest-worker": "^27.4.5", - "schema-utils": "^3.1.1", - "serialize-javascript": "^6.0.0", - "terser": "^5.14.1" + "node_modules/unist-util-visit-parents": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-1.1.2.tgz", + "integrity": "sha512-yvo+MMLjEwdc3RhhPYSximset7rwjMrdt9E41Smmvg25UQIenzrN83cRnF1JMzoMi9zZOQeYXHSDf7p+IQkW3Q==" + }, + "node_modules/unist-util-visit/node_modules/unist-util-visit-parents": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-3.1.1.tgz", + "integrity": "sha512-1KROIZWo6bcMrZEwiH2UrXDyalAa0uqzWCxCJj6lPOvTve2WkfgCytoDTPaMnodXh1WrXOq0haVYHj99ynJlsg==", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-is": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "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==", - "requires": { - "is-number": "^7.0.0" + "node_modules/universalify": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", + "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", + "dev": true, + "engines": { + "node": ">= 4.0.0" } }, - "update-browserslist-db": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.10.tgz", - "integrity": "sha512-OztqDenkfFkbSG+tRxBeAnCVPckDBcvibKd35yDONx6OU8N7sqgwc7rCbkJ/WcYtVRZ4ba68d6byhC21GFh7sQ==", - "requires": { + "node_modules/update-browserslist-db": { + "version": "1.0.13", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz", + "integrity": "sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { "escalade": "^3.1.1", "picocolors": "^1.0.0" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" } }, - "uri-js": { + "node_modules/uri-js": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "requires": { + "dependencies": { "punycode": "^2.1.0" } }, - "util-deprecate": { + "node_modules/url-parse": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", + "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", + "dev": true, + "dependencies": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, + "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" }, - "watchpack": { + "node_modules/v8-to-istanbul": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.2.0.tgz", + "integrity": "sha512-/EH/sDgxU2eGxajKdwLCDmQ4FWq+kpi3uCmBGpw1xJtnAxEjlD8j8PEiGWpCIMIs3ciNAgH0d3TTJiUkYzyZjA==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^2.0.0" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/vfile": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-4.2.1.tgz", + "integrity": "sha512-O6AE4OskCG5S1emQ/4gl8zK586RqA3srz3nfK/Viy0UPToBc5Trp9BVFb1u0CjsKrAWwnpr4ifM/KBXPWwJbCA==", + "dependencies": { + "@types/unist": "^2.0.0", + "is-buffer": "^2.0.0", + "unist-util-stringify-position": "^2.0.0", + "vfile-message": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-message": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-2.0.4.tgz", + "integrity": "sha512-DjssxRGkMvifUOJre00juHoP9DPWuzjxKuMDrhNbk2TdaYYBNMStsNhEOt3idrtI12VQYM/1+iM0KOzXi4pxwQ==", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-stringify-position": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/walker": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", + "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", + "dev": true, + "dependencies": { + "makeerror": "1.0.12" + } + }, + "node_modules/watchpack": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz", "integrity": "sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==", - "requires": { + "dependencies": { "glob-to-regexp": "^0.4.1", "graceful-fs": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "dev": true, + "engines": { + "node": ">=12" } }, - "webpack": { - "version": "5.76.0", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.76.0.tgz", - "integrity": "sha512-l5sOdYBDunyf72HW8dF23rFtWq/7Zgvt/9ftMof71E/yUb1YLOBmTgA2K4vQthB3kotMrSj609txVE0dnr2fjA==", - "requires": { + "node_modules/webpack": { + "version": "5.89.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.89.0.tgz", + "integrity": "sha512-qyfIC10pOr70V+jkmud8tMfajraGCZMBWJtrmuBymQKCrLTRejBI8STDp1MCyZu/QTdZSeacCQYpYNQVOzX5kw==", + "dependencies": { "@types/eslint-scope": "^3.7.3", - "@types/estree": "^0.0.51", - "@webassemblyjs/ast": "1.11.1", - "@webassemblyjs/wasm-edit": "1.11.1", - "@webassemblyjs/wasm-parser": "1.11.1", + "@types/estree": "^1.0.0", + "@webassemblyjs/ast": "^1.11.5", + "@webassemblyjs/wasm-edit": "^1.11.5", + "@webassemblyjs/wasm-parser": "^1.11.5", "acorn": "^8.7.1", - "acorn-import-assertions": "^1.7.6", + "acorn-import-assertions": "^1.9.0", "browserslist": "^4.14.5", "chrome-trace-event": "^1.0.2", - "enhanced-resolve": "^5.10.0", - "es-module-lexer": "^0.9.0", + "enhanced-resolve": "^5.15.0", + "es-module-lexer": "^1.2.1", "eslint-scope": "5.1.1", "events": "^3.2.0", "glob-to-regexp": "^0.4.1", @@ -3150,25 +10900,33 @@ "loader-runner": "^4.2.0", "mime-types": "^2.1.27", "neo-async": "^2.6.2", - "schema-utils": "^3.1.0", + "schema-utils": "^3.2.0", "tapable": "^2.1.1", - "terser-webpack-plugin": "^5.1.3", + "terser-webpack-plugin": "^5.3.7", "watchpack": "^2.4.0", "webpack-sources": "^3.2.3" }, - "dependencies": { - "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==" + "bin": { + "webpack": "bin/webpack.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependenciesMeta": { + "webpack-cli": { + "optional": true } } }, - "webpack-bundle-tracker": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/webpack-bundle-tracker/-/webpack-bundle-tracker-1.7.0.tgz", - "integrity": "sha512-CwdFpeLcc4uBurgmtszCHW6ISJ5RN70jvGWnvUG/7LQS1gmv2g6IdYw9A8DvT4rydHzWnRFwqVsx1hN1IebkQA==", - "requires": { + "node_modules/webpack-bundle-tracker": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/webpack-bundle-tracker/-/webpack-bundle-tracker-1.8.1.tgz", + "integrity": "sha512-X1qtXG4ue92gjWQO2VhLVq8HDEf9GzUWE0OQyAQObVEZsFB1SUtSQ7o47agF5WZIaHfJUTKak4jEErU0gzoPcQ==", + "dependencies": { "lodash.assign": "^4.2.0", "lodash.defaults": "^4.2.0", "lodash.foreach": "^4.5.0", @@ -3178,12 +10936,12 @@ "strip-ansi": "^6.0.0" } }, - "webpack-cli": { + "node_modules/webpack-cli": { "version": "4.10.0", "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-4.10.0.tgz", "integrity": "sha512-NLhDfH/h4O6UOy+0LSso42xvYypClINuMNBVVzX4vX98TmTaTUxwRbXdhucbFMd2qLaCTcLq/PdYrvi8onw90w==", "dev": true, - "requires": { + "dependencies": { "@discoveryjs/json-ext": "^0.5.0", "@webpack-cli/configtest": "^1.2.0", "@webpack-cli/info": "^1.5.0", @@ -3197,53 +10955,300 @@ "rechoir": "^0.7.0", "webpack-merge": "^5.7.3" }, - "dependencies": { - "commander": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", - "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", - "dev": true + "bin": { + "webpack-cli": "bin/cli.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "4.x.x || 5.x.x" + }, + "peerDependenciesMeta": { + "@webpack-cli/generators": { + "optional": true + }, + "@webpack-cli/migrate": { + "optional": true + }, + "webpack-bundle-analyzer": { + "optional": true + }, + "webpack-dev-server": { + "optional": true } } }, - "webpack-merge": { - "version": "5.8.0", - "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.8.0.tgz", - "integrity": "sha512-/SaI7xY0831XwP6kzuwhKWVKDP9t1QY1h65lAFLbZqMPIuYcD9QAW4u9STIbU9kaJbPBB/geU/gLr1wDjOhQ+Q==", + "node_modules/webpack-cli/node_modules/commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "dev": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/webpack-merge": { + "version": "5.10.0", + "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.10.0.tgz", + "integrity": "sha512-+4zXKdx7UnO+1jaN4l2lHVD+mFvnlZQP/6ljaJVb4SZiwIKeUnrT5l0gkT8z+n4hKpC+jpOv6O9R+gLtag7pSA==", "dev": true, - "requires": { + "dependencies": { "clone-deep": "^4.0.1", + "flat": "^5.0.2", "wildcard": "^2.0.0" + }, + "engines": { + "node": ">=10.0.0" } }, - "webpack-sources": { + "node_modules/webpack-sources": { "version": "1.4.3", "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-1.4.3.tgz", "integrity": "sha512-lgTS3Xhv1lCOKo7SA5TjKXMjpSM4sBjNV5+q2bqesbSPs5FjGmU6jjtBSkX9b4qW87vDIsCIlUPOEhbZrMdjeQ==", - "requires": { + "dependencies": { "source-list-map": "^2.0.0", "source-map": "~0.6.1" } }, - "which": { + "node_modules/webpack/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==", + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", "dev": true, - "requires": { + "dependencies": { "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" } }, - "wildcard": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.0.tgz", - "integrity": "sha512-JcKqAHLPxcdb9KM49dufGXn2x3ssnfjbcaQdLlfZsL9rH9wgDQjUtDxbo8NE0F6SFvydeu1VhZe7hZuHsB2/pw==", + "node_modules/which-boxed-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", + "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", + "dev": true, + "dependencies": { + "is-bigint": "^1.0.1", + "is-boolean-object": "^1.1.0", + "is-number-object": "^1.0.4", + "is-string": "^1.0.5", + "is-symbol": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-collection": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.1.tgz", + "integrity": "sha512-W8xeTUwaln8i3K/cY1nGXzdnVZlidBcagyNFtBdD5kxnb4TvGKR7FfSIS3mYpwWS1QUCutfKz8IY8RjftB0+1A==", + "dev": true, + "dependencies": { + "is-map": "^2.0.1", + "is-set": "^2.0.1", + "is-weakmap": "^2.0.1", + "is-weakset": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.13.tgz", + "integrity": "sha512-P5Nra0qjSncduVPEAr7xhoF5guty49ArDTwzJ/yNuPIbZppyRxFQsRCWrocxIY+CnMVG+qfbU2FmDKyvSGClow==", + "dev": true, + "dependencies": { + "available-typed-arrays": "^1.0.5", + "call-bind": "^1.0.4", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/wildcard": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.1.tgz", + "integrity": "sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==", "dev": true }, - "yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + "node_modules/wrap-ansi": { + "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": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi/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/wrap-ansi/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/wrap-ansi/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/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true + }, + "node_modules/write-file-atomic": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", + "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", + "dev": true, + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.7" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/ws": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.16.0.tgz", + "integrity": "sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ==", + "dev": true, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "engines": { + "node": ">=0.4" + } + }, + "node_modules/y18n": { + "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": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==" + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/yocto-queue": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.0.0.tgz", + "integrity": "sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==", + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } } } } diff --git a/package.json b/package.json index 55c4647b2..93fbf62a7 100644 --- a/package.json +++ b/package.json @@ -11,18 +11,27 @@ "npm": "^10.3.0" }, "dependencies": { + "@babel/preset-env": "^7.23.7", + "@babel/preset-react": "^7.23.3", + "@babel/core": "^7.23.2", + "@types/styled-components": "^5.1.29", "accessible-autocomplete": "^2.0.3", "ansi-regex": "^6.0.1", + "babel-loader": "^9.1.3", "chart.js": "^3.9.1", "chartjs-adapter-moment": "^1.0.0", "css-loader": "^5.2.6", "file-loader": "^6.2.0", "govuk-frontend": "^3.13.0", + "govuk-react": "^0.10.6", "mini-css-extract-plugin": "^1.6.0", "moment": "^2.29.4", + "react": "^18.2.0", + "react-dom": "^18.2.0", "sass": "^1.38.2", "sass-loader": "^12.1.0", "style-loader": "^3.0.0", + "styled-components": "^6.1.0", "webpack": "^5.76.0", "webpack-bundle-tracker": "^1.1.0", "webpack-cli": "^4.7.2" @@ -32,9 +41,15 @@ "build": "npx webpack-cli --config webpack.config.js --stats-children", "clean": "rm -f ./run/static/webpack_bundles/*", "heroku-prebuild": "", - "heroku-postbuild": "npm run build" + "heroku-postbuild": "npm run build", + "test": "jest" }, "devDependencies": { - "webpack-cli": "^4.7.2" + "@testing-library/jest-dom": "^6.2.1", + "@testing-library/react": "^14.1.2", + "babel-jest": "^29.7.0", + "jest": "^29.7.0", + "jest-environment-jsdom": "^29.7.0", + "react-test-renderer": "^18.2.0" } -} +} \ No newline at end of file diff --git a/pii-secret-exclude.txt b/pii-secret-exclude.txt index fb246ad85..bef569cf4 100644 --- a/pii-secret-exclude.txt +++ b/pii-secret-exclude.txt @@ -25,3 +25,6 @@ common/jinja2/common/500.jinja settings/envs/docker.env Dockerfile common/jinja2/common/accessibility.jinja +common/static/common/js/components/QuotaOriginFormset/tests/__snapshots__/index.test.js.snap +common/migrations/0001_initial.py +common/migrations/0001_initial.py diff --git a/publishing/tests/test_migrations.py b/publishing/tests/test_migrations.py index d601cb9a1..775c2e13f 100644 --- a/publishing/tests/test_migrations.py +++ b/publishing/tests/test_migrations.py @@ -12,7 +12,7 @@ def test_add_packaged_workbasket_to_loading_report(migrator): ("publishing", "0007_crowndependenciespublishingtask_error"), ) - User = old_state.apps.get_model("auth", "User") + User = old_state.apps.get_model("common", "User") WorkBasket = old_state.apps.get_model("workbaskets", "WorkBasket") PackagedWorkBasket = old_state.apps.get_model("publishing", "PackagedWorkBasket") LoadingReport = old_state.apps.get_model("publishing", "LoadingReport") diff --git a/publishing/tests/test_views.py b/publishing/tests/test_views.py index f79cd2903..bc0dfc646 100644 --- a/publishing/tests/test_views.py +++ b/publishing/tests/test_views.py @@ -54,18 +54,10 @@ def test_packaged_workbasket_create_without_permission(client): def test_packaged_workbasket_create_form_no_rule_check( valid_user_client, - session_workbasket, + user_workbasket, ): """Tests that Packaged WorkBasket Create returns 302 and redirects work basket summary when no rule check has been executed.""" - session = valid_user_client.session - session["workbasket"] = { - "id": session_workbasket.pk, - "status": session_workbasket.status, - "title": session_workbasket.title, - "error_count": session_workbasket.tracked_model_check_errors.count(), - } - session.save() create_url = reverse("publishing:packaged-workbasket-queue-ui-create") form_data = { @@ -78,7 +70,7 @@ def test_packaged_workbasket_create_form_no_rule_check( assert ( not PackagedWorkBasket.objects.all_queued() .filter( - workbasket=session_workbasket, + workbasket=user_workbasket, ) .exists() ) @@ -89,12 +81,14 @@ def test_packaged_workbasket_create_form_no_rule_check( assert response.url[: len(response_url)] == response_url -def test_packaged_workbasket_create_form(valid_user_client): +def test_packaged_workbasket_create_form(client, valid_user): """Tests that Packaged WorkBasket Create returns 302 and redirects to confirm create page on success.""" + client.force_login(valid_user) workbasket = factories.WorkBasketFactory.create( status=WorkflowStatus.EDITING, ) + workbasket.assign_to_user(valid_user) with workbasket.new_transaction() as transaction: TransactionCheckFactory.create( transaction=transaction, @@ -102,14 +96,6 @@ def test_packaged_workbasket_create_form(valid_user_client): completed=True, ) - session = valid_user_client.session - session["workbasket"] = { - "id": workbasket.pk, - "status": workbasket.status, - "title": workbasket.title, - "error_count": workbasket.tracked_model_check_errors.count(), - } - session.save() # creating a packaged workbasket in the queue first_packaged_work_basket = factories.PackagedWorkBasketFactory() create_url = reverse("publishing:packaged-workbasket-queue-ui-create") @@ -119,7 +105,7 @@ def test_packaged_workbasket_create_form(valid_user_client): "jira_url": "www.fakejiraticket.com", } - response = valid_user_client.post(create_url, form_data) + response = client.post(create_url, form_data) assert response.status_code == 302 assert "/confirm-create/" in response.url @@ -136,19 +122,21 @@ def test_packaged_workbasket_create_form(valid_user_client): # Only compare the response URL up to the query string. assert response.url[: len(response_url)] == response_url assert second_packaged_work_basket.theme == form_data["theme"] - # Check in, form field may not contain full URL contianed within URLField object + # Check in, form field may not contain full URL contained within URLField object assert form_data["jira_url"] in second_packaged_work_basket.jira_url assert first_packaged_work_basket.position > 0 assert first_packaged_work_basket.position < second_packaged_work_basket.position -def test_packaged_workbasket_create_form_rule_check_violations(valid_user_client): +def test_packaged_workbasket_create_form_rule_check_violations(client, valid_user): """Tests that Packaged WorkBasket Create returns 302 and redirects to workbasket detail page when there are rule check violations on workbasket.""" + client.force_login(valid_user) workbasket = factories.WorkBasketFactory.create( status=WorkflowStatus.EDITING, ) + workbasket.assign_to_user(valid_user) with workbasket.new_transaction() as transaction: TransactionCheckFactory.create( transaction=transaction, @@ -156,14 +144,6 @@ def test_packaged_workbasket_create_form_rule_check_violations(valid_user_client completed=True, ) - session = valid_user_client.session - session["workbasket"] = { - "id": workbasket.pk, - "status": workbasket.status, - "title": workbasket.title, - "error_count": workbasket.tracked_model_check_errors.count(), - } - session.save() create_url = reverse("publishing:packaged-workbasket-queue-ui-create") form_data = { @@ -171,7 +151,7 @@ def test_packaged_workbasket_create_form_rule_check_violations(valid_user_client "jira_url": "www.fakejiraticket.com", } - response = valid_user_client.post(create_url, form_data) + response = client.post(create_url, form_data) # assert the packaged workbasket does not exist assert ( not PackagedWorkBasket.objects.all_queued() @@ -187,13 +167,15 @@ def test_packaged_workbasket_create_form_rule_check_violations(valid_user_client assert response.url[: len(response_url)] == response_url -def test_create_duplicate_awaiting_instances(valid_user_client, valid_user): +def test_create_duplicate_awaiting_instances(client, valid_user): """Tests that Packaged WorkBasket Create returns 302 and redirects to packaged workbasket queue page when trying to package a workbasket that is already on the queue.""" + client.force_login(valid_user) workbasket = factories.WorkBasketFactory.create( status=WorkflowStatus.EDITING, ) + workbasket.assign_to_user(valid_user) with workbasket.new_transaction() as transaction: TransactionCheckFactory.create( transaction=transaction, @@ -201,15 +183,6 @@ def test_create_duplicate_awaiting_instances(valid_user_client, valid_user): completed=True, ) - session = valid_user_client.session - session["workbasket"] = { - "id": workbasket.pk, - "status": workbasket.status, - "title": workbasket.title, - "error_count": workbasket.tracked_model_check_errors.count(), - } - session.save() - workbasket.queue(valid_user.pk, settings.TRANSACTION_SCHEMA) workbasket.save() existing_packaged = factories.PackagedWorkBasketFactory.create( @@ -218,7 +191,6 @@ def test_create_duplicate_awaiting_instances(valid_user_client, valid_user): workbasket.dequeue() workbasket.save() - """Test that a WorkBasket cannot enter the packaging queue more than once.""" create_url = reverse("publishing:packaged-workbasket-queue-ui-create") @@ -228,7 +200,7 @@ def test_create_duplicate_awaiting_instances(valid_user_client, valid_user): "jira_url": "www.fakejiraticket.com", } - response = valid_user_client.post(create_url, form_data) + response = client.post(create_url, form_data) assert response.status_code == 302 response_url = reverse("publishing:packaged-workbasket-queue-ui-list") diff --git a/quotas/constants.py b/quotas/constants.py index 6a3621f56..95463a779 100644 --- a/quotas/constants.py +++ b/quotas/constants.py @@ -1 +1,3 @@ QUOTA_ORIGIN_EXCLUSIONS_FORMSET_PREFIX = "quota-origin-exclusions-formset" +QUOTA_ORIGINS_FORMSET_PREFIX = "origins" +QUOTA_EXCLUSIONS_FORMSET_PREFIX = "exclusions" diff --git a/quotas/forms.py b/quotas/forms.py index 74dbbc316..ccb296efc 100644 --- a/quotas/forms.py +++ b/quotas/forms.py @@ -9,6 +9,7 @@ from crispy_forms_gds.layout import Size from crispy_forms_gds.layout import Submit from django import forms +from django.core.exceptions import NON_FIELD_ERRORS from django.core.exceptions import ValidationError from django.template.loader import render_to_string from django.urls import reverse_lazy @@ -25,7 +26,9 @@ from measures.models import MeasurementUnit from quotas import models from quotas import validators +from quotas.constants import QUOTA_EXCLUSIONS_FORMSET_PREFIX from quotas.constants import QUOTA_ORIGIN_EXCLUSIONS_FORMSET_PREFIX +from quotas.constants import QUOTA_ORIGINS_FORMSET_PREFIX CATEGORY_HELP_TEXT = "Categories are required for the TAP database but will not appear as a TARIC3 object in your workbasket" SAFEGUARD_HELP_TEXT = ( @@ -132,24 +135,45 @@ class Meta: category = forms.ChoiceField( label="", - choices=[], # set in __init__ + choices=validators.QuotaCategory.choices, error_messages={"invalid_choice": "Please select a valid category"}, ) + def clean_category(self): + value = self.cleaned_data.get("category") + # the widget is disabled and data is not submitted. fall back to instance value + if not value: + return self.instance.category + if ( + self.instance.category == validators.QuotaCategory.SAFEGUARD + and value != validators.QuotaCategory.SAFEGUARD + ): + raise ValidationError(SAFEGUARD_HELP_TEXT) + return value + def __init__(self, *args, **kwargs): + self.request = kwargs.pop("request") + self.geo_area_options = kwargs.pop("geo_area_options") + self.existing_origins = kwargs.pop("existing_origins") super().__init__(*args, **kwargs) self.init_fields() self.set_initial_data(*args, **kwargs) - self.init_layout() + self.init_layout(self.request) def set_initial_data(self, *args, **kwargs): self.fields["category"].initial = self.instance.category def init_fields(self): if self.instance.category == validators.QuotaCategory.SAFEGUARD: + self.fields["category"].required = False self.fields["category"].widget = forms.Select( + choices=[ + ( + validators.QuotaCategory.SAFEGUARD.value, + validators.QuotaCategory.SAFEGUARD.label, + ), + ], attrs={"disabled": True}, - choices=validators.QuotaCategory.choices, ) self.fields["category"].help_text = SAFEGUARD_HELP_TEXT else: @@ -158,7 +182,123 @@ def init_fields(self): self.fields["start_date"].help_text = START_DATE_HELP_TEXT - def init_layout(self): + def get_origins_initial(self): + initial = [ + { + "id": o.pk, # unique identifier used by react + "pk": o.pk, + "exclusions": [ + {"pk": e.pk, "id": e.excluded_geographical_area.pk} + for e in o.quotaordernumberoriginexclusion_set.current() + ], + "geographical_area": o.geographical_area.pk, + "start_date_0": o.valid_between.lower.day, + "start_date_1": o.valid_between.lower.month, + "start_date_2": o.valid_between.lower.year, + "end_date_0": o.valid_between.upper.day + if o.valid_between.upper + else "", + "end_date_1": o.valid_between.upper.month + if o.valid_between.upper + else "", + "end_date_2": o.valid_between.upper.year + if o.valid_between.upper + else "", + } + for o in self.existing_origins + ] + # if we just submitted the form, overwrite initial with submitted data + # this prevents newly added origin data being cleared if the form does not pass validation + if self.data.get("submit"): + new_data = unprefix_formset_data( + QUOTA_ORIGINS_FORMSET_PREFIX, + self.data.copy(), + ) + initial = new_data + + return initial + + def add_extra_error(self, field, error): + """ + A modification of Django's add_error method that allows us to add data + to self._errors under custom keys that are not field names or + NON_FIELD_ERRORS. + + Used to pass errors to the React form. + """ + if not isinstance(error, ValidationError): + error = ValidationError(error) + + if hasattr(error, "error_dict"): + if field is not None: + raise TypeError( + "The argument `field` must be `None` when the `error` " + "argument contains errors for multiple fields.", + ) + else: + error = error.error_dict + else: + error = {field or NON_FIELD_ERRORS: error.error_list} + + for field, error_list in error.items(): + if field not in self.errors: + self._errors[field] = self.error_class() + self._errors[field].extend(error_list) + if field in self.cleaned_data: + del self.cleaned_data[field] + + def clean(self): + # unprefix origins formset + submitted_data = unprefix_formset_data( + QUOTA_ORIGINS_FORMSET_PREFIX, + self.data.copy(), + ) + # for each origin, unprefix exclusions formset + for i, origin_data in enumerate(submitted_data): + exclusions = unprefix_formset_data( + QUOTA_EXCLUSIONS_FORMSET_PREFIX, + origin_data.copy(), + ) + submitted_data[i]["exclusions"] = exclusions + + self.cleaned_data["origins"] = [] + + for i, origin_data in enumerate(submitted_data): + # instantiate a form per origin data to do validation + origin_form = QuotaOrderNumberOriginUpdateReactForm( + data=origin_data, + initial=origin_data, + ) + + cleaned_exclusions = [] + + for exclusion in origin_data["exclusions"]: + exclusion_form = QuotaOriginExclusionsReactForm( + data=exclusion, + initial=exclusion, + ) + if not exclusion_form.is_valid(): + for field, e in exclusion_form.errors.as_data().items(): + self.add_extra_error( + f"{QUOTA_ORIGINS_FORMSET_PREFIX}-{i}-{field}", + e, + ) + else: + cleaned_exclusions.append(exclusion_form.cleaned_data) + + if not origin_form.is_valid(): + for field, e in origin_form.errors.as_data().items(): + self.add_extra_error( + f"{QUOTA_ORIGINS_FORMSET_PREFIX}-{i}-{field}", + e, + ) + else: + origin_form.cleaned_data["exclusions"] = cleaned_exclusions + self.cleaned_data["origins"].append(origin_form.cleaned_data) + + return super().clean() + + def init_layout(self, request): self.helper = FormHelper(self) self.helper.label_size = Size.SMALL self.helper.legend_size = Size.SMALL @@ -167,6 +307,10 @@ def init_layout(self): "includes/quotas/quota-edit-origins.jinja", { "object": self.instance, + "request": request, + "geo_area_options": self.geo_area_options, + "origins_initial": self.get_origins_initial(), + "errors": self.errors, }, ) @@ -188,9 +332,8 @@ def init_layout(self): HTML(origins_html), ), ), - css_class="govuk-grid-column-two-thirds", ), - css_class="govuk-grid-row", + css_class="govuk-width-!-two-thirds", ), Submit( "submit", @@ -619,7 +762,7 @@ def init_layout(self): AccordionSection( "Volume", HTML.p( - "The initial volume is the legal balance applied to the definition period.

    The current volume is the starting balance for the quota." + "The initial volume is the legal balance applied to the definition period.

    The current volume is the starting balance for the quota.", ), "initial_volume", "volume", @@ -673,3 +816,31 @@ def get_geo_area_initial(self): initial[QUOTA_ORIGIN_EXCLUSIONS_FORMSET_PREFIX] = initial_exclusions return initial + + +class QuotaOrderNumberOriginUpdateReactForm(QuotaOrderNumberOriginUpdateForm): + """Used only to validate data sent from the quota edit React form.""" + + pk = forms.IntegerField(required=False) + + +class QuotaOriginExclusionsReactForm(forms.Form): + """Used only to validate data sent from the quota edit React form.""" + + pk = forms.IntegerField(required=False) + # field name is different to match the react form + geographical_area = forms.ModelChoiceField( + label="", + queryset=GeographicalArea.objects.all(), + help_text="Select a country to be excluded:", + required=False, + ) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.fields["geographical_area"].queryset = ( + GeographicalArea.objects.current() + .with_latest_description() + .as_at_today_and_beyond() + .order_by("description") + ) diff --git a/quotas/jinja2/includes/quotas/quota-edit-origins.jinja b/quotas/jinja2/includes/quotas/quota-edit-origins.jinja index ac47709c0..4ac7063b8 100644 --- a/quotas/jinja2/includes/quotas/quota-edit-origins.jinja +++ b/quotas/jinja2/includes/quotas/quota-edit-origins.jinja @@ -1,16 +1,36 @@ {% from "quota-origins/macros/origin_display.jinja" import origin_display %} -
    - {% for origin in object.quotaordernumberorigin_set.current().with_latest_geo_area_description()%} -
    - {{ origin_display(origin) }} +
    +
    + {% for origin in object.quotaordernumberorigin_set.current().with_latest_geo_area_description() %} +
    + {{ origin_display(origin) }} +
    + + {% endfor %}
    - - {% endfor %} + + Create new origin +
    - - Create new origin - \ No newline at end of file + diff --git a/quotas/jinja2/includes/quotas/tabs/core_data.jinja b/quotas/jinja2/includes/quotas/tabs/core_data.jinja index ba9ddac0d..4df963503 100644 --- a/quotas/jinja2/includes/quotas/tabs/core_data.jinja +++ b/quotas/jinja2/includes/quotas/tabs/core_data.jinja @@ -1,7 +1,7 @@ {% from "quota-origins/macros/origin_display.jinja" import origin_display %} {% set origins %} - {% for origin in object.quotaordernumberorigin_set.current() %} + {% for origin in object.quotaordernumberorigin_set.current().with_latest_geo_area_description() %} {{ origin_display(origin) }} {% endfor %} {% endset %} @@ -18,7 +18,7 @@ }, { "key": {"text": "Origins"}, - "value": {"text": origins if object.origins.all() else "-"}, + "value": {"text": origins if object.quotaordernumberorigin_set.current().with_latest_geo_area_description() else "-"}, "actions": {"items": []} }, { diff --git a/quotas/jinja2/quota-origins/macros/origin_display.jinja b/quotas/jinja2/quota-origins/macros/origin_display.jinja index 6f94a83fb..26e17200a 100644 --- a/quotas/jinja2/quota-origins/macros/origin_display.jinja +++ b/quotas/jinja2/quota-origins/macros/origin_display.jinja @@ -13,7 +13,7 @@ {% endset %} {% set geo_area_summary -%} {{ geo_area_name }} - {% if object.geographical_exclusions %} (with exclusions){% endif %} + {% if object.quotaordernumberoriginexclusion_set.current() %} (with exclusions){% endif %} {% endset %} {% set origin_link -%} {{ geo_area_name }} diff --git a/quotas/models.py b/quotas/models.py index 1ce43855e..90b709796 100644 --- a/quotas/models.py +++ b/quotas/models.py @@ -112,23 +112,6 @@ def geographical_exclusion_descriptions(self): return sorted(descriptions) - @property - def geographical_exclusions(self): - origin_ids = list( - self.quotaordernumberorigin_set.current().values_list( - "pk", - flat=True, - ), - ) - exclusions = [ - e.excluded_geographical_area - for e in QuotaOrderNumberOriginExclusion.objects.current().filter( - origin_id__in=origin_ids, - ) - ] - - return exclusions - class Meta: verbose_name = "quota" diff --git a/quotas/tests/test_forms.py b/quotas/tests/test_forms.py index e83d06e62..f5749cedb 100644 --- a/quotas/tests/test_forms.py +++ b/quotas/tests/test_forms.py @@ -1,17 +1,22 @@ +import datetime + import pytest from bs4 import BeautifulSoup +from django.core.exceptions import ValidationError from django.urls import reverse from common.models.transactions import Transaction from common.models.utils import override_current_transaction from common.tests import factories +from common.util import TaricDateRange +from geo_areas.models import GeographicalArea from quotas import forms from quotas import validators pytestmark = pytest.mark.django_db -def test_update_quota_form_safeguard_invalid(): +def test_update_quota_form_safeguard_invalid(session_request_with_workbasket): """When a QuotaOrderNumber with the category safeguard is edited the category cannot be changed.""" quota = factories.QuotaOrderNumberFactory.create( @@ -24,18 +29,50 @@ def test_update_quota_form_safeguard_invalid(): "start_date_2": 2000, } with override_current_transaction(quota.transaction): - form = forms.QuotaUpdateForm(data=data, instance=quota, initial={}) + form = forms.QuotaUpdateForm( + data=data, + instance=quota, + request=session_request_with_workbasket, + initial={}, + geo_area_options=[], + existing_origins=[], + ) assert not form.is_valid() - assert "Please select a valid category" in form.errors["category"] + assert forms.SAFEGUARD_HELP_TEXT in form.errors["category"] + + +def test_update_quota_form_safeguard_no_change(session_request_with_workbasket): + """When a QuotaOrderNumber with the category safeguard is edited the + category cannot be changed.""" + quota = factories.QuotaOrderNumberFactory.create( + category=validators.QuotaCategory.SAFEGUARD, + ) + data = { + # if the widget is disabled the data is not submitted + "start_date_0": 1, + "start_date_1": 1, + "start_date_2": 2000, + } + with override_current_transaction(quota.transaction): + form = forms.QuotaUpdateForm( + data=data, + instance=quota, + request=session_request_with_workbasket, + initial={}, + geo_area_options=[], + existing_origins=[], + ) + assert form.is_valid() + assert quota.category == validators.QuotaCategory.SAFEGUARD -def test_update_quota_form_safeguard_disabled(valid_user_client): +def test_update_quota_form_safeguard_disabled(client_with_current_workbasket): """When a QuotaOrderNumber with the category safeguard is edited the category cannot be changed and the form field is disabled.""" quota = factories.QuotaOrderNumberFactory.create( category=validators.QuotaCategory.SAFEGUARD, ) - response = valid_user_client.get( + response = client_with_current_workbasket.get( reverse("quota-ui-edit", kwargs={"sid": quota.sid}), ) html = response.content.decode(response.charset) @@ -125,3 +162,150 @@ def test_quota_definition_volume_validation(date_ranges): form.errors["__all__"][0] == "Current volume cannot be higher than initial volume" ) + + +def test_quota_update_react_form_cleaned_data(session_request_with_workbasket): + quota = factories.QuotaOrderNumberFactory.create() + geo_group = factories.GeoGroupFactory.create() + area_1 = factories.GeographicalMembershipFactory.create(geo_group=geo_group).member + area_2 = factories.GeographicalMembershipFactory.create(geo_group=geo_group).member + area_3 = factories.GeographicalMembershipFactory.create(geo_group=geo_group).member + # create geo area group with members to be excluded + data = { + "category": quota.category, + "start_date_0": 1, + "start_date_1": 1, + "start_date_2": 2000, + "end_date_0": 1, + "end_date_1": 1, + "end_date_2": 2010, + "origins-0-geographical_area": quota.quotaordernumberorigin_set.first().geographical_area.pk, + "origins-0-start_date_0": 1, + "origins-0-start_date_1": 1, + "origins-0-start_date_2": 2000, + "origins-0-end_date_0": 1, + "origins-0-end_date_1": 1, + "origins-0-end_date_2": 2010, + "origins-0-exclusions-0-geographical_area": area_1.pk, + "origins-0-exclusions-1-geographical_area": area_2.pk, + "submit": "Save", + } + + tx = Transaction.objects.last() + + with override_current_transaction(tx): + geo_area_options = ( + GeographicalArea.objects.all() + .prefetch_related("descriptions") + .with_latest_description() + .as_at_today_and_beyond() + .order_by("description") + ) + existing_origins = ( + quota.quotaordernumberorigin_set.current().with_latest_geo_area_description() + ) + form = forms.QuotaUpdateForm( + data=data, + instance=quota, + initial={}, + request=session_request_with_workbasket, + geo_area_options=geo_area_options, + existing_origins=existing_origins, + ) + assert form.is_valid() + + assert "valid_between" in form.cleaned_data["origins"][0].keys() + assert "exclusions" in form.cleaned_data["origins"][0].keys() + assert "geographical_area" in form.cleaned_data["origins"][0].keys() + + assert form.cleaned_data["origins"][0]["valid_between"] == TaricDateRange( + datetime.date(2000, 1, 1), + datetime.date(2010, 1, 1), + ) + + assert ( + form.cleaned_data["origins"][0]["geographical_area"] + == quota.quotaordernumberorigin_set.first().geographical_area + ) + + assert len(form.cleaned_data["origins"][0]["exclusions"]) == 2 + + +@pytest.mark.parametrize( + "field_name, error", + [ + ("some_field", "There is a problem"), + ("some_field", ValidationError("There is a problem")), + (None, {"some_field": "There is a problem"}), + ], +) +def test_quota_update_add_extra_error( + field_name, + error, + session_request_with_workbasket, +): + quota = factories.QuotaOrderNumberFactory.create() + data = { + "category": quota.category, + "start_date_0": 1, + "start_date_1": 1, + "start_date_2": 2000, + "submit": "Save", + } + with override_current_transaction(quota.transaction): + geo_area_options = ( + GeographicalArea.objects.all() + .prefetch_related("descriptions") + .with_latest_description() + .as_at_today_and_beyond() + .order_by("description") + ) + existing_origins = ( + quota.quotaordernumberorigin_set.current().with_latest_geo_area_description() + ) + form = forms.QuotaUpdateForm( + data=data, + instance=quota, + initial={}, + request=session_request_with_workbasket, + geo_area_options=geo_area_options, + existing_origins=existing_origins, + ) + form.add_extra_error(field_name, error) + + assert "There is a problem" in form.errors["some_field"] + + +def test_quota_update_add_extra_error_type_error(session_request_with_workbasket): + quota = factories.QuotaOrderNumberFactory.create() + data = { + "category": quota.category, + "start_date_0": 1, + "start_date_1": 1, + "start_date_2": 2000, + "submit": "Save", + } + with override_current_transaction(quota.transaction): + geo_area_options = ( + GeographicalArea.objects.all() + .prefetch_related("descriptions") + .with_latest_description() + .as_at_today_and_beyond() + .order_by("description") + ) + existing_origins = ( + quota.quotaordernumberorigin_set.current().with_latest_geo_area_description() + ) + form = forms.QuotaUpdateForm( + data=data, + instance=quota, + initial={}, + request=session_request_with_workbasket, + geo_area_options=geo_area_options, + existing_origins=existing_origins, + ) + with pytest.raises(TypeError): + form.add_extra_error( + "a_field", + {"some_field": "Error", "some_other_field": "Error"}, + ) diff --git a/quotas/tests/test_views.py b/quotas/tests/test_views.py index 5eb69db8d..8aa839f19 100644 --- a/quotas/tests/test_views.py +++ b/quotas/tests/test_views.py @@ -743,7 +743,7 @@ def test_quota_edit_origin_new_versions(valid_user_client): def test_quota_edit_origin_exclusions( - valid_user_client, + client_with_current_workbasket, approved_transaction, geo_group1, geo_group2, @@ -770,7 +770,7 @@ def test_quota_edit_origin_exclusions( "submit": "Save", } - response = valid_user_client.post( + response = client_with_current_workbasket.post( reverse("quota_order_number_origin-ui-edit", kwargs={"sid": origin.sid}), form_data, ) @@ -799,7 +799,7 @@ def test_quota_edit_origin_exclusions( def test_quota_edit_origin_exclusions_remove( - valid_user_client, + client_with_current_workbasket, approved_transaction, geo_group1, country1, @@ -828,7 +828,7 @@ def test_quota_edit_origin_exclusions_remove( "submit": "Save", } - response = valid_user_client.post( + response = client_with_current_workbasket.post( reverse("quota_order_number_origin-ui-edit", kwargs={"sid": origin.sid}), form_data, ) @@ -859,14 +859,14 @@ def test_quota_edit_origin_exclusions_remove( ) -def test_update_quota_definition_page_200(valid_user_client): +def test_update_quota_definition_page_200(client_with_current_workbasket): quota_definition = factories.QuotaDefinitionFactory.create() url = reverse("quota_definition-ui-edit", kwargs={"sid": quota_definition.sid}) - response = valid_user_client.get(url) + response = client_with_current_workbasket.get(url) assert response.status_code == 200 -def test_update_quota_definition(valid_user_client, date_ranges): +def test_update_quota_definition(client_with_current_workbasket, date_ranges): quota_definition = factories.QuotaDefinitionFactory.create( valid_between=date_ranges.big_no_end, ) @@ -889,7 +889,7 @@ def test_update_quota_definition(valid_user_client, date_ranges): "quota_critical": "False", } - response = valid_user_client.post(url, data) + response = client_with_current_workbasket.post(url, data) assert response.status_code == 302 assert response.url == reverse( "quota_definition-ui-confirm-update", @@ -913,20 +913,20 @@ def test_update_quota_definition(valid_user_client, date_ranges): assert updated_definition.quota_critical == False -def test_delete_quota_definition_page_200(valid_user_client): +def test_delete_quota_definition_page_200(client_with_current_workbasket): quota_definition = factories.QuotaDefinitionFactory.create() url = reverse("quota_definition-ui-delete", kwargs={"sid": quota_definition.sid}) - response = valid_user_client.get(url) + response = client_with_current_workbasket.get(url) assert response.status_code == 200 -def test_delete_quota_definition(valid_user_client, date_ranges): +def test_delete_quota_definition(client_with_current_workbasket, date_ranges): quota_definition = factories.QuotaDefinitionFactory.create( valid_between=date_ranges.big_no_end, ) url = reverse("quota_definition-ui-delete", kwargs={"sid": quota_definition.sid}) - response = valid_user_client.post(url, {"submit": "Delete"}) + response = client_with_current_workbasket.post(url, {"submit": "Delete"}) assert response.status_code == 302 assert response.url == reverse( "quota_definition-ui-confirm-delete", @@ -937,7 +937,7 @@ def test_delete_quota_definition(valid_user_client, date_ranges): assert tx.workbasket.tracked_models.first().update_type == UpdateType.DELETE - confirm_response = valid_user_client.get(response.url) + confirm_response = client_with_current_workbasket.get(response.url) soup = BeautifulSoup( confirm_response.content.decode(response.charset), @@ -952,7 +952,7 @@ def test_delete_quota_definition(valid_user_client, date_ranges): def test_quota_create_origin( - valid_user_client, + client_with_current_workbasket, approved_transaction, geo_group1, date_ranges, @@ -971,7 +971,7 @@ def test_quota_create_origin( "submit": "Save", } - response = valid_user_client.post( + response = client_with_current_workbasket.post( reverse("quota_order_number_origin-ui-create", kwargs={"sid": quota.sid}), form_data, ) @@ -987,7 +987,7 @@ def test_quota_create_origin( def test_quota_create_origin_outwith_quota_period( - valid_user_client, + client_with_current_workbasket, approved_transaction, geo_group1, date_ranges, @@ -1007,7 +1007,7 @@ def test_quota_create_origin_outwith_quota_period( "submit": "Save", } - response = valid_user_client.post( + response = client_with_current_workbasket.post( reverse("quota_order_number_origin-ui-create", kwargs={"sid": quota.sid}), form_data, ) @@ -1024,7 +1024,7 @@ def test_quota_create_origin_outwith_quota_period( def test_quota_create_origin_no_overlapping_origins( - valid_user_client, + client_with_current_workbasket, approved_transaction, geo_group1, date_ranges, @@ -1050,7 +1050,7 @@ def test_quota_create_origin_no_overlapping_origins( "submit": "Save", } - response = valid_user_client.post( + response = client_with_current_workbasket.post( reverse("quota_order_number_origin-ui-create", kwargs={"sid": quota.sid}), form_data, ) @@ -1068,7 +1068,7 @@ def test_quota_create_origin_no_overlapping_origins( @pytest.mark.django_db def test_quota_order_number_and_origin_edit_create_view( - valid_user_client, + client_with_current_workbasket, date_ranges, approved_transaction, geo_group1, @@ -1090,14 +1090,14 @@ def test_quota_order_number_and_origin_edit_create_view( "submit": "Save", } - response = valid_user_client.post( + response = client_with_current_workbasket.post( reverse("quota_order_number_origin-ui-edit-create", kwargs={"sid": origin.sid}), form_data, ) assert response.status_code == 302 - response = valid_user_client.get( + response = client_with_current_workbasket.get( reverse("quota-ui-edit-create", kwargs={"sid": quota.sid}), form_data, ) @@ -1107,7 +1107,7 @@ def test_quota_order_number_and_origin_edit_create_view( @pytest.mark.django_db def test_quota_order_number_update_view( - valid_user_client, + client_with_current_workbasket, date_ranges, approved_transaction, geo_group1, @@ -1129,7 +1129,7 @@ def test_quota_order_number_update_view( "submit": "Save", } - response = valid_user_client.get( + response = client_with_current_workbasket.get( reverse("quota-ui-edit-update", kwargs={"sid": quota.sid}), form_data, ) @@ -1138,7 +1138,7 @@ def test_quota_order_number_update_view( def test_create_new_quota_definition( - valid_user_client, + client_with_current_workbasket, approved_transaction, date_ranges, mock_quota_api_no_data, @@ -1168,7 +1168,7 @@ def test_create_new_quota_definition( assert not models.QuotaDefinition.objects.all() url = reverse("quota_definition-ui-create", kwargs={"sid": quota.sid}) - response = valid_user_client.post(url, form_data) + response = client_with_current_workbasket.post(url, form_data) assert response.status_code == 302 created_definition = models.QuotaDefinition.objects.last() @@ -1179,7 +1179,7 @@ def test_create_new_quota_definition( # check definition is listed on quota order number's definition tab url = reverse("quota-ui-detail", kwargs={"sid": quota.sid}) - response = valid_user_client.get(url) + response = client_with_current_workbasket.get(url) soup = BeautifulSoup(response.content.decode(response.charset), "html.parser") definitions_tab = soup.find(id="definition-details") details = [ @@ -1197,7 +1197,7 @@ def test_create_new_quota_definition( def test_create_new_quota_definition_business_rule_violation( - valid_user_client, + client_with_current_workbasket, approved_transaction, date_ranges, ): @@ -1223,7 +1223,7 @@ def test_create_new_quota_definition_business_rule_violation( } url = reverse("quota_definition-ui-create", kwargs={"sid": quota.sid}) - response = valid_user_client.post(url, form_data) + response = client_with_current_workbasket.post(url, form_data) assert response.status_code == 200 @@ -1239,21 +1239,24 @@ def test_create_new_quota_definition_business_rule_violation( @pytest.mark.django_db def test_quota_order_number_create_200( - valid_user_client, + client_with_current_workbasket, ): - response = valid_user_client.get(reverse("quota-ui-create")) + response = client_with_current_workbasket.get(reverse("quota-ui-create")) assert response.status_code == 200 @pytest.mark.django_db def test_quota_order_number_create_errors_required( - valid_user_client, + client_with_current_workbasket, ): form_data = { "submit": "Save", } - response = valid_user_client.post(reverse("quota-ui-create"), form_data) + response = client_with_current_workbasket.post( + reverse("quota-ui-create"), + form_data, + ) assert response.status_code == 200 @@ -1298,7 +1301,7 @@ def test_quota_order_number_create_validation( mechanism, category, exp_error, - valid_user_client, + client_with_current_workbasket, date_ranges, ): form_data = { @@ -1310,7 +1313,10 @@ def test_quota_order_number_create_validation( "category": category, "submit": "Save", } - response = valid_user_client.post(reverse("quota-ui-create"), form_data) + response = client_with_current_workbasket.post( + reverse("quota-ui-create"), + form_data, + ) assert response.status_code == 200 @@ -1323,7 +1329,7 @@ def test_quota_order_number_create_validation( @pytest.mark.django_db def test_quota_order_number_create_success( - valid_user_client, + client_with_current_workbasket, date_ranges, ): form_data = { @@ -1335,7 +1341,10 @@ def test_quota_order_number_create_success( "category": validators.QuotaCategory.WTO.value, "submit": "Save", } - response = valid_user_client.post(reverse("quota-ui-create"), form_data) + response = client_with_current_workbasket.post( + reverse("quota-ui-create"), + form_data, + ) assert response.status_code == 302 @@ -1343,10 +1352,268 @@ def test_quota_order_number_create_success( assert response.url == reverse("quota-ui-confirm-create", kwargs={"sid": quota.sid}) - response2 = valid_user_client.get(response.url) + response2 = client_with_current_workbasket.get(response.url) soup = BeautifulSoup(response2.content.decode(response2.charset), "html.parser") assert ( soup.find("h1").text.strip() == f"Quota {quota.order_number} has been created" ) + + +def test_quota_update_existing_origins(client_with_current_workbasket, date_ranges): + quota = factories.QuotaOrderNumberFactory.create( + category=0, + valid_between=date_ranges.big_no_end, + ) + factories.QuotaOrderNumberOriginFactory.create(order_number=quota) + factories.QuotaOrderNumberOriginFactory.create(order_number=quota) + geo_area1 = factories.GeographicalAreaFactory.create() + geo_area2 = factories.GeographicalAreaFactory.create() + tx = geo_area2.transaction + ( + origin1, + origin2, + origin3, + ) = quota.quotaordernumberorigin_set.approved_up_to_transaction(tx) + + # sanity check + assert quota.quotaordernumberorigin_set.count() == 3 + + data = { + "start_date_0": date_ranges.big_no_end.lower.day, + "start_date_1": date_ranges.big_no_end.lower.month, + "start_date_2": date_ranges.big_no_end.lower.year, + "end_date_0": "", + "end_date_1": "", + "end_date_2": "", + "category": "1", # update category + # keep first origin data the same + "origins-0-pk": origin1.pk, + "origins-0-start_date_0": date_ranges.big_no_end.lower.day, + "origins-0-start_date_1": date_ranges.big_no_end.lower.month, + "origins-0-start_date_2": date_ranges.big_no_end.lower.year, + "origins-0-end_date_0": "", + "origins-0-end_date_1": "", + "origins-0-end_date_2": "", + "origins-0-geographical_area": origin1.geographical_area.pk, + # omit subform for origin2 to delete it + # change origin3 geo area + "origins-1-pk": origin3.pk, + "origins-1-start_date_0": date_ranges.big_no_end.lower.day, + "origins-1-start_date_1": date_ranges.big_no_end.lower.month, + "origins-1-start_date_2": date_ranges.big_no_end.lower.year, + "origins-1-end_date_0": "", + "origins-1-end_date_1": "", + "origins-1-end_date_2": "", + "origins-1-geographical_area": geo_area1.pk, + # add a new origin + "origins-2-pk": "", + "origins-2-start_date_0": date_ranges.big_no_end.lower.day, + "origins-2-start_date_1": date_ranges.big_no_end.lower.month, + "origins-2-start_date_2": date_ranges.big_no_end.lower.year, + "origins-2-end_date_0": "", + "origins-2-end_date_1": "", + "origins-2-end_date_2": "", + "origins-2-geographical_area": geo_area2.pk, + "submit": "Save", + } + url = reverse("quota-ui-edit", kwargs={"sid": quota.sid}) + response = client_with_current_workbasket.post(url, data) + + assert response.status_code == 302 + assert response.url == reverse("quota-ui-confirm-update", kwargs={"sid": quota.sid}) + + tx = Transaction.objects.last() + updated_quota = ( + models.QuotaOrderNumber.objects.approved_up_to_transaction(tx) + .filter(sid=quota.sid) + .first() + ) + assert updated_quota.category == 1 + assert updated_quota.valid_between == date_ranges.big_no_end + + assert updated_quota.origins.approved_up_to_transaction(tx).count() == 3 + assert {o.sid for o in updated_quota.origins.approved_up_to_transaction(tx)} == { + geo_area1.sid, + geo_area2.sid, + origin1.geographical_area.sid, + } + + +def test_quota_update_existing_origin_exclusions( + client_with_current_workbasket, + date_ranges, +): + # make a geo group with 3 member countries + country1 = factories.CountryFactory.create() + country2 = factories.CountryFactory.create() + country3 = factories.CountryFactory.create() + geo_group = factories.GeoGroupFactory.create() + membership1 = factories.GeographicalMembershipFactory.create( + member=country1, + geo_group=geo_group, + ) + membership2 = factories.GeographicalMembershipFactory.create( + member=country2, + geo_group=geo_group, + ) + membership3 = factories.GeographicalMembershipFactory.create( + member=country3, + geo_group=geo_group, + ) + + exclusion = factories.QuotaOrderNumberOriginExclusionFactory.create( + excluded_geographical_area=membership1.member, + ) + origin = exclusion.origin + quota = origin.order_number + + # sanity check + assert quota.quotaordernumberorigin_set.count() == 1 + + data = { + "start_date_0": date_ranges.big_no_end.lower.day, + "start_date_1": date_ranges.big_no_end.lower.month, + "start_date_2": date_ranges.big_no_end.lower.year, + "end_date_0": "", + "end_date_1": "", + "end_date_2": "", + "category": "1", # update category + "origins-0-pk": origin.pk, + "origins-0-start_date_0": date_ranges.big_no_end.lower.day, + "origins-0-start_date_1": date_ranges.big_no_end.lower.month, + "origins-0-start_date_2": date_ranges.big_no_end.lower.year, + "origins-0-end_date_0": "", + "origins-0-end_date_1": "", + "origins-0-end_date_2": "", + "origins-0-geographical_area": geo_group.pk, + # update existing + "origins-0-exclusions-0-pk": exclusion.pk, + "origins-0-exclusions-0-geographical_area": membership2.member.pk, + # add new + "origins-0-exclusions-1-pk": "", + "origins-0-exclusions-1-geographical_area": membership3.member.pk, + "submit": "Save", + } + url = reverse("quota-ui-edit", kwargs={"sid": quota.sid}) + response = client_with_current_workbasket.post(url, data) + + assert response.status_code == 302 + assert response.url == reverse("quota-ui-confirm-update", kwargs={"sid": quota.sid}) + + tx = Transaction.objects.last() + + updated_quota = ( + models.QuotaOrderNumber.objects.approved_up_to_transaction(tx) + .filter(sid=quota.sid) + .first() + ) + + assert updated_quota.origins.approved_up_to_transaction(tx).count() == 1 + updated_origin = ( + updated_quota.quotaordernumberorigin_set.approved_up_to_transaction(tx).first() + ) + assert { + e.excluded_geographical_area.sid + for e in updated_origin.quotaordernumberoriginexclusion_set.approved_up_to_transaction( + tx, + ) + } == { + membership2.member.sid, + membership3.member.sid, + } + + +def test_quota_update_existing_origin_exclusion_remove( + client_with_current_workbasket, + date_ranges, +): + country1 = factories.CountryFactory.create() + geo_group = factories.GeoGroupFactory.create() + membership1 = factories.GeographicalMembershipFactory.create( + member=country1, + geo_group=geo_group, + ) + + exclusion = factories.QuotaOrderNumberOriginExclusionFactory.create( + excluded_geographical_area=membership1.member, + ) + origin1 = exclusion.origin + quota = origin1.order_number + origin2 = factories.QuotaOrderNumberOriginFactory.create(order_number=quota) + + # sanity check + tx = Transaction.objects.last() + assert quota.quotaordernumberorigin_set.approved_up_to_transaction(tx).count() == 2 + assert ( + origin1.quotaordernumberoriginexclusion_set.approved_up_to_transaction( + tx, + ).count() + == 1 + ) + assert ( + origin2.quotaordernumberoriginexclusion_set.approved_up_to_transaction( + tx, + ).count() + == 0 + ) + + data = { + "start_date_0": date_ranges.big_no_end.lower.day, + "start_date_1": date_ranges.big_no_end.lower.month, + "start_date_2": date_ranges.big_no_end.lower.year, + "end_date_0": "", + "end_date_1": "", + "end_date_2": "", + "category": quota.category, + "origins-0-pk": origin1.pk, + "origins-0-start_date_0": date_ranges.big_no_end.lower.day, + "origins-0-start_date_1": date_ranges.big_no_end.lower.month, + "origins-0-start_date_2": date_ranges.big_no_end.lower.year, + "origins-0-end_date_0": "", + "origins-0-end_date_1": "", + "origins-0-end_date_2": "", + "origins-0-geographical_area": geo_group.pk, + # remove the first origin's exclusion + # remove the second origin + "submit": "Save", + } + + url = reverse("quota-ui-edit", kwargs={"sid": quota.sid}) + response = client_with_current_workbasket.post(url, data) + + assert response.status_code == 302 + assert response.url == reverse("quota-ui-confirm-update", kwargs={"sid": quota.sid}) + + last_tx = Transaction.objects.last() + + updated_quota = ( + models.QuotaOrderNumber.objects.approved_up_to_transaction(last_tx) + .filter(sid=quota.sid) + .first() + ) + + assert updated_quota.origins.approved_up_to_transaction(last_tx).count() == 1 + updated_origin = ( + updated_quota.quotaordernumberorigin_set.approved_up_to_transaction( + last_tx, + ).first() + ) + assert ( + updated_origin.quotaordernumberoriginexclusion_set.approved_up_to_transaction( + last_tx, + ).count() + == 0 + ) + # update quota + # update quota origin 1 + # delete quota origin 1 exclusion + # delete quota origin 2 + assert updated_origin.transaction.workbasket.tracked_models.count() == 4 + assert sorted( + [ + item.get_update_type_display() + for item in updated_origin.transaction.workbasket.tracked_models.all() + ], + ) == ["Delete", "Delete", "Update", "Update"] diff --git a/quotas/views.py b/quotas/views.py index 472cc78a6..18f823f6e 100644 --- a/quotas/views.py +++ b/quotas/views.py @@ -23,6 +23,7 @@ from common.views import TamatoListView from common.views import TrackedModelDetailMixin from common.views import TrackedModelDetailView +from geo_areas.models import GeographicalArea from geo_areas.utils import get_all_members_of_geo_groups from measures.models import Measure from quotas import business_rules @@ -120,7 +121,8 @@ class QuotaCreate(QuotaOrderNumberMixin, CreateTaricCreateView): def get_context_data(self, **kwargs): return super().get_context_data( - page_title="Create a new quota order number", **kwargs + page_title="Create a new quota order number", + **kwargs, ) @@ -295,27 +297,154 @@ class QuotaUpdateMixin( UpdateValidity, ) - @transaction.atomic - def get_result_object(self, form): - object = super().get_result_object(form) + def get_form_kwargs(self): + kwargs = super().get_form_kwargs() + kwargs["request"] = self.request + kwargs["geo_area_options"] = ( + GeographicalArea.objects.current() + .prefetch_related("descriptions") + .with_latest_description() + .as_at_today_and_beyond() + .order_by("description") + ) + kwargs[ + "existing_origins" + ] = ( + self.object.quotaordernumberorigin_set.current().with_latest_geo_area_description() + ) + return kwargs - existing_origins = ( - models.QuotaOrderNumberOrigin.objects.approved_up_to_transaction( - object.transaction, - ).filter( - order_number__sid=object.sid, + def update_origins(self, instance, form_origins): + existing_origin_pks = { + o.pk + for o in models.QuotaOrderNumberOrigin.objects.current().filter( + order_number__sid=instance.sid, ) + } + if form_origins: + submitted_origin_pks = {o["pk"] for o in form_origins} + deleted_origin_pks = existing_origin_pks.difference(submitted_origin_pks) + + for origin_pk in deleted_origin_pks: + origin = models.QuotaOrderNumberOrigin.objects.get( + pk=origin_pk, + ) + origin.new_version( + update_type=UpdateType.DELETE, + workbasket=WorkBasket.current(self.request), + transaction=instance.transaction, + ) + # Delete the exclusions as well + exclusions = models.QuotaOrderNumberOriginExclusion.objects.filter( + origin__pk=origin_pk, + ) + for exclusion in exclusions: + exclusion.new_version( + update_type=UpdateType.DELETE, + workbasket=WorkBasket.current(self.request), + transaction=instance.transaction, + ) + + for origin in form_origins: + # If origin exists + if origin.get("pk"): + existing_origin = models.QuotaOrderNumberOrigin.objects.get( + pk=origin.get("pk"), + ) + updated_origin = existing_origin.new_version( + workbasket=WorkBasket.current(self.request), + transaction=instance.transaction, + order_number=instance, + valid_between=origin["valid_between"], + geographical_area=origin["geographical_area"], + ) + + # It's a newly created origin + else: + updated_origin = models.QuotaOrderNumberOrigin.objects.create( + order_number=instance, + valid_between=origin["valid_between"], + geographical_area=origin["geographical_area"], + update_type=UpdateType.CREATE, + transaction=instance.transaction, + ) + + # whether it's edited or new we need to add/update exclusions + self.update_exclusions( + instance, + updated_origin, + origin.get("exclusions"), + ) + else: + # even if no changes were made we must update the existing + # origins to link to the updated order number + existing_origins = ( + models.QuotaOrderNumberOrigin.objects.approved_up_to_transaction( + instance.transaction, + ).filter( + order_number__sid=instance.sid, + ) + ) + for origin in existing_origins: + origin.new_version( + workbasket=WorkBasket.current(self.request), + transaction=instance.transaction, + order_number=instance, + ) + + def update_exclusions(self, quota, updated_origin, exclusions): + existing_exclusion_pks = { + e.pk + for e in models.QuotaOrderNumberOriginExclusion.objects.current().filter( + origin__sid=updated_origin.sid, + ) + } + submitted_exclusion_pks = {e["pk"] for e in exclusions} + deleted_exclusion_pks = existing_exclusion_pks.difference( + submitted_exclusion_pks, ) - # this will be needed even if origins have not been edited in the form - for origin in existing_origins: - origin.new_version( + for exclusion_pk in deleted_exclusion_pks: + exclusion = models.QuotaOrderNumberOriginExclusion.objects.get( + pk=exclusion_pk, + ) + exclusion.new_version( + update_type=UpdateType.DELETE, workbasket=WorkBasket.current(self.request), - transaction=object.transaction, - order_number=object, + transaction=quota.transaction, ) - return object + for exclusion in exclusions: + geo_area = GeographicalArea.objects.get(pk=exclusion["geographical_area"]) + if exclusion.get("pk"): + existing_exclusion = models.QuotaOrderNumberOriginExclusion.objects.get( + pk=exclusion.get("pk"), + ) + existing_exclusion.new_version( + workbasket=WorkBasket.current(self.request), + transaction=quota.transaction, + origin=updated_origin, + excluded_geographical_area=geo_area, + ) + + else: + models.QuotaOrderNumberOriginExclusion.objects.create( + origin=updated_origin, + excluded_geographical_area=geo_area, + update_type=UpdateType.CREATE, + transaction=quota.transaction, + ) + + @transaction.atomic + def get_result_object(self, form): + instance = super().get_result_object(form) + + # if JS is enabled we get data from the React form which includes origins and exclusions + form_origins = form.cleaned_data.get("origins") + + self.update_origins(instance, form_origins) + + return instance class QuotaUpdate( diff --git a/reference_documents/alignment_checks.py b/reference_documents/alignment_checks.py new file mode 100644 index 000000000..bda1123c1 --- /dev/null +++ b/reference_documents/alignment_checks.py @@ -0,0 +1,102 @@ +from commodities.models import GoodsNomenclature +from geo_areas.models import GeographicalArea +from datetime import date + +from commodities.models import GoodsNomenclature +from geo_areas.models import GeographicalArea +from geo_areas.models import GeographicalAreaDescription +from reference_documents.models import PreferentialRate + + +class BaseCheck: + def run_check(self): + raise NotImplemented("Please implement on child classes") + + +class BasePreferentialRateCheck(BaseCheck): + def __init__(self, preferential_rate: PreferentialRate): + self.preferential_rate = preferential_rate + + def comm_code(self): + goods = GoodsNomenclature.objects.latest_approved().filter( + item_id=self.preferential_rate.commodity_code, + valid_between__contains=self.ref_doc_version_eif_date(), + suffix=80, + ) + + if len(goods) == 0: + return None + + return goods.first() + + def geo_area(self): + return ( + GeographicalArea.objects.latest_approved() + .filter( + area_id=self.preferential_rate.reference_document_version.reference_document.area_id, + ) + .first() + ) + + def geo_area_description(self): + geo_area_desc = ( + GeographicalAreaDescription.objects.latest_approved() + .filter(described_geographicalarea=self.geo_area()) + .last() + ) + return geo_area_desc.description + + def ref_doc_version_eif_date(self): + eif_date = ( + self.preferential_rate.reference_document_version.entry_into_force_date + ) + + # todo : make sure EIf dates are all populated correctly - and remove this + if eif_date is None: + eif_date = date.today() + + return eif_date + + def related_measures(self): + return ( + self.comm_code() + .measures.latest_approved() + .filter( + geographical_area=self.geo_area(), + valid_between__contains=self.ref_doc_version_eif_date(), + ) + ) + + def run_check(self): + raise NotImplementedError("Please implement on child classes") + + +class CheckPreferentialRateCommCode(BasePreferentialRateCheck): + def run_check(self): + # comm code live on EIF date + if not self.comm_code(): + print( + "FAIL", + self.preferential_rate.commodity_code, + self.geo_area_description(), + "comm code not live", + ) + return False + + measures = self.related_measures() + + if len(measures) == 1: + print("PASS", self.comm_code(), self.geo_area_description()) + return True + + if len(measures) == 0: + print("FAIL", self.comm_code(), self.geo_area_description()) + return False + + if len(measures) > 1: + print( + "WARNING - multiple measures", + self.comm_code(), + self.geo_area_description(), + ) + return True diff --git a/reference_documents/apps.py b/reference_documents/apps.py index c8e519933..1d11db6f1 100644 --- a/reference_documents/apps.py +++ b/reference_documents/apps.py @@ -2,5 +2,4 @@ class ReferenceDocumentsConfig(AppConfig): - default_auto_field = "django.db.models.BigAutoField" name = "reference_documents" diff --git a/reference_documents/checks/base.py b/reference_documents/checks/base.py new file mode 100644 index 000000000..2f1c6ceb0 --- /dev/null +++ b/reference_documents/checks/base.py @@ -0,0 +1,175 @@ +from datetime import date + +from commodities.models import GoodsNomenclature +from commodities.models.dc import CommodityCollectionLoader +from commodities.models.dc import CommodityTreeSnapshot +from commodities.models.dc import SnapshotMoment +from common.models import Transaction +from geo_areas.models import GeographicalArea +from geo_areas.models import GeographicalAreaDescription +from reference_documents.models import PreferentialQuota +from reference_documents.models import PreferentialRate + + +class BaseCheck: + def __init__(self): + self.dependent_on_passing_check = None + + def run_check(self): + raise NotImplemented("Please implement on child classes") + + +class BasePreferentialQuotaCheck(BaseCheck): + def __init__(self, preferential_quota: PreferentialQuota): + super().__init__() + self.preferential_quota = preferential_quota + + +class BasePreferentialRateCheck(BaseCheck): + def __init__(self, preferential_rate: PreferentialRate): + super().__init__() + self.preferential_rate = preferential_rate + + def get_snapshot(self) -> CommodityTreeSnapshot: + # not liking having to use CommodityTreeSnapshot, but it does to the job + item_id = self.comm_code().item_id + while item_id[-2:] == "00": + item_id = item_id[0 : len(item_id) - 2] + + commodities_collection = CommodityCollectionLoader( + prefix=item_id, + ).load(current_only=True) + + latest_transaction = Transaction.objects.order_by("created_at").last() + + snapshot = CommodityTreeSnapshot( + commodities=commodities_collection.commodities, + moment=SnapshotMoment(transaction=latest_transaction), + ) + + return snapshot + + def comm_code(self): + goods = GoodsNomenclature.objects.latest_approved().filter( + item_id=self.preferential_rate.commodity_code, + valid_between__contains=self.ref_doc_version_eif_date(), + suffix=80, + ) + + if len(goods) == 0: + return None + + return goods.first() + + def geo_area(self): + return ( + GeographicalArea.objects.latest_approved() + .filter( + area_id=self.preferential_rate.reference_document_version.reference_document.area_id, + ) + .first() + ) + + def geo_area_description(self): + geo_area_desc = ( + GeographicalAreaDescription.objects.latest_approved() + .filter(described_geographicalarea=self.geo_area()) + .last() + ) + return geo_area_desc.description + + def ref_doc_version_eif_date(self): + eif_date = ( + self.preferential_rate.reference_document_version.entry_into_force_date + ) + + # todo : make sure EIf dates are all populated correctly - and remove this + if eif_date is None: + eif_date = date.today() + + return eif_date + + def related_measures(self, comm_code_item_id=None): + if comm_code_item_id: + good = GoodsNomenclature.objects.latest_approved().filter( + item_id=comm_code_item_id, + valid_between__contains=self.ref_doc_version_eif_date(), + suffix=80, + ) + + if len(good) == 1: + return ( + good.first() + .measures.latest_approved() + .filter( + geographical_area=self.geo_area(), + valid_between__contains=self.ref_doc_version_eif_date(), + measure_type__sid__in=[ + 142, + 143, + ], # note : these are the measure types used to identify preferential tariffs + ) + ) + else: + return [] + else: + return ( + self.comm_code() + .measures.latest_approved() + .filter( + geographical_area=self.geo_area(), + valid_between__contains=self.ref_doc_version_eif_date(), + measure_type__sid__in=[ + 142, + 143, + ], # note : these are the measure types used to identify preferential tariffs + ) + ) + + def recursive_comm_code_check( + self, + snapshot: CommodityTreeSnapshot, + parent_item_id, + parent_item_suffix, + level=1, + ): + # find comm code from snapshot + child_commodities = [] + for commodity in snapshot.commodities: + if ( + commodity.item_id == parent_item_id + and commodity.suffix == parent_item_suffix + ): + child_commodities = snapshot.get_children(commodity) + break + + if len(child_commodities) == 0: + print(f'{"-" * level} no more children') + return False + + results = [] + for child_commodity in child_commodities: + related_measures = self.related_measures(child_commodity.item_id) + + if len(related_measures) == 0: + print(f'{"-" * level} FAIL : {child_commodity.item_id}') + results.append( + self.recursive_comm_code_check( + snapshot, + child_commodity.item_id, + child_commodity.suffix, + level + 1, + ), + ) + elif len(related_measures) == 1: + print(f'{"-" * level} PASS : {child_commodity.item_id}') + results.append(True) + else: + # Multiple measures + print(f'{"-" * level} PASS : multiple : {child_commodity.item_id}') + results.append(True) + + return False in results + + def run_check(self): + raise NotImplementedError("Please implement on child classes") diff --git a/reference_documents/checks/check_runner.py b/reference_documents/checks/check_runner.py new file mode 100644 index 000000000..43d6c1223 --- /dev/null +++ b/reference_documents/checks/check_runner.py @@ -0,0 +1,43 @@ +from reference_documents.checks.base import BasePreferentialQuotaCheck +from reference_documents.checks.base import BasePreferentialRateCheck +from reference_documents.checks.preferential_quotas import * # noqa +from reference_documents.checks.preferential_rates import * # noqa +from reference_documents.checks.utils import utils +from reference_documents.models import AlignmentReport +from reference_documents.models import AlignmentReportCheck +from reference_documents.models import ReferenceDocumentVersion + + +class Checks: + def __init__(self, reference_document_version: ReferenceDocumentVersion): + self.reference_document_version = reference_document_version + self.alignment_report = AlignmentReport.objects.create( + reference_document_version=self.reference_document_version, + ) + + @staticmethod + def get_checks_for(check_class): + return utils().get_child_checks(check_class) + + def run(self): + for check in Checks.get_checks_for(BasePreferentialRateCheck): + for pref_rate in self.reference_document_version.preferential_rates.all(): + self.capture_check_result(check(pref_rate), pref_rate=pref_rate) + + for check in Checks.get_checks_for(BasePreferentialQuotaCheck): + for pref_quota in self.reference_document_version.preferential_quotas.all(): + self.capture_check_result(check(pref_quota), pref_quota=pref_quota) + + def capture_check_result(self, check, pref_rate=None, pref_quota=None): + status, message = check.run_check() + + kwargs = { + "alignment_report": self.alignment_report, + "check_name": check.__class__.__name__, + "preferential_rate": pref_rate, + "preferential_quota": pref_quota, + "status": status, + "message": message, + } + + AlignmentReportCheck.objects.create(**kwargs) diff --git a/reference_documents/checks/preferential_quotas.py b/reference_documents/checks/preferential_quotas.py new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/reference_documents/checks/preferential_quotas.py @@ -0,0 +1 @@ + diff --git a/reference_documents/checks/preferential_rates.py b/reference_documents/checks/preferential_rates.py new file mode 100644 index 000000000..840b33b21 --- /dev/null +++ b/reference_documents/checks/preferential_rates.py @@ -0,0 +1,45 @@ +from reference_documents.checks.base import BasePreferentialRateCheck +from reference_documents.models import AlignmentReportCheckStatus + + +class MeasureExists(BasePreferentialRateCheck): + def run_check(self): + # comm code live on EIF date + if not self.comm_code(): + message = f"{self.preferential_rate.commodity_code} {self.geo_area_description()} comm code not live" + print("FAIL", message) + return AlignmentReportCheckStatus.FAIL, message + + # query measures + measures = self.related_measures() + + # this is ok - there is a single measure matching the expected query + if len(measures) == 1: + return AlignmentReportCheckStatus.PASS, "" + + # this is not inline with expected measures presence - check comm code children + if len(measures) == 0: + # check 1 level down for presence of measures + match = self.recursive_comm_code_check( + self.get_snapshot(), + self.preferential_rate.commodity_code, + 80, + ) + + message = f"{self.comm_code()}, {self.geo_area_description()}" + + if match: + message += f"\nmatched with children" + print("PASS", message) + + return AlignmentReportCheckStatus.PASS, message + else: + message += f"\nno expected measures found on good code or children" + print("FAIL", message) + + return AlignmentReportCheckStatus.FAIL, message + + if len(measures) > 1: + message = f"multiple measures match {self.comm_code()}, {self.geo_area_description()}" + print("WARNING", message) + return AlignmentReportCheckStatus.WARNING, message diff --git a/reference_documents/checks/utils.py b/reference_documents/checks/utils.py new file mode 100644 index 000000000..242f15289 --- /dev/null +++ b/reference_documents/checks/utils.py @@ -0,0 +1,21 @@ +from reference_documents.checks.base import BaseCheck + + +class utils: + def get_child_checks(self, check_class: BaseCheck.__class__): + result = [] + + check_classes = self.subclasses_for(check_class) + for check_class in check_classes: + result.append(check_class) + + return result + + def subclasses_for(self, cls) -> list: + all_subclasses = [] + + for subclass in cls.__subclasses__(): + all_subclasses.append(subclass) + all_subclasses.extend(self.subclasses_for(subclass)) + + return all_subclasses diff --git a/reference_documents/jinja2/alignment_reports/details.jinja b/reference_documents/jinja2/alignment_reports/details.jinja new file mode 100644 index 000000000..59de1a733 --- /dev/null +++ b/reference_documents/jinja2/alignment_reports/details.jinja @@ -0,0 +1,25 @@ +{% extends "layouts/layout.jinja" %} +{% from "components/table/macro.njk" import govukTable %} + +{% set page_title = 'Reference Documents version details' %} + +{% block breadcrumb %} + {{ breadcrumbs(request, [ + {'text': "Reference Document Version Overview"} + ]) }} +{% endblock %} + +{% block content %} +

    + Alignment reports +

    + Reference document version details. + +
    + {{ govukTable({ "head": alignment_report_headers, "rows": alignment_reports }) }} +
    + +{% endblock %} + + + diff --git a/reference_documents/jinja2/includes/tabs/preferential_quotas.jinja b/reference_documents/jinja2/includes/tabs/preferential_quotas.jinja new file mode 100644 index 000000000..7b97ce0e6 --- /dev/null +++ b/reference_documents/jinja2/includes/tabs/preferential_quotas.jinja @@ -0,0 +1,39 @@ +{% from "components/table/macro.njk" import govukTable %} +{% from 'macros/create_link.jinja' import create_link %} + +
    +
    + {% for key, value in reference_document_version_quotas.items() %} + {% if value["quota_order_number"] != None %} +

    Order Number + + {{ value["quota_order_number"] }} + +

    +
    + validity: from {{ value["quota_order_number"].valid_between.lower }} + {% if value["quota_order_number"].valid_between.upper == None %} + no end date defined + {% else %} + to {{ value["quota_order_number"].valid_between.upper }} + {% endif %} +
    + {% else %} +

    Order Number {{ value["quota_order_number"] }}

    + {% endif %} + {{ govukTable({ + "head": reference_document_version_quotas_headers, + "rows": value['data_rows'] + }) }} + {% endfor %} +
    + +
    + +
    +
    diff --git a/reference_documents/jinja2/includes/tabs/preferential_rates.jinja b/reference_documents/jinja2/includes/tabs/preferential_rates.jinja new file mode 100644 index 000000000..abd737378 --- /dev/null +++ b/reference_documents/jinja2/includes/tabs/preferential_rates.jinja @@ -0,0 +1,20 @@ +{% from "components/table/macro.njk" import govukTable %} +{% from 'macros/create_link.jinja' import create_link %} + +
    +
    + {{ govukTable({ + "head": reference_document_version_duties_headers, + "rows": reference_document_version_duties + }) }} +
    + +
    + +
    +
    diff --git a/reference_documents/jinja2/preferential_quotas/edit.jinja b/reference_documents/jinja2/preferential_quotas/edit.jinja new file mode 100644 index 000000000..f9bc245a2 --- /dev/null +++ b/reference_documents/jinja2/preferential_quotas/edit.jinja @@ -0,0 +1,14 @@ +{% extends "layouts/layout.jinja" %} + +{% set page_title = "Preferential duty rates" %} + +{% block content %} + +
    + + {{ form.as_p() }} + +
    + +{% endblock %} + diff --git a/reference_documents/jinja2/reference_documents/detail.jinja b/reference_documents/jinja2/reference_document_examples/details.jinja similarity index 100% rename from reference_documents/jinja2/reference_documents/detail.jinja rename to reference_documents/jinja2/reference_document_examples/details.jinja diff --git a/reference_documents/jinja2/reference_documents/list.jinja b/reference_documents/jinja2/reference_document_examples/index.jinja similarity index 92% rename from reference_documents/jinja2/reference_documents/list.jinja rename to reference_documents/jinja2/reference_document_examples/index.jinja index 70a49fa35..e7ea9cc94 100644 --- a/reference_documents/jinja2/reference_documents/list.jinja +++ b/reference_documents/jinja2/reference_document_examples/index.jinja @@ -10,7 +10,7 @@ {%- set table_rows = [] -%} {% for ref_doc in object_list %} {% set ref_doc_link -%} - {{ ref_doc.name }} + {{ ref_doc.name }} {%- endset %} {{ table_rows.append([ {"html": ref_doc_link}, diff --git a/reference_documents/jinja2/reference_document_versions/alignment_reports.jinja b/reference_documents/jinja2/reference_document_versions/alignment_reports.jinja new file mode 100644 index 000000000..59de1a733 --- /dev/null +++ b/reference_documents/jinja2/reference_document_versions/alignment_reports.jinja @@ -0,0 +1,25 @@ +{% extends "layouts/layout.jinja" %} +{% from "components/table/macro.njk" import govukTable %} + +{% set page_title = 'Reference Documents version details' %} + +{% block breadcrumb %} + {{ breadcrumbs(request, [ + {'text': "Reference Document Version Overview"} + ]) }} +{% endblock %} + +{% block content %} +

    + Alignment reports +

    + Reference document version details. + +
    + {{ govukTable({ "head": alignment_report_headers, "rows": alignment_reports }) }} +
    + +{% endblock %} + + + diff --git a/reference_documents/jinja2/reference_document_versions/details.jinja b/reference_documents/jinja2/reference_document_versions/details.jinja new file mode 100644 index 000000000..7f32bbb29 --- /dev/null +++ b/reference_documents/jinja2/reference_document_versions/details.jinja @@ -0,0 +1,27 @@ +{% extends "layouts/layout.jinja" %} +{% from "components/table/macro.njk" import govukTable %} + +{% set page_title = 'Reference Documents version details' %} + +{% block breadcrumb %} + {{ breadcrumbs(request, [ + {'text': "Reference Document Version"} + ]) }} +{% endblock %} + +{% block content %} +

    + Reference Document version Overview +

    + Reference document version details. + +
    + {{ govukTable({ "head": reference_document_version_duties_headers, "rows": reference_document_version_duties }) }} +
    +
    + {{ govukTable({ "head": reference_document_version_quotas_headers, "rows": reference_document_version_quotas }) }} +
    +{% endblock %} + + + diff --git a/reference_documents/jinja2/reference_document_versions/edit.jinja b/reference_documents/jinja2/reference_document_versions/edit.jinja new file mode 100644 index 000000000..f9bc245a2 --- /dev/null +++ b/reference_documents/jinja2/reference_document_versions/edit.jinja @@ -0,0 +1,14 @@ +{% extends "layouts/layout.jinja" %} + +{% set page_title = "Preferential duty rates" %} + +{% block content %} + +
    + + {{ form.as_p() }} + +
    + +{% endblock %} + diff --git a/reference_documents/jinja2/reference_document_versions/new_details.jinja b/reference_documents/jinja2/reference_document_versions/new_details.jinja new file mode 100644 index 000000000..77fcdff14 --- /dev/null +++ b/reference_documents/jinja2/reference_document_versions/new_details.jinja @@ -0,0 +1,47 @@ +{% extends "layouts/layout.jinja" %} + +{% from "components/table/macro.njk" import govukTable %} +{% from "components/tabs/macro.njk" import govukTabs %} + +{% set page_title = "Preferential duty rates" %} + +{% set preferential_rates_html %} + {% include "includes/tabs/preferential_rates.jinja" %} +{% endset %} +{% set preferential_quotas_html %} + {% include "includes/tabs/preferential_quotas.jinja" %} +{% endset %} + +{% set tabs = { + "items": [ + { + "label": "Preferential duty rates", + "id": "core-data", + "panel": { + "html": preferential_rates_html + } + }, + { + "label": "Tariff quotas", + "id": "tariff-quotas", + "panel": { + "html": preferential_quotas_html + } + }, + ] + } %} + +{% block content %} +

    {{ page_title }}

    +

    Title:

    +

    {{ ref_doc_title }}

    +

    Version:

    +

    {{ object.version }}

    +

    Date published:

    +

    {{ object.published_date }}

    +

    Entry in to force date:

    +

    {{ object.entry_into_force_date or 'unknown' }}

    + + {{ govukTabs(tabs) }} + +{% endblock %} diff --git a/reference_documents/jinja2/reference_documents/details.jinja b/reference_documents/jinja2/reference_documents/details.jinja new file mode 100644 index 000000000..0c301c051 --- /dev/null +++ b/reference_documents/jinja2/reference_documents/details.jinja @@ -0,0 +1,24 @@ +{% extends "layouts/layout.jinja" %} +{% from "components/table/macro.njk" import govukTable %} + +{% set page_title = 'Reference Documents versions Overview' %} + +{% block breadcrumb %} + {{ breadcrumbs(request, [ + {'text': "Reference Documents"} + ]) }} +{% endblock %} + +{% block content %} +

    + Reference Document Overview +

    + You will find a list of reference document versions below that can be viewed. + +
    + {{ govukTable({ "head": reference_document_versions_headers, "rows": reference_document_versions }) }} +
    +{% endblock %} + + + diff --git a/reference_documents/jinja2/reference_documents/index.jinja b/reference_documents/jinja2/reference_documents/index.jinja new file mode 100644 index 000000000..b281f6a3d --- /dev/null +++ b/reference_documents/jinja2/reference_documents/index.jinja @@ -0,0 +1,24 @@ +{% extends "layouts/layout.jinja" %} +{% from "components/table/macro.njk" import govukTable %} + +{% set page_title = 'Reference Documents Index' %} + +{% block breadcrumb %} + {{ breadcrumbs(request, [ + {'text': "Reference Documents"} + ]) }} +{% endblock %} + +{% block content %} +

    + Reference Documents +

    + You will find a list of reference documents below that can be viewed. + +
    + {{ govukTable({ "head": reference_document_headers, "rows": reference_documents }) }} +
    +{% endblock %} + + + diff --git a/reference_documents/jinja2/reference_documents/overview.jinja b/reference_documents/jinja2/reference_documents/overview.jinja new file mode 100644 index 000000000..fc442ceb7 --- /dev/null +++ b/reference_documents/jinja2/reference_documents/overview.jinja @@ -0,0 +1,24 @@ +{% extends "layouts/layout.jinja" %} +{% from "components/table/macro.njk" import govukTable %} + +{% set page_title = 'Reference Documents versions Overview' %} + +{% block breadcrumb %} + {{ breadcrumbs(request, [ + {'text': "Reference Documents"} + ]) }} +{% endblock %} + +{% block content %} +

    + Reference Document Overview +

    + You will find a list of reference document versions below that can be viewed. + +
    + {{ govukTable({ "head": reference_document_versions_headers, "rows": reference_documents_versions }) }} +
    +{% endblock %} + + + diff --git a/reference_documents/migrations/0001_initial.py b/reference_documents/migrations/0001_initial.py new file mode 100644 index 000000000..4470166cc --- /dev/null +++ b/reference_documents/migrations/0001_initial.py @@ -0,0 +1,237 @@ +# Generated by Django 3.2.23 on 2024-02-22 15:02 + +import django.db.models.deletion +import django_fsm +from django.db import migrations +from django.db import models + +import common.fields + + +class Migration(migrations.Migration): + initial = True + + dependencies = [] + + operations = [ + migrations.CreateModel( + name="AlignmentReport", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("created_at", models.DateTimeField(auto_now_add=True)), + ], + ), + migrations.CreateModel( + name="ReferenceDocument", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("created_at", models.DateTimeField(auto_now_add=True)), + ( + "title", + models.CharField( + db_index=True, + help_text="Short name for this workbasket", + max_length=255, + unique=True, + ), + ), + ("area_id", models.CharField(db_index=True, max_length=4)), + ], + ), + migrations.CreateModel( + name="ReferenceDocumentVersion", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ("version", models.FloatField()), + ("published_date", models.DateField(blank=True, null=True)), + ("entry_into_force_date", models.DateField(blank=True, null=True)), + ( + "status", + django_fsm.FSMField( + choices=[ + ("EDITING", "Editing"), + ("IN_REVIEW", "In Review"), + ("PUBLISHED", "Published"), + ], + db_index=True, + default="EDITING", + editable=False, + max_length=50, + ), + ), + ( + "reference_document", + models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, + related_name="reference_document_versions", + to="reference_documents.referencedocument", + ), + ), + ], + ), + migrations.CreateModel( + name="PreferentialRate", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("commodity_code", models.CharField(db_index=True, max_length=10)), + ("duty_rate", models.CharField(max_length=255)), + ("order", models.IntegerField()), + ( + "valid_between", + common.fields.TaricDateRangeField( + blank=True, + db_index=True, + default=None, + null=True, + ), + ), + ( + "reference_document_version", + models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, + related_name="preferential_rates", + to="reference_documents.referencedocumentversion", + ), + ), + ], + ), + migrations.CreateModel( + name="PreferentialQuota", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("quota_order_number", models.CharField(db_index=True, max_length=6)), + ("commodity_code", models.CharField(db_index=True, max_length=10)), + ("quota_duty_rate", models.CharField(max_length=255)), + ("volume", models.CharField(max_length=255)), + ( + "valid_between", + common.fields.TaricDateRangeField( + blank=True, + db_index=True, + default=None, + null=True, + ), + ), + ("measurement", models.CharField(max_length=255)), + ("order", models.IntegerField()), + ( + "reference_document_version", + models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, + related_name="preferential_quotas", + to="reference_documents.referencedocumentversion", + ), + ), + ], + ), + migrations.CreateModel( + name="AlignmentReportCheck", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("check_name", models.CharField(max_length=255)), + ( + "status", + django_fsm.FSMField( + choices=[ + ("PASS", "Passing"), + ("FAIL", "Failed"), + ("WARNING", "Warning"), + ], + db_index=True, + default="FAIL", + editable=False, + max_length=50, + ), + ), + ("message", models.TextField(null=True)), + ( + "alignment_report", + models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, + related_name="alignment_report_checks", + to="reference_documents.alignmentreport", + ), + ), + ( + "preferential_quota", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.PROTECT, + related_name="preferential_quota_checks", + to="reference_documents.preferentialquota", + ), + ), + ( + "preferential_rate", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.PROTECT, + related_name="preferential_rate_checks", + to="reference_documents.preferentialrate", + ), + ), + ], + ), + migrations.AddField( + model_name="alignmentreport", + name="reference_document_version", + field=models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, + related_name="alignment_reports", + to="reference_documents.referencedocumentversion", + ), + ), + ] diff --git a/reference_documents/models.py b/reference_documents/models.py index 6b2021999..82f5323a6 100644 --- a/reference_documents/models.py +++ b/reference_documents/models.py @@ -1 +1,168 @@ -# Create your models here. +from django.db import models +from django.db.models import fields +from django_fsm import FSMField + +from common.fields import TaricDateRangeField + + +class ReferenceDocumentVersionStatus(models.TextChoices): + # Reference document version can be edited + EDITING = "EDITING", "Editing" + # Reference document version ius locked and in review + IN_REVIEW = "IN_REVIEW", "In Review" + # reference document version has been approved and published + PUBLISHED = "PUBLISHED", "Published" + + +class AlignmentReportCheckStatus(models.TextChoices): + # Reference document version can be edited + PASS = "PASS", "Passing" + # Reference document version ius locked and in review + FAIL = "FAIL", "Failed" + # reference document version has been approved and published + WARNING = "WARNING", "Warning" + + +class ReferenceDocument(models.Model): + created_at = models.DateTimeField(auto_now_add=True) + + title = models.CharField( + max_length=255, + help_text="Short name for this workbasket", + db_index=True, + unique=True, + ) + + area_id = models.CharField( + max_length=4, + db_index=True, + ) + + +class ReferenceDocumentVersion(models.Model): + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + version = models.FloatField() + published_date = models.DateField(blank=True, null=True) + entry_into_force_date = models.DateField(blank=True, null=True) + + reference_document = models.ForeignKey( + "reference_documents.ReferenceDocument", + on_delete=models.PROTECT, + related_name="reference_document_versions", + ) + status = FSMField( + default=ReferenceDocumentVersionStatus.EDITING, + choices=ReferenceDocumentVersionStatus.choices, + db_index=True, + protected=False, + editable=False, + ) + + +class PreferentialRate(models.Model): + commodity_code = models.CharField( + max_length=10, + db_index=True, + ) + duty_rate = models.CharField( + max_length=255, + ) + order = models.IntegerField() + + reference_document_version = models.ForeignKey( + "reference_documents.ReferenceDocumentVersion", + on_delete=models.PROTECT, + related_name="preferential_rates", + ) + + valid_between = TaricDateRangeField( + db_index=True, + null=True, + blank=True, + default=None, + ) + + +class PreferentialQuota(models.Model): + quota_order_number = models.CharField( + max_length=6, + db_index=True, + ) + commodity_code = models.CharField( + max_length=10, + db_index=True, + ) + quota_duty_rate = models.CharField( + max_length=255, + ) + volume = models.CharField( + max_length=255, + ) + + valid_between = TaricDateRangeField( + db_index=True, + null=True, + blank=True, + default=None, + ) + + measurement = models.CharField( + max_length=255, + ) + + order = models.IntegerField() + + reference_document_version = models.ForeignKey( + "reference_documents.ReferenceDocumentVersion", + on_delete=models.PROTECT, + related_name="preferential_quotas", + ) + + +class AlignmentReport(models.Model): + created_at = models.DateTimeField(auto_now_add=True) + reference_document_version = models.ForeignKey( + "reference_documents.ReferenceDocumentVersion", + on_delete=models.PROTECT, + related_name="alignment_reports", + ) + + +class AlignmentReportCheck(models.Model): + created_at = models.DateTimeField(auto_now_add=True) + + alignment_report = models.ForeignKey( + "reference_documents.AlignmentReport", + on_delete=models.PROTECT, + related_name="alignment_report_checks", + ) + + check_name = fields.CharField(max_length=255) + """A string identifying the type of check carried out.""" + + status = FSMField( + default=AlignmentReportCheckStatus.FAIL, + choices=AlignmentReportCheckStatus.choices, + db_index=True, + protected=False, + editable=False, + ) + message = fields.TextField(null=True) + """The text content returned by the check, if any.""" + + preferential_quota = models.ForeignKey( + "reference_documents.PreferentialQuota", + on_delete=models.PROTECT, + related_name="preferential_quota_checks", + blank=True, + null=True, + ) + + preferential_rate = models.ForeignKey( + "reference_documents.PreferentialRate", + on_delete=models.PROTECT, + related_name="preferential_rate_checks", + blank=True, + null=True, + ) diff --git a/reference_documents/static/reference_documents/scss/_reference_documents.scss b/reference_documents/static/reference_documents/scss/_reference_documents.scss new file mode 100644 index 000000000..59f254e15 --- /dev/null +++ b/reference_documents/static/reference_documents/scss/_reference_documents.scss @@ -0,0 +1,8 @@ +.check-passing { + color: #1d640f; + font-weight: bold; +} +.check-failing { + color: #671111; + font-weight: bold; +} \ No newline at end of file diff --git a/reference_documents/urls.py b/reference_documents/urls.py index 3c823912b..2f0448f90 100644 --- a/reference_documents/urls.py +++ b/reference_documents/urls.py @@ -1,16 +1,70 @@ from django.urls import path +from rest_framework import routers -from reference_documents import views +from reference_documents.views import alignment_report_views +from reference_documents.views import example_views +from reference_documents.views import preferential_quotas +from reference_documents.views import reference_document_version_views +from reference_documents.views import reference_document_views + +app_name = "reference_documents" + +api_router = routers.DefaultRouter() urlpatterns = [ + # Example views + path( + "reference-documents-example/", + example_views.ReferenceDocumentsListView.as_view(), + name="example-ui-index", + ), + path( + f"reference-documents-example-albania/", + example_views.ReferenceDocumentsDetailView.as_view(), + name="example-ui-details", + ), + # Reference document views + path( + "reference_documents/", + reference_document_views.ReferenceDocumentList.as_view(), + name="index", + ), + path( + "reference_documents//", + reference_document_views.ReferenceDocumentDetails.as_view(), + name="details", + ), + # reference document version views + path( + "reference_document_versions//", + reference_document_version_views.ReferenceDocumentVersionDetails.as_view(), + name="version_details", + ), + path( + "reference_document_versions/edit//", + reference_document_version_views.ReferenceDocumentVersionEditView.as_view(), + name="reference_document_version_edit", + ), + # Alignment report views + path( + "reference_document_version_alignment_reports//", + alignment_report_views.ReferenceDocumentVersionAlignmentReportsDetailsView.as_view(), + name="version_alignment_reports", + ), + path( + "alignment_reports//", + alignment_report_views.AlignmentReportsDetailsView.as_view(), + name="alignment_reports", + ), + # Preferential Quotas path( - "reference-documents/", - views.ReferenceDocumentsListView.as_view(), - name="reference_documents-ui-list", + "preferential_quotas/delete//", + preferential_quotas.PreferentialQuotaDeleteView.as_view(), + name="preferential_quotas_delete", ), path( - f"reference-documents/albania/", - views.ReferenceDocumentsDetailView.as_view(), - name="reference_documents-ui-detail", + "preferential_quotas/edit//", + preferential_quotas.PreferentialQuotaEditView.as_view(), + name="preferential_quotas_edit", ), ] diff --git a/reference_documents/views/alignment_report_views.py b/reference_documents/views/alignment_report_views.py new file mode 100644 index 000000000..b59aa751d --- /dev/null +++ b/reference_documents/views/alignment_report_views.py @@ -0,0 +1,89 @@ +from django.contrib.auth.mixins import PermissionRequiredMixin +from django.views.generic import DetailView + +from reference_documents.models import AlignmentReport +from reference_documents.models import AlignmentReportCheckStatus +from reference_documents.models import ReferenceDocumentVersion + + +class ReferenceDocumentVersionAlignmentReportsDetailsView( + PermissionRequiredMixin, + DetailView, +): + template_name = "reference_document_versions/alignment_reports.jinja" + permission_required = "reference_documents.view_reference_document" + model = ReferenceDocumentVersion + + def get_context_data(self, *args, **kwargs): + context = super( + ReferenceDocumentVersionAlignmentReportsDetailsView, + self, + ).get_context_data( + *args, + **kwargs, + ) + + context["alignment_report_headers"] = [ + {"text": "Created"}, + {"text": "Passed"}, + {"text": "failed"}, + {"text": "Percent"}, + {"text": "Actions"}, + ] + + alignment_reports = [] + for report in context["object"].alignment_reports.order_by("-created_at"): + failure_count = ( + report.alignment_report_checks.all() + .filter(status=AlignmentReportCheckStatus.FAIL) + .count() + ) + pass_count = ( + report.alignment_report_checks.all() + .filter(status=AlignmentReportCheckStatus.PASS) + .count() + ) + + if pass_count > 0: + pass_percentage = round( + (pass_count / (pass_count + failure_count)) * 100, + 2, + ) + else: + pass_percentage = 100 + + alignment_reports.append( + [ + { + "text": report.created_at.strftime("%d/%m/%Y %H:%M"), + }, + { + "text": pass_count, + }, + { + "text": failure_count, + }, + { + "text": f"{pass_percentage} %", + }, + { + "html": f"Details", + }, + ], + ) + + context["alignment_reports"] = alignment_reports + + return context + + +class AlignmentReportsDetailsView(PermissionRequiredMixin, DetailView): + template_name = "alignment_reports/details.jinja" + permission_required = "reference_documents.view_reference_document" + model = AlignmentReport + + def get_context_data(self, *args, **kwargs): + context = super(AlignmentReportsDetailsView, self).get_context_data( + *args, + **kwargs, + ) diff --git a/reference_documents/views.py b/reference_documents/views/example_views.py similarity index 83% rename from reference_documents/views.py rename to reference_documents/views/example_views.py index e0b090905..906f463bc 100644 --- a/reference_documents/views.py +++ b/reference_documents/views/example_views.py @@ -1,15 +1,15 @@ -# Create your views here. from datetime import date from django.db.models import Q from django.views.generic import TemplateView from geo_areas.models import GeographicalArea +from geo_areas.models import GeographicalAreaDescription from measures.models import Measure class ReferenceDocumentsListView(TemplateView): - template_name = "reference_documents/list.jinja" + template_name = "reference_document_examples/index.jinja" def get_context_data(self, *args, **kwargs): context = super().get_context_data(*args, **kwargs) @@ -26,7 +26,7 @@ def get_context_data(self, *args, **kwargs): class ReferenceDocumentsDetailView(TemplateView): - template_name = "reference_documents/detail.jinja" + template_name = "reference_document_examples/details.jinja" def get_pref_duty_rates(self): """Returns a list of measures associated with the Albania Preferential @@ -95,3 +95,16 @@ def get_context_data(self, *args, **kwargs): "quotas": self.get_tariff_quota_data(), } return context + + def get_name_by_area_id(self, area_id): + geo_area = ( + GeographicalArea.objects.latest_approved().filter(area_id=area_id).first() + ) + if geo_area: + geo_area_name = ( + GeographicalAreaDescription.objects.latest_approved() + .filter(described_geographicalarea_id=geo_area.trackedmodel_ptr_id) + .last() + ) + return geo_area_name.description if geo_area_name else "None" + return "None" diff --git a/reference_documents/views/preferential_quotas.py b/reference_documents/views/preferential_quotas.py new file mode 100644 index 000000000..6a454325b --- /dev/null +++ b/reference_documents/views/preferential_quotas.py @@ -0,0 +1,37 @@ +from django.contrib.auth.mixins import PermissionRequiredMixin +from django.shortcuts import redirect +from django.urls import reverse +from django.views.generic import UpdateView + +from reference_documents.models import PreferentialQuota + + +class PreferentialQuotaEditView(PermissionRequiredMixin, UpdateView): + template_name = "preferential_quotas/edit.jinja" + permission_required = "reference_documents.edit_reference_document" + model = PreferentialQuota + fields = [ + "quota_order_number", + "commodity_code", + "quota_duty_rate", + "volume", + "measurement", + "valid_between", + ] + + def post(self, request, *args, **kwargs): + quota = self.get_object() + quota.save() + return redirect( + reverse( + "reference_documents:version_details", + args=[quota.reference_document_version.pk], + ) + + "#tariff-quotas", + ) + + +class PreferentialQuotaDeleteView(PermissionRequiredMixin, UpdateView): + template_name = "preferential_quotas/delete.jinja" + permission_required = "reference_documents.edit_reference_document" + model = PreferentialQuota diff --git a/reference_documents/views/reference_document_version_views.py b/reference_documents/views/reference_document_version_views.py new file mode 100644 index 000000000..f24788ca3 --- /dev/null +++ b/reference_documents/views/reference_document_version_views.py @@ -0,0 +1,244 @@ +from django.contrib.auth.mixins import PermissionRequiredMixin +from django.urls import reverse +from django.urls import reverse_lazy +from django.views.generic import DetailView +from django.views.generic import UpdateView + +from commodities.models import GoodsNomenclature +from geo_areas.models import GeographicalAreaDescription +from quotas.models import QuotaOrderNumber +from reference_documents.models import AlignmentReportCheckStatus +from reference_documents.models import ReferenceDocumentVersion + + +class ReferenceDocumentVersionDetails(PermissionRequiredMixin, DetailView): + template_name = "reference_document_versions/new_details.jinja" + permission_required = "reference_documents.view_reference_document" + model = ReferenceDocumentVersion + + def get_country_by_area_id(self, area_id): + description = ( + GeographicalAreaDescription.objects.latest_approved() + .filter(described_geographicalarea__area_id=area_id) + .order_by("-validity_start") + .first() + ) + if description: + return description.description + else: + return f"{area_id} (unknown description)" + + def get_tap_comm_code(self, duty): + if duty.reference_document_version.entry_into_force_date is not None: + contains_date = duty.reference_document_version.entry_into_force_date + else: + contains_date = duty.reference_document_version.published_date + + goods = GoodsNomenclature.objects.latest_approved().filter( + item_id=duty.commodity_code, + valid_between__contains=contains_date, + suffix=80, + ) + + if len(goods) == 0: + return None + + return goods.first() + + def get_tap_order_number(self, quota): + # todo: This needs to consider the validity period(s) + # may need to handle in the pre processing of the data e.g. where the volume defines multiple periods + + if quota.reference_document_version.entry_into_force_date is not None: + contains_date = quota.reference_document_version.entry_into_force_date + else: + contains_date = quota.reference_document_version.published_date + + quota_order_number = QuotaOrderNumber.objects.latest_approved().filter( + order_number=quota.quota_order_number, + valid_between__contains=contains_date, + ) + + if len(quota_order_number) == 0: + return None + + return quota_order_number.first() + + def get_context_data(self, *args, **kwargs): + context = super(ReferenceDocumentVersionDetails, self).get_context_data( + *args, + **kwargs, + ) + + # title + context[ + "ref_doc_title" + ] = f'Reference Document for {self.get_country_by_area_id(context["object"].reference_document.area_id)}' + + context["reference_document_version_duties_headers"] = [ + {"text": "Comm Code"}, + {"text": "Duty Rate"}, + {"text": "Validity"}, + {"text": "Checks"}, + {"text": "Actions"}, + ] + + context["reference_document_version_quotas_headers"] = [ + {"text": "Comm Code"}, + {"text": "Rate"}, + {"text": "Volume"}, + {"text": "Validity"}, + {"text": "Checks"}, + {"text": "Actions"}, + ] + + reference_document_version_duties = [] + reference_document_version_quotas = {} + + latest_alignment_report = context["object"].alignment_reports.last() + + for duty in context["object"].preferential_rates.order_by("order"): + failure_count = ( + duty.preferential_rate_checks.all() + .filter( + alignment_report=latest_alignment_report, + status=AlignmentReportCheckStatus.FAIL, + ) + .count() + ) + + check_count = ( + duty.preferential_rate_checks.all() + .filter( + alignment_report=latest_alignment_report, + ) + .count() + ) + + if failure_count > 0: + checks_output = f'
    FAIL
    ' + elif check_count == 0: + checks_output = f"N/A" + else: + checks_output = f'
    PASS
    ' + + comm_code = self.get_tap_comm_code(duty) + + if comm_code: + comm_code_link = f'{comm_code.item_id}' + else: + comm_code_link = f"{duty.commodity_code}" + + reference_document_version_duties.append( + [ + { + "html": comm_code_link, + }, + { + "text": duty.duty_rate, + }, + { + "text": duty.valid_between, + }, + { + "html": checks_output, + }, + { + "text": "", + }, + ], + ) + + # order numbers + for quota in context["object"].preferential_quotas.order_by("order"): + failure_count = ( + quota.preferential_quota_checks.all() + .filter( + alignment_report=latest_alignment_report, + status=AlignmentReportCheckStatus.FAIL, + ) + .count() + ) + + check_count = ( + quota.preferential_quota_checks.all() + .filter( + alignment_report=latest_alignment_report, + ) + .count() + ) + + if failure_count > 0: + checks_output = f'
    FAIL
    ' + elif check_count == 0: + checks_output = f"N/A" + else: + checks_output = f'
    PASS
    ' + + quota_order_number = self.get_tap_order_number(quota) + + comm_code = self.get_tap_comm_code(quota) + if comm_code: + comm_code_link = f'{comm_code.structure_code}' + else: + comm_code_link = f"{quota.commodity_code}" + + row_to_add = [ + { + "html": comm_code_link, + }, + { + "text": quota.quota_duty_rate, + }, + { + "text": f"{quota.volume} {quota.measurement}", + }, + { + "text": quota.valid_between, + }, + { + "html": checks_output, + }, + { + "html": f"Edit " + f"Delete", + }, + ] + + if quota.quota_order_number in reference_document_version_quotas.keys(): + reference_document_version_quotas[quota.quota_order_number][ + "data_rows" + ].append( + row_to_add, + ) + else: + reference_document_version_quotas[quota.quota_order_number] = { + "data_rows": [row_to_add], + "quota_order_number": quota_order_number, + } + + context["reference_document_version_duties"] = reference_document_version_duties + context["reference_document_version_quotas"] = reference_document_version_quotas + + return context + + +class ReferenceDocumentVersionEditView(PermissionRequiredMixin, UpdateView): + template_name = "reference_document_versions/edit.jinja" + permission_required = "reference_documents.edit_reference_document" + model = ReferenceDocumentVersion + fields = ["version", "published_date", "entry_into_force_date"] + + # def post(self, request, *args, **kwargs): + # reference_document_version = self.get_object() + # reference_document_version.save() + # return redirect(reverse("reference_documents:details", args=[reference_document_version.reference_document.pk])) + + def form_valid(self, form): + return super(ReferenceDocumentVersionEditView, self).form_valid(form) + + def get_success_url(self): + return reverse_lazy( + "reference_documents:details", + args=[self.object.id], + ) diff --git a/reference_documents/views/reference_document_views.py b/reference_documents/views/reference_document_views.py new file mode 100644 index 000000000..7b000b722 --- /dev/null +++ b/reference_documents/views/reference_document_views.py @@ -0,0 +1,128 @@ +from django.contrib.auth.mixins import PermissionRequiredMixin +from django.urls import reverse +from django.views.generic import DetailView +from django.views.generic import ListView + +from geo_areas.models import GeographicalAreaDescription +from reference_documents.models import ReferenceDocument + + +class ReferenceDocumentList(PermissionRequiredMixin, ListView): + """UI endpoint for viewing and filtering workbaskets.""" + + template_name = "reference_documents/index.jinja" + permission_required = "reference_documents.view_reference_document" + model = ReferenceDocument + + def get_name_by_area_id(self, area_id): + description = ( + GeographicalAreaDescription.objects.latest_approved() + .filter(described_geographicalarea__area_id=area_id) + .order_by("-validity_start") + .first() + ) + if description: + return description.description + else: + return f"{area_id} (unknown description)" + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + reference_documents = [] + + for reference in ReferenceDocument.objects.all().order_by("area_id"): + if reference.reference_document_versions.count() == 0: + reference_documents.append( + [ + {"text": "None"}, + { + "text": f"{reference.area_id} - ({self.get_name_by_area_id(reference.area_id)})", + }, + {"text": 0}, + {"text": 0}, + { + "html": f'Details', + }, + ], + ) + + else: + reference_documents.append( + [ + {"text": reference.reference_document_versions.last().version}, + { + "text": f"{reference.area_id} - ({self.get_name_by_area_id(reference.area_id)})", + }, + { + "text": reference.reference_document_versions.last().preferential_rates.count(), + }, + { + "text": reference.reference_document_versions.last().preferential_quotas.count(), + }, + { + "html": f'Details', + }, + ], + ) + + context["reference_documents"] = reference_documents + context["reference_document_headers"] = [ + {"text": "Latest Version"}, + {"text": "Country"}, + {"text": "Duties"}, + {"text": "Quotas"}, + {"text": "Actions"}, + ] + return context + + +class ReferenceDocumentDetails(PermissionRequiredMixin, DetailView): + template_name = "reference_documents/details.jinja" + permission_required = "reference_documents.view_reference_document" + model = ReferenceDocument + + def get_context_data(self, *args, **kwargs): + context = super(ReferenceDocumentDetails, self).get_context_data( + *args, + **kwargs, + ) + + context["reference_document_versions_headers"] = [ + {"text": "Version"}, + {"text": "Duties"}, + {"text": "Quotas"}, + {"text": "EIF date"}, + {"text": "Actions"}, + ] + reference_document_versions = [] + + print(self.request) + + for version in context["object"].reference_document_versions.order_by( + "version", + ): + reference_document_versions.append( + [ + { + "text": version.version, + }, + { + "text": version.preferential_rates.count(), + }, + { + "text": version.preferential_quotas.count(), + }, + { + "text": version.entry_into_force_date, + }, + { + "html": f'version details
    ' + f'Edit
    ' + f'Alignment reports', + }, + ], + ) + + context["reference_document_versions"] = reference_document_versions + + return context diff --git a/regulations/tests/test_views.py b/regulations/tests/test_views.py index be4652d56..09fb45f18 100644 --- a/regulations/tests/test_views.py +++ b/regulations/tests/test_views.py @@ -60,7 +60,7 @@ def test_regulation_detail_views( view, url_pattern, valid_user_client, - session_with_workbasket, + session_request_with_workbasket, ): """Verify that regulation detail views are under the url regulations/ and don't return an error.""" @@ -284,7 +284,7 @@ def test_regulation_list_view( view, url_pattern, valid_user_client, - session_with_workbasket, + session_request_with_workbasket, ): """Verify that regulation list view is under the url regulations/ and doesn't return an error.""" @@ -353,7 +353,10 @@ def test_regulation_api_list_view(valid_user_client, date_ranges): ) -def test_regulation_update_view_new_regulation_id(date_ranges, valid_user_client): +def test_regulation_update_view_new_regulation_id( + date_ranges, + client_with_current_workbasket, +): """Test that an update to a regulation's `regulation_id` creates a new regulation, updates associated measures, and deletes old one.""" regulation = factories.UIDraftRegulationFactory.create() @@ -387,7 +390,7 @@ def test_regulation_update_view_new_regulation_id(date_ranges, valid_user_client "regulation_id": regulation.regulation_id, }, ) - response = valid_user_client.post(url, form_data) + response = client_with_current_workbasket.post(url, form_data) assert response.status_code == 302 new_regulation = Regulation.objects.get(regulation_id=new_regulation_id) diff --git a/reports/jinja2/generics/table.jinja b/reports/jinja2/generics/table.jinja index 8f190f2cf..1d5bcc2a2 100644 --- a/reports/jinja2/generics/table.jinja +++ b/reports/jinja2/generics/table.jinja @@ -1,6 +1,69 @@ {% from "components/table/macro.njk" import govukTable -%} -{{ govukTable({ - "head": report.headers(), - "rows": report.rows() -}) }} +{% if report.tabular_reports -%} + +
    + +
    +

    {{ report.tab_name }}

    + Export to CSV + {{ govukTable({ + "head": report.headers(), + "rows": report.rows() + }) }} +
    +
    +
    +

    {{ report.tab_name2 }}

    + Export to CSV +
    + {{ govukTable({ + "head": report.headers2(), + "rows": report.rows2() + }) }} +
    +
    +

    {{ report.tab_name3 }}

    + Export to CSV + {{ govukTable({ + "head": report.headers3(), + "rows": report.rows3() + }) }} +
    +
    +

    {{ report.tab_name4 }}

    + Export to CSV + {{ govukTable({ + "head": report.headers4(), + "rows": report.rows4() + }) }} +
    +
    + +{% else -%} + {{ govukTable({ + "head": report.headers(), + "rows": report.rows() + }) }} +{% endif -%} \ No newline at end of file diff --git a/reports/jinja2/reports/report_chart_timescale.jinja b/reports/jinja2/reports/report_chart_timescale.jinja index f77c9b831..3e2a87a0e 100644 --- a/reports/jinja2/reports/report_chart_timescale.jinja +++ b/reports/jinja2/reports/report_chart_timescale.jinja @@ -82,10 +82,13 @@

    Report: {{ report.name }}

    - Timescale chart

    {{ report.description|safe }}

    +
    diff --git a/reports/jinja2/reports/report_table.jinja b/reports/jinja2/reports/report_table.jinja index 16ce779a2..24e5ea9ce 100644 --- a/reports/jinja2/reports/report_table.jinja +++ b/reports/jinja2/reports/report_table.jinja @@ -12,12 +12,13 @@

    Report: {{ report.name }}

    - Export to CSV - Table + {% if not report.tab_name2 %} + Export to CSV + {% endif %}

    {{ report.description|safe }}

    -
    +
    {% include "generics/table.jinja" %}
    {% endblock %} diff --git a/reports/reports/base_table.py b/reports/reports/base_table.py index 7724b191e..b638c9353 100644 --- a/reports/reports/base_table.py +++ b/reports/reports/base_table.py @@ -1,4 +1,6 @@ from abc import abstractmethod +from django.urls import reverse +from django.utils.safestring import mark_safe from reports.reports.base import ReportBase @@ -6,10 +8,18 @@ class ReportBaseTable(ReportBase): name = "Base Table Report" report_template = "table" + tabular_reports = False def __init__(self): pass + def link_renderer_for_quotas(self, order_number, text, fragment=None): + url = reverse("quota-ui-detail", args=[order_number.sid]) + href = url + fragment if fragment else url + return mark_safe( + f"{text}" + ) + @abstractmethod def query(self): pass @@ -19,9 +29,42 @@ def headers(self) -> [dict]: pass @abstractmethod - def rows(self) -> [[dict]]: + def rows(self) -> [dict]: pass @abstractmethod def row(self, row) -> [dict]: pass + + def headers2(self) -> [dict]: + return [] + + def rows2(self) -> [dict]: + return [] + + def row2(self, row) -> [dict]: + return [] + + def headers3(self) -> [dict]: + return [] + + def rows3(self) -> [dict]: + return [] + + def row3(self, row) -> [dict]: + return [] + + def headers4(self) -> [dict]: + return [] + + def rows4(self) -> [dict]: + return [] + + def row4(self, row) -> [dict]: + return [] + + def query3(self): + return [] + + def query4(self): + return [] diff --git a/reports/reports/cds_approved.py b/reports/reports/cds_approved.py index b91bce1d1..86cad2e20 100644 --- a/reports/reports/cds_approved.py +++ b/reports/reports/cds_approved.py @@ -11,7 +11,7 @@ class Report(ReportBaseChart): name = "CDS approvals in the last 12 months" - description = "This report shows the count of approved (published) workbaskets in the last 12 months per day" + description = "This chart shows the count of approved (published) workbaskets in the last 12 months per day" chart_type = "line" report_template = "chart_timescale" days_in_past = 365 diff --git a/reports/reports/cds_approved_7_day_avg.py b/reports/reports/cds_approved_7_day_avg.py index 1b8e50a18..208bc0573 100644 --- a/reports/reports/cds_approved_7_day_avg.py +++ b/reports/reports/cds_approved_7_day_avg.py @@ -12,7 +12,7 @@ class Report(ReportBaseChart): name = "CDS approvals (7 day average) in the last 12 months" description = ( - "This report shows the 7 day average of approved (published) " + "This chart shows the 7 day average of approved (published) " "workbaskets in the last 12 months per day" ) chart_type = "line" diff --git a/reports/reports/cds_rejections.py b/reports/reports/cds_rejections.py index 5f0f4f6de..eca175d48 100644 --- a/reports/reports/cds_rejections.py +++ b/reports/reports/cds_rejections.py @@ -12,9 +12,9 @@ class Report(ReportBaseChart): name = "CDS rejections in the last 12 months" description = ( - "This report shows the count of rejected (errored) workbaskets in the last 12 months per day. " + "This chart shows the count of rejected (errored) workbaskets in the last 12 months per day. " "

    " - "Note: workbaskets that are rejected, tend to be removed, so this report is for demonstration " + "Note: workbaskets that are rejected, tend to be removed, so this chart is for demonstration " "purposes only at this point. there remains some work to do to confidently track and collect " "rejection information in TAP suitable for reporting purposes." ) diff --git a/reports/reports/expiring_quotas_with_no_definition_period.py b/reports/reports/expiring_quotas_with_no_definition_period.py index 8f02a8f13..9d75bec07 100644 --- a/reports/reports/expiring_quotas_with_no_definition_period.py +++ b/reports/reports/expiring_quotas_with_no_definition_period.py @@ -1,21 +1,25 @@ import datetime -from django.db.models import Exists, Q +from django.db.models import Q from reports.reports.base_table import ReportBaseTable from quotas.models import ( QuotaOrderNumber, QuotaDefinition, - QuotaOrderNumberOriginExclusion, - QuotaOrderNumberOrigin, + QuotaBlocking, + QuotaSuspension, ) -from measures.models import Measure, MeasureExcludedGeographicalArea class Report(ReportBaseTable): name = "Quotas Expiring Soon" enabled = True - description = ( - "Quotas with definition periods about to expire and no future definition period" - ) + description = "Quotas with definition, sub-quota, blocking or suspension periods about to expire and no future definition period." + tabular_reports = True + tab_name = "Definitions" + tab_name2 = "Sub-quota associations" + tab_name3 = "Blocking periods" + tab_name4 = "Suspension periods" + current_time = datetime.datetime.now() + future_time = current_time + datetime.timedelta(weeks=5) def headers(self) -> [dict]: return [ @@ -24,59 +28,255 @@ def headers(self) -> [dict]: {"text": "Definition End Date"}, ] - def row(self, row: QuotaDefinition) -> [dict]: + def row(self, row) -> [dict]: return [ - {"text": row.order_number}, + {"text": self.link_renderer_for_quotas(row.order_number, row.order_number)}, {"text": row.valid_between.lower}, {"text": row.valid_between.upper}, ] def rows(self) -> [[dict]]: - table_rows = [] - for row in self.query(): - table_rows.append(self.row(row)) + table_rows = [self.row(row) for row in self.query()] + + if not any(table_rows): + return [ + [{"text": "There is no data for this report at present"}] + + [{"text": " "} for _ in range(len(self.headers()) - 1)] + ] return table_rows def query(self): - expiring_quotas = self.find_quotas_expiring_soon() + expiring_quotas = self.find_quota_definitions_expiring_soon() quotas_without_future_definition = self.find_quotas_without_future_definition( expiring_quotas ) return quotas_without_future_definition - def find_quotas_expiring_soon(self): - current_time = datetime.datetime.now() - future_time = current_time + datetime.timedelta(weeks=5) - - filter_query = ( - Q(valid_between__endswith__gte=current_time) + def find_quota_definitions_expiring_soon(self): + expiring_quotas = QuotaDefinition.objects.latest_approved().filter( + Q( + valid_between__isnull=False, + valid_between__endswith__lte=self.future_time, + ) + & Q(valid_between__endswith__gte=self.current_time) | Q(valid_between__endswith=None) - ) & Q( - valid_between__isnull=False, - valid_between__endswith__lte=future_time, ) - quotas_expiring_soon = QuotaDefinition.objects.latest_approved().filter( - filter_query - ) + # Filter out quota definitions with associated future definitions + filtered_quotas = [] + for quota in expiring_quotas: + future_definitions = QuotaDefinition.objects.latest_approved().filter( + order_number__order_number=quota.order_number, + valid_between__startswith__gt=quota.valid_between.upper, + ) - return list(quotas_expiring_soon) + if not future_definitions.exists(): + filtered_quotas.append(quota) + + return filtered_quotas def find_quotas_without_future_definition(self, expiring_quotas): matching_data = set() for quota in expiring_quotas: - future_definitions = QuotaDefinition.objects.latest_approved().filter( + future_definitions = QuotaOrderNumber.objects.latest_approved().filter( order_number=quota.order_number, valid_between__startswith__gt=quota.valid_between.upper, ) if not future_definitions.exists(): + quota.definition_start_date = quota.valid_between.lower + quota.definition_end_date = quota.valid_between.upper matching_data.add(quota) - for quota in matching_data: - quota.definition_start_date = quota.valid_between.lower - quota.definition_end_date = quota.valid_between.upper + return list(matching_data) + + def headers2(self) -> [dict]: + return [ + {"text": "Quota Order Number"}, + {"text": "Sub-quota associations SID"}, + {"text": "Sub-quota associations Start Date"}, + {"text": "Sub-quota associations End Date"}, + {"text": "Definition Period SID"}, + ] + + def row2(self, row) -> [dict]: + sub_quotas_array = [] + + for sub_quotas in row.sub_quotas.all(): + sub_quotas_array.append( + { + "text": self.link_renderer_for_quotas( + row.order_number, row.order_number + ) + }, + { + "text": self.link_renderer_for_quotas( + row.order_number, sub_quotas.sid, "#sub-quotas" + ) + }, + {"text": sub_quotas.valid_between.lower}, + {"text": sub_quotas.valid_between.upper}, + { + "text": self.link_renderer_for_quotas( + row.order_number, row.sid, "#definition-details" + ) + }, + ) + + return sub_quotas_array + + def rows2(self) -> [[dict]]: + table_rows = [self.row2(row) for row in self.query()] + + if not any(table_rows): + return [ + [{"text": "There is no data for this report at present"}] + + [{"text": " "} for _ in range(len(self.headers2()) - 1)] + ] + + return table_rows + + def find_quota_blocking_without_future_definition(self, expiring_quotas): + matching_data = set() + + for quota_definition in expiring_quotas: + associated_blocking_definitions = ( + QuotaBlocking.objects.latest_approved().filter( + quota_definition=quota_definition, + ) + ) + + if associated_blocking_definitions.exists(): + quota_definition.definition_start_date = ( + quota_definition.valid_between.lower + ) + quota_definition.definition_end_date = ( + quota_definition.valid_between.upper + ) + matching_data.add(quota_definition) return list(matching_data) + + def headers3(self) -> [dict]: + return [ + {"text": "Quota Order Number"}, + {"text": "Blocking Period SIDs"}, + {"text": "Blocking Period Start Date"}, + {"text": "Blocking Period End Date"}, + {"text": "Definition Period SID"}, + ] + + def row3(self, row) -> [dict]: + return [ + {"text": self.link_renderer_for_quotas(row.order_number, row.order_number)}, + { + "text": self.link_renderer_for_quotas( + row.order_number, blocking.sid, "#blocking-periods" + ) + for blocking in row.quotablocking_set.all() + }, + { + "text": blocking.valid_between.lower + for blocking in row.quotablocking_set.all() + }, + { + "text": blocking.valid_between.upper + for blocking in row.quotablocking_set.all() + }, + { + "text": self.link_renderer_for_quotas( + row.order_number, row.sid, "#definition-details" + ) + }, + ] + + def rows3(self) -> [[dict]]: + table_rows = [self.row3(row) for row in self.query3()] + + if not any(table_rows): + return [ + [{"text": "There is no data for this report at present"}] + + [{"text": " "} for _ in range(len(self.headers3()) - 1)] + ] + + return table_rows + + def query3(self): + expiring_quotas = self.find_quota_definitions_expiring_soon() + quota_blocking_without_future_definition = ( + self.find_quota_blocking_without_future_definition(expiring_quotas) + ) + return quota_blocking_without_future_definition + + def find_quota_suspension_without_future_definition(self, expiring_quotas): + matching_data = set() + + for quota_definition in expiring_quotas: + future_definitions = QuotaSuspension.objects.latest_approved().filter( + quota_definition=quota_definition, + ) + + if future_definitions.exists(): + quota_definition.definition_start_date = ( + quota_definition.valid_between.lower + ) + quota_definition.definition_end_date = ( + quota_definition.valid_between.upper + ) + matching_data.add(quota_definition) + + return list(matching_data) + + def headers4(self) -> [dict]: + return [ + {"text": "Quota Order Number"}, + {"text": "Suspension Period SIDs"}, + {"text": "Suspension Period Start Date"}, + {"text": "Suspension Period End Date"}, + {"text": "Definition Period SID"}, + ] + + def row4(self, row) -> [dict]: + return [ + {"text": self.link_renderer_for_quotas(row.order_number, row.order_number)}, + { + "text": self.link_renderer_for_quotas( + row.order_number, suspension.sid, "#blocking-periods" + ) + for suspension in row.quotasuspension_set.all() + }, + { + "text": suspension.valid_between.lower + for suspension in row.quotasuspension_set.all() + }, + { + "text": suspension.valid_between.upper + for suspension in row.quotasuspension_set.all() + }, + { + "text": self.link_renderer_for_quotas( + row.order_number, row.sid, "#definition-details" + ) + }, + ] + + def rows4(self) -> [[dict]]: + table_rows = [self.row4(row) for row in self.query4()] + + if not any(table_rows): + return [ + [{"text": "There is no data for this report at present"}] + + [{"text": " "} for _ in range(len(self.headers4()) - 1)] + ] + + return table_rows + + def query4(self): + expiring_quotas = self.find_quota_definitions_expiring_soon() + quota_suspension_without_future_definition = ( + self.find_quota_suspension_without_future_definition(expiring_quotas) + ) + + return quota_suspension_without_future_definition diff --git a/reports/reports/quotas_cannot_be_used.py b/reports/reports/quotas_cannot_be_used.py index 5fe7f0369..b83d3694f 100644 --- a/reports/reports/quotas_cannot_be_used.py +++ b/reports/reports/quotas_cannot_be_used.py @@ -28,7 +28,7 @@ def headers(self) -> [dict]: def row(self, row: QuotaDefinition) -> [dict]: return [ - {"text": row.order_number}, + {"text": self.link_renderer_for_quotas(row, row.order_number)}, {"text": row.valid_between.lower}, {"text": row.valid_between.upper}, {"text": row.reason}, @@ -118,20 +118,19 @@ def find_quotas_that_cannot_be_used(self, quotas_with_definition_periods): break else: matching_data.add(quota.order_number) - - for quota in matching_data: + for quota_order_number in matching_data: matching_definition = next( ( quota_definition for quota_definition in quotas_with_definition_periods - if quota_definition.order_number == quota + if quota_definition.order_number == quota_order_number ), None, ) if matching_definition: - quota.reason = "Geographical area/exclusions data does not have any measures with matching data" + quota_order_number.reason = "Geographical area/exclusions data does not have any measures with matching data" else: - quota.reason = "Definition period has not been set" + quota_order_number.reason = "Definition period has not been set" return list(matching_data) diff --git a/reports/tests/test_expiring_quotas_with_no_definition_period.py b/reports/tests/test_expiring_quotas_with_no_definition_period.py index c83e471bb..1965e20e1 100644 --- a/reports/tests/test_expiring_quotas_with_no_definition_period.py +++ b/reports/tests/test_expiring_quotas_with_no_definition_period.py @@ -1,79 +1,97 @@ import pytest import datetime -from datetime import timedelta from dateutil.relativedelta import relativedelta from common.tests import factories from common.util import TaricDateRange from reports.reports.expiring_quotas_with_no_definition_period import Report -from quotas.models import QuotaDefinition -@pytest.fixture -def quota_order_number(db): - return factories.QuotaOrderNumberFactory.create() - - -@pytest.fixture -def expired_quota_definition(quota_order_number): - return factories.QuotaDefinitionFactory.create( - order_number=quota_order_number, - valid_between=TaricDateRange( - datetime.datetime.today().date() + relativedelta(weeks=-2), - datetime.datetime.today().date() + relativedelta(weeks=-1), - ), - ) +@pytest.mark.django_db +class TestQuotasExpiringSoonReport: + def test_find_quota_definitions_expiring_soon(self, quota_order_number): + report = Report() + expiring_quota_definition = factories.QuotaDefinitionFactory.create( + order_number=quota_order_number, + valid_between=TaricDateRange( + datetime.datetime.today().date() + relativedelta(weeks=1), + datetime.datetime.today().date() + relativedelta(weeks=2), + ), + ) + result = report.find_quota_definitions_expiring_soon() -@pytest.fixture -def expiring_soon_quota_definition(quota_order_number): - return factories.QuotaDefinitionFactory.create( - order_number=quota_order_number, - valid_between=TaricDateRange( - datetime.datetime.today().date() + relativedelta(weeks=1), - datetime.datetime.today().date() + relativedelta(weeks=2), - ), - ) + assert expiring_quota_definition in result + def test_find_quotas_without_future_definition(self, quota_order_number): + report = Report() + expiring_quota_definition = factories.QuotaDefinitionFactory.create( + order_number=quota_order_number, + valid_between=TaricDateRange( + datetime.datetime.today().date() + relativedelta(weeks=1), + datetime.datetime.today().date() + relativedelta(weeks=2), + ), + ) -@pytest.fixture -def future_quota_definition(quota_order_number): - return factories.QuotaDefinitionFactory.create( - order_number=quota_order_number, - valid_between=TaricDateRange( - datetime.datetime.today().date() + relativedelta(weeks=3), - datetime.datetime.today().date() + relativedelta(weeks=4), - ), - ) + result = report.find_quotas_without_future_definition( + [expiring_quota_definition] + ) + assert expiring_quota_definition in result -@pytest.mark.django_db -class TestQuotasExpiringSoonReport: - def test_quotas_expiring_soon_report_logic( - self, - quota_order_number, - expired_quota_definition, - expiring_soon_quota_definition, - future_quota_definition, - ): + def test_find_quota_blocking_without_future_definition(self, quota_order_number): + report = Report() + expiring_quota_definition = factories.QuotaDefinitionFactory.create( + order_number=quota_order_number, + valid_between=TaricDateRange( + datetime.datetime.today().date() + relativedelta(weeks=1), + datetime.datetime.today().date() + relativedelta(weeks=2), + ), + ) + blocking = factories.QuotaBlockingFactory.create( + quota_definition=expiring_quota_definition + ) + + result = report.find_quota_blocking_without_future_definition( + [expiring_quota_definition] + ) + + assert expiring_quota_definition in result + assert blocking in expiring_quota_definition.quotablocking_set.all() + + def test_find_quota_suspension_without_future_definition(self, quota_order_number): + report = Report() + expiring_quota_definition = factories.QuotaDefinitionFactory.create( + order_number=quota_order_number, + valid_between=TaricDateRange( + datetime.datetime.today().date() + relativedelta(weeks=1), + datetime.datetime.today().date() + relativedelta(weeks=2), + ), + ) + suspension = factories.QuotaSuspensionFactory.create( + quota_definition=expiring_quota_definition + ) + + result = report.find_quota_suspension_without_future_definition( + [expiring_quota_definition] + ) + + assert expiring_quota_definition in result + assert suspension in expiring_quota_definition.quotasuspension_set.all() + + def test_rows_no_data(self): report = Report() - quotas = report.query() - - assert len(quotas) == 1 - - assert future_quota_definition in quotas - assert expiring_soon_quota_definition not in quotas + result = report.rows() - assert expired_quota_definition not in quotas + assert len(result) == 1 + assert result[0][0]["text"] == "There is no data for this report at present" - def test_quotas_expiring_soon_report_row( - self, quota_order_number, expiring_soon_quota_definition - ): + def test_rows2_no_data(self, quota_order_number): report = Report() - row_data = report.row(expiring_soon_quota_definition) - assert len(row_data) == 3 + # Assuming there are no expiring quota definitions + result = report.rows2() - # Check if the correct columns are present - assert {"text": expiring_soon_quota_definition.valid_between.lower} in row_data - assert {"text": expiring_soon_quota_definition.valid_between.upper} in row_data + # Check that the result contains the "no data" message + assert len(result) == 1 + assert result[0][0]["text"] == "There is no data for this report at present" diff --git a/reports/tests/test_report_utils.py b/reports/tests/test_report_utils.py index 235e4ec6a..80409a469 100644 --- a/reports/tests/test_report_utils.py +++ b/reports/tests/test_report_utils.py @@ -1,6 +1,9 @@ # Create your tests here. import pytest +from django.urls import reverse +from django.utils.safestring import SafeString +from common.tests import factories from reports.reports.base import ReportBase from reports.reports.base_table import ReportBaseTable from reports.reports.blank_goods_nomenclature_descriptions import Report @@ -45,3 +48,26 @@ def test_get_template_by_type(self): with pytest.raises(Exception) as ex: get_template_by_type("werwer") assert str(ex) == "Unknown chart type : werwer" + + @pytest.mark.django_db + def test_link_renderer_for_quotas(self, db): + order_number_obj = factories.QuotaOrderNumberFactory.create() + + report_instance = Report() + + # Test without fragment + result = report_instance.link_renderer_for_quotas(order_number_obj, "Test Text") + expected_url = reverse("quota-ui-detail", args=[order_number_obj.sid]) + expected_output = f"Test Text" + assert result == SafeString(expected_output) + + # Test with fragment + result = report_instance.link_renderer_for_quotas( + order_number_obj, "Test Text", fragment="#blocking-periods" + ) + expected_url = ( + reverse("quota-ui-detail", args=[order_number_obj.sid]) + + "#blocking-periods" + ) + expected_output = f"Test Text" + assert result == SafeString(expected_output) diff --git a/reports/tests/test_report_views.py b/reports/tests/test_report_views.py index b99ba3ded..bbbabb3f1 100644 --- a/reports/tests/test_report_views.py +++ b/reports/tests/test_report_views.py @@ -4,7 +4,10 @@ from django.test import RequestFactory from reports.utils import get_reports -from reports.views import export_report_to_csv +from reports.views import export_report_to_csv, export_report_to_excel +from reports.reports.expiring_quotas_with_no_definition_period import Report +from reports.reports.cds_approved import Report as ChartReport + pytestmark = pytest.mark.django_db @@ -40,8 +43,7 @@ def test_all_report_unauthorised(self, client_name, http_status, request): assert response.status_code == http_status def test_export_report_to_csv(self, request): - request = RequestFactory().get("/") - report_slug = "cds_rejections_in_the_last_12_months" + report_slug = "blank_goods_nomenclature_descriptions" response = export_report_to_csv(request, report_slug) @@ -51,3 +53,27 @@ def test_export_report_to_csv(self, request): response["Content-Disposition"] == f'attachment; filename="{report_slug}_report.csv"' ) + + def test_export_report_invalid_tab(self, request): + report_slug = Report.slug() + invalid_tab = "Invalid tab" + + with pytest.raises( + ValueError, match=f"Invalid current_tab value: {invalid_tab}" + ): + export_report_to_csv(request, report_slug, current_tab=invalid_tab) + + def test_export_report_to_excel(self, request): + report_slug = ChartReport.slug() + + response = export_report_to_excel(request, report_slug) + + assert response.status_code == 200 + assert ( + response["Content-Type"] + == "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" + ) + assert ( + response["Content-Disposition"] + == f'attachment; filename="{report_slug}_report.xlsx"' + ) diff --git a/reports/urls.py b/reports/urls.py index 45e46dbb9..f625a0bc6 100644 --- a/reports/urls.py +++ b/reports/urls.py @@ -8,10 +8,20 @@ urlpatterns = [ path("reports/", views.index, name="index"), path( - "reports//export-csv/", + "reports//export-to-csv", views.export_report_to_csv, name="export_report_to_csv", ), + path( + "reports//export-to-excel", + views.export_report_to_excel, + name="export_report_to_excel", + ), + path( + "reports///export-report-with-tabs-to-csv/", + views.export_report_to_csv, + name="export_report_with_tabs_to_csv", + ), ] for report in utils.get_reports(): diff --git a/reports/views.py b/reports/views.py index 902efe299..608a4cd76 100644 --- a/reports/views.py +++ b/reports/views.py @@ -3,10 +3,11 @@ from django.contrib.auth.decorators import permission_required from django.http import HttpResponse from django.shortcuts import render +from openpyxl import Workbook +from openpyxl.chart import BarChart, Reference import reports.reports.index as index_model -# Create your views here. import reports.utils as utils @@ -35,25 +36,112 @@ def report(request): ) -def export_report_to_csv(request, report_slug): +def export_report_to_csv(request, report_slug, current_tab=None): report_class = utils.get_report_by_slug(report_slug) report_instance = report_class() response = HttpResponse(content_type="text/csv") - response["Content-Disposition"] = f'attachment; filename="{report_slug}_report.csv"' + + if current_tab: + response[ + "Content-Disposition" + ] = f'attachment; filename="{report_slug + "_for_" + current_tab}_report.csv"' + formatted_current_tab = current_tab.capitalize().replace("_", " ") + + # Define a dictionary to map current_tab values to methods + tab_mapping = { + report_instance.tab_name: ( + report_instance.headers(), + report_instance.rows(), + ), + report_instance.tab_name2: ( + report_instance.headers2(), + report_instance.rows2(), + ), + report_instance.tab_name3: ( + report_instance.headers3(), + report_instance.rows3(), + ), + report_instance.tab_name4: ( + report_instance.headers4(), + report_instance.rows4(), + ), + } + + # Use the dictionary to get the methods based on current_tab + methods = tab_mapping.get(formatted_current_tab) + + if methods: + headers, rows = methods + else: + # Raise an exception if current_tab doesn't match any expected values + raise ValueError(f"Invalid current_tab value: {formatted_current_tab}") + else: + response[ + "Content-Disposition" + ] = f'attachment; filename="{report_slug}_report.csv"' + headers = ( + report_instance.headers() if hasattr(report_instance, "headers") else None + ) + rows = report_instance.rows() if hasattr(report_instance, "rows") else None writer = csv.writer(response) # Check if the report is a table or a chart if hasattr(report_instance, "headers"): # For table reports - writer.writerow([header["text"] for header in report_instance.headers()]) - for row in report_instance.rows(): + writer.writerow([header["text"] for header in headers]) + for row in rows: writer.writerow([column["text"] for column in row]) else: - # For chart reports - writer.writerow(["Label", "Data"]) - for label, data in zip(report_instance.labels(), report_instance.data()): - writer.writerow([label, data]) + writer.writerow(["Date", "Data"]) + + for item in report_instance.data(): + writer.writerow([item["x"], item["y"]]) + + # Add an additional row with empty values because Excel needs this for data recognition + writer.writerow(["", ""]) + + return response + + +def export_report_to_excel(request, report_slug): + report_class = utils.get_report_by_slug(report_slug) + report_instance = report_class() + + response = HttpResponse( + content_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" + ) + + response[ + "Content-Disposition" + ] = f'attachment; filename="{report_slug}_report.xlsx"' + + workbook = Workbook() + sheet = workbook.active + + sheet.append(["Date", "Data"]) + + for item in report_instance.data(): + sheet.append([item["x"], item["y"]]) + + # Add an additional row with empty values because Excel needs this for data recognition + sheet.append(["", ""]) + + chart = BarChart() + data = Reference(sheet, min_col=2, min_row=1, max_col=2, max_row=sheet.max_row) + categories = Reference(sheet, min_col=1, min_row=2, max_row=sheet.max_row) + chart.add_data(data, titles_from_data=True) + chart.set_categories(categories) + chart.title = report_instance.name + chart.x_axis.title = "Date" + chart.y_axis.title = "Data" + + chart.width = 40 + chart.height = 20 + + sheet.add_chart(chart, "E5") + + workbook.save(response) return response diff --git a/requirements.txt b/requirements.txt index ada75aab1..5db4dd0cc 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -aiohttp==3.9.1 +aiohttp==3.9.2 apsw==3.43.0.0 allure-pytest-bdd==2.8.40 api-client==1.3.0 @@ -11,7 +11,7 @@ coverage[toml]==5.4 crispy-forms-gds @ git+https://github.com/uktrade/crispy-forms-gds.git@b50168d0e23ffacbdd30e7819ec8f9a08e055c8e defusedxml==0.7.* dj-database-url==0.5.0 -django==3.2.23 +django==3.2.24 django-chunk-upload-handlers==0.0.13 django-crispy-forms==1.12.0 django-dotenv==1.4.2 @@ -39,7 +39,6 @@ factory-boy==3.2.0 flower==1.2.0 freezegun==1.1.0 gevent==23.9.1 -govuk-frontend-jinja @ git+https://github.com/alphagov/govuk-frontend-jinja.git@15845e4cca3a05df72c6e13ec6a7e35acc682f52 govuk-tech-docs-sphinx-theme==1.0.0 gunicorn==20.1.0 Jinja2==3.1.3 diff --git a/sample.env b/sample.env index d8b634a5e..46de51fd8 100644 --- a/sample.env +++ b/sample.env @@ -9,6 +9,8 @@ LOG_LEVEL=DEBUG SENTRY_DSN= CELERY_BROKER_URL=redis://127.0.0.1:6379/1 +MAINTENANCE_MODE=False + # S3 Bucket for HMRC envelope uploads. HMRC_STORAGE_BUCKET_NAME=hmrc HMRC_STORAGE_DIRECTORY=tohmrc/staging/ diff --git a/settings/common.py b/settings/common.py index 55b73816a..b5f87e259 100644 --- a/settings/common.py +++ b/settings/common.py @@ -23,6 +23,8 @@ VCAP_SERVICES = json.loads(os.environ.get("VCAP_SERVICES", "{}")) VCAP_APPLICATION = json.loads(os.environ.get("VCAP_APPLICATION", "{}")) +MAINTENANCE_MODE = is_truthy(os.environ.get("MAINTENANCE_MODE", "False")) + # -- Debug # Activates debugging @@ -122,10 +124,10 @@ "importer", "notifications", # XXX need to keep this for migrations. delete later. + "reference_documents", "publishing", "taric", "workbaskets", - "reference_documents", "exporter.apps.ExporterConfig", "crispy_forms", "crispy_forms_gds", @@ -150,15 +152,26 @@ "django.contrib.auth.middleware.AuthenticationMiddleware", "django.contrib.messages.middleware.MessageMiddleware", "django.middleware.clickjacking.XFrameOptionsMiddleware", - "common.models.utils.ValidateSessionWorkBasketMiddleware", + "common.models.utils.ValidateUserWorkBasketMiddleware", "common.models.utils.TransactionMiddleware", "csp.middleware.CSPMiddleware", ] -if SSO_ENABLED: + +if SSO_ENABLED and not MAINTENANCE_MODE: MIDDLEWARE += [ "authbroker_client.middleware.ProtectAllViewsMiddleware", ] +if MAINTENANCE_MODE: + INSTALLED_APPS.remove("django.contrib.admin") + + MIDDLEWARE.remove("django.contrib.sessions.middleware.SessionMiddleware") + MIDDLEWARE.remove("django.contrib.auth.middleware.AuthenticationMiddleware") + MIDDLEWARE.remove("django.contrib.messages.middleware.MessageMiddleware") + MIDDLEWARE.remove("common.models.utils.TransactionMiddleware") + MIDDLEWARE.remove("common.models.utils.ValidateUserWorkBasketMiddleware") + MIDDLEWARE.append("common.middleware.MaintenanceModeMiddleware") + TEMPLATES = [ { "BACKEND": "django.template.backends.jinja2.Jinja2", @@ -244,6 +257,8 @@ "authbroker_client.backends.AuthbrokerBackend", ] +AUTH_USER_MODEL = "common.User" + # -- Security SECRET_KEY = os.environ.get("SECRET_KEY", "@@i$w*ct^hfihgh21@^8n+&ba@_l3x") @@ -295,9 +310,12 @@ else: DB_URL = os.environ.get("DATABASE_URL", "postgres://localhost:5432/tamato") -DATABASES = { - "default": dj_database_url.parse(DB_URL), -} +if not MAINTENANCE_MODE: + DATABASES = { + "default": dj_database_url.parse(DB_URL), + } +else: + DATABASES = {} SQLITE = DB_URL.startswith("sqlite") @@ -792,3 +810,5 @@ "django.core.files.uploadhandler.MemoryFileUploadHandler", # defaults "django.core.files.uploadhandler.TemporaryFileUploadHandler", # defaults ) # Order is important + +DATA_MIGRATION_BATCH_SIZE = int(os.environ.get("DATA_MIGRATION_BATCH_SIZE", "10000")) diff --git a/taric_parsers/forms.py b/taric_parsers/forms.py index a48b83359..67abc9e74 100644 --- a/taric_parsers/forms.py +++ b/taric_parsers/forms.py @@ -6,7 +6,7 @@ from crispy_forms_gds.layout import Submit from django import forms from django.conf import settings -from django.contrib.auth.models import User +from django.contrib.auth import get_user_model from django.core.files.uploadedfile import InMemoryUploadedFile from django.db import transaction @@ -15,6 +15,8 @@ from taric_parsers.chunker import chunk_taric from taric_parsers.importer import run_batch +User = get_user_model() + class TaricParserFormMixin: """Mixin for taric parser forms, providing common taric_file clean and diff --git a/taric_parsers/parsers/taric_parser.py b/taric_parsers/parsers/taric_parser.py index 9f0b93134..5987917c2 100644 --- a/taric_parsers/parsers/taric_parser.py +++ b/taric_parsers/parsers/taric_parser.py @@ -548,9 +548,24 @@ def get_linked_model( filtered_models = [] for model in models: if hasattr(model, "valid_between"): - # Check if this record is current - if date.today() in model.valid_between: - filtered_models.append(model) + # Check if this record is the most current + if len(filtered_models) > 0: + # if current filtered model has no end date, don't replace + if filtered_models[0].valid_between.upper_inf: + continue + # If the model does not have an end date, replace filtered models with non end dated model + elif model.valid_between.upper_inf: + filtered_models = [model] + elif ( + filtered_models[0].valid_between.upper + > model.valid_between.upper + ): + continue + else: + filtered_models = [model] + + elif len(filtered_models) == 0: + filtered_models = [model] elif hasattr(model, "validity_start"): # check for latest @@ -565,9 +580,15 @@ def get_linked_model( if len(filtered_models) == 1: return filtered_models[0] + if len(filtered_models) > 1: + raise Exception( + f"multiple models matched query for {related_model.__name__} using {fields_and_values}, please check data and query", + ) + raise Exception( - f"multiple models matched query for {related_model.__name__} using {fields_and_values}, please check data and query", + f"no models matched query for {related_model.__name__} using {fields_and_values}, please check data and query", ) + else: return None diff --git a/taric_parsers/tasks.py b/taric_parsers/tasks.py index 0b6d08870..95eb68b4d 100644 --- a/taric_parsers/tasks.py +++ b/taric_parsers/tasks.py @@ -1,7 +1,7 @@ from logging import getLogger from typing import Sequence -from django.contrib.auth.models import User +from django.contrib.auth import get_user_model import taric_parsers.importer from common.celery import app @@ -14,6 +14,8 @@ from workbaskets.models import WorkBasket from workbaskets.models import get_partition_scheme +User = get_user_model() + logger = getLogger(__name__) diff --git a/taric_parsers/tests/test_views.py b/taric_parsers/tests/test_views.py index 8397ed238..01f6f7968 100644 --- a/taric_parsers/tests/test_views.py +++ b/taric_parsers/tests/test_views.py @@ -3,7 +3,7 @@ import pytest from bs4 import BeautifulSoup -from django.contrib.auth.models import User +from django.contrib.auth import get_user_model from django.core.files.uploadedfile import SimpleUploadedFile from django.test import Client from django.urls import reverse @@ -11,6 +11,8 @@ from common.tests import factories from common.tests.factories import ImportBatchFactory +User = get_user_model() + pytestmark = pytest.mark.django_db TEST_FILES_PATH = path.join(path.dirname(__file__), "support") diff --git a/urls.py b/urls.py index 162a33940..ab9650a14 100644 --- a/urls.py +++ b/urls.py @@ -33,14 +33,18 @@ path("", include("measures.urls")), path("", include("publishing.urls", namespace="publishing")), path("", include("quotas.urls")), - path("", include("reference_documents.urls")), path("", include("regulations.urls")), path("", include("reports.urls")), path("", include("taric_parsers.urls")), path("", include("workbaskets.urls", namespace="workbaskets")), - path("admin/", admin.site.urls), + path("", include("reference_documents.urls", namespace="reference_documents")), ] +if not settings.MAINTENANCE_MODE: + urlpatterns += { + path("admin/", admin.site.urls), + } + handler403 = "common.views.handler403" handler500 = "common.views.handler500" diff --git a/webpack.config.js b/webpack.config.js index 2b6e1b1bb..b900d501f 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -48,6 +48,18 @@ module.exports = { ] }, + // Babel + { + test: /\.m?js$/, + exclude: /node_modules/, + use: { + loader: "babel-loader", + options: { + presets: ["@babel/preset-env", "@babel/react"] + } + } + }, + // Extract compiled SCSS separately from JS { test: /\.s[ac]ss$/i, @@ -68,6 +80,7 @@ module.exports = { 'publishing/static/publishing/scss', 'regulations/static/regulations/scss', 'workbaskets/static/workbaskets/scss', + 'reference_documents/static/reference_documents/scss', ], }, }, diff --git a/workbaskets/jinja2/includes/workbaskets/navigation.jinja b/workbaskets/jinja2/includes/workbaskets/navigation.jinja index 3a6ef141d..9025ce8b0 100644 --- a/workbaskets/jinja2/includes/workbaskets/navigation.jinja +++ b/workbaskets/jinja2/includes/workbaskets/navigation.jinja @@ -11,7 +11,7 @@ Check workbasket diff --git a/workbaskets/jinja2/includes/workbaskets/review-quota-blocking-periods.jinja b/workbaskets/jinja2/includes/workbaskets/review-quota-blocking-periods.jinja new file mode 100644 index 000000000..e587f5a0d --- /dev/null +++ b/workbaskets/jinja2/includes/workbaskets/review-quota-blocking-periods.jinja @@ -0,0 +1,37 @@ +{% set table_rows = [] %} +{% for obj in object_list %} + {% set quota_link %} + + {{ obj.quota_definition.order_number.order_number }} + + {% endset %} + + {% set quota_definition_link %} + + {{ obj.quota_definition.sid }} + + {% endset %} + + {{ table_rows.append([ + {"text": obj.sid}, + {"html": quota_link}, + {"html": quota_definition_link}, + {"text": "{:%d %b %Y}".format(obj.valid_between.lower)}, + {"text": "{:%d %b %Y}".format(obj.valid_between.upper) if obj.valid_between.upper else "-"}, + {"text": obj.blocking_period_type}, + {"text": obj.description if obj.description else "-", "classes": "govuk-!-width-one-quarter"}, + ]) or "" }} +{% endfor %} + +{{ govukTable({ + "head": [ + {"text": "Blocking period SID"}, + {"text": "Order number"}, + {"text": "Quota definition SID"}, + {"text": "Start date"}, + {"text": "End date"}, + {"text": "Blocking period type"}, + {"text": "Description"}, + ], + "rows": table_rows +}) }} diff --git a/workbaskets/jinja2/includes/workbaskets/review-quota-suspension-periods.jinja b/workbaskets/jinja2/includes/workbaskets/review-quota-suspension-periods.jinja new file mode 100644 index 000000000..fd5885c14 --- /dev/null +++ b/workbaskets/jinja2/includes/workbaskets/review-quota-suspension-periods.jinja @@ -0,0 +1,35 @@ +{% set table_rows = [] %} +{% for obj in object_list %} + {% set quota_link %} + + {{ obj.quota_definition.order_number.order_number }} + + {% endset %} + + {% set quota_definition_link %} + + {{ obj.quota_definition.sid }} + + {% endset %} + + {{ table_rows.append([ + {"text": obj.sid}, + {"html": quota_link}, + {"html": quota_definition_link}, + {"text": "{:%d %b %Y}".format(obj.valid_between.lower)}, + {"text": "{:%d %b %Y}".format(obj.valid_between.upper) if obj.valid_between.upper else "-"}, + {"text": obj.description if obj.description else "-", "classes": "govuk-!-width-one-quarter"}, + ]) or "" }} +{% endfor %} + +{{ govukTable({ + "head": [ + {"text": "Suspension period SID"}, + {"text": "Order number"}, + {"text": "Quota definition SID"}, + {"text": "Start date"}, + {"text": "End date"}, + {"text": "Description"}, + ], + "rows": table_rows +}) }} diff --git a/workbaskets/jinja2/includes/workbaskets/review-sub-quotas.jinja b/workbaskets/jinja2/includes/workbaskets/review-sub-quotas.jinja new file mode 100644 index 000000000..0c02a8bbd --- /dev/null +++ b/workbaskets/jinja2/includes/workbaskets/review-sub-quotas.jinja @@ -0,0 +1,35 @@ +{% set table_rows = [] %} +{% for obj in object_list %} + {% set sub_quota_link %} + + {{ obj.sub_quota.order_number.order_number }} + + {% endset %} + + {% set main_quota_link %} + + {{ obj.main_quota.order_number.order_number }} + + {% endset %} + + {{ table_rows.append([ + {"html": main_quota_link}, + {"html": sub_quota_link}, + {"text": "{:%d %b %Y}".format(obj.sub_quota.valid_between.lower) }, + {"text": "{:%d %b %Y}".format(obj.sub_quota.valid_between.upper) if obj.sub_quota.valid_between.upper else "-"}, + {"text": obj.sub_quota_relation_type}, + {"text": obj.coefficient}, + ]) or "" }} +{% endfor %} + +{{ govukTable({ + "head": [ + {"text": "Order number"}, + {"text": "Sub-quota order number"}, + {"text": "Start date"}, + {"text": "End date"}, + {"text": "Relation type"}, + {"text": "Co-efficient"}, + ], + "rows": table_rows +}) }} diff --git a/workbaskets/jinja2/workbaskets/checks.jinja b/workbaskets/jinja2/workbaskets/checks.jinja index 6c02c08e8..7271a8383 100644 --- a/workbaskets/jinja2/workbaskets/checks.jinja +++ b/workbaskets/jinja2/workbaskets/checks.jinja @@ -3,7 +3,7 @@ {% from "includes/workbaskets/navigation.jinja" import navigation %} {% set page_title %} - Workbasket {{ workbasket.id if workbasket else request.session.workbasket.id }} - Checks + Workbasket {{ workbasket.id if workbasket else request.user.current_workbasket.id }} - Checks {% endset %} {% block content %} diff --git a/workbaskets/jinja2/workbaskets/compare.jinja b/workbaskets/jinja2/workbaskets/compare.jinja index 25982b7f6..6399db269 100644 --- a/workbaskets/jinja2/workbaskets/compare.jinja +++ b/workbaskets/jinja2/workbaskets/compare.jinja @@ -9,7 +9,7 @@ {% from "components/table/macro.njk" import govukTable %} {% set page_title %} - Workbasket {{ workbasket.id if workbasket else request.session.workbasket.id }} - Compare with worksheet data + Workbasket {{ workbasket.id if workbasket else request.user.current_workbasket.id }} - Compare with worksheet data {% endset %} {% set change_workbasket_details_link = url("workbaskets:workbasket-ui-update", kwargs={"pk": workbasket.pk}) %} @@ -19,9 +19,9 @@ "items": [ {"text": "Home", "href": url("home")}, {"text": "Edit an existing workbasket", "href": url("workbaskets:workbasket-ui-list")}, - {"text": "Workbasket " ~ request.session.workbasket.id ~ " - Summary", "href": url("workbaskets:current-workbasket") }, - {"text": "Workbasket " ~ request.session.workbasket.id ~ " - Checks", "href": url("workbaskets:workbasket-checks") }, - {"text": "Workbasket " ~ request.session.workbasket.id ~ " - Compare" } + {"text": "Workbasket " ~ request.user.current_workbasket.id ~ " - Summary", "href": url("workbaskets:current-workbasket") }, + {"text": "Workbasket " ~ request.user.current_workbasket.id ~ " - Checks", "href": url("workbaskets:workbasket-checks") }, + {"text": "Workbasket " ~ request.user.current_workbasket.id ~ " - Compare" } ]}) }} {% endblock %} diff --git a/workbaskets/jinja2/workbaskets/delete_changes.jinja b/workbaskets/jinja2/workbaskets/delete_changes.jinja index 46800e442..51b9cee61 100644 --- a/workbaskets/jinja2/workbaskets/delete_changes.jinja +++ b/workbaskets/jinja2/workbaskets/delete_changes.jinja @@ -6,7 +6,7 @@ {% set page_title = "Remove tariff changes" %} {% block breadcrumb %} - {% if request.session.workbasket.id != view.workbasket.pk %} + {% if request.user.current_workbasket.id != view.workbasket.pk %} {{ breadcrumbs(request, [ {"text": "Find and view workbaskets", "href": url("workbaskets:workbasket-ui-list-all")}, { diff --git a/workbaskets/jinja2/workbaskets/delete_changes_confirm.jinja b/workbaskets/jinja2/workbaskets/delete_changes_confirm.jinja index e65bcc8ca..6c4e09e02 100644 --- a/workbaskets/jinja2/workbaskets/delete_changes_confirm.jinja +++ b/workbaskets/jinja2/workbaskets/delete_changes_confirm.jinja @@ -7,7 +7,7 @@ {% set page_title = "Remove tariff changes" %} {% block breadcrumb %} - {% if view_workbasket != session_workbasket %} + {% if view_workbasket != user_workbasket %} {{ breadcrumbs(request, [ {"text": "Find and view workbaskets", "href": url("workbaskets:workbasket-ui-list-all")}, { @@ -30,7 +30,7 @@ "classes": "govuk-!-margin-bottom-7" }) }} - {% if view_workbasket != session_workbasket %} + {% if view_workbasket != user_workbasket %} {{ govukButton({ "text": "Return to workbasket", "href": url("workbaskets:workbasket-ui-changes", kwargs={"pk": view_workbasket.pk}), diff --git a/workbaskets/jinja2/workbaskets/delete_workbasket.jinja b/workbaskets/jinja2/workbaskets/delete_workbasket.jinja index 65ab6555d..26f2ff7a8 100644 --- a/workbaskets/jinja2/workbaskets/delete_workbasket.jinja +++ b/workbaskets/jinja2/workbaskets/delete_workbasket.jinja @@ -67,7 +67,7 @@ "name": "action", "value": "delete" }) }} - {% if object.pk == request.session.workbasket["id"] %} + {% if object.pk == request.user.current_workbasket.id %} {{ govukButton({ "text": "Cancel", "href": url("workbaskets:current-workbasket"), diff --git a/workbaskets/jinja2/workbaskets/edit-details.jinja b/workbaskets/jinja2/workbaskets/edit-details.jinja index 2f684f6cf..63f52f180 100644 --- a/workbaskets/jinja2/workbaskets/edit-details.jinja +++ b/workbaskets/jinja2/workbaskets/edit-details.jinja @@ -8,7 +8,7 @@ {{ govukBreadcrumbs({ "items": [ {"text": "Home", "href": url("home")}, - {"text": "Workbasket " ~ request.session.workbasket.id ~ " - Summary", "href": url("workbaskets:current-workbasket") }, + {"text": "Workbasket " ~ request.user.current_workbasket.id ~ " - Summary", "href": url("workbaskets:current-workbasket") }, {"text": page_title} ] }) }} diff --git a/workbaskets/jinja2/workbaskets/edit-workbasket.jinja b/workbaskets/jinja2/workbaskets/edit-workbasket.jinja index fb11d19c3..785349dbe 100644 --- a/workbaskets/jinja2/workbaskets/edit-workbasket.jinja +++ b/workbaskets/jinja2/workbaskets/edit-workbasket.jinja @@ -3,8 +3,8 @@ {% from "includes/workbaskets/navigation.jinja" import navigation %} {% set page_title %} - Workbasket {{ request.session.workbasket.id }} - {% if request.session.workbasket.title %} - Add/edit items{% endif %} + Workbasket {{ request.user.current_workbasket.id }} + {% if request.user.current_workbasket.title %} - Add/edit items{% endif %} {% endset %} @@ -13,8 +13,8 @@ "items": [ {"text": "Home", "href": url("home")}, {"text": "Edit an existing workbasket", "href": url("workbaskets:workbasket-ui-list")}, - {"text": "Workbasket " ~ request.session.workbasket.id ~ " - Summary", "href": url("workbaskets:current-workbasket") }, - {"text": "Workbasket " ~ request.session.workbasket.id ~ " - Add/edit items" } + {"text": "Workbasket " ~ request.user.current_workbasket.id ~ " - Summary", "href": url("workbaskets:current-workbasket") }, + {"text": "Workbasket " ~ request.user.current_workbasket.id ~ " - Add/edit items" } ] }) }} {% endblock %} diff --git a/workbaskets/jinja2/workbaskets/no_active_workbasket.jinja b/workbaskets/jinja2/workbaskets/no_active_workbasket.jinja new file mode 100644 index 000000000..c274c34d0 --- /dev/null +++ b/workbaskets/jinja2/workbaskets/no_active_workbasket.jinja @@ -0,0 +1,37 @@ +{% extends "layouts/layout.jinja" %} + +{% from "components/button/macro.njk" import govukButton %} + +{% set page_title = "You need an active workbasket to access this page" %} + +{% block breadcrumb %}{% endblock %} + +{% block content %} +
    +
    +
    +
    +

    You need an active workbasket to access this page

    +

    + You either do not have a workbasket or your previous workbasket was no longer in the editing state. Since + you last edited it, it may have been archived, queued, published or deleted. +

    +

    Ensure you are in the correct workbasket before continuing.

    +
    +
    +
    +
    +
    +{{ govukButton({ + "text": "Select a new workbasket", + "href": url("workbaskets:workbasket-ui-list"), + "classes": "govuk-button", + }) }} + +{{ govukButton({ + "text": "Return to homepage", + "href": url("home"), + "classes": "govuk-button--secondary", + }) }} +
    +{% endblock %} \ No newline at end of file diff --git a/workbaskets/jinja2/workbaskets/review-quotas.jinja b/workbaskets/jinja2/workbaskets/review-quotas.jinja index b45313a2b..3f5114d3f 100644 --- a/workbaskets/jinja2/workbaskets/review-quotas.jinja +++ b/workbaskets/jinja2/workbaskets/review-quotas.jinja @@ -11,6 +11,21 @@ "href": url("workbaskets:workbasket-ui-review-quota-definitions", kwargs={"pk": workbasket.pk}), "selected": selected_nested_tab == "quota-definitions", }, + { + "text": "Sub-quota associations", + "href": url("workbaskets:workbasket-ui-review-sub-quotas", kwargs={"pk": workbasket.pk}), + "selected": selected_nested_tab == "sub-quotas", + }, + { + "text": "Blocking periods", + "href": url("workbaskets:workbasket-ui-review-quota-blocking-periods", kwargs={"pk": workbasket.pk}), + "selected": selected_nested_tab == "blocking-periods", + }, + { + "text": "Suspension periods", + "href": url("workbaskets:workbasket-ui-review-quota-suspension-periods", kwargs={"pk": workbasket.pk}), + "selected": selected_nested_tab == "suspension-periods", + }, ] %} diff --git a/workbaskets/jinja2/workbaskets/review.jinja b/workbaskets/jinja2/workbaskets/review.jinja index d817ec569..427d023a3 100644 --- a/workbaskets/jinja2/workbaskets/review.jinja +++ b/workbaskets/jinja2/workbaskets/review.jinja @@ -6,7 +6,7 @@ {% set page_title %} Workbasket {{ workbasket.id }} - {{ tab_page_title }} {% endset %} -{% if workbasket == session_workbasket %} +{% if workbasket == user_workbasket %} {% set page_heading %} Workbasket {{ workbasket.id }} - Review changes {% endset %} {% else %} {% set page_heading %} Workbasket {{ workbasket.id }} - {{ workbasket.status }} {% endset %} @@ -57,7 +57,7 @@ %} {% block breadcrumb %} - {% if workbasket != session_workbasket %} + {% if workbasket != user_workbasket %} {{ breadcrumbs(request, [ {"text": "Find and view workbaskets", "href": url("workbaskets:workbasket-ui-list-all")}, { @@ -77,7 +77,7 @@ {{ page_heading }} - {% if workbasket == session_workbasket %} + {% if workbasket == user_workbasket %} {{ navigation(request, "review") }} {% else %} {{ create_workbasket_detail_navigation(active_tab="review") }} @@ -114,7 +114,7 @@ {% if object_list %} {% include tab_template %} {% else %} -

    0 {{ view.model._meta.verbose_name_plural }} available to review.

    +

    0 {{ selected_nested_tab.replace("-", " ") if selected_nested_tab else selected_tab }} available to review.

    {% endif %} {% include "includes/common/pagination.jinja" %} {% endblock %} diff --git a/workbaskets/jinja2/workbaskets/summary-workbasket.jinja b/workbaskets/jinja2/workbaskets/summary-workbasket.jinja index 8854f7df1..c1792e689 100644 --- a/workbaskets/jinja2/workbaskets/summary-workbasket.jinja +++ b/workbaskets/jinja2/workbaskets/summary-workbasket.jinja @@ -5,7 +5,7 @@ {% from "includes/workbaskets/navigation.jinja" import navigation %} {% set page_title %} - Workbasket {{ workbasket.id if workbasket else request.session.workbasket.id }} - Summary + Workbasket {{ workbasket.id if workbasket else request.user.current_.workbasket.id }} - Summary {% endset %} {% set change_workbasket_details_link = url("workbaskets:workbasket-ui-update", kwargs={"pk": workbasket.pk}) %} @@ -18,7 +18,7 @@ "items": [ {"text": "Home", "href": url("home")}, {"text": "Edit an existing workbasket", "href": url("workbaskets:workbasket-ui-list")}, - {"text": "Workbasket " ~ request.session.workbasket.id ~ " - Summary" } + {"text": "Workbasket " ~ request.user.current_workbasket.id ~ " - Summary" } ]}) }} {% endblock %} diff --git a/workbaskets/jinja2/workbaskets/taric/transaction.xml b/workbaskets/jinja2/workbaskets/taric/transaction.xml index 0b9e1b7ac..8d0c77ee6 100644 --- a/workbaskets/jinja2/workbaskets/taric/transaction.xml +++ b/workbaskets/jinja2/workbaskets/taric/transaction.xml @@ -1,6 +1,6 @@ {%- for record in tracked_models -%} - {%- set sequence = counter_generator() -%} + {%- set sequence = counter_generator -%} {%- include record.taric_template -%} {%- endfor %} diff --git a/workbaskets/jinja2/workbaskets/violation_detail.jinja b/workbaskets/jinja2/workbaskets/violation_detail.jinja index c48f687db..0c6c9e761 100644 --- a/workbaskets/jinja2/workbaskets/violation_detail.jinja +++ b/workbaskets/jinja2/workbaskets/violation_detail.jinja @@ -6,7 +6,7 @@ {% from "components/warning-text/macro.njk" import govukWarningText %} {% from "includes/workbaskets/navigation.jinja" import navigation %} -{% set page_title %}Workbasket {{ workbasket.id if workbasket else request.session.workbasket.id }} - Rule violation +{% set page_title %}Workbasket {{ workbasket.id if workbasket else request.user.current_workbasket.id }} - Rule violation details {% endset %} diff --git a/workbaskets/jinja2/workbaskets/violations.jinja b/workbaskets/jinja2/workbaskets/violations.jinja index 6cfb09dfa..242cb92d5 100644 --- a/workbaskets/jinja2/workbaskets/violations.jinja +++ b/workbaskets/jinja2/workbaskets/violations.jinja @@ -5,7 +5,7 @@ {% from "components/create_sortable_anchor.jinja" import create_sortable_anchor %} {% from "includes/workbaskets/navigation.jinja" import navigation %} -{% set page_title %}Workbasket {{ workbasket.id if workbasket else request.session.workbasket.id }} - Rule violations {% endset %} +{% set page_title %}Workbasket {{ workbasket.id if workbasket else request.user.current_workbasket.id }} - Rule violations {% endset %} {% block content %}

    {{ page_title }}

    diff --git a/workbaskets/migrations/0001_initial.py b/workbaskets/migrations/0001_initial.py index fb993f996..6b625cd5a 100644 --- a/workbaskets/migrations/0001_initial.py +++ b/workbaskets/migrations/0001_initial.py @@ -1,7 +1,5 @@ # Generated by Django 3.1 on 2021-01-06 15:33 -import django.db.models.deletion import django_fsm -from django.conf import settings from django.db import migrations from django.db import models @@ -9,9 +7,7 @@ class Migration(migrations.Migration): initial = True - dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ] + dependencies = [] operations = [ migrations.CreateModel( @@ -76,24 +72,6 @@ class Migration(migrations.Migration): max_length=50, ), ), - ( - "approver", - models.ForeignKey( - editable=False, - null=True, - on_delete=django.db.models.deletion.PROTECT, - related_name="approved_workbaskets", - to=settings.AUTH_USER_MODEL, - ), - ), - ( - "author", - models.ForeignKey( - editable=False, - on_delete=django.db.models.deletion.PROTECT, - to=settings.AUTH_USER_MODEL, - ), - ), ], options={ "abstract": False, diff --git a/workbaskets/migrations/0002_change_status_per_ADR008.py b/workbaskets/migrations/0002_change_status_per_ADR008.py index 76b66720d..08d043392 100644 --- a/workbaskets/migrations/0002_change_status_per_ADR008.py +++ b/workbaskets/migrations/0002_change_status_per_ADR008.py @@ -1,15 +1,40 @@ # Generated by Django 3.1.12 on 2021-09-21 14:10 +import django.db.models.deletion import django_fsm +from django.conf import settings from django.db import migrations +from django.db import models class Migration(migrations.Migration): dependencies = [ ("workbaskets", "0001_initial"), + ("common", "0001_initial"), ] operations = [ + # AddField operations for approver and author fields have been moved here from workbaskets.0001_initial to resolve a circular dependency issue with common.0001_initial following a change mid-project to a custom user model. + migrations.AddField( + model_name="workbasket", + name="approver", + field=models.ForeignKey( + editable=False, + null=True, + on_delete=django.db.models.deletion.PROTECT, + related_name="approved_workbaskets", + to=settings.AUTH_USER_MODEL, + ), + ), + migrations.AddField( + model_name="workbasket", + name="author", + field=models.ForeignKey( + editable=False, + on_delete=django.db.models.deletion.PROTECT, + to=settings.AUTH_USER_MODEL, + ), + ), migrations.AlterField( model_name="workbasket", name="status", diff --git a/workbaskets/models.py b/workbaskets/models.py index 97fc0933c..61f3f19f1 100644 --- a/workbaskets/models.py +++ b/workbaskets/models.py @@ -8,9 +8,11 @@ from celery.result import AsyncResult from django.conf import settings +from django.contrib.auth import get_user_model from django.core.exceptions import ImproperlyConfigured from django.core.exceptions import ValidationError from django.db import models +from django.db.models import Max from django.db.models import QuerySet from django.db.models import Subquery from django_fsm import FSMField @@ -32,6 +34,8 @@ logger = logging.getLogger(__name__) +User = get_user_model() + class TransactionPartitionScheme: """ @@ -494,14 +498,10 @@ def restore(self): """WorkBasket is ready to be worked on again after being rejected by CDS.""" - def save_to_session(self, session): - session["workbasket"] = { - "id": self.pk, - "status": self.status, - "title": self.title, - "error_count": self.tracked_model_check_errors.count(), - "measure_count": self.measures.count(), - } + def assign_to_user(self, user) -> None: + """Assigns this instance as `user`'s current workbasket.""" + user.current_workbasket = self + user.save() @property def tracked_models(self) -> TrackedModelQuerySet: @@ -511,28 +511,18 @@ def tracked_models(self) -> TrackedModelQuerySet: def measures(self) -> MeasuresQuerySet: return Measure.objects.filter(transaction__workbasket=self) - @classmethod - def load_from_session(cls, session): - if "workbasket" not in session: - raise KeyError("WorkBasket not in session") - return WorkBasket.objects.get(pk=session["workbasket"]["id"]) - - @classmethod - def remove_current_from_session(cls, session): - """Remove the current workbasket from the user's session.""" - if "workbasket" in session: - del session["workbasket"] - @classmethod def current(cls, request): - """Get the current workbasket in the session.""" - if "workbasket" in request.session: - workbasket = cls.load_from_session(request.session) + """Get the user's current workbasket.""" + try: + workbasket = request.user.current_workbasket + except AttributeError: + return None + if workbasket is not None: if workbasket.status != WorkflowStatus.EDITING: - cls.remove_current_from_session(request.session) + request.user.remove_current_workbasket() return None - return workbasket else: return None @@ -619,15 +609,33 @@ def delete_checks(self): @property def unchecked_or_errored_transactions(self): - return self.transactions.exclude( + """ + Returns unchecked, errored or out of date transactions from the + workbaskets. + + The query excludes transactions which have a corresponding transaction + check which has been completed, successful and was created after the + latest updated_at in the transactions tracked models. Lasted is + retrieved by annotating all the transactions for the workbasket with the + latest updated for its containing tracked models then we aggregate the + latest time from all the transactions. + """ + latest = ( + self.transactions.all() + .annotate(latest_updated_in_transaction=Max("tracked_models__updated_at")) + .aggregate(Max("latest_updated_in_transaction")) + ) + returned = self.transactions.exclude( pk__in=TransactionCheck.objects.requires_update(False) .filter( completed=True, successful=True, transaction__workbasket=self, + created_at__gt=latest["latest_updated_in_transaction__max"], ) .values("transaction__pk"), ) + return returned class Meta: verbose_name = "workbasket" diff --git a/workbaskets/tests/test_models.py b/workbaskets/tests/test_models.py index 240473454..898cd16a0 100644 --- a/workbaskets/tests/test_models.py +++ b/workbaskets/tests/test_models.py @@ -432,3 +432,10 @@ def test_invalid_workbasket_purge_transactions(workbasket_status): workbasket.purge_empty_transactions() assert workbasket.transactions.count() == 2 + + +def test_workbasket_assign_to_user(valid_user, workbasket): + """Test assign_to_user() sets the user's current workbasket.""" + assert not valid_user.current_workbasket + workbasket.assign_to_user(valid_user) + assert valid_user.current_workbasket == workbasket diff --git a/workbaskets/tests/test_views.py b/workbaskets/tests/test_views.py index 667f390c2..700814ce4 100644 --- a/workbaskets/tests/test_views.py +++ b/workbaskets/tests/test_views.py @@ -1,3 +1,4 @@ +import datetime import os import re from unittest.mock import MagicMock @@ -16,6 +17,7 @@ from checks.tests.factories import TrackedModelCheckFactory from common.models.utils import override_current_transaction from common.tests import factories +from common.tests.util import date_post_data from common.validators import UpdateType from exporter.tasks import upload_workbaskets from importer.models import ImportBatch @@ -89,17 +91,13 @@ def test_workbasket_create_without_permission(client): def test_workbasket_update_view_updates_workbasket_title_and_description( valid_user_client, - session_workbasket, + user_workbasket, ): """Test that a workbasket's title and description can be updated.""" - session = valid_user_client.session - session["workbasket"] = {"id": session_workbasket.pk} - session.save() - url = reverse( "workbaskets:workbasket-ui-update", - kwargs={"pk": session_workbasket.pk}, + kwargs={"pk": user_workbasket.pk}, ) new_title = "123321" new_description = "Newly updated test description" @@ -107,8 +105,8 @@ def test_workbasket_update_view_updates_workbasket_title_and_description( "title": new_title, "reason": new_description, } - assert not session_workbasket.title == new_title - assert not session_workbasket.reason == new_description + assert not user_workbasket.title == new_title + assert not user_workbasket.reason == new_description response = valid_user_client.get(url) assert response.status_code == 200 @@ -117,12 +115,12 @@ def test_workbasket_update_view_updates_workbasket_title_and_description( assert response.status_code == 302 assert response.url == reverse( "workbaskets:workbasket-ui-confirm-update", - kwargs={"pk": session_workbasket.pk}, + kwargs={"pk": user_workbasket.pk}, ) - session_workbasket.refresh_from_db() - assert session_workbasket.title == new_title - assert session_workbasket.reason == new_description + user_workbasket.refresh_from_db() + assert user_workbasket.title == new_title + assert user_workbasket.reason == new_description def test_download( @@ -163,12 +161,12 @@ def test_download( def test_review_workbasket_displays_rule_violation_summary( valid_user_client, - session_workbasket, + user_workbasket, ): """Test that the review workbasket page includes an error summary box detailing the number of tracked model changes and business rule violations, dated to the most recent `TrackedModelCheck`.""" - with session_workbasket.new_transaction() as transaction: + with user_workbasket.new_transaction() as transaction: good = factories.GoodsNomenclatureFactory.create(transaction=transaction) check = TrackedModelCheckFactory.create( transaction_check__transaction=transaction, @@ -187,7 +185,7 @@ def test_review_workbasket_displays_rule_violation_summary( ) error_headings = page.find_all("h2", attrs={"class": "govuk-body"}) - tracked_model_count = session_workbasket.tracked_models.count() + tracked_model_count = user_workbasket.tracked_models.count() local_created_at = localtime(check.created_at) created_at = f"{local_created_at:%d %b %Y %H:%M}" @@ -196,18 +194,18 @@ def test_review_workbasket_displays_rule_violation_summary( assert f"Number of violations: 1" in error_headings[1].text -def test_edit_workbasket_page_sets_workbasket(valid_user_client, session_workbasket): +def test_edit_workbasket_page_sets_workbasket(valid_user_client, user_workbasket): response = valid_user_client.get( reverse("workbaskets:edit-workbasket"), ) assert response.status_code == 200 soup = BeautifulSoup(str(response.content), "html.parser") - assert str(session_workbasket.pk) in soup.select(".govuk-heading-xl")[0].text + assert str(user_workbasket.pk) in soup.select(".govuk-heading-xl")[0].text def test_workbasket_detail_page_url_params( valid_user_client, - session_workbasket, + user_workbasket, ): url = reverse( "workbaskets:current-workbasket", @@ -306,7 +304,7 @@ def test_select_workbasket_redirects_to_tab( "workbaskets:edit-workbasket", ), ) -def test_workbasket_views_without_permission(url_name, client, session_workbasket): +def test_workbasket_views_without_permission(url_name, client, user_workbasket): """Tests that select, list-all, delete, and edit workbasket view endpoints return 403 to users without change_workbasket permission.""" url = reverse( @@ -314,6 +312,7 @@ def test_workbasket_views_without_permission(url_name, client, session_workbaske ) user = factories.UserFactory.create() client.force_login(user) + user_workbasket.assign_to_user(user) response = client.get(url) assert response.status_code == 403 @@ -454,6 +453,21 @@ def test_workbasket_review_tabs_without_permission(url, client): lambda: factories.QuotaDefinitionFactory.create(), 11, ), + ( + "workbaskets:workbasket-ui-review-sub-quotas", + lambda: factories.QuotaAssociationFactory.create(), + 6, + ), + ( + "workbaskets:workbasket-ui-review-quota-blocking-periods", + lambda: factories.QuotaBlockingFactory.create(), + 7, + ), + ( + "workbaskets:workbasket-ui-review-quota-suspension-periods", + lambda: factories.QuotaSuspensionFactory.create(), + 6, + ), ( "workbaskets:workbasket-ui-review-regulations", lambda: factories.RegulationFactory.create(), @@ -466,13 +480,13 @@ def test_workbasket_review_tabs( object_factory, num_columns, valid_user_client, - session_workbasket, + user_workbasket, ): """Tests that workbasket review tabs return 200 and display objects in table.""" - with session_workbasket.new_transaction(): + with user_workbasket.new_transaction(): object_factory() - url = reverse(url, kwargs={"pk": session_workbasket.pk}) + url = reverse(url, kwargs={"pk": user_workbasket.pk}) response = valid_user_client.get(url) assert response.status_code == 200 @@ -548,21 +562,21 @@ def test_workbasket_review_measures_filters_update_type( update_type, expected_measure_count, valid_user_client, - session_workbasket, + user_workbasket, ): """Tests that `WorkBasketReviewMeasuresView` filters measures by `update_type`.""" - with session_workbasket.new_transaction(): + with user_workbasket.new_transaction(): created_measures = factories.MeasureFactory.create_batch(2) - updated_measure = created_measures[0].new_version(workbasket=session_workbasket) + updated_measure = created_measures[0].new_version(workbasket=user_workbasket) deleted_measure = created_measures[1].new_version( update_type=UpdateType.DELETE, - workbasket=session_workbasket, + workbasket=user_workbasket, ) url = reverse( "workbaskets:workbasket-ui-review-measures", - kwargs={"pk": session_workbasket.pk}, + kwargs={"pk": user_workbasket.pk}, ) search_filter = f"?update_type={update_type}" response = valid_user_client.get(url + search_filter) @@ -634,15 +648,15 @@ def test_workbasket_review_measures_conditions(valid_user_client): @patch("workbaskets.tasks.call_check_workbasket_sync.apply_async") -def test_run_business_rules(check_workbasket, valid_user_client, session_workbasket): +def test_run_business_rules(check_workbasket, valid_user_client, user_workbasket): """Test that a GET request to the run-business-rules endpoint returns a 302, redirecting to the review workbasket page, runs the `check_workbasket` task, saves the task id on the workbasket, and deletes pre-existing `TrackedModelCheck` objects associated with the workbasket.""" check_workbasket.return_value.id = 123 - assert not session_workbasket.rule_check_task_id + assert not user_workbasket.rule_check_task_id - with session_workbasket.new_transaction() as transaction: + with user_workbasket.new_transaction() as transaction: good = factories.GoodsNomenclatureFactory.create(transaction=transaction) check = TrackedModelCheckFactory.create( transaction_check__transaction=transaction, @@ -650,13 +664,6 @@ def test_run_business_rules(check_workbasket, valid_user_client, session_workbas successful=False, ) - session = valid_user_client.session - session["workbasket"] = { - "id": session_workbasket.pk, - "status": session_workbasket.status, - "title": session_workbasket.title, - } - session.save() url = reverse( "workbaskets:workbasket-checks", ) @@ -669,21 +676,21 @@ def test_run_business_rules(check_workbasket, valid_user_client, session_workbas # Only compare the response URL up to the query string. assert response.url[: len(url)] == url - session_workbasket.refresh_from_db() + user_workbasket.refresh_from_db() check_workbasket.assert_called_once_with( - (session_workbasket.pk,), + (user_workbasket.pk,), countdown=1, ) - assert session_workbasket.rule_check_task_id - assert not session_workbasket.tracked_model_checks.exists() + assert user_workbasket.rule_check_task_id + assert not user_workbasket.tracked_model_checks.exists() -def test_workbasket_business_rule_status(valid_user_client, session_empty_workbasket): +def test_workbasket_business_rule_status(valid_user_client, user_empty_workbasket): """Testing that the live status of a workbasket resets after an item has been updated, created or deleted in the workbasket.""" - with session_empty_workbasket.new_transaction() as transaction: + with user_empty_workbasket.new_transaction() as transaction: footnote = factories.FootnoteFactory.create( transaction=transaction, footnote_type__transaction=transaction, @@ -704,7 +711,46 @@ def test_workbasket_business_rule_status(valid_user_client, session_empty_workba assert success_banner factories.FootnoteFactory.create( - transaction=session_empty_workbasket.new_transaction(), + transaction=user_empty_workbasket.new_transaction(), + ) + response = valid_user_client.get(url) + page = BeautifulSoup(response.content.decode(response.charset)) + assert not page.find("div", attrs={"class": "govuk-notification-banner--success"}) + + +def test_workbasket_business_rule_status_real_edit( + valid_user_client, + use_edit_view_no_workbasket, + user_empty_workbasket, + published_footnote_type, +): + """Testing that the live status of a workbasket resets after an item has + been updated, created or deleted in the workbasket.""" + + with user_empty_workbasket.new_transaction() as transaction: + footnote = factories.FootnoteFactory.create( + update_type=UpdateType.CREATE, + transaction=transaction, + footnote_type=published_footnote_type, + ) + TrackedModelCheckFactory.create( + transaction_check__transaction=transaction, + model=footnote, + successful=True, + ) + + url = reverse("workbaskets:workbasket-checks") + response = valid_user_client.get(url) + page = BeautifulSoup(response.content.decode(response.charset)) + success_banner = page.find( + "div", + attrs={"class": "govuk-notification-banner--success"}, + ) + assert success_banner + + use_edit_view_no_workbasket( + footnote, + {**date_post_data("start_date", datetime.date.today())}, ) response = valid_user_client.get(url) page = BeautifulSoup(response.content.decode(response.charset)) @@ -712,9 +758,9 @@ def test_workbasket_business_rule_status(valid_user_client, session_empty_workba @pytest.fixture -def successful_business_rules_setup(session_workbasket, valid_user_client): +def successful_business_rules_setup(user_workbasket, valid_user_client): """Sets up data and runs business rules.""" - with session_workbasket.new_transaction() as transaction: + with user_workbasket.new_transaction() as transaction: good = factories.GoodsNomenclatureFactory.create(transaction=transaction) measure = factories.MeasureFactory.create(transaction=transaction) geo_area = factories.GeographicalAreaFactory.create(transaction=transaction) @@ -725,17 +771,9 @@ def successful_business_rules_setup(session_workbasket, valid_user_client): model=obj, successful=True, ) - session = valid_user_client.session - session["workbasket"] = { - "id": session_workbasket.pk, - "status": session_workbasket.status, - "title": session_workbasket.title, - "error_count": session_workbasket.tracked_model_check_errors.count(), - } - session.save() # run rule checks so unchecked_or_errored_transactions is set - check_workbasket_sync(session_workbasket) + check_workbasket_sync(user_workbasket) def import_batch_with_notification(): @@ -745,9 +783,10 @@ def import_batch_with_notification(): taric_file="goods.xml", ) - return factories.GoodsSuccessfulImportNotificationFactory( + factories.GoodsSuccessfulImportNotificationFactory( notified_object_pk=import_batch.id, ) + return import_batch @pytest.mark.parametrize( @@ -785,7 +824,7 @@ def import_batch_with_notification(): def test_submit_for_packaging_disabled( successful_business_rules_setup, valid_user_client, - session_workbasket, + user_workbasket, import_batch_factory, disabled, ): @@ -795,7 +834,7 @@ def test_submit_for_packaging_disabled( import_batch = import_batch_factory() if import_batch: - import_batch.workbasket_id = session_workbasket.id + import_batch.workbasket_id = user_workbasket.id if isinstance(import_batch, ImportBatch): import_batch.save() url = reverse( @@ -823,7 +862,7 @@ def test_submit_for_packaging_disabled( def test_submit_for_packaging( successful_business_rules_setup, valid_user_client, - session_workbasket, + user_workbasket, ): """Test that a link to the publishing/create url shows following a successful rule check.""" @@ -832,7 +871,7 @@ def test_submit_for_packaging( goods_import=True, ) - import_batch.workbasket_id = session_workbasket.id + import_batch.workbasket_id = user_workbasket.id if isinstance(import_batch, ImportBatch): import_batch.save() url = reverse( @@ -852,8 +891,8 @@ def test_submit_for_packaging( assert soup.find("a", href="/publishing/create/") -def test_terminate_rule_check(valid_user_client, session_workbasket): - session_workbasket.rule_check_task_id = 123 +def test_terminate_rule_check(valid_user_client, user_workbasket): + user_workbasket.rule_check_task_id = 123 url = reverse( "workbaskets:workbasket-checks", @@ -865,33 +904,25 @@ def test_terminate_rule_check(valid_user_client, session_workbasket): assert response.status_code == 302 assert response.url[: len(url)] == url - session_workbasket.refresh_from_db() + user_workbasket.refresh_from_db() - assert not session_workbasket.rule_check_task_id + assert not user_workbasket.rule_check_task_id -def test_workbasket_violations(valid_user_client, session_workbasket): +def test_workbasket_violations(valid_user_client, user_workbasket): """Test that a GET request to the violations endpoint returns a 200 and displays the correct column values for one unsuccessful `TrackedModelCheck`.""" url = reverse( "workbaskets:workbasket-ui-violations", ) - with session_workbasket.new_transaction() as transaction: + with user_workbasket.new_transaction() as transaction: good = factories.GoodsNomenclatureFactory.create(transaction=transaction) check = TrackedModelCheckFactory.create( transaction_check__transaction=transaction, model=good, successful=False, ) - session = valid_user_client.session - session["workbasket"] = { - "id": session_workbasket.pk, - "status": session_workbasket.status, - "title": session_workbasket.title, - "error_count": session_workbasket.tracked_model_check_errors.count(), - } - session.save() response = valid_user_client.get(url) assert response.status_code == 200 @@ -910,14 +941,14 @@ def test_workbasket_violations(valid_user_client, session_workbasket): def test_workbasket_violations_summary_pagination( valid_user_client, - session_workbasket, + user_workbasket, ): """Tests that the violations page paginates if there are more than 50 violations.""" url = reverse("workbaskets:workbasket-ui-violations") - with session_workbasket.new_transaction() as transaction: + with user_workbasket.new_transaction() as transaction: measures = factories.MeasureFactory.create_batch( 59, transaction=transaction, @@ -928,14 +959,6 @@ def test_workbasket_violations_summary_pagination( model=measure, successful=False, ) - session = valid_user_client.session - session["workbasket"] = { - "id": session_workbasket.pk, - "status": session_workbasket.status, - "title": session_workbasket.title, - "error_count": session_workbasket.tracked_model_check_errors.count(), - } - session.save() response = valid_user_client.get(url) assert response.status_code == 200 @@ -949,8 +972,8 @@ def test_workbasket_violations_summary_pagination( assert "Showing 50 of 59" in pagination_div_text -def test_violation_detail_page(valid_user_client, session_workbasket): - with session_workbasket.new_transaction() as transaction: +def test_violation_detail_page(valid_user_client, user_workbasket): + with user_workbasket.new_transaction() as transaction: good = factories.GoodsNomenclatureFactory.create(transaction=transaction) check = TrackedModelCheckFactory.create( transaction_check__transaction=transaction, @@ -959,16 +982,8 @@ def test_violation_detail_page(valid_user_client, session_workbasket): ) url = reverse( "workbaskets:workbasket-ui-violation-detail", - kwargs={"wb_pk": session_workbasket.pk, "pk": check.pk}, - ) - session = valid_user_client.session - session["workbasket"] = { - "id": session_workbasket.pk, - "status": session_workbasket.status, - "title": session_workbasket.title, - "error_count": session_workbasket.tracked_model_check_errors.count(), - } - session.save() + kwargs={"wb_pk": user_workbasket.pk, "pk": check.pk}, + ) response = valid_user_client.get(url) assert response.status_code == 200 @@ -982,26 +997,26 @@ def test_violation_detail_page(valid_user_client, session_workbasket): def test_violation_detail_page_superuser_override_last_violation( - superuser_client, - session_workbasket, + superuser, + client, + user_workbasket, ): """Override the last unsuccessful TrackedModelCheck on a TransactionCheck.""" - model_check = TrackedModelCheckFactory.create( successful=False, transaction_check__successful=False, ) - model_check.transaction_check.transaction.workbasket.save_to_session( - superuser_client.session, + client.force_login(superuser) + model_check.transaction_check.transaction.workbasket.assign_to_user( + superuser, ) - superuser_client.session.save() url = reverse( "workbaskets:workbasket-ui-violation-detail", - kwargs={"wb_pk": session_workbasket.pk, "pk": model_check.pk}, + kwargs={"wb_pk": user_workbasket.pk, "pk": model_check.pk}, ) - response = superuser_client.post(url, data={"action": "delete"}) + response = client.post(url, data={"action": "delete"}) assert response.status_code == 302 redirect_url = reverse("workbaskets:workbasket-ui-violations") @@ -1013,8 +1028,9 @@ def test_violation_detail_page_superuser_override_last_violation( def test_violation_detail_page_superuser_override_one_of_two_violation( - superuser_client, - session_workbasket, + superuser, + client, + user_workbasket, ): """Override an unsuccessful TrackedModelCheck on a TransactionCheck that has more TrackedModelCheck.""" @@ -1023,10 +1039,10 @@ def test_violation_detail_page_superuser_override_one_of_two_violation( successful=False, transaction_check__successful=False, ) - model_check.transaction_check.transaction.workbasket.save_to_session( - superuser_client.session, + client.force_login(superuser) + model_check.transaction_check.transaction.workbasket.assign_to_user( + superuser, ) - superuser_client.session.save() TrackedModelCheckFactory.create( successful=False, @@ -1043,9 +1059,9 @@ def test_violation_detail_page_superuser_override_one_of_two_violation( url = reverse( "workbaskets:workbasket-ui-violation-detail", - kwargs={"wb_pk": session_workbasket.pk, "pk": model_check.pk}, + kwargs={"wb_pk": user_workbasket.pk, "pk": model_check.pk}, ) - response = superuser_client.post(url, data={"action": "delete"}) + response = client.post(url, data={"action": "delete"}) assert response.status_code == 302 redirect_url = reverse("workbaskets:workbasket-ui-violations") @@ -1064,8 +1080,9 @@ def test_violation_detail_page_superuser_override_one_of_two_violation( def test_violation_detail_page_non_superuser_override_violation( - valid_user_client, - session_workbasket, + valid_user, + client, + user_workbasket, ): """Ensure a user without superuser status is unable to override a TrackedModelCheck.""" @@ -1074,16 +1091,16 @@ def test_violation_detail_page_non_superuser_override_violation( successful=False, transaction_check__successful=False, ) - model_check.transaction_check.transaction.workbasket.save_to_session( - valid_user_client.session, + client.force_login(valid_user) + model_check.transaction_check.transaction.workbasket.assign_to_user( + valid_user, ) - valid_user_client.session.save() url = reverse( "workbaskets:workbasket-ui-violation-detail", - kwargs={"wb_pk": session_workbasket.pk, "pk": model_check.pk}, + kwargs={"wb_pk": user_workbasket.pk, "pk": model_check.pk}, ) - response = valid_user_client.post(url, data={"action": "delete"}) + response = client.post(url, data={"action": "delete"}) assert response.status_code == 302 model_check.refresh_from_db() @@ -1092,8 +1109,8 @@ def test_violation_detail_page_non_superuser_override_violation( @pytest.fixture -def setup(session_workbasket, valid_user_client): - with session_workbasket.new_transaction() as transaction: +def setup(user_workbasket, valid_user_client): + with user_workbasket.new_transaction() as transaction: good = factories.GoodsNomenclatureFactory.create(transaction=transaction) measure = factories.MeasureFactory.create(transaction=transaction) geo_area = factories.GeographicalAreaFactory.create(transaction=transaction) @@ -1118,17 +1135,9 @@ def setup(session_workbasket, valid_user_client): model=obj, successful=False, ) - session = valid_user_client.session - session["workbasket"] = { - "id": session_workbasket.pk, - "status": session_workbasket.status, - "title": session_workbasket.title, - "error_count": session_workbasket.tracked_model_check_errors.count(), - } - session.save() -def test_violation_list_page_sorting_date(setup, valid_user_client, session_workbasket): +def test_violation_list_page_sorting_date(setup, valid_user_client, user_workbasket): """Tests the sorting of the queryset when GET params are set.""" url = reverse( "workbaskets:workbasket-ui-violations", @@ -1137,7 +1146,7 @@ def test_violation_list_page_sorting_date(setup, valid_user_client, session_work assert response.status_code == 200 - checks = session_workbasket.tracked_model_check_errors + checks = user_workbasket.tracked_model_check_errors soup = BeautifulSoup(str(response.content), "html.parser") activity_dates = [ @@ -1158,7 +1167,7 @@ def test_violation_list_page_sorting_date(setup, valid_user_client, session_work def test_violation_list_page_sorting_model_name( setup, valid_user_client, - session_workbasket, + user_workbasket, ): """Tests the sorting of the queryset when GET params are set.""" url = reverse( @@ -1168,7 +1177,7 @@ def test_violation_list_page_sorting_model_name( assert response.status_code == 200 - checks = session_workbasket.tracked_model_check_errors + checks = user_workbasket.tracked_model_check_errors soup = BeautifulSoup(str(response.content), "html.parser") activity_dates = [ @@ -1189,7 +1198,7 @@ def test_violation_list_page_sorting_model_name( def test_violation_list_page_sorting_check_name( setup, valid_user_client, - session_workbasket, + user_workbasket, ): """Tests the sorting of the queryset when GET params are set.""" url = reverse( @@ -1199,7 +1208,7 @@ def test_violation_list_page_sorting_check_name( assert response.status_code == 200 - checks = session_workbasket.tracked_model_check_errors + checks = user_workbasket.tracked_model_check_errors soup = BeautifulSoup(str(response.content), "html.parser") rule_codes = [ @@ -1217,7 +1226,7 @@ def test_violation_list_page_sorting_check_name( def test_violation_list_page_sorting_ignores_invalid_params( setup, valid_user_client, - session_workbasket, + user_workbasket, ): """Tests that the page doesn't break if invalid params are sent.""" url = reverse( @@ -1252,14 +1261,14 @@ def test_workbasket_detail_views_without_view_permission(url_name, client): def test_workbasket_detail_view_displays_workbasket_details( valid_user_client, - session_workbasket, + user_workbasket, ): """Tests that `WorkBasketDetailView` returns 200 and displays workbasket details in table.""" url = reverse( "workbaskets:workbasket-ui-detail", - kwargs={"pk": session_workbasket.pk}, + kwargs={"pk": user_workbasket.pk}, ) response = valid_user_client.get(url) assert response.status_code == 200 @@ -1268,23 +1277,23 @@ def test_workbasket_detail_view_displays_workbasket_details( table = soup.select("table")[0] row_text = [row.text for row in table.findChildren("td")] - assert session_workbasket.get_status_display().upper() in row_text[0] - assert str(session_workbasket.id) in row_text[1] - assert session_workbasket.title in row_text[2] - assert session_workbasket.reason in row_text[3] - assert str(session_workbasket.tracked_models.count()) in row_text[4] - assert session_workbasket.created_at.strftime("%d %b %y %H:%M") in row_text[5] - assert session_workbasket.updated_at.strftime("%d %b %y %H:%M") in row_text[6] + assert user_workbasket.get_status_display().upper() in row_text[0] + assert str(user_workbasket.id) in row_text[1] + assert user_workbasket.title in row_text[2] + assert user_workbasket.reason in row_text[3] + assert str(user_workbasket.tracked_models.count()) in row_text[4] + assert user_workbasket.created_at.strftime("%d %b %y %H:%M") in row_text[5] + assert user_workbasket.updated_at.strftime("%d %b %y %H:%M") in row_text[6] -def test_workbasket_changes_view_without_change_permission(client, session_workbasket): +def test_workbasket_changes_view_without_change_permission(client, user_workbasket): """Tests that `WorkBasketChangesView` displays changes in a workbasket without the ability to remove items to users without `change_workbasket` permission.""" url = reverse( "workbaskets:workbasket-ui-changes", - kwargs={"pk": session_workbasket.pk}, + kwargs={"pk": user_workbasket.pk}, ) user = factories.UserFactory.create() user.user_permissions.add(Permission.objects.get(codename="view_workbasket")) @@ -1299,21 +1308,21 @@ def test_workbasket_changes_view_without_change_permission(client, session_workb remove_button = page.find("button", value="remove-selected") assert len(columns) == 5 - assert len(rows) == session_workbasket.tracked_models.count() + assert len(rows) == user_workbasket.tracked_models.count() assert not checkboxes assert not remove_button def test_workbasket_changes_view_with_change_permission( valid_user_client, - session_workbasket, + user_workbasket, ): """Tests that `WorkBasketChangesView` displays changes in a workbasket with the ability to remove items to users with `change_workbasket` permission.""" url = reverse( "workbaskets:workbasket-ui-changes", - kwargs={"pk": session_workbasket.pk}, + kwargs={"pk": user_workbasket.pk}, ) response = valid_user_client.get(url) assert response.status_code == 200 @@ -1325,7 +1334,7 @@ def test_workbasket_changes_view_with_change_permission( remove_button = page.find("button", value="remove-selected") assert len(columns) == 6 - assert len(rows) == session_workbasket.tracked_models.count() + assert len(rows) == user_workbasket.tracked_models.count() assert checkboxes assert remove_button @@ -1683,7 +1692,7 @@ def test_workbasket_transaction_order_first_or_last_transaction_in_workbasket(): def test_successfully_delete_workbasket( valid_user_client, valid_user, - session_empty_workbasket, + user_empty_workbasket, ): """Test that deleting an empty workbasket by a user having the necessary `workbasket.can_delete` permssion.""" @@ -1691,7 +1700,7 @@ def test_successfully_delete_workbasket( valid_user.user_permissions.add( Permission.objects.get(codename="delete_workbasket"), ) - workbasket_pk = session_empty_workbasket.pk + workbasket_pk = user_empty_workbasket.pk delete_url = reverse( "workbaskets:workbasket-ui-delete", kwargs={"pk": workbasket_pk}, @@ -1724,12 +1733,12 @@ def test_successfully_delete_workbasket( def test_delete_workbasket_missing_user_permission( valid_user_client, - session_empty_workbasket, + user_empty_workbasket, ): """Test that attempts to access the delete workbasket view and delete a workbasket fails for a user without the necessary permissions.""" - workbasket_pk = session_empty_workbasket.pk + workbasket_pk = user_empty_workbasket.pk url = reverse( "workbaskets:workbasket-ui-delete", kwargs={"pk": workbasket_pk}, @@ -1749,15 +1758,15 @@ def test_delete_workbasket_missing_user_permission( def test_delete_nonempty_workbasket( valid_user_client, valid_user, - session_workbasket, + user_workbasket, ): """Test that attempts to delete a non-empty workbasket fails.""" valid_user.user_permissions.add( Permission.objects.get(codename="delete_workbasket"), ) - workbasket_pk = session_workbasket.pk - workbasket_object_count = session_workbasket.tracked_models.count() + workbasket_pk = user_workbasket.pk + workbasket_object_count = user_workbasket.tracked_models.count() delete_url = reverse( "workbaskets:workbasket-ui-delete", kwargs={"pk": workbasket_pk}, @@ -1781,7 +1790,7 @@ def test_delete_nonempty_workbasket( def test_application_access_after_workbasket_delete( valid_user_client, - session_empty_workbasket, + user_empty_workbasket, ): """ Test that after deleting a user's 'current' workbasket, the user is still @@ -1792,13 +1801,13 @@ def test_application_access_after_workbasket_delete( ensuring application avoids 500-series errors under the above conditions. """ - workbasket_pk = session_empty_workbasket.pk + workbasket_pk = user_empty_workbasket.pk url = reverse("workbaskets:workbasket-ui-list") response = valid_user_client.get(url) page = BeautifulSoup(response.content, "html.parser") # A workbasket link should be available in the header nav bar before - # session workbasket deletion. + # user workbasket deletion. assert response.status_code == 200 assert ( @@ -1806,11 +1815,11 @@ def test_application_access_after_workbasket_delete( in page.select("header nav a.workbasket-link")[0].text ) - session_empty_workbasket.delete() + user_empty_workbasket.delete() response = valid_user_client.get(url) page = BeautifulSoup(response.content, "html.parser") - # No workbasket link should exist in the header nav bar after session + # No workbasket link should exist in the header nav bar after user # workbasket deletion. assert response.status_code == 200 assert not page.select("header nav a.workbasket-link") @@ -1852,25 +1861,25 @@ def test_workbasket_delete_previously_queued_workbasket( assert workbasket.status == WorkflowStatus.ARCHIVED -def test_workbasket_compare_200(valid_user_client, session_workbasket): +def test_workbasket_compare_200(valid_user_client, user_workbasket): url = reverse("workbaskets:workbasket-check-ui-compare") response = valid_user_client.get(url) assert response.status_code == 200 -def test_workbasket_compare_prev_uploaded(valid_user_client, session_workbasket): +def test_workbasket_compare_prev_uploaded(valid_user_client, user_workbasket): factories.GoodsNomenclatureFactory() factories.GoodsNomenclatureFactory() - factories.DataUploadFactory(workbasket=session_workbasket) + factories.DataUploadFactory(workbasket=user_workbasket) url = reverse("workbaskets:workbasket-check-ui-compare") response = valid_user_client.get(url) assert "Worksheet data" in response.content.decode(response.charset) -def test_workbasket_update_prev_uploaded(valid_user_client, session_workbasket): +def test_workbasket_update_prev_uploaded(valid_user_client, user_workbasket): factories.GoodsNomenclatureFactory() factories.GoodsNomenclatureFactory() - data_upload = factories.DataUploadFactory(workbasket=session_workbasket) + data_upload = factories.DataUploadFactory(workbasket=user_workbasket) url = reverse("workbaskets:workbasket-check-ui-compare") data = { "data": ( @@ -1884,7 +1893,7 @@ def test_workbasket_update_prev_uploaded(valid_user_client, session_workbasket): assert data_upload.raw_data == data["data"] -def test_workbasket_compare_form_submit_302(valid_user_client, session_workbasket): +def test_workbasket_compare_form_submit_302(valid_user_client, user_workbasket): url = reverse("workbaskets:workbasket-check-ui-compare") data = { "data": ( @@ -1899,14 +1908,14 @@ def test_workbasket_compare_form_submit_302(valid_user_client, session_workbaske def test_workbasket_compare_found_measures( valid_user_client, - session_workbasket, + user_workbasket, date_ranges, duty_sentence_parser, percent_or_amount, ): commodity = factories.GoodsNomenclatureFactory() - with session_workbasket.new_transaction(): + with user_workbasket.new_transaction(): measure = factories.MeasureFactory( valid_between=date_ranges.normal, goods_nomenclature=commodity, @@ -1993,7 +2002,7 @@ def make_goods_import_batch(importer_storage, **kwargs): def test_review_goods_notification_button( successful_business_rules_setup, valid_user_client, - session_workbasket, + user_workbasket, import_batch_factory, visable, ): @@ -2003,7 +2012,7 @@ def test_review_goods_notification_button( import_batch = import_batch_factory() if import_batch: - import_batch.workbasket_id = session_workbasket.id + import_batch.workbasket_id = user_workbasket.id if isinstance(import_batch, ImportBatch): import_batch.save() @@ -2035,3 +2044,47 @@ def mock_xlsx_open(filename, mode): assert notify_button else: assert not notify_button + + +def test_no_active_workbasket_view(valid_user_client): + """Test that NoActiveWorkBasket view returns 200 and displays headings and + buttons.""" + response = valid_user_client.get(reverse("workbaskets:no-active-workbasket")) + + soup = BeautifulSoup(str(response.content), "html.parser") + message = soup.find("h1", text="You need an active workbasket to access this page") + select_a_new_workbasket = soup.find( + "a", + href=reverse("workbaskets:workbasket-ui-list"), + ) + return_to_homepage = soup.find("a", href=reverse("home")) + + assert response.status_code == 200 + assert message + assert select_a_new_workbasket + assert return_to_homepage + + +@pytest.mark.parametrize( + "workbasket_factory", + [ + lambda: None, + factories.ArchivedWorkBasketFactory, + factories.QueuedWorkBasketFactory, + factories.PublishedWorkBasketFactory, + ], +) +def test_require_current_workbasket_redirect(workbasket_factory, client, valid_user): + """Test that views using require_current_workbasket decorator redirect to + NoActiveWorkBasket view if the user's current workbasket is no longer in + editing state.""" + client.force_login(valid_user) + + valid_user.current_workbasket == workbasket_factory() + valid_user.save() + + # view that has require_current_workbasket decorator + response = client.get(reverse("workbaskets:current-workbasket")) + + assert response.status_code == 302 + assert response.url == reverse("workbaskets:no-active-workbasket") diff --git a/workbaskets/urls.py b/workbaskets/urls.py index 0defdcd0e..bb56b2062 100644 --- a/workbaskets/urls.py +++ b/workbaskets/urls.py @@ -96,6 +96,21 @@ ui_views.WorkBasketReviewQuotaDefinitionsView.as_view(), name="workbasket-ui-review-quota-definitions", ), + path( + f"/review-sub-quotas/", + ui_views.WorkBasketReviewSubQuotasView.as_view(), + name="workbasket-ui-review-sub-quotas", + ), + path( + f"/review-quota-blocking-periods/", + ui_views.WorkBasketReviewQuotaBlockingView.as_view(), + name="workbasket-ui-review-quota-blocking-periods", + ), + path( + f"/review-quota-suspension-periods/", + ui_views.WorkBasketReviewQuotaSuspensionView.as_view(), + name="workbasket-ui-review-quota-suspension-periods", + ), path( f"/review-regulations/", ui_views.WorkBasketReviewRegulationsView.as_view(), @@ -121,6 +136,11 @@ ui_views.WorkBasketCompare.as_view(), name="workbasket-check-ui-compare", ), + path( + "no-active-workbasket/", + ui_views.NoActiveWorkBasket.as_view(), + name="no-active-workbasket", + ), path( f"/", ui_views.WorkBasketDetailView.as_view(), diff --git a/workbaskets/views/decorators.py b/workbaskets/views/decorators.py index 4f4f7f6ac..4466be210 100644 --- a/workbaskets/views/decorators.py +++ b/workbaskets/views/decorators.py @@ -1,23 +1,21 @@ from functools import wraps +from django.shortcuts import redirect + from workbaskets.models import WorkBasket def require_current_workbasket(view_func): - """View decorator which redirects user to choose or create a workbasket + """View decorator which redirects user to select a new current workbasket before continuing.""" @wraps(view_func) def check_for_current_workbasket(request, *args, **kwargs): - if WorkBasket.current(request) is None: - workbasket = WorkBasket.objects.editable().last() - if not workbasket: - workbasket = WorkBasket.objects.create( - author=request.user, - ) - - workbasket.save_to_session(request.session) - - return view_func(request, *args, **kwargs) + try: + if WorkBasket.current(request): + return view_func(request, *args, **kwargs) + return redirect("workbaskets:no-active-workbasket") + except WorkBasket.DoesNotExist: + return redirect("workbaskets:no-active-workbasket") return check_for_current_workbasket diff --git a/workbaskets/views/ui.py b/workbaskets/views/ui.py index 57ce51d91..82ae76f73 100644 --- a/workbaskets/views/ui.py +++ b/workbaskets/views/ui.py @@ -49,8 +49,11 @@ from notifications.models import Notification from notifications.models import NotificationTypeChoices from publishing.models import PackagedWorkBasket +from quotas.models import QuotaAssociation +from quotas.models import QuotaBlocking from quotas.models import QuotaDefinition from quotas.models import QuotaOrderNumber +from quotas.models import QuotaSuspension from regulations.models import Regulation from workbaskets import forms from workbaskets.models import DataRow @@ -99,7 +102,7 @@ def form_valid(self, form): self.object = form.save(commit=False) self.object.author = user self.object.save() - self.object.save_to_session(self.request.session) + self.object.assign_to_user(self.request.user) return redirect( reverse( "workbaskets:workbasket-ui-confirm-create", @@ -184,7 +187,7 @@ def post(self, request, *args, **kwargs): workbasket.restore() workbasket.save() - workbasket.save_to_session(request.session) + workbasket.assign_to_user(request.user) if workbasket_tab: view = workbasket_tab_map[workbasket_tab] @@ -272,7 +275,7 @@ class WorkBasketChangesConfirmDelete(TemplateView): def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - context["session_workbasket"] = WorkBasket.current(self.request) + context["user_workbasket"] = WorkBasket.current(self.request) context["view_workbasket"] = WorkBasket.objects.get(pk=self.kwargs["pk"]) return context @@ -434,7 +437,7 @@ def get_context_data(self, **kwargs): if result.status != "SUCCESS": context.update({"rule_check_in_progress": True}) else: - self.workbasket.save_to_session(self.request.session) + self.workbasket.assign_to_user(self.request.user) num_completed, total = self.workbasket.rule_check_progress() context.update( @@ -1218,7 +1221,7 @@ def get_context_data(self, **kwargs): if result.status != "SUCCESS": context.update({"rule_check_in_progress": True}) else: - self.workbasket.save_to_session(self.request.session) + self.workbasket.assign_to_user(self.request.user) num_completed, total = self.workbasket.rule_check_progress() context.update( @@ -1249,7 +1252,7 @@ def get_queryset(self): def get_context_data(self, *args, **kwargs): context = super().get_context_data(*args, **kwargs) - context["session_workbasket"] = WorkBasket.current(self.request) + context["user_workbasket"] = WorkBasket.current(self.request) context["workbasket"] = self.workbasket return context @@ -1297,7 +1300,7 @@ def get_context_data(self, *args, **kwargs): context = super().get_context_data(*args, **kwargs) context["tab_page_title"] = "Review commodities" context["selected_tab"] = "commodities" - context["session_workbasket"] = WorkBasket.current(self.request) + context["user_workbasket"] = WorkBasket.current(self.request) context["workbasket"] = self.workbasket context["report_lines"] = [] context["import_batch_pk"] = None @@ -1363,7 +1366,7 @@ def get_context_data(self, *args, **kwargs): context["import_batch_pk"] = import_batch.pk # notifications only relevant to a goods import - if context["workbasket"] == context["session_workbasket"]: + if context["workbasket"] == context["user_workbasket"]: context["unsent_notification"] = ( import_batch.goods_import and not Notification.objects.filter( @@ -1478,6 +1481,58 @@ def get_context_data(self, *args, **kwargs): return context +class WorkBasketReviewSubQuotasView(WorkBasketReviewView): + """UI endpoint for reviewing sub-quota association changes in a + workbasket.""" + + model = QuotaAssociation + template_name = "workbaskets/review-quotas.jinja" + + def get_context_data(self, *args, **kwargs): + context = super().get_context_data(*args, **kwargs) + context["tab_page_title"] = "Review sub-quota associations" + context["selected_tab"] = "quotas" + context["selected_nested_tab"] = "sub-quotas" + context["tab_template"] = "includes/workbaskets/review-sub-quotas.jinja" + return context + + +class WorkBasketReviewQuotaBlockingView(WorkBasketReviewView): + """UI endpoint for reviewing quota blocking period changes in a + workbasket.""" + + model = QuotaBlocking + template_name = "workbaskets/review-quotas.jinja" + + def get_context_data(self, *args, **kwargs): + context = super().get_context_data(*args, **kwargs) + context["tab_page_title"] = "Review quota blocking periods" + context["selected_tab"] = "quotas" + context["selected_nested_tab"] = "blocking-periods" + context[ + "tab_template" + ] = "includes/workbaskets/review-quota-blocking-periods.jinja" + return context + + +class WorkBasketReviewQuotaSuspensionView(WorkBasketReviewView): + """UI endpoint for reviewing quota suspension period changes in a + workbasket.""" + + model = QuotaSuspension + template_name = "workbaskets/review-quotas.jinja" + + def get_context_data(self, *args, **kwargs): + context = super().get_context_data(*args, **kwargs) + context["tab_page_title"] = "Review quota suspension periods" + context["selected_tab"] = "quotas" + context["selected_nested_tab"] = "suspension-periods" + context[ + "tab_template" + ] = "includes/workbaskets/review-quota-suspension-periods.jinja" + return context + + class WorkBasketReviewRegulationsView(WorkBasketReviewView): """UI endpoint for reviewing regulation changes in a workbasket.""" @@ -1489,3 +1544,10 @@ def get_context_data(self, *args, **kwargs): context["selected_tab"] = "regulations" context["tab_template"] = "includes/regulations/list.jinja" return context + + +class NoActiveWorkBasket(TemplateView): + """Redirect endpoint for users without an active workbasket and views that + require one.""" + + template_name = "workbaskets/no_active_workbasket.jinja" From ef0c12574202babebcea20dc793e641ba2587620 Mon Sep 17 00:00:00 2001 From: Doug Mills Date: Fri, 23 Feb 2024 12:57:04 +0000 Subject: [PATCH 053/118] prep for merge to mega branch --- .../migrations/0001_initial.py | 22 ++++++++++++++++++- reference_documents/models.py | 15 +++++++++++++ 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/reference_documents/migrations/0001_initial.py b/reference_documents/migrations/0001_initial.py index 4470166cc..ae20a7f04 100644 --- a/reference_documents/migrations/0001_initial.py +++ b/reference_documents/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 3.2.23 on 2024-02-22 15:02 +# Generated by Django 3.2.23 on 2024-02-23 12:20 import django.db.models.deletion import django_fsm @@ -145,6 +145,16 @@ class Migration(migrations.Migration): ("commodity_code", models.CharField(db_index=True, max_length=10)), ("quota_duty_rate", models.CharField(max_length=255)), ("volume", models.CharField(max_length=255)), + ( + "coefficient", + models.DecimalField( + blank=True, + decimal_places=4, + default=None, + max_digits=6, + null=True, + ), + ), ( "valid_between", common.fields.TaricDateRangeField( @@ -156,6 +166,16 @@ class Migration(migrations.Migration): ), ("measurement", models.CharField(max_length=255)), ("order", models.IntegerField()), + ( + "main_quota", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.PROTECT, + related_name="sub_quotas", + to="reference_documents.preferentialquota", + ), + ), ( "reference_document_version", models.ForeignKey( diff --git a/reference_documents/models.py b/reference_documents/models.py index 82f5323a6..305f1c2e8 100644 --- a/reference_documents/models.py +++ b/reference_documents/models.py @@ -99,6 +99,21 @@ class PreferentialQuota(models.Model): volume = models.CharField( max_length=255, ) + coefficient = models.DecimalField( + max_digits=6, + decimal_places=4, + blank=True, + null=True, + default=None, + ) + + main_quota = models.ForeignKey( + "self", + related_name="sub_quotas", + blank=True, + null=True, + on_delete=models.PROTECT, + ) valid_between = TaricDateRangeField( db_index=True, From 5a72f9bff811e0f95aa4486329e02b4ad55bd8af Mon Sep 17 00:00:00 2001 From: Doug Mills Date: Fri, 23 Feb 2024 14:47:42 +0000 Subject: [PATCH 054/118] Add command to import duties and quotas --- reference_documents/management/__init__.py | 0 .../management/commands/__init__.py | 0 .../commands/ref_doc_csv_importer.py | 248 ++++++++++++++++++ requirements.txt | 1 + 4 files changed, 249 insertions(+) create mode 100644 reference_documents/management/__init__.py create mode 100644 reference_documents/management/commands/__init__.py create mode 100644 reference_documents/management/commands/ref_doc_csv_importer.py diff --git a/reference_documents/management/__init__.py b/reference_documents/management/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/reference_documents/management/commands/__init__.py b/reference_documents/management/commands/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/reference_documents/management/commands/ref_doc_csv_importer.py b/reference_documents/management/commands/ref_doc_csv_importer.py new file mode 100644 index 000000000..fb89ed0f2 --- /dev/null +++ b/reference_documents/management/commands/ref_doc_csv_importer.py @@ -0,0 +1,248 @@ +import os +from datetime import date + +import pandas as pd +from django.core.management import BaseCommand + +from reference_documents.models import PreferentialQuota +from reference_documents.models import PreferentialRate +from reference_documents.models import ReferenceDocument +from reference_documents.models import ReferenceDocumentVersion +from reference_documents.models import ReferenceDocumentVersionStatus + + +class Command(BaseCommand): + help = "Basic HELP .. todo" + + def add_arguments(self, parser) -> None: + parser.add_argument( + "duties_csv_path", + type=str, + help="The absolute path to the duties csv file to import", + ) + parser.add_argument( + "quotas_csv_path", + type=str, + help="The absolute path to the quotas csv file to import", + ) + + return super().add_arguments(parser) + + def handle(self, *args, **options): + # TODO: remove all ref doc data - temp while testing + PreferentialQuota.objects.all().delete() + PreferentialRate.objects.all().delete() + ReferenceDocumentVersion.objects.all().delete() + ReferenceDocument.objects.all().delete() + + # verify each file exists + if not os.path.isfile(options["duties_csv_path"]): + raise FileNotFoundError(options["duties_csv_path"]) + + if not os.path.isfile(options["quotas_csv_path"]): + raise FileNotFoundError(options["duties_csv_path"]) + + self.duties_csv_path = options["duties_csv_path"] + self.quotas_csv_path = options["quotas_csv_path"] + + self.quotas_df = self.load_quotas_csv() + self.duties_df = self.load_duties_csv() + + self.create_ref_docs_and_versions() + + def load_duties_csv(self): + df = pd.read_csv( + self.duties_csv_path, + dtype={ + "Standardised Commodity Code": "object", + "Valid From": "object", + "Valid To": "object", + }, + ) + return df + + def load_quotas_csv(self): + df = pd.read_csv( + self.quotas_csv_path, + dtype={ + "Standardised Commodity Code": "object", + }, + ) + return df + + def add_pt_quota_if_no_exists( + self, + df_row, + order, + order_number, + reference_document_version, + ): + if len(order_number) != 6: + print(f"skipping wonky order number : -{order_number}-") + return + + comm_code = df_row["Standardised Commodity Code"] + comm_code = comm_code + ("0" * (len(comm_code) - 10)) + + quota_duty_rate = df_row["Quota Duty Rate"] + + volume = df_row["Quota Volume"].replace(",", "") + + units = df_row["Units"] + + quota = reference_document_version.preferential_quotas.filter( + commodity_code=comm_code, + quota_order_number=order_number, + ).first() + + if not quota: + # add a new one + quota = PreferentialQuota.objects.create( + commodity_code=comm_code, + quota_order_number=order_number, + quota_duty_rate=quota_duty_rate, + order=order, + reference_document_version=reference_document_version, + volume=volume, + valid_between=None, + measurement=units, + ) + + quota.save() + + def add_pt_duty_if_no_exist(self, df_row, df_row_index, reference_document_version): + # 'Commodity Code', 'Preferential Duty Rate', 'Staging', 'Validity', + # 'Notes', 'description', 'area_id', 'sid', + # 'TAP_measure__geographical_area__description', + # 'measure__geographical_area__sid', 'Document Date', 'Document Version', + # 'Date Processed', 'Standardised Commodity Code', 'Valid From', + # 'Valid To', 'Valid Date Difference' + + # check for existing entry for comm code + comm_code = df_row["Standardised Commodity Code"] + comm_code = comm_code + ("0" * (len(comm_code) - 10)) + + pref_rate = reference_document_version.preferential_rates.filter( + commodity_code=comm_code, + ).first() + + if not pref_rate: + # add a new one + pref_rate = PreferentialRate.objects.create( + commodity_code=comm_code, + duty_rate=df_row["Preferential Duty Rate"], + order=df_row_index, + reference_document_version=reference_document_version, + valid_between=None, + ) + + pref_rate.save() + + # Create base documents + # Load duties, get unique countries and create base document for each + + def create_ref_docs_and_versions(self): + areas = pd.unique(self.duties_df["area_id"].values) + + for area in areas: + # # isolating mexico + # if area != 'MX': + # continue + + print(area) + ref_doc = ( + ReferenceDocument.objects.all() + .filter( + title=f"Reference document for {area}", + area_id=area, + ) + .first() + ) + # Create records + + if not ref_doc: + ref_doc = ReferenceDocument.objects.create( + title=f"Reference document for {area}", + area_id=area, + ) + ref_doc.save() + + versions = pd.unique( + self.duties_df[self.duties_df["area_id"] == area][ + "Document Version" + ].values, + ) + + for version in versions: + print(f" -- {version}") + # try and find existing + ref_doc_version = ref_doc.reference_document_versions.filter( + version=float(version), + ).first() + if ( + self.duties_df[self.duties_df["area_id"] == area][ + "Document Date" + ].values[0] + == "empty_cell" + ): + document_publish_date = None + else: + doc_date_string = str( + self.duties_df[self.duties_df["area_id"] == area][ + "Document Date" + ].values[0], + ) + document_publish_date = date( + int(doc_date_string[:4]), + int(doc_date_string[4:6]), + int(doc_date_string[6:]), + ) + + if not ref_doc_version: + # Create version + ref_doc_version = ReferenceDocumentVersion.objects.create( + reference_document=ref_doc, + version=float(version), + published_date=document_publish_date, + entry_into_force_date=None, + status=ReferenceDocumentVersionStatus.EDITING, + ) + + ref_doc_version.save() + + # Add duties + + # get duties for area + df_area_duties = self.duties_df.loc[self.duties_df["area_id"] == area] + for index, row in df_area_duties.iterrows(): + print(f' -- -- {row["Standardised Commodity Code"]}') + self.add_pt_duty_if_no_exist(row, index, ref_doc_version) + + # Quotas + + # Filter by area_id and document version + quotas_df = self.quotas_df[self.quotas_df["area_id"] == area] + quotas_df = self.quotas_df[ + self.quotas_df["Document Version"] == version + ] + + add_to_index = 1 + for index, row in quotas_df.iterrows(): + # split order numbers + order_number = row["Quota Number"] + order_number = order_number.replace(".", "") + + if len(order_number) > 6: + order_numbers = order_number.split(" ") + else: + order_numbers = [order_number] + + for on in order_numbers: + print(f' -- -- {on} - {row["Standardised Commodity Code"]}') + self.add_pt_quota_if_no_exists( + row, + index + add_to_index, + on, + ref_doc_version, + ) + add_to_index += 1 diff --git a/requirements.txt b/requirements.txt index 5db4dd0cc..91e394660 100644 --- a/requirements.txt +++ b/requirements.txt @@ -61,6 +61,7 @@ pytest-forked==1.4.0 pytest-responses==0.5.0 pytest-xdist==2.5.0 python-magic==0.4.25 +pandas~=2.0 requests-oauthlib==1.3.0 requests-mock==1.10.0 responses==0.12.1 From d547f8402c19497477e40d3f974e709691c21156 Mon Sep 17 00:00:00 2001 From: Doug Mills Date: Fri, 23 Feb 2024 14:51:45 +0000 Subject: [PATCH 055/118] Add command to import duties and quotas --- README.rst | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/README.rst b/README.rst index ee16932d0..44b68b686 100644 --- a/README.rst +++ b/README.rst @@ -398,6 +398,21 @@ This tool is available as an importer alternative found within the web front end This tool addresses several short falls that the current importer has. +Reference document data import +------------------------------ + +WARNING: this feature is in alpha : do not use in production until this feature has been +fully tested. + +In order to populate the reference document data from extracted data from an external tool +you can use the management command ref_doc_csv_importer + +Example: + + .. code:: sh + + $ python manage.py ref_doc_csv_importer "/absolute/path/to/duties.csv" "/absolute/path/to/quotas.csv" + Using the exporter ------------------ From 6167197f44fea558b412e22ab07bdc990aa0dbec Mon Sep 17 00:00:00 2001 From: Matthew McKenzie <97194636+mattjamc@users.noreply.github.com> Date: Mon, 26 Feb 2024 09:21:28 +0000 Subject: [PATCH 056/118] Functionality to edit reference documents --- reference_documents/forms.py | 42 ++++++++++++++++++ .../jinja2/reference_documents/create.jinja | 4 ++ .../jinja2/reference_documents/update.jinja | 14 ++++++ reference_documents/urls.py | 10 +++++ .../views/reference_document_views.py | 43 ++++++++++++++++++- 5 files changed, 111 insertions(+), 2 deletions(-) create mode 100644 reference_documents/forms.py create mode 100644 reference_documents/jinja2/reference_documents/create.jinja create mode 100644 reference_documents/jinja2/reference_documents/update.jinja diff --git a/reference_documents/forms.py b/reference_documents/forms.py new file mode 100644 index 000000000..12c9f988a --- /dev/null +++ b/reference_documents/forms.py @@ -0,0 +1,42 @@ +from crispy_forms_gds.helper import FormHelper +from crispy_forms_gds.layout import Field +from crispy_forms_gds.layout import Layout +from crispy_forms_gds.layout import Size +from crispy_forms_gds.layout import Submit +from django import forms + +from reference_documents import models + + +class ReferenceDocumentForm(forms.ModelForm): + title = forms.CharField( + label="Reference Document title", + ) + area_id = forms.CharField( + label="Area ID", + ) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + self.fields["title"].label = "Reference Document title" + self.fields["area_id"].help_text = "Two character ID for the area referenced" + + self.helper = FormHelper(self) + self.helper.label_size = Size.SMALL + self.helper.legend_size = Size.SMALL + + self.helper.layout = Layout( + Field.text("title"), + Field("area_id"), + Submit( + "submit", + "Save", + data_module="govuk-button", + data_prevent_double_click="true", + ), + ) + + class Meta: + model = models.ReferenceDocument + fields = ["title", "area_id"] diff --git a/reference_documents/jinja2/reference_documents/create.jinja b/reference_documents/jinja2/reference_documents/create.jinja new file mode 100644 index 000000000..d5506f4c2 --- /dev/null +++ b/reference_documents/jinja2/reference_documents/create.jinja @@ -0,0 +1,4 @@ +{% extends "layouts/create.jinja" %} + +{% set page_title = "Create a new reference document" %} + diff --git a/reference_documents/jinja2/reference_documents/update.jinja b/reference_documents/jinja2/reference_documents/update.jinja new file mode 100644 index 000000000..825175661 --- /dev/null +++ b/reference_documents/jinja2/reference_documents/update.jinja @@ -0,0 +1,14 @@ +{% extends "layouts/form.jinja" %} +{% from "components/breadcrumbs.jinja" import breadcrumbs %} + +{% set page_title = "Edit " ~ object._meta.verbose_name ~ " details" %} + +{% block breadcrumb %} + +{% endblock %} + +{% block form %} + {% call django_form() %} + {{ crispy(form) }} + {% endcall %} +{% endblock %} diff --git a/reference_documents/urls.py b/reference_documents/urls.py index 2f0448f90..eb4b00016 100644 --- a/reference_documents/urls.py +++ b/reference_documents/urls.py @@ -34,6 +34,16 @@ reference_document_views.ReferenceDocumentDetails.as_view(), name="details", ), + path( + "reference_documents/create/", + reference_document_views.ReferenceDocumentCreate.as_view(), + name="create", + ), + path( + "reference_documents//update/", + reference_document_views.ReferenceDocumentUpdate.as_view(), + name="update", + ), # reference document version views path( "reference_document_versions//", diff --git a/reference_documents/views/reference_document_views.py b/reference_documents/views/reference_document_views.py index 7b000b722..97f91b417 100644 --- a/reference_documents/views/reference_document_views.py +++ b/reference_documents/views/reference_document_views.py @@ -1,9 +1,15 @@ from django.contrib.auth.mixins import PermissionRequiredMixin +from django.db import transaction +from django.http import HttpResponseRedirect from django.urls import reverse +from django.views.generic import CreateView from django.views.generic import DetailView from django.views.generic import ListView +from django.views.generic import UpdateView from geo_areas.models import GeographicalAreaDescription +from reference_documents import forms +from reference_documents import models from reference_documents.models import ReferenceDocument @@ -60,7 +66,8 @@ def get_context_data(self, **kwargs): "text": reference.reference_document_versions.last().preferential_quotas.count(), }, { - "html": f'Details', + "html": f'Details
    ' + f"Edit", }, ], ) @@ -116,7 +123,7 @@ def get_context_data(self, *args, **kwargs): "text": version.entry_into_force_date, }, { - "html": f'version details
    ' + "html": f'Version details
    ' f'Edit
    ' f'Alignment reports', }, @@ -126,3 +133,35 @@ def get_context_data(self, *args, **kwargs): context["reference_document_versions"] = reference_document_versions return context + + +class ReferenceDocumentCreate(CreateView): + model = models.ReferenceDocument + template_name = "reference_documents/create.jinja" + form_class = forms.ReferenceDocumentForm + + @transaction.atomic + def form_valid(self, form): + self.object.save() + return HttpResponseRedirect(self.get_success_url()) + + def get_form_kwargs(self): + kwargs = super().get_form_kwargs() + # kwargs["request"] = self.request + return kwargs + + +class ReferenceDocumentUpdate(UpdateView): + model = models.ReferenceDocument + template_name = "reference_documents/update.jinja" + form_class = forms.ReferenceDocumentForm + + @transaction.atomic + def form_valid(self, form): + self.object.save() + return HttpResponseRedirect(reverse("reference_documents:index")) + + def get_form_kwargs(self): + kwargs = super().get_form_kwargs() + # kwargs["request"] = self.request + return kwargs From c432681e8a0c2b3cc01f8e2a4743dd125063ba43 Mon Sep 17 00:00:00 2001 From: Matthew McKenzie <97194636+mattjamc@users.noreply.github.com> Date: Mon, 26 Feb 2024 17:15:15 +0000 Subject: [PATCH 057/118] CRUD templates --- .../reference_documents/confirm_create.jinja | 39 ++++++++++ .../reference_documents/confirm_delete.jinja | 32 +++++++++ .../reference_documents/confirm_update.jinja | 39 ++++++++++ .../jinja2/reference_documents/delete.jinja | 72 +++++++++++++++++++ .../jinja2/reference_documents/index.jinja | 7 +- .../jinja2/reference_documents/update.jinja | 10 ++- 6 files changed, 195 insertions(+), 4 deletions(-) create mode 100644 reference_documents/jinja2/reference_documents/confirm_create.jinja create mode 100644 reference_documents/jinja2/reference_documents/confirm_delete.jinja create mode 100644 reference_documents/jinja2/reference_documents/confirm_update.jinja create mode 100644 reference_documents/jinja2/reference_documents/delete.jinja diff --git a/reference_documents/jinja2/reference_documents/confirm_create.jinja b/reference_documents/jinja2/reference_documents/confirm_create.jinja new file mode 100644 index 000000000..3ea829afe --- /dev/null +++ b/reference_documents/jinja2/reference_documents/confirm_create.jinja @@ -0,0 +1,39 @@ +{% extends "layouts/layout.jinja" %} + +{% from "components/panel/macro.njk" import govukPanel %} +{% from "components/button/macro.njk" import govukButton %} +{% from "components/breadcrumbs/macro.njk" import govukBreadcrumbs %} + +{% set page_title = "Reference Document " ~ object.area_id ~ " successfully created" %} + +{% block breadcrumb %} + {{ govukBreadcrumbs({ + "items": [{"text": "Home", "href": url("home")}, + {"text": "View reference documents", "href": url("reference_documents:index")}, + {"text": "Create a Reference Document", "href": url("reference_documents:create")}, + {"text": page_title}] + }) }} +{% endblock %} + +{% block content %} +
    +
    + {{ govukPanel({ + "titleText": "Reference Document " ~ object.area_id ~ " has been created", + "text": "This change has taken immediate effect", + "classes": "govuk-!-margin-bottom-7" + }) }} +
    +
    +
    + {{ govukButton({ + "text": "View Reference Document " ~ object.area_id, + "href": url("reference_documents:details", kwargs={"pk":object.pk}), + }) }} + {{ govukButton({ + "text": "Back to View reference documents", + "href": url("reference_documents:index"), + "classes": "govuk-button--secondary" + }) }} +
    +{% endblock %} diff --git a/reference_documents/jinja2/reference_documents/confirm_delete.jinja b/reference_documents/jinja2/reference_documents/confirm_delete.jinja new file mode 100644 index 000000000..29b53ec0c --- /dev/null +++ b/reference_documents/jinja2/reference_documents/confirm_delete.jinja @@ -0,0 +1,32 @@ +{% extends "layouts/layout.jinja" %} + +{% from "components/panel/macro.njk" import govukPanel %} +{% from "components/button/macro.njk" import govukButton %} +{% from "components/breadcrumbs/macro.njk" import govukBreadcrumbs %} + +{% set page_title = "Reference Document " ~ deleted_pk ~ " deleted" %} + +{% block breadcrumb %} + {{ govukBreadcrumbs({ + "items": [{"text": "Home", "href": url("home")}, + {"text": "View reference documents", "href": url("reference_documents:index")}, + {"text": page_title}] + }) }} +{% endblock %} + +{% block content %} +
    +
    + {{ govukPanel({ + "titleText": "Reference Document " ~ deleted_pk ~ " has been deleted", + "text": "This change has taken immediate effect", + "classes": "govuk-!-margin-bottom-7" + }) }} +
    +
    + {{ govukButton({ + "text": "Back to View reference documents", + "href": url("reference_documents:index"), + "classes": "govuk-button--secondary" + }) }} +{% endblock %} diff --git a/reference_documents/jinja2/reference_documents/confirm_update.jinja b/reference_documents/jinja2/reference_documents/confirm_update.jinja new file mode 100644 index 000000000..414e23298 --- /dev/null +++ b/reference_documents/jinja2/reference_documents/confirm_update.jinja @@ -0,0 +1,39 @@ +{% extends "layouts/layout.jinja" %} + +{% from "components/panel/macro.njk" import govukPanel %} +{% from "components/button/macro.njk" import govukButton %} +{% from "components/breadcrumbs/macro.njk" import govukBreadcrumbs %} + +{% set page_title = "Reference Document " ~ object.area_id ~ " successfully updated" %} + +{% block breadcrumb %} + {{ govukBreadcrumbs({ + "items": [{"text": "Home", "href": url("home")}, + {"text": "View reference documents", "href": url("reference_documents:index")}, + {"text": "Edit Reference Document " ~ object.area_id, "href": url("reference_documents:update", kwargs={"pk":object.pk})}, + {"text": page_title}] + }) }} +{% endblock %} + +{% block content %} +
    +
    + {{ govukPanel({ + "titleText": "Reference Document " ~ object.area_id ~ " has been updated", + "text": "This change has taken immediate effect", + "classes": "govuk-!-margin-bottom-7" + }) }} +
    +
    +
    + {{ govukButton({ + "text": "View your Reference Document", + "href": url("reference_documents:details", kwargs={"pk":object.pk}), + }) }} + {{ govukButton({ + "text": "Back to View Reference Documents", + "href": url("reference_documents:index"), + "classes": "govuk-button--secondary" + }) }} +
    +{% endblock %} diff --git a/reference_documents/jinja2/reference_documents/delete.jinja b/reference_documents/jinja2/reference_documents/delete.jinja new file mode 100644 index 000000000..8a6494c20 --- /dev/null +++ b/reference_documents/jinja2/reference_documents/delete.jinja @@ -0,0 +1,72 @@ +{% extends "layouts/layout.jinja" %} + +{% from "components/breadcrumbs/macro.njk" import govukBreadcrumbs %} +{% from "components/warning-text/macro.njk" import govukWarningText %} +{% from "components/button/macro.njk" import govukButton %} + +{% set page_title = "Delete Reference Document " ~ object.area_id %} + +{% block breadcrumb %} + {{ govukBreadcrumbs({ + "items": [{"text": "Home", "href": url("home")}, + {"text": "View reference documents", "href": url("reference_documents:index")}, + {"text": page_title}] + }) }} +{% endblock %} + +{% block content %} +
    +
    +

    {{ page_title}}

    +
    +
    + +
    +
    +

    Are you sure you want to permanently delete Reference Document {{ object.area_id }}?

    + + {{ govukWarningText({ + "text": "Deleted Reference Documents can not be recovered.", + "iconFallbackText": "Warning" + }) }} + +
    + + + {% set error_list = [] %} + + {% for field, errors in form.errors.items() %} + {% for error in errors.data %} + {% if error.message|length > 1 %} + {{ error_list.append({ + "text": error.message, + "href": "#" ~ (form.prefix ~ "-" if form.prefix else "") ~ field ~ ("_" ~ error.subfield if error.subfield is defined else ""), + }) or "" }} + {% endif %} + {% endfor %} + {% endfor %} + + {% if error_list|length > 0 %} + {{ govukErrorSummary({ + "titleText": "There is a problem", + "errorList": error_list + }) }} + {% endif %} + +
    + {{ govukButton({ + "text": "Delete", + "classes": "govuk-button--warning", + "name": "action", + "value": "delete" + }) }} + {{ govukButton({ + "text": "Cancel", + "href": url("reference_documents:index"), + "classes": "govuk-button--secondary" + }) }} +
    +
    +
    +
    +{% endblock %} diff --git a/reference_documents/jinja2/reference_documents/index.jinja b/reference_documents/jinja2/reference_documents/index.jinja index b281f6a3d..0b38d2389 100644 --- a/reference_documents/jinja2/reference_documents/index.jinja +++ b/reference_documents/jinja2/reference_documents/index.jinja @@ -2,6 +2,7 @@ {% from "components/table/macro.njk" import govukTable %} {% set page_title = 'Reference Documents Index' %} +{% set create_url = "create" %} {% block breadcrumb %} {{ breadcrumbs(request, [ @@ -14,7 +15,11 @@ Reference Documents You will find a list of reference documents below that can be viewed. - +

    + + Create a new Reference Document + +

    {{ govukTable({ "head": reference_document_headers, "rows": reference_documents }) }}
    diff --git a/reference_documents/jinja2/reference_documents/update.jinja b/reference_documents/jinja2/reference_documents/update.jinja index 825175661..1c50f421b 100644 --- a/reference_documents/jinja2/reference_documents/update.jinja +++ b/reference_documents/jinja2/reference_documents/update.jinja @@ -1,10 +1,14 @@ {% extends "layouts/form.jinja" %} -{% from "components/breadcrumbs.jinja" import breadcrumbs %} +{% from "components/breadcrumbs/macro.njk" import govukBreadcrumbs %} -{% set page_title = "Edit " ~ object._meta.verbose_name ~ " details" %} +{% set page_title = "Edit Reference Document " ~ object.area_id ~ " details" %} {% block breadcrumb %} - + {{ govukBreadcrumbs({ + "items": [{"text": "Home", "href": url("home")}, + {"text": "View reference documents", "href": url("reference_documents:index")}, + {"text": page_title}] + }) }} {% endblock %} {% block form %} From 7af88a74c9df44039ebf4dfec85f247b9ac0b43d Mon Sep 17 00:00:00 2001 From: Matthew McKenzie <97194636+mattjamc@users.noreply.github.com> Date: Tue, 27 Feb 2024 16:11:31 +0000 Subject: [PATCH 058/118] Reference Doc CRUD --- reference_documents/forms.py | 45 ++++++++- .../jinja2/reference_documents/delete.jinja | 3 +- reference_documents/models.py | 1 + reference_documents/urls.py | 30 +++++- .../views/reference_document_views.py | 94 ++++++++++++++----- 5 files changed, 140 insertions(+), 33 deletions(-) diff --git a/reference_documents/forms.py b/reference_documents/forms.py index 12c9f988a..30cdf68af 100644 --- a/reference_documents/forms.py +++ b/reference_documents/forms.py @@ -1,5 +1,6 @@ from crispy_forms_gds.helper import FormHelper from crispy_forms_gds.layout import Field +from crispy_forms_gds.layout import Fixed from crispy_forms_gds.layout import Layout from crispy_forms_gds.layout import Size from crispy_forms_gds.layout import Submit @@ -8,18 +9,28 @@ from reference_documents import models -class ReferenceDocumentForm(forms.ModelForm): +class ReferenceDocumentCreateUpdateForm(forms.ModelForm): title = forms.CharField( label="Reference Document title", + error_messages={ + "required": "A Reference Document title is required", + "unique": "A Reference Document with this title already exists", + }, ) area_id = forms.CharField( label="Area ID", + error_messages={ + "required": "An area ID is required", + "unique": "A Reference Document with this area ID already exists", + }, ) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - self.fields["title"].label = "Reference Document title" + self.fields[ + "title" + ].help_text = "For example, 'Reference document for XX' where XX is the Area ID" self.fields["area_id"].help_text = "Two character ID for the area referenced" self.helper = FormHelper(self) @@ -27,8 +38,14 @@ def __init__(self, *args, **kwargs): self.helper.legend_size = Size.SMALL self.helper.layout = Layout( - Field.text("title"), - Field("area_id"), + Field.text( + "title", + field_width=Fixed.TWENTY, + ), + Field.text( + "area_id", + field_width=Fixed.TEN, + ), Submit( "submit", "Save", @@ -40,3 +57,23 @@ def __init__(self, *args, **kwargs): class Meta: model = models.ReferenceDocument fields = ["title", "area_id"] + + +class ReferenceDocumentDeleteForm(forms.Form): + def __init__(self, *args, **kwargs) -> None: + self.instance = kwargs.pop("instance") + super().__init__(*args, **kwargs) + + def clean(self): + cleaned_data = super().clean() + reference_document = self.instance + versions = models.ReferenceDocumentVersion.objects.all().filter( + reference_document=reference_document, + ) + if versions: + raise forms.ValidationError( + f"Reference Document {reference_document.area_id} cannot be deleted as it has" + f" active versions.", + ) + + return cleaned_data diff --git a/reference_documents/jinja2/reference_documents/delete.jinja b/reference_documents/jinja2/reference_documents/delete.jinja index 8a6494c20..78abd9d31 100644 --- a/reference_documents/jinja2/reference_documents/delete.jinja +++ b/reference_documents/jinja2/reference_documents/delete.jinja @@ -3,6 +3,7 @@ {% from "components/breadcrumbs/macro.njk" import govukBreadcrumbs %} {% from "components/warning-text/macro.njk" import govukWarningText %} {% from "components/button/macro.njk" import govukButton %} +{% from "components/error-summary/macro.njk" import govukErrorSummary %} {% set page_title = "Delete Reference Document " ~ object.area_id %} @@ -17,7 +18,7 @@ {% block content %}
    -

    {{ page_title}}

    +

    {{ page_title }}

    diff --git a/reference_documents/models.py b/reference_documents/models.py index 305f1c2e8..3ba9970e2 100644 --- a/reference_documents/models.py +++ b/reference_documents/models.py @@ -36,6 +36,7 @@ class ReferenceDocument(models.Model): area_id = models.CharField( max_length=4, db_index=True, + unique=True, ) diff --git a/reference_documents/urls.py b/reference_documents/urls.py index eb4b00016..3ed7ecc25 100644 --- a/reference_documents/urls.py +++ b/reference_documents/urls.py @@ -29,21 +29,41 @@ reference_document_views.ReferenceDocumentList.as_view(), name="index", ), - path( - "reference_documents//", - reference_document_views.ReferenceDocumentDetails.as_view(), - name="details", - ), path( "reference_documents/create/", reference_document_views.ReferenceDocumentCreate.as_view(), name="create", ), + path( + "reference_documents//", + reference_document_views.ReferenceDocumentDetails.as_view(), + name="details", + ), path( "reference_documents//update/", reference_document_views.ReferenceDocumentUpdate.as_view(), name="update", ), + path( + f"/confirm-create/", + reference_document_views.ReferenceDocumentConfirmCreate.as_view(), + name="confirm-create", + ), + path( + f"/confirm-update/", + reference_document_views.ReferenceDocumentConfirmUpdate.as_view(), + name="confirm-update", + ), + path( + f"/delete/", + reference_document_views.ReferenceDocumentDelete.as_view(), + name="delete", + ), + path( + f"/confirm-delete/", + reference_document_views.ReferenceDocumentConfirmDelete.as_view(), + name="confirm-delete", + ), # reference document version views path( "reference_document_versions//", diff --git a/reference_documents/views/reference_document_views.py b/reference_documents/views/reference_document_views.py index 97f91b417..1bdc1dd79 100644 --- a/reference_documents/views/reference_document_views.py +++ b/reference_documents/views/reference_document_views.py @@ -1,11 +1,13 @@ from django.contrib.auth.mixins import PermissionRequiredMixin -from django.db import transaction -from django.http import HttpResponseRedirect +from django.shortcuts import redirect from django.urls import reverse from django.views.generic import CreateView +from django.views.generic import DeleteView from django.views.generic import DetailView from django.views.generic import ListView +from django.views.generic import TemplateView from django.views.generic import UpdateView +from django.views.generic.edit import FormMixin from geo_areas.models import GeographicalAreaDescription from reference_documents import forms @@ -47,7 +49,9 @@ def get_context_data(self, **kwargs): {"text": 0}, {"text": 0}, { - "html": f'Details', + "html": f'Details
    ' + f"Edit
    " + f"Delete", }, ], ) @@ -67,7 +71,8 @@ def get_context_data(self, **kwargs): }, { "html": f'Details
    ' - f"Edit", + f"Edit
    " + f"Delete", }, ], ) @@ -135,33 +140,76 @@ def get_context_data(self, *args, **kwargs): return context -class ReferenceDocumentCreate(CreateView): - model = models.ReferenceDocument +class ReferenceDocumentCreate(PermissionRequiredMixin, CreateView): template_name = "reference_documents/create.jinja" - form_class = forms.ReferenceDocumentForm - - @transaction.atomic - def form_valid(self, form): - self.object.save() - return HttpResponseRedirect(self.get_success_url()) + permission_required = "reference_documents.edit_reference_document" + form_class = forms.ReferenceDocumentCreateUpdateForm - def get_form_kwargs(self): - kwargs = super().get_form_kwargs() - # kwargs["request"] = self.request - return kwargs + def get_success_url(self): + return reverse( + "reference_documents:confirm-create", + kwargs={"pk": self.object.pk}, + ) -class ReferenceDocumentUpdate(UpdateView): +class ReferenceDocumentUpdate(PermissionRequiredMixin, UpdateView): model = models.ReferenceDocument + permission_required = "reference_documents.edit_reference_document" template_name = "reference_documents/update.jinja" - form_class = forms.ReferenceDocumentForm + form_class = forms.ReferenceDocumentCreateUpdateForm - @transaction.atomic - def form_valid(self, form): - self.object.save() - return HttpResponseRedirect(reverse("reference_documents:index")) + def get_success_url(self): + return reverse( + "reference_documents:confirm-update", + kwargs={"pk": self.object.pk}, + ) + + +class ReferenceDocumentDelete(PermissionRequiredMixin, FormMixin, DeleteView): + form_class = forms.ReferenceDocumentDeleteForm + model = ReferenceDocument + permission_required = "reference_documents.edit_reference_document" + template_name = "reference_documents/delete.jinja" + + # TODO: Update this to get rid of FormMixin with Django 4.2 as no need to overwrite the post anymore + def get_success_url(self) -> str: + return reverse( + "reference_documents:confirm-delete", + kwargs={"deleted_pk": self.kwargs["pk"]}, + ) def get_form_kwargs(self): kwargs = super().get_form_kwargs() - # kwargs["request"] = self.request + kwargs["instance"] = self.get_object() return kwargs + + def post(self, request, *args, **kwargs): + self.object = self.get_object() + form = self.get_form() + if form.is_valid(): + return self.form_valid(form) + else: + return self.form_invalid(form) + + def form_valid(self, form): + self.object.delete() + return redirect(self.get_success_url()) + + +class ReferenceDocumentConfirmCreate(DetailView): + template_name = "reference_documents/confirm_create.jinja" + model = ReferenceDocument + + +class ReferenceDocumentConfirmUpdate(DetailView): + template_name = "reference_documents/confirm_update.jinja" + model = ReferenceDocument + + +class ReferenceDocumentConfirmDelete(TemplateView): + template_name = "reference_documents/confirm_delete.jinja" + + def get_context_data(self, **kwargs): + context_data = super().get_context_data(**kwargs) + context_data["deleted_pk"] = self.kwargs["deleted_pk"] + return context_data From 35655e9d05b83e62e0b54202d2a0d1cde4bc61ed Mon Sep 17 00:00:00 2001 From: Doug Mills Date: Wed, 28 Feb 2024 09:22:47 +0000 Subject: [PATCH 059/118] Add factories, add editing for rates - still WIP but working --- common/forms.py | 1 - common/jinja2/common/edit.jinja | 2 +- reference_documents/apps.py | 4 +- reference_documents/forms.py | 160 ++++++++++++++ .../includes/tabs/preferential_quotas.jinja | 2 + .../includes/tabs/preferential_rates.jinja | 1 + .../reference_document_versions/details.jinja | 27 --- .../reference_document_versions/edit.jinja | 14 -- .../alignment_reports/details.jinja | 0 .../preferential_quotas/edit.jinja | 15 ++ .../preferential_rates/create.jinja | 9 + .../preferential_rates/delete.jinja | 11 + .../preferential_rates/edit.jinja | 9 + .../reference_document_examples/details.jinja | 0 .../reference_document_examples/index.jinja | 0 .../alignment_reports.jinja | 1 - .../details.jinja} | 0 .../reference_document_versions}/edit.jinja | 0 .../tests/alignment_reports/test_forms.py | 3 + .../tests/alignment_reports/test_models.py | 14 ++ .../tests/alignment_reports/test_views.py | 3 + reference_documents/tests/factories.py | 201 ++++++++++++++++++ .../tests/preferential_quotas/test_forms.py | 3 + .../tests/preferential_quotas/test_models.py | 3 + .../tests/preferential_quotas/test_views.py | 3 + .../tests/preferential_rates/test_forms.py | 3 + .../tests/preferential_rates/test_models.py | 3 + .../tests/preferential_rates/test_views.py | 3 + .../reference_document_versions/test_forms.py | 3 + .../test_models.py | 3 + .../reference_document_versions/test_views.py | 3 + .../tests/reference_documents/test_forms.py | 3 + .../tests/reference_documents/test_models.py | 3 + .../tests/reference_documents/test_views.py | 3 + reference_documents/urls.py | 18 ++ reference_documents/validators.py | 3 + .../views/preferential_rates.py | 80 +++++++ .../views/reference_document_version_views.py | 7 +- 38 files changed, 572 insertions(+), 49 deletions(-) create mode 100644 reference_documents/forms.py delete mode 100644 reference_documents/jinja2/reference_document_versions/details.jinja delete mode 100644 reference_documents/jinja2/reference_document_versions/edit.jinja rename reference_documents/jinja2/{ => reference_documents}/alignment_reports/details.jinja (100%) create mode 100644 reference_documents/jinja2/reference_documents/preferential_quotas/edit.jinja create mode 100644 reference_documents/jinja2/reference_documents/preferential_rates/create.jinja create mode 100644 reference_documents/jinja2/reference_documents/preferential_rates/delete.jinja create mode 100644 reference_documents/jinja2/reference_documents/preferential_rates/edit.jinja rename reference_documents/jinja2/{ => reference_documents}/reference_document_examples/details.jinja (100%) rename reference_documents/jinja2/{ => reference_documents}/reference_document_examples/index.jinja (100%) rename reference_documents/jinja2/{ => reference_documents}/reference_document_versions/alignment_reports.jinja (99%) rename reference_documents/jinja2/{reference_document_versions/new_details.jinja => reference_documents/reference_document_versions/details.jinja} (100%) rename reference_documents/jinja2/{preferential_quotas => reference_documents/reference_document_versions}/edit.jinja (100%) create mode 100644 reference_documents/tests/alignment_reports/test_forms.py create mode 100644 reference_documents/tests/alignment_reports/test_models.py create mode 100644 reference_documents/tests/alignment_reports/test_views.py create mode 100644 reference_documents/tests/factories.py create mode 100644 reference_documents/tests/preferential_quotas/test_forms.py create mode 100644 reference_documents/tests/preferential_quotas/test_models.py create mode 100644 reference_documents/tests/preferential_quotas/test_views.py create mode 100644 reference_documents/tests/preferential_rates/test_forms.py create mode 100644 reference_documents/tests/preferential_rates/test_models.py create mode 100644 reference_documents/tests/preferential_rates/test_views.py create mode 100644 reference_documents/tests/reference_document_versions/test_forms.py create mode 100644 reference_documents/tests/reference_document_versions/test_models.py create mode 100644 reference_documents/tests/reference_document_versions/test_views.py create mode 100644 reference_documents/tests/reference_documents/test_forms.py create mode 100644 reference_documents/tests/reference_documents/test_models.py create mode 100644 reference_documents/tests/reference_documents/test_views.py create mode 100644 reference_documents/validators.py create mode 100644 reference_documents/views/preferential_rates.py diff --git a/common/forms.py b/common/forms.py index 76428cdf9..fd3d7d6b8 100644 --- a/common/forms.py +++ b/common/forms.py @@ -427,7 +427,6 @@ class ValidityPeriodForm(forms.ModelForm): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - self.fields["end_date"].help_text = ( f"Leave empty if {get_model_indefinite_article(self.instance)} " f"{self.instance._meta.verbose_name} is needed for an unlimited time" diff --git a/common/jinja2/common/edit.jinja b/common/jinja2/common/edit.jinja index 32d405917..932f7c298 100644 --- a/common/jinja2/common/edit.jinja +++ b/common/jinja2/common/edit.jinja @@ -13,7 +13,7 @@ {% endblock %} {% block form %} - {% call django_form(action=object.get_url("edit")) %} + {% call django_form() %} {{ crispy(form) }} {% endcall %} {% endblock %} diff --git a/reference_documents/apps.py b/reference_documents/apps.py index 1d11db6f1..19f1d5436 100644 --- a/reference_documents/apps.py +++ b/reference_documents/apps.py @@ -1,5 +1,5 @@ -from django.apps import AppConfig +from common.app_config import CommonConfig -class ReferenceDocumentsConfig(AppConfig): +class ReferenceDocumentsConfig(CommonConfig): name = "reference_documents" diff --git a/reference_documents/forms.py b/reference_documents/forms.py new file mode 100644 index 000000000..8ed3d4a81 --- /dev/null +++ b/reference_documents/forms.py @@ -0,0 +1,160 @@ +from crispy_forms.helper import FormHelper +from crispy_forms_gds.layout import Layout +from crispy_forms_gds.layout import Size +from crispy_forms_gds.layout import Submit +from django import forms + +from common.forms import ValidityPeriodForm +from reference_documents.models import PreferentialRate +from reference_documents.validators import commodity_code_validator + + +class PreferentialRateEditForm( + ValidityPeriodForm, + forms.ModelForm, +): + class Meta: + model = PreferentialRate + fields = [ + "commodity_code", + "duty_rate", + "valid_between", + ] + + commodity_code = forms.CharField( + help_text="Commodity Code", + validators=[commodity_code_validator], + error_messages={ + "invalid": "Commodity code should be 10 digits", + "required": "Enter the commodity code", + }, + ) + + duty_rate = forms.CharField( + help_text="Duty Rate", + validators=[], + error_messages={ + "invalid": "Duty rate is invalid", + "required": "This is required", + }, + ) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.init_layout() + self.init_fields() + + def init_layout(self): + self.helper = FormHelper(self) + self.helper.label_size = Size.SMALL + self.helper.legend_size = Size.SMALL + self.helper.layout = Layout( + "commodity_code", + "duty_rate", + "start_date", + "end_date", + Submit( + "submit", + "Save", + data_module="govuk-button", + data_prevent_double_click="true", + ), + ) + + def init_fields(self): + pass + + def clean(self): + return super().clean() + + +class PreferentialRateCreateForm( + ValidityPeriodForm, + forms.ModelForm, +): + class Meta: + model = PreferentialRate + fields = [ + "commodity_code", + "duty_rate", + "valid_between", + "reference_document_version", + ] + + commodity_code = forms.CharField( + help_text="Commodity Code", + validators=[commodity_code_validator], + error_messages={ + "invalid": "Commodity code should be 10 digits", + "required": "Enter the commodity code", + }, + ) + + duty_rate = forms.CharField( + help_text="Duty Rate", + validators=[], + error_messages={ + "invalid": "Duty rate is invalid", + "required": "This is required", + }, + ) + + reference_document_version = forms.CharField(widget=forms.HiddenInput()) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.init_layout() + self.init_fields() + + def init_layout(self): + self.helper = FormHelper(self) + self.helper.label_size = Size.SMALL + self.helper.legend_size = Size.SMALL + self.helper.layout = Layout( + "commodity_code", + "duty_rate", + "start_date", + "end_date", + Submit( + "submit", + "Create", + data_module="govuk-button", + data_prevent_double_click="true", + ), + ) + + def init_fields(self): + pass + + def clean(self): + return super().clean() + + +class PreferentialRateDeleteForm(forms.ModelForm): + class Meta: + model = PreferentialRate + fields = [] + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.init_layout() + self.init_fields() + + def init_layout(self): + self.helper = FormHelper(self) + self.helper.label_size = Size.SMALL + self.helper.legend_size = Size.SMALL + self.helper.layout = Layout( + Submit( + "submit", + "Confirm Delete", + data_module="govuk-button", + data_prevent_double_click="true", + ), + ) + + def init_fields(self): + pass + + def clean(self): + return super().clean() diff --git a/reference_documents/jinja2/includes/tabs/preferential_quotas.jinja b/reference_documents/jinja2/includes/tabs/preferential_quotas.jinja index 7b97ce0e6..b22e625f1 100644 --- a/reference_documents/jinja2/includes/tabs/preferential_quotas.jinja +++ b/reference_documents/jinja2/includes/tabs/preferential_quotas.jinja @@ -33,7 +33,9 @@

    Actions

    +
    diff --git a/reference_documents/jinja2/includes/tabs/preferential_rates.jinja b/reference_documents/jinja2/includes/tabs/preferential_rates.jinja index abd737378..99b568454 100644 --- a/reference_documents/jinja2/includes/tabs/preferential_rates.jinja +++ b/reference_documents/jinja2/includes/tabs/preferential_rates.jinja @@ -14,6 +14,7 @@

    Actions

    diff --git a/reference_documents/jinja2/reference_document_versions/details.jinja b/reference_documents/jinja2/reference_document_versions/details.jinja deleted file mode 100644 index 7f32bbb29..000000000 --- a/reference_documents/jinja2/reference_document_versions/details.jinja +++ /dev/null @@ -1,27 +0,0 @@ -{% extends "layouts/layout.jinja" %} -{% from "components/table/macro.njk" import govukTable %} - -{% set page_title = 'Reference Documents version details' %} - -{% block breadcrumb %} - {{ breadcrumbs(request, [ - {'text': "Reference Document Version"} - ]) }} -{% endblock %} - -{% block content %} -

    - Reference Document version Overview -

    - Reference document version details. - -
    - {{ govukTable({ "head": reference_document_version_duties_headers, "rows": reference_document_version_duties }) }} -
    -
    - {{ govukTable({ "head": reference_document_version_quotas_headers, "rows": reference_document_version_quotas }) }} -
    -{% endblock %} - - - diff --git a/reference_documents/jinja2/reference_document_versions/edit.jinja b/reference_documents/jinja2/reference_document_versions/edit.jinja deleted file mode 100644 index f9bc245a2..000000000 --- a/reference_documents/jinja2/reference_document_versions/edit.jinja +++ /dev/null @@ -1,14 +0,0 @@ -{% extends "layouts/layout.jinja" %} - -{% set page_title = "Preferential duty rates" %} - -{% block content %} - -
    - - {{ form.as_p() }} - -
    - -{% endblock %} - diff --git a/reference_documents/jinja2/alignment_reports/details.jinja b/reference_documents/jinja2/reference_documents/alignment_reports/details.jinja similarity index 100% rename from reference_documents/jinja2/alignment_reports/details.jinja rename to reference_documents/jinja2/reference_documents/alignment_reports/details.jinja diff --git a/reference_documents/jinja2/reference_documents/preferential_quotas/edit.jinja b/reference_documents/jinja2/reference_documents/preferential_quotas/edit.jinja new file mode 100644 index 000000000..2950aad1f --- /dev/null +++ b/reference_documents/jinja2/reference_documents/preferential_quotas/edit.jinja @@ -0,0 +1,15 @@ +{% extends "layouts/layout.jinja" %} + +{% set page_title = "Edit preferential quota" %} + +{% block content %} + +
    + + {{ form.as_p() }} + + +
    + +{% endblock %} + diff --git a/reference_documents/jinja2/reference_documents/preferential_rates/create.jinja b/reference_documents/jinja2/reference_documents/preferential_rates/create.jinja new file mode 100644 index 000000000..ee17e1435 --- /dev/null +++ b/reference_documents/jinja2/reference_documents/preferential_rates/create.jinja @@ -0,0 +1,9 @@ +{% extends 'layouts/form.jinja' %} + +{% set page_title = "Create preferential duty rate" %} + +{% block form %} + {% call django_form() %} + {{ crispy(form) }} + {% endcall %} +{% endblock %} \ No newline at end of file diff --git a/reference_documents/jinja2/reference_documents/preferential_rates/delete.jinja b/reference_documents/jinja2/reference_documents/preferential_rates/delete.jinja new file mode 100644 index 000000000..c98fe3e01 --- /dev/null +++ b/reference_documents/jinja2/reference_documents/preferential_rates/delete.jinja @@ -0,0 +1,11 @@ +{% extends "layouts/layout.jinja" %} + +{% set page_title = "Delete preferential duty rate" %} + +{% block content %} +

    Are you sure that you want to delete this Preferential rate

    +
    + + +
    +{% endblock %} \ No newline at end of file diff --git a/reference_documents/jinja2/reference_documents/preferential_rates/edit.jinja b/reference_documents/jinja2/reference_documents/preferential_rates/edit.jinja new file mode 100644 index 000000000..400da07a9 --- /dev/null +++ b/reference_documents/jinja2/reference_documents/preferential_rates/edit.jinja @@ -0,0 +1,9 @@ +{% extends 'layouts/form.jinja' %} + +{% set page_title = "Edit preferential duty rate" %} + +{% block form %} + {% call django_form() %} + {{ crispy(form) }} + {% endcall %} +{% endblock %} \ No newline at end of file diff --git a/reference_documents/jinja2/reference_document_examples/details.jinja b/reference_documents/jinja2/reference_documents/reference_document_examples/details.jinja similarity index 100% rename from reference_documents/jinja2/reference_document_examples/details.jinja rename to reference_documents/jinja2/reference_documents/reference_document_examples/details.jinja diff --git a/reference_documents/jinja2/reference_document_examples/index.jinja b/reference_documents/jinja2/reference_documents/reference_document_examples/index.jinja similarity index 100% rename from reference_documents/jinja2/reference_document_examples/index.jinja rename to reference_documents/jinja2/reference_documents/reference_document_examples/index.jinja diff --git a/reference_documents/jinja2/reference_document_versions/alignment_reports.jinja b/reference_documents/jinja2/reference_documents/reference_document_versions/alignment_reports.jinja similarity index 99% rename from reference_documents/jinja2/reference_document_versions/alignment_reports.jinja rename to reference_documents/jinja2/reference_documents/reference_document_versions/alignment_reports.jinja index 59de1a733..948ea8916 100644 --- a/reference_documents/jinja2/reference_document_versions/alignment_reports.jinja +++ b/reference_documents/jinja2/reference_documents/reference_document_versions/alignment_reports.jinja @@ -14,7 +14,6 @@ Alignment reports Reference document version details. -
    {{ govukTable({ "head": alignment_report_headers, "rows": alignment_reports }) }}
    diff --git a/reference_documents/jinja2/reference_document_versions/new_details.jinja b/reference_documents/jinja2/reference_documents/reference_document_versions/details.jinja similarity index 100% rename from reference_documents/jinja2/reference_document_versions/new_details.jinja rename to reference_documents/jinja2/reference_documents/reference_document_versions/details.jinja diff --git a/reference_documents/jinja2/preferential_quotas/edit.jinja b/reference_documents/jinja2/reference_documents/reference_document_versions/edit.jinja similarity index 100% rename from reference_documents/jinja2/preferential_quotas/edit.jinja rename to reference_documents/jinja2/reference_documents/reference_document_versions/edit.jinja diff --git a/reference_documents/tests/alignment_reports/test_forms.py b/reference_documents/tests/alignment_reports/test_forms.py new file mode 100644 index 000000000..a7057c2cc --- /dev/null +++ b/reference_documents/tests/alignment_reports/test_forms.py @@ -0,0 +1,3 @@ +import pytest + +pytestmark = pytest.mark.django_db diff --git a/reference_documents/tests/alignment_reports/test_models.py b/reference_documents/tests/alignment_reports/test_models.py new file mode 100644 index 000000000..30f969408 --- /dev/null +++ b/reference_documents/tests/alignment_reports/test_models.py @@ -0,0 +1,14 @@ +import pytest + +from reference_documents.models import AlignmentReport + +pytestmark = pytest.mark.django_db + + +class TestAlignmentReport: + def test_create_with_defaults(self): + subject = AlignmentReport.objects.create() + + subject.save() + assert subject.reference_document_version.count() == 0 + assert subject.created_at is not None diff --git a/reference_documents/tests/alignment_reports/test_views.py b/reference_documents/tests/alignment_reports/test_views.py new file mode 100644 index 000000000..a7057c2cc --- /dev/null +++ b/reference_documents/tests/alignment_reports/test_views.py @@ -0,0 +1,3 @@ +import pytest + +pytestmark = pytest.mark.django_db diff --git a/reference_documents/tests/factories.py b/reference_documents/tests/factories.py new file mode 100644 index 000000000..6f9f6bee6 --- /dev/null +++ b/reference_documents/tests/factories.py @@ -0,0 +1,201 @@ +import string +from datetime import UTC +from datetime import datetime +from datetime import timedelta + +import factory +from factory.fuzzy import FuzzyDateTime +from factory.fuzzy import FuzzyDecimal +from factory.fuzzy import FuzzyInteger +from factory.fuzzy import FuzzyText + +from common.util import TaricDateRange +from reference_documents.models import AlignmentReportCheckStatus +from reference_documents.models import ReferenceDocumentVersionStatus + + +class ReferenceDocumentFactory(factory.django.DjangoModelFactory): + class Meta: + model = "reference_documents.ReferenceDocument" + + area_id = FuzzyText("", 2, "", string.ascii_uppercase) + created_at = FuzzyDateTime(datetime(2008, 1, 1, tzinfo=UTC), datetime.now()) + title = FuzzyText("Reference Document for ", 5, "", string.ascii_uppercase) + + +class ReferenceDocumentVersionFactory(factory.django.DjangoModelFactory): + class Meta: + model = "reference_documents.ReferenceDocumentVersion" + + created_at = FuzzyDateTime(datetime(2020, 1, 1, tzinfo=UTC), datetime.now()) + updated_at = FuzzyDateTime(datetime(2020, 1, 1, tzinfo=UTC), datetime.now()) + version = FuzzyDecimal(1.0, 5.0, 1) + published_date = FuzzyDateTime(datetime(2022, 1, 1, tzinfo=UTC), datetime.now()) + entry_into_force_date = FuzzyDateTime( + datetime(2022, 1, 1, tzinfo=UTC), + datetime.now(), + ) + + reference_document = factory.SubFactory(ReferenceDocumentFactory) + + status = ReferenceDocumentVersionStatus.EDITING + + class Params: + in_review = factory.Trait( + status=ReferenceDocumentVersionStatus.IN_REVIEW, + ) + published = factory.Trait( + status=ReferenceDocumentVersionStatus.PUBLISHED, + ) + editing = factory.Trait( + status=ReferenceDocumentVersionStatus.EDITING, + ) + + +class PreferentialRateFactory(factory.django.DjangoModelFactory): + class Meta: + model = "reference_documents.PreferentialRate" + + commodity_code = FuzzyText(length=6, chars=string.digits, suffix="0000") + + duty_rate = FuzzyText(length=2, chars=string.digits, suffix="%") + + order = FuzzyInteger(0, 100, 1) + + reference_document_version = factory.SubFactory(ReferenceDocumentVersionFactory) + + valid_between = TaricDateRange( + FuzzyDateTime( + datetime.now() + timedelta(days=-(365 * 2)), + datetime.now() + timedelta(days=-365), + ), + FuzzyDateTime(datetime.now() + timedelta(days=-364), datetime.now()), + ) + + class Params: + valid_between_current = factory.Trait( + valid_between=TaricDateRange( + lower=datetime.now() + timedelta(days=-200), + upper=datetime.now() + timedelta(days=165), + ), + ) + valid_between_current_open_ended = factory.Trait( + valid_between=TaricDateRange( + lower=datetime.now() + timedelta(days=-200), + upper=None, + ), + ) + valid_between_in_past = factory.Trait( + valid_between=TaricDateRange( + lower=datetime.now() + timedelta(days=-375), + upper=datetime.now() + timedelta(days=-10), + ), + ) + valid_between_in_future = factory.Trait( + valid_between=TaricDateRange( + lower=datetime.now() + timedelta(days=10), + upper=datetime.now() + timedelta(days=375), + ), + ) + + +class PreferentialQuotaFactory(factory.django.DjangoModelFactory): + class Meta: + model = "reference_documents.PreferentialQuota" + + commodity_code = FuzzyText(length=6, chars=string.digits, suffix="0000") + + quota_duty_rate = FuzzyText(length=2, chars=string.digits, suffix="%") + + quota_order_number = FuzzyText(prefix="054", length=3, chars=string.digits) + + volume = FuzzyDecimal(100.0, 10000.0, 1) + + coefficient = None + + main_quota = None + + measurement = "tonnes" + order = FuzzyInteger(0, 100, 1) + reference_document_version = factory.SubFactory(ReferenceDocumentVersionFactory) + + valid_between = TaricDateRange( + FuzzyDateTime( + datetime.now() + timedelta(days=-(365 * 2)), + datetime.now() + timedelta(days=-365), + ), + FuzzyDateTime(datetime.now() + timedelta(days=-364), datetime.now()), + ) + + class Params: + valid_between_current = factory.Trait( + valid_between=TaricDateRange( + lower=datetime.now() + timedelta(days=-200), + upper=datetime.now() + timedelta(days=165), + ), + ) + valid_between_current_open_ended = factory.Trait( + valid_between=TaricDateRange( + lower=datetime.now() + timedelta(days=-200), + upper=None, + ), + ) + valid_between_in_past = factory.Trait( + valid_between=TaricDateRange( + lower=datetime.now() + timedelta(days=-375), + upper=datetime.now() + timedelta(days=-10), + ), + ) + valid_between_in_future = factory.Trait( + valid_between=TaricDateRange( + lower=datetime.now() + timedelta(days=10), + upper=datetime.now() + timedelta(days=375), + ), + ) + + +class AlignmentReportFactory(factory.django.DjangoModelFactory): + class Meta: + model = "reference_documents.AlignmentReport" + + created_at = FuzzyDateTime(datetime(2020, 1, 1, tzinfo=UTC), datetime.now()) + reference_document_version = factory.SubFactory(ReferenceDocumentVersionFactory) + + +class AlignmentReportCheckFactory(factory.django.DjangoModelFactory): + class Meta: + model = "reference_documents.AlignmentReportCheck" + + created_at = FuzzyDateTime(datetime(2020, 1, 1, tzinfo=UTC), datetime.now()) + alignment_report = factory.SubFactory(AlignmentReportFactory) + + check_name = FuzzyText( + prefix="SomeClassName ", + length=5, + chars=string.ascii_uppercase, + ) + + status = (AlignmentReportCheckStatus.FAIL,) + + message = FuzzyText( + prefix="Some Random Message ", + length=5, + chars=string.ascii_uppercase, + ) + + preferential_quota = None + preferential_rate = None + + class Params: + with_quota = factory.Trait( + preferential_quota=factory.SubFactory(PreferentialQuotaFactory), + ) + with_rate = factory.Trait( + preferential_rate=factory.SubFactory(PreferentialRateFactory), + ) + passing = factory.Trait( + status=AlignmentReportCheckStatus.PASS, + ) + warning = factory.Trait( + status=AlignmentReportCheckStatus.WARNING, + ) diff --git a/reference_documents/tests/preferential_quotas/test_forms.py b/reference_documents/tests/preferential_quotas/test_forms.py new file mode 100644 index 000000000..a7057c2cc --- /dev/null +++ b/reference_documents/tests/preferential_quotas/test_forms.py @@ -0,0 +1,3 @@ +import pytest + +pytestmark = pytest.mark.django_db diff --git a/reference_documents/tests/preferential_quotas/test_models.py b/reference_documents/tests/preferential_quotas/test_models.py new file mode 100644 index 000000000..a7057c2cc --- /dev/null +++ b/reference_documents/tests/preferential_quotas/test_models.py @@ -0,0 +1,3 @@ +import pytest + +pytestmark = pytest.mark.django_db diff --git a/reference_documents/tests/preferential_quotas/test_views.py b/reference_documents/tests/preferential_quotas/test_views.py new file mode 100644 index 000000000..a7057c2cc --- /dev/null +++ b/reference_documents/tests/preferential_quotas/test_views.py @@ -0,0 +1,3 @@ +import pytest + +pytestmark = pytest.mark.django_db diff --git a/reference_documents/tests/preferential_rates/test_forms.py b/reference_documents/tests/preferential_rates/test_forms.py new file mode 100644 index 000000000..a7057c2cc --- /dev/null +++ b/reference_documents/tests/preferential_rates/test_forms.py @@ -0,0 +1,3 @@ +import pytest + +pytestmark = pytest.mark.django_db diff --git a/reference_documents/tests/preferential_rates/test_models.py b/reference_documents/tests/preferential_rates/test_models.py new file mode 100644 index 000000000..a7057c2cc --- /dev/null +++ b/reference_documents/tests/preferential_rates/test_models.py @@ -0,0 +1,3 @@ +import pytest + +pytestmark = pytest.mark.django_db diff --git a/reference_documents/tests/preferential_rates/test_views.py b/reference_documents/tests/preferential_rates/test_views.py new file mode 100644 index 000000000..a7057c2cc --- /dev/null +++ b/reference_documents/tests/preferential_rates/test_views.py @@ -0,0 +1,3 @@ +import pytest + +pytestmark = pytest.mark.django_db diff --git a/reference_documents/tests/reference_document_versions/test_forms.py b/reference_documents/tests/reference_document_versions/test_forms.py new file mode 100644 index 000000000..a7057c2cc --- /dev/null +++ b/reference_documents/tests/reference_document_versions/test_forms.py @@ -0,0 +1,3 @@ +import pytest + +pytestmark = pytest.mark.django_db diff --git a/reference_documents/tests/reference_document_versions/test_models.py b/reference_documents/tests/reference_document_versions/test_models.py new file mode 100644 index 000000000..a7057c2cc --- /dev/null +++ b/reference_documents/tests/reference_document_versions/test_models.py @@ -0,0 +1,3 @@ +import pytest + +pytestmark = pytest.mark.django_db diff --git a/reference_documents/tests/reference_document_versions/test_views.py b/reference_documents/tests/reference_document_versions/test_views.py new file mode 100644 index 000000000..a7057c2cc --- /dev/null +++ b/reference_documents/tests/reference_document_versions/test_views.py @@ -0,0 +1,3 @@ +import pytest + +pytestmark = pytest.mark.django_db diff --git a/reference_documents/tests/reference_documents/test_forms.py b/reference_documents/tests/reference_documents/test_forms.py new file mode 100644 index 000000000..a7057c2cc --- /dev/null +++ b/reference_documents/tests/reference_documents/test_forms.py @@ -0,0 +1,3 @@ +import pytest + +pytestmark = pytest.mark.django_db diff --git a/reference_documents/tests/reference_documents/test_models.py b/reference_documents/tests/reference_documents/test_models.py new file mode 100644 index 000000000..a7057c2cc --- /dev/null +++ b/reference_documents/tests/reference_documents/test_models.py @@ -0,0 +1,3 @@ +import pytest + +pytestmark = pytest.mark.django_db diff --git a/reference_documents/tests/reference_documents/test_views.py b/reference_documents/tests/reference_documents/test_views.py new file mode 100644 index 000000000..a7057c2cc --- /dev/null +++ b/reference_documents/tests/reference_documents/test_views.py @@ -0,0 +1,3 @@ +import pytest + +pytestmark = pytest.mark.django_db diff --git a/reference_documents/urls.py b/reference_documents/urls.py index 2f0448f90..2a32d5a84 100644 --- a/reference_documents/urls.py +++ b/reference_documents/urls.py @@ -4,6 +4,7 @@ from reference_documents.views import alignment_report_views from reference_documents.views import example_views from reference_documents.views import preferential_quotas +from reference_documents.views import preferential_rates from reference_documents.views import reference_document_version_views from reference_documents.views import reference_document_views @@ -11,6 +12,7 @@ api_router = routers.DefaultRouter() +detail = "" urlpatterns = [ # Example views path( @@ -67,4 +69,20 @@ preferential_quotas.PreferentialQuotaEditView.as_view(), name="preferential_quotas_edit", ), + # Preferential Rates + path( + "preferential_rates/delete//", + preferential_rates.PreferentialRateDeleteView.as_view(), + name="preferential_rates_delete", + ), + path( + "preferential_rates/edit//", + preferential_rates.PreferentialRateEditView.as_view(), + name="preferential_rates_edit", + ), + path( + "reference_document_versions//create_preferential_rates/", + preferential_rates.PreferentialRateCreateView.as_view(), + name="preferential_rates_create", + ), ] diff --git a/reference_documents/validators.py b/reference_documents/validators.py new file mode 100644 index 000000000..4a35a6a18 --- /dev/null +++ b/reference_documents/validators.py @@ -0,0 +1,3 @@ +from django.core.validators import RegexValidator + +commodity_code_validator = RegexValidator(r"\d{10}") diff --git a/reference_documents/views/preferential_rates.py b/reference_documents/views/preferential_rates.py new file mode 100644 index 000000000..f24e0ff5c --- /dev/null +++ b/reference_documents/views/preferential_rates.py @@ -0,0 +1,80 @@ +from django.contrib.auth.mixins import PermissionRequiredMixin +from django.http import HttpResponseRedirect +from django.urls import reverse +from django.views.generic import CreateView +from django.views.generic import DeleteView +from django.views.generic import UpdateView + +from reference_documents.forms import PreferentialRateEditForm +from reference_documents.models import PreferentialRate +from reference_documents.models import ReferenceDocumentVersion + + +class PreferentialRateEditView(PermissionRequiredMixin, UpdateView): + form_class = PreferentialRateEditForm + permission_required = "reference_documents.edit_reference_document" + model = PreferentialRate + template_name = "reference_documents/preferential_rates/edit.jinja" + + def get_success_url(self): + return reverse( + "reference_documents:version_details", + args=[self.object.reference_document_version.pk], + ) + + def form_valid(self, form): + form.save() + return super(PreferentialRateEditView, self).form_valid(form) + + +class PreferentialRateCreateView(PermissionRequiredMixin, CreateView): + form_class = PreferentialRateEditForm + permission_required = "reference_documents.edit_reference_document" + model = PreferentialRate + template_name = "reference_documents/preferential_rates/create.jinja" + + def get_success_url(self): + return reverse( + "reference_documents:version_details", + args=[self.object.reference_document_version.pk], + ) + + def form_valid(self, form): + instance = form.instance + reference_document_version = ReferenceDocumentVersion.objects.get( + pk=self.kwargs["pk"], + ) + instance.order = len(reference_document_version.preferential_rates.all()) + 1 + instance.reference_document_version = reference_document_version + form.save() + return super(PreferentialRateCreateView, self).form_valid(form) + + +class PreferentialRateDeleteView(PermissionRequiredMixin, DeleteView): + template_name = "reference_documents/preferential_rates/delete.jinja" + permission_required = "reference_documents.edit_reference_document" + model = PreferentialRate + + def get_success_url(self): + return reverse( + "reference_documents:version_details", + args=[self.object.reference_document_version.pk], + ) + + def form_valid(self, form): + instance = form.instance + success_url = reverse( + "reference_documents:version_details", + args=[instance.reference_document_version.pk], + ) + instance.delete() + return HttpResponseRedirect(success_url) + + def post(self, request, *args, **kwargs): + object = PreferentialRate.objects.all().get(pk=kwargs["pk"]) + success_url = reverse( + "reference_documents:version_details", + args=[object.reference_document_version.pk], + ) + object.delete() + return HttpResponseRedirect(success_url) diff --git a/reference_documents/views/reference_document_version_views.py b/reference_documents/views/reference_document_version_views.py index f24788ca3..e0f9c8aea 100644 --- a/reference_documents/views/reference_document_version_views.py +++ b/reference_documents/views/reference_document_version_views.py @@ -12,7 +12,7 @@ class ReferenceDocumentVersionDetails(PermissionRequiredMixin, DetailView): - template_name = "reference_document_versions/new_details.jinja" + template_name = "reference_documents/reference_document_versions/details.jinja" permission_required = "reference_documents.view_reference_document" model = ReferenceDocumentVersion @@ -97,7 +97,7 @@ def get_context_data(self, *args, **kwargs): latest_alignment_report = context["object"].alignment_reports.last() - for duty in context["object"].preferential_rates.order_by("order"): + for duty in context["object"].preferential_rates.order_by("commodity_code"): failure_count = ( duty.preferential_rate_checks.all() .filter( @@ -144,7 +144,8 @@ def get_context_data(self, *args, **kwargs): "html": checks_output, }, { - "text": "", + "html": f"Edit " + f"Delete", }, ], ) From c198cc0b0be513246c60795b1295c55b2cb4cec4 Mon Sep 17 00:00:00 2001 From: Doug Mills Date: Wed, 28 Feb 2024 09:22:59 +0000 Subject: [PATCH 060/118] Add factories, add editing for rates - still WIP but working --- .../views/preferential_quotas.py | 37 ------------------- 1 file changed, 37 deletions(-) delete mode 100644 reference_documents/views/preferential_quotas.py diff --git a/reference_documents/views/preferential_quotas.py b/reference_documents/views/preferential_quotas.py deleted file mode 100644 index 6a454325b..000000000 --- a/reference_documents/views/preferential_quotas.py +++ /dev/null @@ -1,37 +0,0 @@ -from django.contrib.auth.mixins import PermissionRequiredMixin -from django.shortcuts import redirect -from django.urls import reverse -from django.views.generic import UpdateView - -from reference_documents.models import PreferentialQuota - - -class PreferentialQuotaEditView(PermissionRequiredMixin, UpdateView): - template_name = "preferential_quotas/edit.jinja" - permission_required = "reference_documents.edit_reference_document" - model = PreferentialQuota - fields = [ - "quota_order_number", - "commodity_code", - "quota_duty_rate", - "volume", - "measurement", - "valid_between", - ] - - def post(self, request, *args, **kwargs): - quota = self.get_object() - quota.save() - return redirect( - reverse( - "reference_documents:version_details", - args=[quota.reference_document_version.pk], - ) - + "#tariff-quotas", - ) - - -class PreferentialQuotaDeleteView(PermissionRequiredMixin, UpdateView): - template_name = "preferential_quotas/delete.jinja" - permission_required = "reference_documents.edit_reference_document" - model = PreferentialQuota From 442f0bb87ad5fa71578da3b42f4451dc24124cd1 Mon Sep 17 00:00:00 2001 From: Doug Mills Date: Wed, 28 Feb 2024 10:06:41 +0000 Subject: [PATCH 061/118] Add factories, add editing for rates - still WIP but working --- .../views/preferential_quotas.py | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 reference_documents/views/preferential_quotas.py diff --git a/reference_documents/views/preferential_quotas.py b/reference_documents/views/preferential_quotas.py new file mode 100644 index 000000000..6a454325b --- /dev/null +++ b/reference_documents/views/preferential_quotas.py @@ -0,0 +1,37 @@ +from django.contrib.auth.mixins import PermissionRequiredMixin +from django.shortcuts import redirect +from django.urls import reverse +from django.views.generic import UpdateView + +from reference_documents.models import PreferentialQuota + + +class PreferentialQuotaEditView(PermissionRequiredMixin, UpdateView): + template_name = "preferential_quotas/edit.jinja" + permission_required = "reference_documents.edit_reference_document" + model = PreferentialQuota + fields = [ + "quota_order_number", + "commodity_code", + "quota_duty_rate", + "volume", + "measurement", + "valid_between", + ] + + def post(self, request, *args, **kwargs): + quota = self.get_object() + quota.save() + return redirect( + reverse( + "reference_documents:version_details", + args=[quota.reference_document_version.pk], + ) + + "#tariff-quotas", + ) + + +class PreferentialQuotaDeleteView(PermissionRequiredMixin, UpdateView): + template_name = "preferential_quotas/delete.jinja" + permission_required = "reference_documents.edit_reference_document" + model = PreferentialQuota From 56e1e3f75c673dcc74f2f67ae9815d06374ea5e8 Mon Sep 17 00:00:00 2001 From: Matthew McKenzie <97194636+mattjamc@users.noreply.github.com> Date: Wed, 28 Feb 2024 10:08:42 +0000 Subject: [PATCH 062/118] TP2000-1232 Reference doc UI management (#1165) * Functionality to edit reference documents * CRUD templates * Reference Doc CRUD --- reference_documents/forms.py | 75 ++++++++++++++- .../reference_documents/confirm_create.jinja | 39 ++++++++ .../reference_documents/confirm_delete.jinja | 32 +++++++ .../reference_documents/confirm_update.jinja | 39 ++++++++ .../jinja2/reference_documents/create.jinja | 4 + .../jinja2/reference_documents/delete.jinja | 73 +++++++++++++++ .../jinja2/reference_documents/index.jinja | 7 +- .../jinja2/reference_documents/update.jinja | 18 ++++ reference_documents/models.py | 1 + reference_documents/urls.py | 30 ++++++ .../views/reference_document_views.py | 93 ++++++++++++++++++- 11 files changed, 406 insertions(+), 5 deletions(-) create mode 100644 reference_documents/jinja2/reference_documents/confirm_create.jinja create mode 100644 reference_documents/jinja2/reference_documents/confirm_delete.jinja create mode 100644 reference_documents/jinja2/reference_documents/confirm_update.jinja create mode 100644 reference_documents/jinja2/reference_documents/create.jinja create mode 100644 reference_documents/jinja2/reference_documents/delete.jinja create mode 100644 reference_documents/jinja2/reference_documents/update.jinja diff --git a/reference_documents/forms.py b/reference_documents/forms.py index 8ed3d4a81..51e765754 100644 --- a/reference_documents/forms.py +++ b/reference_documents/forms.py @@ -1,10 +1,13 @@ -from crispy_forms.helper import FormHelper +from crispy_forms_gds.helper import FormHelper +from crispy_forms_gds.layout import Field +from crispy_forms_gds.layout import Fixed from crispy_forms_gds.layout import Layout from crispy_forms_gds.layout import Size from crispy_forms_gds.layout import Submit from django import forms from common.forms import ValidityPeriodForm +from reference_documents import models from reference_documents.models import PreferentialRate from reference_documents.validators import commodity_code_validator @@ -68,6 +71,76 @@ def clean(self): return super().clean() +class ReferenceDocumentCreateUpdateForm(forms.ModelForm): + title = forms.CharField( + label="Reference Document title", + error_messages={ + "required": "A Reference Document title is required", + "unique": "A Reference Document with this title already exists", + }, + ) + area_id = forms.CharField( + label="Area ID", + error_messages={ + "required": "An area ID is required", + "unique": "A Reference Document with this area ID already exists", + }, + ) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + self.fields[ + "title" + ].help_text = "For example, 'Reference document for XX' where XX is the Area ID" + self.fields["area_id"].help_text = "Two character ID for the area referenced" + + self.helper = FormHelper(self) + self.helper.label_size = Size.SMALL + self.helper.legend_size = Size.SMALL + + self.helper.layout = Layout( + Field.text( + "title", + field_width=Fixed.TWENTY, + ), + Field.text( + "area_id", + field_width=Fixed.TEN, + ), + Submit( + "submit", + "Save", + data_module="govuk-button", + data_prevent_double_click="true", + ), + ) + + class Meta: + model = models.ReferenceDocument + fields = ["title", "area_id"] + + +class ReferenceDocumentDeleteForm(forms.Form): + def __init__(self, *args, **kwargs) -> None: + self.instance = kwargs.pop("instance") + super().__init__(*args, **kwargs) + + def clean(self): + cleaned_data = super().clean() + reference_document = self.instance + versions = models.ReferenceDocumentVersion.objects.all().filter( + reference_document=reference_document, + ) + if versions: + raise forms.ValidationError( + f"Reference Document {reference_document.area_id} cannot be deleted as it has" + f" active versions.", + ) + + return cleaned_data + + class PreferentialRateCreateForm( ValidityPeriodForm, forms.ModelForm, diff --git a/reference_documents/jinja2/reference_documents/confirm_create.jinja b/reference_documents/jinja2/reference_documents/confirm_create.jinja new file mode 100644 index 000000000..3ea829afe --- /dev/null +++ b/reference_documents/jinja2/reference_documents/confirm_create.jinja @@ -0,0 +1,39 @@ +{% extends "layouts/layout.jinja" %} + +{% from "components/panel/macro.njk" import govukPanel %} +{% from "components/button/macro.njk" import govukButton %} +{% from "components/breadcrumbs/macro.njk" import govukBreadcrumbs %} + +{% set page_title = "Reference Document " ~ object.area_id ~ " successfully created" %} + +{% block breadcrumb %} + {{ govukBreadcrumbs({ + "items": [{"text": "Home", "href": url("home")}, + {"text": "View reference documents", "href": url("reference_documents:index")}, + {"text": "Create a Reference Document", "href": url("reference_documents:create")}, + {"text": page_title}] + }) }} +{% endblock %} + +{% block content %} +
    +
    + {{ govukPanel({ + "titleText": "Reference Document " ~ object.area_id ~ " has been created", + "text": "This change has taken immediate effect", + "classes": "govuk-!-margin-bottom-7" + }) }} +
    +
    +
    + {{ govukButton({ + "text": "View Reference Document " ~ object.area_id, + "href": url("reference_documents:details", kwargs={"pk":object.pk}), + }) }} + {{ govukButton({ + "text": "Back to View reference documents", + "href": url("reference_documents:index"), + "classes": "govuk-button--secondary" + }) }} +
    +{% endblock %} diff --git a/reference_documents/jinja2/reference_documents/confirm_delete.jinja b/reference_documents/jinja2/reference_documents/confirm_delete.jinja new file mode 100644 index 000000000..29b53ec0c --- /dev/null +++ b/reference_documents/jinja2/reference_documents/confirm_delete.jinja @@ -0,0 +1,32 @@ +{% extends "layouts/layout.jinja" %} + +{% from "components/panel/macro.njk" import govukPanel %} +{% from "components/button/macro.njk" import govukButton %} +{% from "components/breadcrumbs/macro.njk" import govukBreadcrumbs %} + +{% set page_title = "Reference Document " ~ deleted_pk ~ " deleted" %} + +{% block breadcrumb %} + {{ govukBreadcrumbs({ + "items": [{"text": "Home", "href": url("home")}, + {"text": "View reference documents", "href": url("reference_documents:index")}, + {"text": page_title}] + }) }} +{% endblock %} + +{% block content %} +
    +
    + {{ govukPanel({ + "titleText": "Reference Document " ~ deleted_pk ~ " has been deleted", + "text": "This change has taken immediate effect", + "classes": "govuk-!-margin-bottom-7" + }) }} +
    +
    + {{ govukButton({ + "text": "Back to View reference documents", + "href": url("reference_documents:index"), + "classes": "govuk-button--secondary" + }) }} +{% endblock %} diff --git a/reference_documents/jinja2/reference_documents/confirm_update.jinja b/reference_documents/jinja2/reference_documents/confirm_update.jinja new file mode 100644 index 000000000..414e23298 --- /dev/null +++ b/reference_documents/jinja2/reference_documents/confirm_update.jinja @@ -0,0 +1,39 @@ +{% extends "layouts/layout.jinja" %} + +{% from "components/panel/macro.njk" import govukPanel %} +{% from "components/button/macro.njk" import govukButton %} +{% from "components/breadcrumbs/macro.njk" import govukBreadcrumbs %} + +{% set page_title = "Reference Document " ~ object.area_id ~ " successfully updated" %} + +{% block breadcrumb %} + {{ govukBreadcrumbs({ + "items": [{"text": "Home", "href": url("home")}, + {"text": "View reference documents", "href": url("reference_documents:index")}, + {"text": "Edit Reference Document " ~ object.area_id, "href": url("reference_documents:update", kwargs={"pk":object.pk})}, + {"text": page_title}] + }) }} +{% endblock %} + +{% block content %} +
    +
    + {{ govukPanel({ + "titleText": "Reference Document " ~ object.area_id ~ " has been updated", + "text": "This change has taken immediate effect", + "classes": "govuk-!-margin-bottom-7" + }) }} +
    +
    +
    + {{ govukButton({ + "text": "View your Reference Document", + "href": url("reference_documents:details", kwargs={"pk":object.pk}), + }) }} + {{ govukButton({ + "text": "Back to View Reference Documents", + "href": url("reference_documents:index"), + "classes": "govuk-button--secondary" + }) }} +
    +{% endblock %} diff --git a/reference_documents/jinja2/reference_documents/create.jinja b/reference_documents/jinja2/reference_documents/create.jinja new file mode 100644 index 000000000..d5506f4c2 --- /dev/null +++ b/reference_documents/jinja2/reference_documents/create.jinja @@ -0,0 +1,4 @@ +{% extends "layouts/create.jinja" %} + +{% set page_title = "Create a new reference document" %} + diff --git a/reference_documents/jinja2/reference_documents/delete.jinja b/reference_documents/jinja2/reference_documents/delete.jinja new file mode 100644 index 000000000..78abd9d31 --- /dev/null +++ b/reference_documents/jinja2/reference_documents/delete.jinja @@ -0,0 +1,73 @@ +{% extends "layouts/layout.jinja" %} + +{% from "components/breadcrumbs/macro.njk" import govukBreadcrumbs %} +{% from "components/warning-text/macro.njk" import govukWarningText %} +{% from "components/button/macro.njk" import govukButton %} +{% from "components/error-summary/macro.njk" import govukErrorSummary %} + +{% set page_title = "Delete Reference Document " ~ object.area_id %} + +{% block breadcrumb %} + {{ govukBreadcrumbs({ + "items": [{"text": "Home", "href": url("home")}, + {"text": "View reference documents", "href": url("reference_documents:index")}, + {"text": page_title}] + }) }} +{% endblock %} + +{% block content %} +
    +
    +

    {{ page_title }}

    +
    +
    + +
    +
    +

    Are you sure you want to permanently delete Reference Document {{ object.area_id }}?

    + + {{ govukWarningText({ + "text": "Deleted Reference Documents can not be recovered.", + "iconFallbackText": "Warning" + }) }} + +
    + + + {% set error_list = [] %} + + {% for field, errors in form.errors.items() %} + {% for error in errors.data %} + {% if error.message|length > 1 %} + {{ error_list.append({ + "text": error.message, + "href": "#" ~ (form.prefix ~ "-" if form.prefix else "") ~ field ~ ("_" ~ error.subfield if error.subfield is defined else ""), + }) or "" }} + {% endif %} + {% endfor %} + {% endfor %} + + {% if error_list|length > 0 %} + {{ govukErrorSummary({ + "titleText": "There is a problem", + "errorList": error_list + }) }} + {% endif %} + +
    + {{ govukButton({ + "text": "Delete", + "classes": "govuk-button--warning", + "name": "action", + "value": "delete" + }) }} + {{ govukButton({ + "text": "Cancel", + "href": url("reference_documents:index"), + "classes": "govuk-button--secondary" + }) }} +
    +
    +
    +
    +{% endblock %} diff --git a/reference_documents/jinja2/reference_documents/index.jinja b/reference_documents/jinja2/reference_documents/index.jinja index b281f6a3d..0b38d2389 100644 --- a/reference_documents/jinja2/reference_documents/index.jinja +++ b/reference_documents/jinja2/reference_documents/index.jinja @@ -2,6 +2,7 @@ {% from "components/table/macro.njk" import govukTable %} {% set page_title = 'Reference Documents Index' %} +{% set create_url = "create" %} {% block breadcrumb %} {{ breadcrumbs(request, [ @@ -14,7 +15,11 @@ Reference Documents You will find a list of reference documents below that can be viewed. - +

    + + Create a new Reference Document + +

    {{ govukTable({ "head": reference_document_headers, "rows": reference_documents }) }}
    diff --git a/reference_documents/jinja2/reference_documents/update.jinja b/reference_documents/jinja2/reference_documents/update.jinja new file mode 100644 index 000000000..1c50f421b --- /dev/null +++ b/reference_documents/jinja2/reference_documents/update.jinja @@ -0,0 +1,18 @@ +{% extends "layouts/form.jinja" %} +{% from "components/breadcrumbs/macro.njk" import govukBreadcrumbs %} + +{% set page_title = "Edit Reference Document " ~ object.area_id ~ " details" %} + +{% block breadcrumb %} + {{ govukBreadcrumbs({ + "items": [{"text": "Home", "href": url("home")}, + {"text": "View reference documents", "href": url("reference_documents:index")}, + {"text": page_title}] + }) }} +{% endblock %} + +{% block form %} + {% call django_form() %} + {{ crispy(form) }} + {% endcall %} +{% endblock %} diff --git a/reference_documents/models.py b/reference_documents/models.py index 305f1c2e8..3ba9970e2 100644 --- a/reference_documents/models.py +++ b/reference_documents/models.py @@ -36,6 +36,7 @@ class ReferenceDocument(models.Model): area_id = models.CharField( max_length=4, db_index=True, + unique=True, ) diff --git a/reference_documents/urls.py b/reference_documents/urls.py index 2a32d5a84..bcd2a8175 100644 --- a/reference_documents/urls.py +++ b/reference_documents/urls.py @@ -31,11 +31,41 @@ reference_document_views.ReferenceDocumentList.as_view(), name="index", ), + path( + "reference_documents/create/", + reference_document_views.ReferenceDocumentCreate.as_view(), + name="create", + ), path( "reference_documents//", reference_document_views.ReferenceDocumentDetails.as_view(), name="details", ), + path( + "reference_documents//update/", + reference_document_views.ReferenceDocumentUpdate.as_view(), + name="update", + ), + path( + f"/confirm-create/", + reference_document_views.ReferenceDocumentConfirmCreate.as_view(), + name="confirm-create", + ), + path( + f"/confirm-update/", + reference_document_views.ReferenceDocumentConfirmUpdate.as_view(), + name="confirm-update", + ), + path( + f"/delete/", + reference_document_views.ReferenceDocumentDelete.as_view(), + name="delete", + ), + path( + f"/confirm-delete/", + reference_document_views.ReferenceDocumentConfirmDelete.as_view(), + name="confirm-delete", + ), # reference document version views path( "reference_document_versions//", diff --git a/reference_documents/views/reference_document_views.py b/reference_documents/views/reference_document_views.py index 7b000b722..1bdc1dd79 100644 --- a/reference_documents/views/reference_document_views.py +++ b/reference_documents/views/reference_document_views.py @@ -1,9 +1,17 @@ from django.contrib.auth.mixins import PermissionRequiredMixin +from django.shortcuts import redirect from django.urls import reverse +from django.views.generic import CreateView +from django.views.generic import DeleteView from django.views.generic import DetailView from django.views.generic import ListView +from django.views.generic import TemplateView +from django.views.generic import UpdateView +from django.views.generic.edit import FormMixin from geo_areas.models import GeographicalAreaDescription +from reference_documents import forms +from reference_documents import models from reference_documents.models import ReferenceDocument @@ -41,7 +49,9 @@ def get_context_data(self, **kwargs): {"text": 0}, {"text": 0}, { - "html": f'Details', + "html": f'Details
    ' + f"Edit
    " + f"Delete", }, ], ) @@ -60,7 +70,9 @@ def get_context_data(self, **kwargs): "text": reference.reference_document_versions.last().preferential_quotas.count(), }, { - "html": f'Details', + "html": f'Details
    ' + f"Edit
    " + f"Delete", }, ], ) @@ -116,7 +128,7 @@ def get_context_data(self, *args, **kwargs): "text": version.entry_into_force_date, }, { - "html": f'version details
    ' + "html": f'Version details
    ' f'Edit
    ' f'Alignment reports', }, @@ -126,3 +138,78 @@ def get_context_data(self, *args, **kwargs): context["reference_document_versions"] = reference_document_versions return context + + +class ReferenceDocumentCreate(PermissionRequiredMixin, CreateView): + template_name = "reference_documents/create.jinja" + permission_required = "reference_documents.edit_reference_document" + form_class = forms.ReferenceDocumentCreateUpdateForm + + def get_success_url(self): + return reverse( + "reference_documents:confirm-create", + kwargs={"pk": self.object.pk}, + ) + + +class ReferenceDocumentUpdate(PermissionRequiredMixin, UpdateView): + model = models.ReferenceDocument + permission_required = "reference_documents.edit_reference_document" + template_name = "reference_documents/update.jinja" + form_class = forms.ReferenceDocumentCreateUpdateForm + + def get_success_url(self): + return reverse( + "reference_documents:confirm-update", + kwargs={"pk": self.object.pk}, + ) + + +class ReferenceDocumentDelete(PermissionRequiredMixin, FormMixin, DeleteView): + form_class = forms.ReferenceDocumentDeleteForm + model = ReferenceDocument + permission_required = "reference_documents.edit_reference_document" + template_name = "reference_documents/delete.jinja" + + # TODO: Update this to get rid of FormMixin with Django 4.2 as no need to overwrite the post anymore + def get_success_url(self) -> str: + return reverse( + "reference_documents:confirm-delete", + kwargs={"deleted_pk": self.kwargs["pk"]}, + ) + + def get_form_kwargs(self): + kwargs = super().get_form_kwargs() + kwargs["instance"] = self.get_object() + return kwargs + + def post(self, request, *args, **kwargs): + self.object = self.get_object() + form = self.get_form() + if form.is_valid(): + return self.form_valid(form) + else: + return self.form_invalid(form) + + def form_valid(self, form): + self.object.delete() + return redirect(self.get_success_url()) + + +class ReferenceDocumentConfirmCreate(DetailView): + template_name = "reference_documents/confirm_create.jinja" + model = ReferenceDocument + + +class ReferenceDocumentConfirmUpdate(DetailView): + template_name = "reference_documents/confirm_update.jinja" + model = ReferenceDocument + + +class ReferenceDocumentConfirmDelete(TemplateView): + template_name = "reference_documents/confirm_delete.jinja" + + def get_context_data(self, **kwargs): + context_data = super().get_context_data(**kwargs) + context_data["deleted_pk"] = self.kwargs["deleted_pk"] + return context_data From fde420f54c7c99f45a4facb82f13e7a1e7422391 Mon Sep 17 00:00:00 2001 From: Doug Mills Date: Wed, 28 Feb 2024 12:26:16 +0000 Subject: [PATCH 063/118] fixed time zone issue with factories.py --- reference_documents/tests/__init__.py | 0 .../tests/alignment_reports/test_models.py | 9 +++---- reference_documents/tests/factories.py | 27 +++++++++---------- 3 files changed, 16 insertions(+), 20 deletions(-) create mode 100644 reference_documents/tests/__init__.py diff --git a/reference_documents/tests/__init__.py b/reference_documents/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/reference_documents/tests/alignment_reports/test_models.py b/reference_documents/tests/alignment_reports/test_models.py index 30f969408..c8261008f 100644 --- a/reference_documents/tests/alignment_reports/test_models.py +++ b/reference_documents/tests/alignment_reports/test_models.py @@ -1,14 +1,11 @@ import pytest -from reference_documents.models import AlignmentReport +from reference_documents.tests.factories import AlignmentReportFactory pytestmark = pytest.mark.django_db class TestAlignmentReport: def test_create_with_defaults(self): - subject = AlignmentReport.objects.create() - - subject.save() - assert subject.reference_document_version.count() == 0 - assert subject.created_at is not None + target = AlignmentReportFactory() + assert target.created_at is not None diff --git a/reference_documents/tests/factories.py b/reference_documents/tests/factories.py index 6f9f6bee6..ed3424d48 100644 --- a/reference_documents/tests/factories.py +++ b/reference_documents/tests/factories.py @@ -1,12 +1,11 @@ import string -from datetime import UTC from datetime import datetime from datetime import timedelta import factory -from factory.fuzzy import FuzzyDateTime from factory.fuzzy import FuzzyDecimal from factory.fuzzy import FuzzyInteger +from factory.fuzzy import FuzzyNaiveDateTime from factory.fuzzy import FuzzyText from common.util import TaricDateRange @@ -19,7 +18,7 @@ class Meta: model = "reference_documents.ReferenceDocument" area_id = FuzzyText("", 2, "", string.ascii_uppercase) - created_at = FuzzyDateTime(datetime(2008, 1, 1, tzinfo=UTC), datetime.now()) + created_at = FuzzyNaiveDateTime(datetime(2008, 1, 1), datetime.now()) title = FuzzyText("Reference Document for ", 5, "", string.ascii_uppercase) @@ -27,12 +26,12 @@ class ReferenceDocumentVersionFactory(factory.django.DjangoModelFactory): class Meta: model = "reference_documents.ReferenceDocumentVersion" - created_at = FuzzyDateTime(datetime(2020, 1, 1, tzinfo=UTC), datetime.now()) - updated_at = FuzzyDateTime(datetime(2020, 1, 1, tzinfo=UTC), datetime.now()) + created_at = FuzzyNaiveDateTime(datetime(2020, 1, 1), datetime.now()) + updated_at = FuzzyNaiveDateTime(datetime(2020, 1, 1), datetime.now()) version = FuzzyDecimal(1.0, 5.0, 1) - published_date = FuzzyDateTime(datetime(2022, 1, 1, tzinfo=UTC), datetime.now()) - entry_into_force_date = FuzzyDateTime( - datetime(2022, 1, 1, tzinfo=UTC), + published_date = FuzzyNaiveDateTime(datetime(2022, 1, 1), datetime.now()) + entry_into_force_date = FuzzyNaiveDateTime( + datetime(2022, 1, 1), datetime.now(), ) @@ -65,11 +64,11 @@ class Meta: reference_document_version = factory.SubFactory(ReferenceDocumentVersionFactory) valid_between = TaricDateRange( - FuzzyDateTime( + FuzzyNaiveDateTime( datetime.now() + timedelta(days=-(365 * 2)), datetime.now() + timedelta(days=-365), ), - FuzzyDateTime(datetime.now() + timedelta(days=-364), datetime.now()), + FuzzyNaiveDateTime(datetime.now() + timedelta(days=-364), datetime.now()), ) class Params: @@ -120,11 +119,11 @@ class Meta: reference_document_version = factory.SubFactory(ReferenceDocumentVersionFactory) valid_between = TaricDateRange( - FuzzyDateTime( + FuzzyNaiveDateTime( datetime.now() + timedelta(days=-(365 * 2)), datetime.now() + timedelta(days=-365), ), - FuzzyDateTime(datetime.now() + timedelta(days=-364), datetime.now()), + FuzzyNaiveDateTime(datetime.now() + timedelta(days=-364), datetime.now()), ) class Params: @@ -158,7 +157,7 @@ class AlignmentReportFactory(factory.django.DjangoModelFactory): class Meta: model = "reference_documents.AlignmentReport" - created_at = FuzzyDateTime(datetime(2020, 1, 1, tzinfo=UTC), datetime.now()) + created_at = FuzzyNaiveDateTime(datetime(2020, 1, 1), datetime.now()) reference_document_version = factory.SubFactory(ReferenceDocumentVersionFactory) @@ -166,7 +165,7 @@ class AlignmentReportCheckFactory(factory.django.DjangoModelFactory): class Meta: model = "reference_documents.AlignmentReportCheck" - created_at = FuzzyDateTime(datetime(2020, 1, 1, tzinfo=UTC), datetime.now()) + created_at = FuzzyNaiveDateTime(datetime(2020, 1, 1), datetime.now()) alignment_report = factory.SubFactory(AlignmentReportFactory) check_name = FuzzyText( From 455cc73060f61b0b8573ed7f4c141c2e8cdc2c12 Mon Sep 17 00:00:00 2001 From: Matthew McKenzie <97194636+mattjamc@users.noreply.github.com> Date: Wed, 28 Feb 2024 12:31:06 +0000 Subject: [PATCH 064/118] Create update form tests --- reference_documents/forms.py | 1 + reference_documents/tests/__init__.py | 0 .../tests/reference_documents/test_forms.py | 38 +++++++++++++++++++ 3 files changed, 39 insertions(+) create mode 100644 reference_documents/tests/__init__.py diff --git a/reference_documents/forms.py b/reference_documents/forms.py index 51e765754..70d0fc78f 100644 --- a/reference_documents/forms.py +++ b/reference_documents/forms.py @@ -84,6 +84,7 @@ class ReferenceDocumentCreateUpdateForm(forms.ModelForm): error_messages={ "required": "An area ID is required", "unique": "A Reference Document with this area ID already exists", + "max_length": "The area ID must be 2 characters long", }, ) diff --git a/reference_documents/tests/__init__.py b/reference_documents/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/reference_documents/tests/reference_documents/test_forms.py b/reference_documents/tests/reference_documents/test_forms.py index a7057c2cc..419205863 100644 --- a/reference_documents/tests/reference_documents/test_forms.py +++ b/reference_documents/tests/reference_documents/test_forms.py @@ -1,3 +1,41 @@ import pytest +# from reference_documents.tests import factories +from reference_documents import forms + pytestmark = pytest.mark.django_db + + +def test_ref_doc_create_update_form_valid_data(): + """Test that ReferenceDocumentCreateUpdateForm is valid when completed + correctly.""" + data = {"title": "Reference document for XY", "area_id": "XY"} + form = forms.ReferenceDocumentCreateUpdateForm(data=data) + + assert form.is_valid() + + +def test_ref_doc_create_update_form_invalid_data(): + """Test that ReferenceDocumentCreateUpdateForm is invalid when not complete + correctly.""" + form = forms.ReferenceDocumentCreateUpdateForm(data={}) + assert not form.is_valid() + assert "A Reference Document title is required" in form.errors["title"] + assert "An area ID is required" in form.errors["area_id"] + + # factories.ReferenceDocumentFactory.create(title="Reference document for XY", area_id="XY") + data = {"title": "Reference document for XY", "area_id": "VWXYZ"} + form = forms.ReferenceDocumentCreateUpdateForm(data=data) + assert not form.is_valid() + assert "The area ID must be 2 characters long" in form.errors["area_id"] + assert "A Reference Document with this title already exists" in form.errors["title"] + + +def test_ref_doc_delete_form_valid(): + """Test that ReferenceDocumentDeleteForm is valid for a reference document + with no versions.""" + + +def test_ref_doc_delete_form_invalid(): + """Test that ReferenceDocumentDeleteForm is invalid for a reference document + with versions.""" From 3fbf34f3804b6cb6edfe439fd698034d6533d617 Mon Sep 17 00:00:00 2001 From: Doug Mills Date: Wed, 28 Feb 2024 15:33:39 +0000 Subject: [PATCH 065/118] fixed time zone issue with factories.py --- .../0002_alter_referencedocument_area_id.py | 18 +++++ .../tests/alignment_reports/test_models.py | 15 ++++ reference_documents/tests/factories.py | 81 +++++++++++-------- .../tests/preferential_quotas/test_models.py | 18 +++++ .../tests/preferential_rates/test_models.py | 12 +++ .../test_models.py | 15 ++++ .../tests/reference_documents/test_models.py | 11 +++ 7 files changed, 137 insertions(+), 33 deletions(-) create mode 100644 reference_documents/migrations/0002_alter_referencedocument_area_id.py diff --git a/reference_documents/migrations/0002_alter_referencedocument_area_id.py b/reference_documents/migrations/0002_alter_referencedocument_area_id.py new file mode 100644 index 000000000..62552ae52 --- /dev/null +++ b/reference_documents/migrations/0002_alter_referencedocument_area_id.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.24 on 2024-02-28 15:17 + +from django.db import migrations +from django.db import models + + +class Migration(migrations.Migration): + dependencies = [ + ("reference_documents", "0001_initial"), + ] + + operations = [ + migrations.AlterField( + model_name="referencedocument", + name="area_id", + field=models.CharField(db_index=True, max_length=4, unique=True), + ), + ] diff --git a/reference_documents/tests/alignment_reports/test_models.py b/reference_documents/tests/alignment_reports/test_models.py index c8261008f..881f6a13c 100644 --- a/reference_documents/tests/alignment_reports/test_models.py +++ b/reference_documents/tests/alignment_reports/test_models.py @@ -1,5 +1,6 @@ import pytest +from reference_documents.tests.factories import AlignmentReportCheckFactory from reference_documents.tests.factories import AlignmentReportFactory pytestmark = pytest.mark.django_db @@ -9,3 +10,17 @@ class TestAlignmentReport: def test_create_with_defaults(self): target = AlignmentReportFactory() assert target.created_at is not None + assert target.reference_document_version is not None + + +class TestAlignmentReportCheck: + def test_create_with_defaults(self): + target = AlignmentReportCheckFactory() + + assert target.created_at is not None + assert target.alignment_report is not None + assert target.check_name is not None + assert target.status is not None + assert target.message is not None + assert target.preferential_quota is None + assert target.preferential_rate is None diff --git a/reference_documents/tests/factories.py b/reference_documents/tests/factories.py index ed3424d48..960a5964a 100644 --- a/reference_documents/tests/factories.py +++ b/reference_documents/tests/factories.py @@ -1,11 +1,12 @@ import string +from datetime import date from datetime import datetime from datetime import timedelta +from random import randint import factory from factory.fuzzy import FuzzyDecimal from factory.fuzzy import FuzzyInteger -from factory.fuzzy import FuzzyNaiveDateTime from factory.fuzzy import FuzzyText from common.util import TaricDateRange @@ -13,12 +14,20 @@ from reference_documents.models import ReferenceDocumentVersionStatus +def get_random_date(start_date, end_date): + days_diff = abs((end_date - start_date).days) + return start_date + timedelta(days=randint(0, days_diff)) + + class ReferenceDocumentFactory(factory.django.DjangoModelFactory): class Meta: model = "reference_documents.ReferenceDocument" area_id = FuzzyText("", 2, "", string.ascii_uppercase) - created_at = FuzzyNaiveDateTime(datetime(2008, 1, 1), datetime.now()) + created_at = get_random_date( + date(2008, 1, 1), + date.today(), + ) title = FuzzyText("Reference Document for ", 5, "", string.ascii_uppercase) @@ -26,11 +35,11 @@ class ReferenceDocumentVersionFactory(factory.django.DjangoModelFactory): class Meta: model = "reference_documents.ReferenceDocumentVersion" - created_at = FuzzyNaiveDateTime(datetime(2020, 1, 1), datetime.now()) - updated_at = FuzzyNaiveDateTime(datetime(2020, 1, 1), datetime.now()) + created_at = get_random_date(datetime(2020, 1, 1), datetime.now()) + updated_at = get_random_date(datetime(2020, 1, 1), datetime.now()) version = FuzzyDecimal(1.0, 5.0, 1) - published_date = FuzzyNaiveDateTime(datetime(2022, 1, 1), datetime.now()) - entry_into_force_date = FuzzyNaiveDateTime( + published_date = get_random_date(datetime(2022, 1, 1), datetime.now()) + entry_into_force_date = get_random_date( datetime(2022, 1, 1), datetime.now(), ) @@ -63,37 +72,40 @@ class Meta: reference_document_version = factory.SubFactory(ReferenceDocumentVersionFactory) - valid_between = TaricDateRange( - FuzzyNaiveDateTime( - datetime.now() + timedelta(days=-(365 * 2)), - datetime.now() + timedelta(days=-365), + valid_between = ( + get_random_date( + date.today() + timedelta(days=-(365 * 2)), + date.today() + timedelta(days=-365), + ), + get_random_date( + date.today() + timedelta(days=-364), + date.today(), ), - FuzzyNaiveDateTime(datetime.now() + timedelta(days=-364), datetime.now()), ) class Params: valid_between_current = factory.Trait( valid_between=TaricDateRange( - lower=datetime.now() + timedelta(days=-200), - upper=datetime.now() + timedelta(days=165), + date.today() + timedelta(days=-200), + date.today() + timedelta(days=165), ), ) valid_between_current_open_ended = factory.Trait( valid_between=TaricDateRange( - lower=datetime.now() + timedelta(days=-200), - upper=None, + date.today() + timedelta(days=-200), + None, ), ) valid_between_in_past = factory.Trait( valid_between=TaricDateRange( - lower=datetime.now() + timedelta(days=-375), - upper=datetime.now() + timedelta(days=-10), + date.today() + timedelta(days=-375), + date.today() + timedelta(days=-10), ), ) valid_between_in_future = factory.Trait( valid_between=TaricDateRange( - lower=datetime.now() + timedelta(days=10), - upper=datetime.now() + timedelta(days=375), + date.today() + timedelta(days=10), + date.today() + timedelta(days=375), ), ) @@ -119,36 +131,39 @@ class Meta: reference_document_version = factory.SubFactory(ReferenceDocumentVersionFactory) valid_between = TaricDateRange( - FuzzyNaiveDateTime( - datetime.now() + timedelta(days=-(365 * 2)), - datetime.now() + timedelta(days=-365), + get_random_date( + date.today() + timedelta(days=-(365 * 2)), + date.today() + timedelta(days=-365), + ), + get_random_date( + date.today() + timedelta(days=-364), + date.today(), ), - FuzzyNaiveDateTime(datetime.now() + timedelta(days=-364), datetime.now()), ) class Params: valid_between_current = factory.Trait( valid_between=TaricDateRange( - lower=datetime.now() + timedelta(days=-200), - upper=datetime.now() + timedelta(days=165), + date.today() + timedelta(days=-200), + date.today() + timedelta(days=165), ), ) valid_between_current_open_ended = factory.Trait( valid_between=TaricDateRange( - lower=datetime.now() + timedelta(days=-200), - upper=None, + date.today() + timedelta(days=-200), + None, ), ) valid_between_in_past = factory.Trait( valid_between=TaricDateRange( - lower=datetime.now() + timedelta(days=-375), - upper=datetime.now() + timedelta(days=-10), + date.today() + timedelta(days=-375), + date.today() + timedelta(days=-10), ), ) valid_between_in_future = factory.Trait( valid_between=TaricDateRange( - lower=datetime.now() + timedelta(days=10), - upper=datetime.now() + timedelta(days=375), + date.today() + timedelta(days=10), + date.today() + timedelta(days=375), ), ) @@ -157,7 +172,7 @@ class AlignmentReportFactory(factory.django.DjangoModelFactory): class Meta: model = "reference_documents.AlignmentReport" - created_at = FuzzyNaiveDateTime(datetime(2020, 1, 1), datetime.now()) + created_at = get_random_date(date(2020, 1, 1), date.today()) reference_document_version = factory.SubFactory(ReferenceDocumentVersionFactory) @@ -165,7 +180,7 @@ class AlignmentReportCheckFactory(factory.django.DjangoModelFactory): class Meta: model = "reference_documents.AlignmentReportCheck" - created_at = FuzzyNaiveDateTime(datetime(2020, 1, 1), datetime.now()) + created_at = get_random_date(date(2020, 1, 1), date.today()) alignment_report = factory.SubFactory(AlignmentReportFactory) check_name = FuzzyText( diff --git a/reference_documents/tests/preferential_quotas/test_models.py b/reference_documents/tests/preferential_quotas/test_models.py index a7057c2cc..bd78253cc 100644 --- a/reference_documents/tests/preferential_quotas/test_models.py +++ b/reference_documents/tests/preferential_quotas/test_models.py @@ -1,3 +1,21 @@ import pytest +from reference_documents.tests.factories import PreferentialQuotaFactory + pytestmark = pytest.mark.django_db + + +class TestPreferentialQuota: + def test_create_with_defaults(self): + target = PreferentialQuotaFactory() + + assert target.quota_order_number is not None + assert target.commodity_code is not None + assert target.quota_duty_rate is not None + assert target.volume is not None + assert target.coefficient is None + assert target.main_quota is None + assert target.valid_between is not None + assert target.measurement is not None + assert target.order is not None + assert target.reference_document_version is not None diff --git a/reference_documents/tests/preferential_rates/test_models.py b/reference_documents/tests/preferential_rates/test_models.py index a7057c2cc..8faf82669 100644 --- a/reference_documents/tests/preferential_rates/test_models.py +++ b/reference_documents/tests/preferential_rates/test_models.py @@ -1,3 +1,15 @@ import pytest +from reference_documents.tests.factories import PreferentialRateFactory + pytestmark = pytest.mark.django_db + + +class TestPreferentialRate: + def test_create_with_defaults(self): + target = PreferentialRateFactory() + assert target.commodity_code is not None + assert target.duty_rate is not None + assert target.order is not None + assert target.reference_document_version is not None + assert target.valid_between is not None diff --git a/reference_documents/tests/reference_document_versions/test_models.py b/reference_documents/tests/reference_document_versions/test_models.py index a7057c2cc..761c7f439 100644 --- a/reference_documents/tests/reference_document_versions/test_models.py +++ b/reference_documents/tests/reference_document_versions/test_models.py @@ -1,3 +1,18 @@ import pytest +from reference_documents.tests.factories import ReferenceDocumentVersionFactory + pytestmark = pytest.mark.django_db + + +class TestReferenceDocumentVersion: + def test_create_with_defaults(self): + target = ReferenceDocumentVersionFactory() + + assert target.created_at is not None + assert target.updated_at is not None + assert target.version is not None + assert target.published_date is not None + assert target.entry_into_force_date is not None + assert target.reference_document is not None + assert target.status is not None diff --git a/reference_documents/tests/reference_documents/test_models.py b/reference_documents/tests/reference_documents/test_models.py index a7057c2cc..a3b60fd32 100644 --- a/reference_documents/tests/reference_documents/test_models.py +++ b/reference_documents/tests/reference_documents/test_models.py @@ -1,3 +1,14 @@ import pytest +from reference_documents.tests.factories import ReferenceDocumentFactory + pytestmark = pytest.mark.django_db + + +class TestReferenceDocumentVersion: + def test_create_with_defaults(self): + target = ReferenceDocumentFactory() + + assert target.created_at is not None + assert target.title is not None + assert target.area_id is not None From 8545e49dc62a544b098ec7e1be79733e1e6d4137 Mon Sep 17 00:00:00 2001 From: Doug Mills Date: Thu, 29 Feb 2024 08:14:11 +0000 Subject: [PATCH 066/118] add basic tests for models --- pyproject.toml | 3 ++- .../test_forms.py => test_alignment_reports_forms.py} | 0 .../test_models.py => test_alignment_reports_models.py} | 2 ++ .../test_models.py => test_preferential_quotas_models.py} | 1 + .../test_models.py => test_preferential_rates_models.py} | 1 + ...st_models.py => test_reference_document_versions_models.py} | 1 + .../test_models.py => test_reference_documents_models.py} | 1 + 7 files changed, 8 insertions(+), 1 deletion(-) rename reference_documents/tests/{alignment_reports/test_forms.py => test_alignment_reports_forms.py} (100%) rename reference_documents/tests/{alignment_reports/test_models.py => test_alignment_reports_models.py} (92%) rename reference_documents/tests/{preferential_quotas/test_models.py => test_preferential_quotas_models.py} (95%) rename reference_documents/tests/{preferential_rates/test_models.py => test_preferential_rates_models.py} (93%) rename reference_documents/tests/{reference_document_versions/test_models.py => test_reference_document_versions_models.py} (94%) rename reference_documents/tests/{reference_documents/test_models.py => test_reference_documents_models.py} (91%) diff --git a/pyproject.toml b/pyproject.toml index ec3ae3bda..28bf40391 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -97,5 +97,6 @@ norecursedirs = [ "venv", ] markers = [ - "importer_v2" + "importer_v2", + "reference_documents" ] diff --git a/reference_documents/tests/alignment_reports/test_forms.py b/reference_documents/tests/test_alignment_reports_forms.py similarity index 100% rename from reference_documents/tests/alignment_reports/test_forms.py rename to reference_documents/tests/test_alignment_reports_forms.py diff --git a/reference_documents/tests/alignment_reports/test_models.py b/reference_documents/tests/test_alignment_reports_models.py similarity index 92% rename from reference_documents/tests/alignment_reports/test_models.py rename to reference_documents/tests/test_alignment_reports_models.py index 881f6a13c..3a4a8ac96 100644 --- a/reference_documents/tests/alignment_reports/test_models.py +++ b/reference_documents/tests/test_alignment_reports_models.py @@ -6,6 +6,7 @@ pytestmark = pytest.mark.django_db +@pytest.mark.reference_documents class TestAlignmentReport: def test_create_with_defaults(self): target = AlignmentReportFactory() @@ -13,6 +14,7 @@ def test_create_with_defaults(self): assert target.reference_document_version is not None +@pytest.mark.reference_documents class TestAlignmentReportCheck: def test_create_with_defaults(self): target = AlignmentReportCheckFactory() diff --git a/reference_documents/tests/preferential_quotas/test_models.py b/reference_documents/tests/test_preferential_quotas_models.py similarity index 95% rename from reference_documents/tests/preferential_quotas/test_models.py rename to reference_documents/tests/test_preferential_quotas_models.py index bd78253cc..44aa20c4a 100644 --- a/reference_documents/tests/preferential_quotas/test_models.py +++ b/reference_documents/tests/test_preferential_quotas_models.py @@ -5,6 +5,7 @@ pytestmark = pytest.mark.django_db +@pytest.mark.reference_documents class TestPreferentialQuota: def test_create_with_defaults(self): target = PreferentialQuotaFactory() diff --git a/reference_documents/tests/preferential_rates/test_models.py b/reference_documents/tests/test_preferential_rates_models.py similarity index 93% rename from reference_documents/tests/preferential_rates/test_models.py rename to reference_documents/tests/test_preferential_rates_models.py index 8faf82669..70d493d9a 100644 --- a/reference_documents/tests/preferential_rates/test_models.py +++ b/reference_documents/tests/test_preferential_rates_models.py @@ -5,6 +5,7 @@ pytestmark = pytest.mark.django_db +@pytest.mark.reference_documents class TestPreferentialRate: def test_create_with_defaults(self): target = PreferentialRateFactory() diff --git a/reference_documents/tests/reference_document_versions/test_models.py b/reference_documents/tests/test_reference_document_versions_models.py similarity index 94% rename from reference_documents/tests/reference_document_versions/test_models.py rename to reference_documents/tests/test_reference_document_versions_models.py index 761c7f439..f0cacc009 100644 --- a/reference_documents/tests/reference_document_versions/test_models.py +++ b/reference_documents/tests/test_reference_document_versions_models.py @@ -5,6 +5,7 @@ pytestmark = pytest.mark.django_db +@pytest.mark.reference_documents class TestReferenceDocumentVersion: def test_create_with_defaults(self): target = ReferenceDocumentVersionFactory() diff --git a/reference_documents/tests/reference_documents/test_models.py b/reference_documents/tests/test_reference_documents_models.py similarity index 91% rename from reference_documents/tests/reference_documents/test_models.py rename to reference_documents/tests/test_reference_documents_models.py index a3b60fd32..c5c9a86fb 100644 --- a/reference_documents/tests/reference_documents/test_models.py +++ b/reference_documents/tests/test_reference_documents_models.py @@ -5,6 +5,7 @@ pytestmark = pytest.mark.django_db +@pytest.mark.reference_documents class TestReferenceDocumentVersion: def test_create_with_defaults(self): target = ReferenceDocumentFactory() From b5599e902db7a167e5e235865c238d00ba5dcb38 Mon Sep 17 00:00:00 2001 From: Doug Mills Date: Thu, 29 Feb 2024 08:14:24 +0000 Subject: [PATCH 067/118] add basic tests for models --- .../test_views.py => test_preferential_quotas_forms.py} | 0 .../test_forms.py => test_preferential_quotas_views.py} | 0 .../test_views.py => test_preferential_rates_forms.py} | 0 .../test_forms.py => test_preferential_rates_views.py} | 0 .../test_views.py => test_reference_document_versions_forms.py} | 0 .../test_forms.py => test_reference_document_versions_views.py} | 0 .../test_views.py => test_reference_documents_forms.py} | 0 .../test_forms.py => test_reference_documents_views.py} | 0 reference_documents/tests/{reference_documents => }/test_views.py | 0 9 files changed, 0 insertions(+), 0 deletions(-) rename reference_documents/tests/{alignment_reports/test_views.py => test_preferential_quotas_forms.py} (100%) rename reference_documents/tests/{preferential_quotas/test_forms.py => test_preferential_quotas_views.py} (100%) rename reference_documents/tests/{preferential_quotas/test_views.py => test_preferential_rates_forms.py} (100%) rename reference_documents/tests/{preferential_rates/test_forms.py => test_preferential_rates_views.py} (100%) rename reference_documents/tests/{preferential_rates/test_views.py => test_reference_document_versions_forms.py} (100%) rename reference_documents/tests/{reference_document_versions/test_forms.py => test_reference_document_versions_views.py} (100%) rename reference_documents/tests/{reference_document_versions/test_views.py => test_reference_documents_forms.py} (100%) rename reference_documents/tests/{reference_documents/test_forms.py => test_reference_documents_views.py} (100%) rename reference_documents/tests/{reference_documents => }/test_views.py (100%) diff --git a/reference_documents/tests/alignment_reports/test_views.py b/reference_documents/tests/test_preferential_quotas_forms.py similarity index 100% rename from reference_documents/tests/alignment_reports/test_views.py rename to reference_documents/tests/test_preferential_quotas_forms.py diff --git a/reference_documents/tests/preferential_quotas/test_forms.py b/reference_documents/tests/test_preferential_quotas_views.py similarity index 100% rename from reference_documents/tests/preferential_quotas/test_forms.py rename to reference_documents/tests/test_preferential_quotas_views.py diff --git a/reference_documents/tests/preferential_quotas/test_views.py b/reference_documents/tests/test_preferential_rates_forms.py similarity index 100% rename from reference_documents/tests/preferential_quotas/test_views.py rename to reference_documents/tests/test_preferential_rates_forms.py diff --git a/reference_documents/tests/preferential_rates/test_forms.py b/reference_documents/tests/test_preferential_rates_views.py similarity index 100% rename from reference_documents/tests/preferential_rates/test_forms.py rename to reference_documents/tests/test_preferential_rates_views.py diff --git a/reference_documents/tests/preferential_rates/test_views.py b/reference_documents/tests/test_reference_document_versions_forms.py similarity index 100% rename from reference_documents/tests/preferential_rates/test_views.py rename to reference_documents/tests/test_reference_document_versions_forms.py diff --git a/reference_documents/tests/reference_document_versions/test_forms.py b/reference_documents/tests/test_reference_document_versions_views.py similarity index 100% rename from reference_documents/tests/reference_document_versions/test_forms.py rename to reference_documents/tests/test_reference_document_versions_views.py diff --git a/reference_documents/tests/reference_document_versions/test_views.py b/reference_documents/tests/test_reference_documents_forms.py similarity index 100% rename from reference_documents/tests/reference_document_versions/test_views.py rename to reference_documents/tests/test_reference_documents_forms.py diff --git a/reference_documents/tests/reference_documents/test_forms.py b/reference_documents/tests/test_reference_documents_views.py similarity index 100% rename from reference_documents/tests/reference_documents/test_forms.py rename to reference_documents/tests/test_reference_documents_views.py diff --git a/reference_documents/tests/reference_documents/test_views.py b/reference_documents/tests/test_views.py similarity index 100% rename from reference_documents/tests/reference_documents/test_views.py rename to reference_documents/tests/test_views.py From 3bd74723cab77772aa94cd0e749eea35052beb6b Mon Sep 17 00:00:00 2001 From: Matthew McKenzie <97194636+mattjamc@users.noreply.github.com> Date: Thu, 29 Feb 2024 15:06:42 +0000 Subject: [PATCH 068/118] Add test init files --- reference_documents/tests/alignment_reports/__init__.py | 0 reference_documents/tests/preferential_quotas/__init__.py | 0 reference_documents/tests/preferential_rates/__init__.py | 0 reference_documents/tests/reference_document_versions/__init__.py | 0 reference_documents/tests/reference_documents/__init__.py | 0 5 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 reference_documents/tests/alignment_reports/__init__.py create mode 100644 reference_documents/tests/preferential_quotas/__init__.py create mode 100644 reference_documents/tests/preferential_rates/__init__.py create mode 100644 reference_documents/tests/reference_document_versions/__init__.py create mode 100644 reference_documents/tests/reference_documents/__init__.py diff --git a/reference_documents/tests/alignment_reports/__init__.py b/reference_documents/tests/alignment_reports/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/reference_documents/tests/preferential_quotas/__init__.py b/reference_documents/tests/preferential_quotas/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/reference_documents/tests/preferential_rates/__init__.py b/reference_documents/tests/preferential_rates/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/reference_documents/tests/reference_document_versions/__init__.py b/reference_documents/tests/reference_document_versions/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/reference_documents/tests/reference_documents/__init__.py b/reference_documents/tests/reference_documents/__init__.py new file mode 100644 index 000000000..e69de29bb From 22b47f35ae0b19b359299cd479fc4321da9e88a9 Mon Sep 17 00:00:00 2001 From: Matthew McKenzie <97194636+mattjamc@users.noreply.github.com> Date: Thu, 29 Feb 2024 15:10:47 +0000 Subject: [PATCH 069/118] View and form tests for ref doc crud --- reference_documents/forms.py | 2 +- .../tests/reference_documents/test_forms.py | 26 ++- .../tests/reference_documents/test_views.py | 151 ++++++++++++++++++ .../views/reference_document_views.py | 6 +- workbaskets/tests/test_views.py | 2 +- 5 files changed, 179 insertions(+), 8 deletions(-) diff --git a/reference_documents/forms.py b/reference_documents/forms.py index 70d0fc78f..f40344e3e 100644 --- a/reference_documents/forms.py +++ b/reference_documents/forms.py @@ -84,7 +84,7 @@ class ReferenceDocumentCreateUpdateForm(forms.ModelForm): error_messages={ "required": "An area ID is required", "unique": "A Reference Document with this area ID already exists", - "max_length": "The area ID must be 2 characters long", + "max_length": "The area ID can be at most 4 characters long", }, ) diff --git a/reference_documents/tests/reference_documents/test_forms.py b/reference_documents/tests/reference_documents/test_forms.py index 419205863..6c43ed88f 100644 --- a/reference_documents/tests/reference_documents/test_forms.py +++ b/reference_documents/tests/reference_documents/test_forms.py @@ -1,11 +1,12 @@ import pytest -# from reference_documents.tests import factories from reference_documents import forms +from reference_documents.tests import factories pytestmark = pytest.mark.django_db +@pytest.mark.reference_documents def test_ref_doc_create_update_form_valid_data(): """Test that ReferenceDocumentCreateUpdateForm is valid when completed correctly.""" @@ -15,6 +16,7 @@ def test_ref_doc_create_update_form_valid_data(): assert form.is_valid() +@pytest.mark.reference_documents def test_ref_doc_create_update_form_invalid_data(): """Test that ReferenceDocumentCreateUpdateForm is invalid when not complete correctly.""" @@ -23,19 +25,37 @@ def test_ref_doc_create_update_form_invalid_data(): assert "A Reference Document title is required" in form.errors["title"] assert "An area ID is required" in form.errors["area_id"] - # factories.ReferenceDocumentFactory.create(title="Reference document for XY", area_id="XY") + factories.ReferenceDocumentFactory.create( + title="Reference document for XY", + area_id="XY", + ) data = {"title": "Reference document for XY", "area_id": "VWXYZ"} form = forms.ReferenceDocumentCreateUpdateForm(data=data) assert not form.is_valid() - assert "The area ID must be 2 characters long" in form.errors["area_id"] + assert "The area ID can be at most 4 characters long" in form.errors["area_id"] assert "A Reference Document with this title already exists" in form.errors["title"] +@pytest.mark.reference_documents def test_ref_doc_delete_form_valid(): """Test that ReferenceDocumentDeleteForm is valid for a reference document with no versions.""" + ref_doc = factories.ReferenceDocumentFactory.create( + title="Reference document for XY", + area_id="XY", + ) + form = forms.ReferenceDocumentDeleteForm(data={}, instance=ref_doc) + assert form.is_valid() +@pytest.mark.reference_documents def test_ref_doc_delete_form_invalid(): """Test that ReferenceDocumentDeleteForm is invalid for a reference document with versions.""" + ref_doc = factories.ReferenceDocumentFactory.create( + title="Reference document for XY", + area_id="XY", + ) + factories.ReferenceDocumentVersionFactory(reference_document=ref_doc) + form = forms.ReferenceDocumentDeleteForm(data={}, instance=ref_doc) + assert not form.is_valid() diff --git a/reference_documents/tests/reference_documents/test_views.py b/reference_documents/tests/reference_documents/test_views.py index a7057c2cc..536b7dc54 100644 --- a/reference_documents/tests/reference_documents/test_views.py +++ b/reference_documents/tests/reference_documents/test_views.py @@ -1,3 +1,154 @@ import pytest +from bs4 import BeautifulSoup +from django.contrib.auth.models import Permission +from django.urls import reverse + +from reference_documents.models import ReferenceDocument +from reference_documents.tests import factories pytestmark = pytest.mark.django_db + + +@pytest.mark.reference_documents +def test_ref_doc_create_creates_object_and_redirects(valid_user, client): + """Tests that posting the reference document create form adds the new + reference document to the database and redirects to the confirm-create + page.""" + valid_user.user_permissions.add( + Permission.objects.get(codename="add_referencedocument"), + ) + client.force_login(valid_user) + create_url = reverse("reference_documents:create") + form_data = { + "title": "Reference document for XY", + "area_id": "XY", + } + response = client.post(create_url, form_data) + assert response.status_code == 302 + + ref_doc = ReferenceDocument.objects.get(title=form_data["title"]) + assert ref_doc + assert response.url == reverse( + "reference_documents:confirm-create", + kwargs={"pk": ref_doc.pk}, + ) + + +@pytest.mark.reference_documents +def test_ref_doc_edit_updates_ref_doc_object(valid_user, client): + """Tests that posting the reference document edit form updates the reference + document and redirects to the confirm-update page.""" + valid_user.user_permissions.add( + Permission.objects.get(codename="change_referencedocument"), + ) + client.force_login(valid_user) + + ref_doc = factories.ReferenceDocumentFactory.create() + + edit_url = reverse( + "reference_documents:update", + kwargs={"pk": ref_doc.pk}, + ) + + new_title = "Updated title for this reference document" + new_area_id = "XY" + form_data = { + "title": new_title, + "area_id": new_area_id, + } + + assert not ref_doc.title == new_title + assert not ref_doc.area_id == new_area_id + + response = client.get(edit_url) + assert response.status_code == 200 + + response = client.post(edit_url, form_data) + assert response.status_code == 302 + assert response.url == reverse( + "reference_documents:confirm-update", + kwargs={"pk": ref_doc.pk}, + ) + + ref_doc.refresh_from_db() + assert ref_doc.title == new_title + assert ref_doc.area_id == new_area_id + + +@pytest.mark.reference_documents +def test_successfully_delete_ref_doc(valid_user, client): + """Tests that posting the reference document delete form deletes the + reference document and redirects to the confirm-delete page.""" + valid_user.user_permissions.add( + Permission.objects.get(codename="delete_referencedocument"), + ) + client.force_login(valid_user) + + ref_doc = factories.ReferenceDocumentFactory.create() + assert ReferenceDocument.objects.filter(pk=ref_doc.pk) + delete_url = reverse( + "reference_documents:delete", + kwargs={"pk": ref_doc.pk}, + ) + + response = client.get(delete_url) + page = BeautifulSoup(response.content, "html.parser") + assert response.status_code == 200 + assert ( + f"Delete Reference Document {ref_doc.area_id}" in page.select("main h1")[0].text + ) + + response = client.post(delete_url) + assert response.status_code == 302 + assert response.url == reverse( + "reference_documents:confirm-delete", + kwargs={"deleted_pk": ref_doc.pk}, + ) + assert not ReferenceDocument.objects.filter(pk=ref_doc.pk) + + +@pytest.mark.reference_documents +def test_delete_ref_doc_with_versions(valid_user, client): + """Test that deleting a reference document with versions does not work.""" + valid_user.user_permissions.add( + Permission.objects.get(codename="delete_referencedocument"), + ) + client.force_login(valid_user) + + ref_doc = factories.ReferenceDocumentFactory.create() + factories.ReferenceDocumentVersionFactory(reference_document=ref_doc) + + delete_url = reverse( + "reference_documents:delete", + kwargs={"pk": ref_doc.pk}, + ) + + response = client.get(delete_url) + assert response.status_code == 200 + + response = client.post(delete_url) + assert response.status_code == 200 + assert ( + f"Reference Document {ref_doc.area_id} cannot be deleted as it has active versions." + in str(response.content) + ) + assert ReferenceDocument.objects.filter(pk=ref_doc.pk) + + +@pytest.mark.reference_documents +def test_ref_doc_crud_without_permission(valid_user_client): + # TODO: potentially update this if the permissions for reference doc behaviour changes + ref_doc = factories.ReferenceDocumentFactory.create() + create_url = reverse("reference_documents:create") + edit_url = reverse("reference_documents:update", kwargs={"pk": ref_doc.pk}) + delete_url = reverse("reference_documents:delete", kwargs={"pk": ref_doc.pk}) + form_data = { + "title": "Reference document for XY", + "area_id": "XY", + } + response = valid_user_client.post(create_url, form_data) + assert response.status_code == 403 + response = valid_user_client.post(edit_url, form_data) + assert response.status_code == 403 + response = valid_user_client.post(delete_url) + assert response.status_code == 403 diff --git a/reference_documents/views/reference_document_views.py b/reference_documents/views/reference_document_views.py index 1bdc1dd79..3a466f8d9 100644 --- a/reference_documents/views/reference_document_views.py +++ b/reference_documents/views/reference_document_views.py @@ -142,7 +142,7 @@ def get_context_data(self, *args, **kwargs): class ReferenceDocumentCreate(PermissionRequiredMixin, CreateView): template_name = "reference_documents/create.jinja" - permission_required = "reference_documents.edit_reference_document" + permission_required = "reference_documents.add_referencedocument" form_class = forms.ReferenceDocumentCreateUpdateForm def get_success_url(self): @@ -154,7 +154,7 @@ def get_success_url(self): class ReferenceDocumentUpdate(PermissionRequiredMixin, UpdateView): model = models.ReferenceDocument - permission_required = "reference_documents.edit_reference_document" + permission_required = "reference_documents.change_referencedocument" template_name = "reference_documents/update.jinja" form_class = forms.ReferenceDocumentCreateUpdateForm @@ -168,7 +168,7 @@ def get_success_url(self): class ReferenceDocumentDelete(PermissionRequiredMixin, FormMixin, DeleteView): form_class = forms.ReferenceDocumentDeleteForm model = ReferenceDocument - permission_required = "reference_documents.edit_reference_document" + permission_required = "reference_documents.delete_referencedocument" template_name = "reference_documents/delete.jinja" # TODO: Update this to get rid of FormMixin with Django 4.2 as no need to overwrite the post anymore diff --git a/workbaskets/tests/test_views.py b/workbaskets/tests/test_views.py index 700814ce4..ec9fc7c60 100644 --- a/workbaskets/tests/test_views.py +++ b/workbaskets/tests/test_views.py @@ -2080,7 +2080,7 @@ def test_require_current_workbasket_redirect(workbasket_factory, client, valid_u editing state.""" client.force_login(valid_user) - valid_user.current_workbasket == workbasket_factory() + valid_user.current_workbasket = workbasket_factory() valid_user.save() # view that has require_current_workbasket decorator From fc1d3f02dd3e889885e28036487a51ff96911a2b Mon Sep 17 00:00:00 2001 From: Matthew McKenzie <97194636+mattjamc@users.noreply.github.com> Date: Thu, 29 Feb 2024 15:41:07 +0000 Subject: [PATCH 070/118] Remove init files --- reference_documents/tests/alignment_reports/__init__.py | 0 reference_documents/tests/preferential_quotas/__init__.py | 0 reference_documents/tests/preferential_rates/__init__.py | 0 reference_documents/tests/reference_document_versions/__init__.py | 0 reference_documents/tests/reference_documents/__init__.py | 0 5 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 reference_documents/tests/alignment_reports/__init__.py delete mode 100644 reference_documents/tests/preferential_quotas/__init__.py delete mode 100644 reference_documents/tests/preferential_rates/__init__.py delete mode 100644 reference_documents/tests/reference_document_versions/__init__.py delete mode 100644 reference_documents/tests/reference_documents/__init__.py diff --git a/reference_documents/tests/alignment_reports/__init__.py b/reference_documents/tests/alignment_reports/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/reference_documents/tests/preferential_quotas/__init__.py b/reference_documents/tests/preferential_quotas/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/reference_documents/tests/preferential_rates/__init__.py b/reference_documents/tests/preferential_rates/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/reference_documents/tests/reference_document_versions/__init__.py b/reference_documents/tests/reference_document_versions/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/reference_documents/tests/reference_documents/__init__.py b/reference_documents/tests/reference_documents/__init__.py deleted file mode 100644 index e69de29bb..000000000 From a4b461bb665d931f3c0113f1e01caf34e1f094f2 Mon Sep 17 00:00:00 2001 From: Matthew McKenzie <97194636+mattjamc@users.noreply.github.com> Date: Thu, 29 Feb 2024 15:54:04 +0000 Subject: [PATCH 071/118] Add reference doc form and view tests --- .../tests/test_reference_documents_forms.py | 58 +++++++ .../tests/test_reference_documents_views.py | 151 ++++++++++++++++++ 2 files changed, 209 insertions(+) diff --git a/reference_documents/tests/test_reference_documents_forms.py b/reference_documents/tests/test_reference_documents_forms.py index a7057c2cc..6c43ed88f 100644 --- a/reference_documents/tests/test_reference_documents_forms.py +++ b/reference_documents/tests/test_reference_documents_forms.py @@ -1,3 +1,61 @@ import pytest +from reference_documents import forms +from reference_documents.tests import factories + pytestmark = pytest.mark.django_db + + +@pytest.mark.reference_documents +def test_ref_doc_create_update_form_valid_data(): + """Test that ReferenceDocumentCreateUpdateForm is valid when completed + correctly.""" + data = {"title": "Reference document for XY", "area_id": "XY"} + form = forms.ReferenceDocumentCreateUpdateForm(data=data) + + assert form.is_valid() + + +@pytest.mark.reference_documents +def test_ref_doc_create_update_form_invalid_data(): + """Test that ReferenceDocumentCreateUpdateForm is invalid when not complete + correctly.""" + form = forms.ReferenceDocumentCreateUpdateForm(data={}) + assert not form.is_valid() + assert "A Reference Document title is required" in form.errors["title"] + assert "An area ID is required" in form.errors["area_id"] + + factories.ReferenceDocumentFactory.create( + title="Reference document for XY", + area_id="XY", + ) + data = {"title": "Reference document for XY", "area_id": "VWXYZ"} + form = forms.ReferenceDocumentCreateUpdateForm(data=data) + assert not form.is_valid() + assert "The area ID can be at most 4 characters long" in form.errors["area_id"] + assert "A Reference Document with this title already exists" in form.errors["title"] + + +@pytest.mark.reference_documents +def test_ref_doc_delete_form_valid(): + """Test that ReferenceDocumentDeleteForm is valid for a reference document + with no versions.""" + ref_doc = factories.ReferenceDocumentFactory.create( + title="Reference document for XY", + area_id="XY", + ) + form = forms.ReferenceDocumentDeleteForm(data={}, instance=ref_doc) + assert form.is_valid() + + +@pytest.mark.reference_documents +def test_ref_doc_delete_form_invalid(): + """Test that ReferenceDocumentDeleteForm is invalid for a reference document + with versions.""" + ref_doc = factories.ReferenceDocumentFactory.create( + title="Reference document for XY", + area_id="XY", + ) + factories.ReferenceDocumentVersionFactory(reference_document=ref_doc) + form = forms.ReferenceDocumentDeleteForm(data={}, instance=ref_doc) + assert not form.is_valid() diff --git a/reference_documents/tests/test_reference_documents_views.py b/reference_documents/tests/test_reference_documents_views.py index a7057c2cc..536b7dc54 100644 --- a/reference_documents/tests/test_reference_documents_views.py +++ b/reference_documents/tests/test_reference_documents_views.py @@ -1,3 +1,154 @@ import pytest +from bs4 import BeautifulSoup +from django.contrib.auth.models import Permission +from django.urls import reverse + +from reference_documents.models import ReferenceDocument +from reference_documents.tests import factories pytestmark = pytest.mark.django_db + + +@pytest.mark.reference_documents +def test_ref_doc_create_creates_object_and_redirects(valid_user, client): + """Tests that posting the reference document create form adds the new + reference document to the database and redirects to the confirm-create + page.""" + valid_user.user_permissions.add( + Permission.objects.get(codename="add_referencedocument"), + ) + client.force_login(valid_user) + create_url = reverse("reference_documents:create") + form_data = { + "title": "Reference document for XY", + "area_id": "XY", + } + response = client.post(create_url, form_data) + assert response.status_code == 302 + + ref_doc = ReferenceDocument.objects.get(title=form_data["title"]) + assert ref_doc + assert response.url == reverse( + "reference_documents:confirm-create", + kwargs={"pk": ref_doc.pk}, + ) + + +@pytest.mark.reference_documents +def test_ref_doc_edit_updates_ref_doc_object(valid_user, client): + """Tests that posting the reference document edit form updates the reference + document and redirects to the confirm-update page.""" + valid_user.user_permissions.add( + Permission.objects.get(codename="change_referencedocument"), + ) + client.force_login(valid_user) + + ref_doc = factories.ReferenceDocumentFactory.create() + + edit_url = reverse( + "reference_documents:update", + kwargs={"pk": ref_doc.pk}, + ) + + new_title = "Updated title for this reference document" + new_area_id = "XY" + form_data = { + "title": new_title, + "area_id": new_area_id, + } + + assert not ref_doc.title == new_title + assert not ref_doc.area_id == new_area_id + + response = client.get(edit_url) + assert response.status_code == 200 + + response = client.post(edit_url, form_data) + assert response.status_code == 302 + assert response.url == reverse( + "reference_documents:confirm-update", + kwargs={"pk": ref_doc.pk}, + ) + + ref_doc.refresh_from_db() + assert ref_doc.title == new_title + assert ref_doc.area_id == new_area_id + + +@pytest.mark.reference_documents +def test_successfully_delete_ref_doc(valid_user, client): + """Tests that posting the reference document delete form deletes the + reference document and redirects to the confirm-delete page.""" + valid_user.user_permissions.add( + Permission.objects.get(codename="delete_referencedocument"), + ) + client.force_login(valid_user) + + ref_doc = factories.ReferenceDocumentFactory.create() + assert ReferenceDocument.objects.filter(pk=ref_doc.pk) + delete_url = reverse( + "reference_documents:delete", + kwargs={"pk": ref_doc.pk}, + ) + + response = client.get(delete_url) + page = BeautifulSoup(response.content, "html.parser") + assert response.status_code == 200 + assert ( + f"Delete Reference Document {ref_doc.area_id}" in page.select("main h1")[0].text + ) + + response = client.post(delete_url) + assert response.status_code == 302 + assert response.url == reverse( + "reference_documents:confirm-delete", + kwargs={"deleted_pk": ref_doc.pk}, + ) + assert not ReferenceDocument.objects.filter(pk=ref_doc.pk) + + +@pytest.mark.reference_documents +def test_delete_ref_doc_with_versions(valid_user, client): + """Test that deleting a reference document with versions does not work.""" + valid_user.user_permissions.add( + Permission.objects.get(codename="delete_referencedocument"), + ) + client.force_login(valid_user) + + ref_doc = factories.ReferenceDocumentFactory.create() + factories.ReferenceDocumentVersionFactory(reference_document=ref_doc) + + delete_url = reverse( + "reference_documents:delete", + kwargs={"pk": ref_doc.pk}, + ) + + response = client.get(delete_url) + assert response.status_code == 200 + + response = client.post(delete_url) + assert response.status_code == 200 + assert ( + f"Reference Document {ref_doc.area_id} cannot be deleted as it has active versions." + in str(response.content) + ) + assert ReferenceDocument.objects.filter(pk=ref_doc.pk) + + +@pytest.mark.reference_documents +def test_ref_doc_crud_without_permission(valid_user_client): + # TODO: potentially update this if the permissions for reference doc behaviour changes + ref_doc = factories.ReferenceDocumentFactory.create() + create_url = reverse("reference_documents:create") + edit_url = reverse("reference_documents:update", kwargs={"pk": ref_doc.pk}) + delete_url = reverse("reference_documents:delete", kwargs={"pk": ref_doc.pk}) + form_data = { + "title": "Reference document for XY", + "area_id": "XY", + } + response = valid_user_client.post(create_url, form_data) + assert response.status_code == 403 + response = valid_user_client.post(edit_url, form_data) + assert response.status_code == 403 + response = valid_user_client.post(delete_url) + assert response.status_code == 403 From 290a3da2a157dbff0a41b76f6418287f71589ec9 Mon Sep 17 00:00:00 2001 From: Matthew McKenzie <97194636+mattjamc@users.noreply.github.com> Date: Fri, 1 Mar 2024 12:14:15 +0000 Subject: [PATCH 072/118] Move get_area_name_by_id to model --- reference_documents/models.py | 14 ++++++++++++++ .../views/reference_document_views.py | 19 +++---------------- 2 files changed, 17 insertions(+), 16 deletions(-) diff --git a/reference_documents/models.py b/reference_documents/models.py index 3ba9970e2..de40c3719 100644 --- a/reference_documents/models.py +++ b/reference_documents/models.py @@ -39,6 +39,20 @@ class ReferenceDocument(models.Model): unique=True, ) + def get_area_name_by_area_id(self): + from geo_areas.models import GeographicalAreaDescription + + description = ( + GeographicalAreaDescription.objects.latest_approved() + .filter(described_geographicalarea__area_id=self.area_id) + .order_by("-validity_start") + .first() + ) + if description: + return description.description + else: + return f"{self.area_id} (unknown description)" + class ReferenceDocumentVersion(models.Model): created_at = models.DateTimeField(auto_now_add=True) diff --git a/reference_documents/views/reference_document_views.py b/reference_documents/views/reference_document_views.py index 3a466f8d9..cf5fd8ac4 100644 --- a/reference_documents/views/reference_document_views.py +++ b/reference_documents/views/reference_document_views.py @@ -9,7 +9,6 @@ from django.views.generic import UpdateView from django.views.generic.edit import FormMixin -from geo_areas.models import GeographicalAreaDescription from reference_documents import forms from reference_documents import models from reference_documents.models import ReferenceDocument @@ -22,29 +21,17 @@ class ReferenceDocumentList(PermissionRequiredMixin, ListView): permission_required = "reference_documents.view_reference_document" model = ReferenceDocument - def get_name_by_area_id(self, area_id): - description = ( - GeographicalAreaDescription.objects.latest_approved() - .filter(described_geographicalarea__area_id=area_id) - .order_by("-validity_start") - .first() - ) - if description: - return description.description - else: - return f"{area_id} (unknown description)" - def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) reference_documents = [] - for reference in ReferenceDocument.objects.all().order_by("area_id"): + for reference in context["object_list"].order_by("area_id"): if reference.reference_document_versions.count() == 0: reference_documents.append( [ {"text": "None"}, { - "text": f"{reference.area_id} - ({self.get_name_by_area_id(reference.area_id)})", + "text": f"{reference.area_id} - ({reference.get_area_name_by_area_id()})", }, {"text": 0}, {"text": 0}, @@ -61,7 +48,7 @@ def get_context_data(self, **kwargs): [ {"text": reference.reference_document_versions.last().version}, { - "text": f"{reference.area_id} - ({self.get_name_by_area_id(reference.area_id)})", + "text": f"{reference.area_id} - ({reference.get_area_name_by_area_id()})", }, { "text": reference.reference_document_versions.last().preferential_rates.count(), From 5cb642f005d05eb625d15336f29c4e4cc04fc140 Mon Sep 17 00:00:00 2001 From: Doug Mills Date: Fri, 1 Mar 2024 12:29:22 +0000 Subject: [PATCH 073/118] add basic tests for models --- package-lock.json | 169 ++---------------- reference_documents/forms.py | 15 +- .../edit_preferential_rates.jinja | 18 ++ .../preferential_rates/edit.jinja | 9 - .../tests/test_preferential_quotas_views.py | 27 +++ .../tests/test_preferential_rates_forms.py | 41 +++++ .../tests/test_preferential_rates_views.py | 78 ++++++++ .../views/preferential_quotas.py | 2 +- .../views/preferential_rates.py | 10 +- 9 files changed, 194 insertions(+), 175 deletions(-) create mode 100644 reference_documents/jinja2/reference_documents/edit_preferential_rates.jinja delete mode 100644 reference_documents/jinja2/reference_documents/preferential_rates/edit.jinja diff --git a/package-lock.json b/package-lock.json index 3a72fcc25..1231a986b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,8 @@ "version": "0.1.0", "dependencies": { "@babel/core": "^7.23.2", + "@babel/preset-env": "^7.23.7", + "@babel/preset-react": "^7.23.3", "@types/styled-components": "^5.1.29", "accessible-autocomplete": "^2.0.3", "ansi-regex": "^6.0.1", @@ -32,15 +34,12 @@ "webpack-cli": "^4.7.2" }, "devDependencies": { - "@babel/preset-env": "^7.23.7", - "@babel/preset-react": "^7.23.3", "@testing-library/jest-dom": "^6.2.1", "@testing-library/react": "^14.1.2", "babel-jest": "^29.7.0", "jest": "^29.7.0", "jest-environment-jsdom": "^29.7.0", - "react-test-renderer": "^18.2.0", - "webpack-cli": "^4.7.2" + "react-test-renderer": "^18.2.0" }, "engines": { "node": "^20.10.0", @@ -132,7 +131,6 @@ "version": "7.22.5", "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.22.5.tgz", "integrity": "sha512-LvBTxu8bQSQkcyKOU+a1btnNFQ1dMAd0R6PyW3arXes06F6QLWLIrd681bxRPIXlrMGR3XYnW9JyML7dP3qgxg==", - "dev": true, "dependencies": { "@babel/types": "^7.22.5" }, @@ -144,7 +142,6 @@ "version": "7.22.15", "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.22.15.tgz", "integrity": "sha512-QkBXwGgaoC2GtGZRoma6kv7Szfv06khvhFav67ZExau2RaXzy8MpHSMO2PNoP2XtmQphJQRHFfg77Bq731Yizw==", - "dev": true, "dependencies": { "@babel/types": "^7.22.15" }, @@ -171,7 +168,6 @@ "version": "7.23.7", "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.23.7.tgz", "integrity": "sha512-xCoqR/8+BoNnXOY7RVSgv6X+o7pmT5q1d+gGcRlXYkI+9B31glE4jeejhKVpA04O1AtzOt7OSQ6VYKP5FcRl9g==", - "dev": true, "dependencies": { "@babel/helper-annotate-as-pure": "^7.22.5", "@babel/helper-environment-visitor": "^7.22.20", @@ -194,7 +190,6 @@ "version": "7.22.15", "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.22.15.tgz", "integrity": "sha512-29FkPLFjn4TPEa3RE7GpW+qbE8tlsu3jntNYNfcGsc49LphF1PQIiD+vMZ1z1xVOKt+93khA9tc2JBs3kBjA7w==", - "dev": true, "dependencies": { "@babel/helper-annotate-as-pure": "^7.22.5", "regexpu-core": "^5.3.1", @@ -211,7 +206,6 @@ "version": "0.4.4", "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.4.4.tgz", "integrity": "sha512-QcJMILQCu2jm5TFPGA3lCpJJTeEP+mqeXooG/NZbg/h5FTFi6V0+99ahlRsW8/kRLyb24LZVCCiclDedhLKcBA==", - "dev": true, "dependencies": { "@babel/helper-compilation-targets": "^7.22.6", "@babel/helper-plugin-utils": "^7.22.5", @@ -258,7 +252,6 @@ "version": "7.23.0", "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.23.0.tgz", "integrity": "sha512-6gfrPwh7OuT6gZyJZvd6WbTfrqAo7vm4xCzAXOusKqq/vWdKXphTpj5klHKNmRUU6/QRGlBsyU9mAIPaWHlqJA==", - "dev": true, "dependencies": { "@babel/types": "^7.23.0" }, @@ -299,7 +292,6 @@ "version": "7.22.5", "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.22.5.tgz", "integrity": "sha512-HBwaojN0xFRx4yIvpwGqxiV2tUfl7401jlok564NgB9EHS1y6QT17FmKWm4ztqjeVdXLuC4fSvHc5ePpQjoTbw==", - "dev": true, "dependencies": { "@babel/types": "^7.22.5" }, @@ -311,7 +303,6 @@ "version": "7.22.5", "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.22.5.tgz", "integrity": "sha512-uLls06UVKgFG9QD4OeFYLEGteMIAa5kpTPcFL28yuCIIzsf6ZyKZMllKVOCZFhiZ5ptnwX4mtKdWCBE/uT4amg==", - "dev": true, "engines": { "node": ">=6.9.0" } @@ -320,7 +311,6 @@ "version": "7.22.20", "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.22.20.tgz", "integrity": "sha512-pBGyV4uBqOns+0UvhsTO8qgl8hO89PmiDYv+/COyp1aeMcmfrfruz+/nCMFiYyFF/Knn0yfrC85ZzNFjembFTw==", - "dev": true, "dependencies": { "@babel/helper-annotate-as-pure": "^7.22.5", "@babel/helper-environment-visitor": "^7.22.20", @@ -337,7 +327,6 @@ "version": "7.22.20", "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.22.20.tgz", "integrity": "sha512-qsW0In3dbwQUbK8kejJ4R7IHVGwHJlV6lpG6UA7a9hSa2YEiAib+N1T2kr6PEeUT+Fl7najmSOS6SmAwCHK6Tw==", - "dev": true, "dependencies": { "@babel/helper-environment-visitor": "^7.22.20", "@babel/helper-member-expression-to-functions": "^7.22.15", @@ -365,7 +354,6 @@ "version": "7.22.5", "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.22.5.tgz", "integrity": "sha512-tK14r66JZKiC43p8Ki33yLBVJKlQDFoA8GYN67lWCDCqoL6EMMSuM9b+Iff2jHaM/RRFYl7K+iiru7hbRqNx8Q==", - "dev": true, "dependencies": { "@babel/types": "^7.22.5" }, @@ -412,7 +400,6 @@ "version": "7.22.20", "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.22.20.tgz", "integrity": "sha512-pms/UwkOpnQe/PDAEdV/d7dVCoBbB+R4FvYoHGZz+4VPcg7RtYy2KP7S2lbuWM6FCSgob5wshfGESbC/hzNXZw==", - "dev": true, "dependencies": { "@babel/helper-function-name": "^7.22.5", "@babel/template": "^7.22.15", @@ -463,7 +450,6 @@ "version": "7.23.3", "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.23.3.tgz", "integrity": "sha512-iRkKcCqb7iGnq9+3G6rZ+Ciz5VywC4XNRHe57lKM+jOeYAoR0lVqdeeDRfh0tQcTfw/+vBhHn926FmQhLtlFLQ==", - "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" }, @@ -478,7 +464,6 @@ "version": "7.23.3", "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.23.3.tgz", "integrity": "sha512-WwlxbfMNdVEpQjZmK5mhm7oSwD3dS6eU+Iwsi4Knl9wAletWem7kaRsGOG+8UEbRyqxY4SS5zvtfXwX+jMxUwQ==", - "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5", "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", @@ -495,7 +480,6 @@ "version": "7.23.7", "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.23.7.tgz", "integrity": "sha512-LlRT7HgaifEpQA1ZgLVOIJZZFVPWN5iReq/7/JixwBtwcoeVGDBD53ZV28rrsLYOZs1Y/EHhA8N/Z6aazHR8cw==", - "dev": true, "dependencies": { "@babel/helper-environment-visitor": "^7.22.20", "@babel/helper-plugin-utils": "^7.22.5" @@ -511,7 +495,6 @@ "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" }, @@ -523,7 +506,6 @@ "version": "7.8.4", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", - "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" }, @@ -547,7 +529,6 @@ "version": "7.12.13", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", - "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.12.13" }, @@ -559,7 +540,6 @@ "version": "7.14.5", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", - "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.14.5" }, @@ -574,7 +554,6 @@ "version": "7.8.3", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz", "integrity": "sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==", - "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" }, @@ -586,7 +565,6 @@ "version": "7.8.3", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-export-namespace-from/-/plugin-syntax-export-namespace-from-7.8.3.tgz", "integrity": "sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q==", - "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.8.3" }, @@ -598,7 +576,6 @@ "version": "7.23.3", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.23.3.tgz", "integrity": "sha512-lPgDSU+SJLK3xmFDTV2ZRQAiM7UuUjGidwBywFavObCiZc1BeAAcMtHJKUya92hPHO+at63JJPLygilZard8jw==", - "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" }, @@ -613,7 +590,6 @@ "version": "7.23.3", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.23.3.tgz", "integrity": "sha512-pawnE0P9g10xgoP7yKr6CK63K2FMsTE+FZidZO/1PwRdzmAPVs+HS1mAURUsgaoxammTJvULUdIkEK0gOcU2tA==", - "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" }, @@ -628,7 +604,6 @@ "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", - "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.10.4" }, @@ -640,7 +615,6 @@ "version": "7.8.3", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", - "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" }, @@ -652,7 +626,6 @@ "version": "7.22.5", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.22.5.tgz", "integrity": "sha512-gvyP4hZrgrs/wWMaocvxZ44Hw0b3W8Pe+cMxc8V1ULQ07oh8VNbIRaoD1LRZVTvD+0nieDKjfgKg89sD7rrKrg==", - "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" }, @@ -667,7 +640,6 @@ "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", - "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.10.4" }, @@ -679,7 +651,6 @@ "version": "7.8.3", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", - "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" }, @@ -691,7 +662,6 @@ "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", - "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.10.4" }, @@ -703,7 +673,6 @@ "version": "7.8.3", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", - "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" }, @@ -715,7 +684,6 @@ "version": "7.8.3", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", - "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" }, @@ -727,7 +695,6 @@ "version": "7.8.3", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", - "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" }, @@ -739,7 +706,6 @@ "version": "7.14.5", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", - "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.14.5" }, @@ -754,7 +720,6 @@ "version": "7.14.5", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", - "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.14.5" }, @@ -784,7 +749,6 @@ "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" @@ -800,7 +764,6 @@ "version": "7.23.3", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.23.3.tgz", "integrity": "sha512-NzQcQrzaQPkaEwoTm4Mhyl8jI1huEL/WWIEvudjTCMJ9aBZNpsJbMASx7EQECtQQPS/DcnFpo0FIh3LvEO9cxQ==", - "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" }, @@ -815,7 +778,6 @@ "version": "7.23.7", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.23.7.tgz", "integrity": "sha512-PdxEpL71bJp1byMG0va5gwQcXHxuEYC/BgI/e88mGTtohbZN28O5Yit0Plkkm/dBzCF/BxmbNcses1RH1T+urA==", - "dev": true, "dependencies": { "@babel/helper-environment-visitor": "^7.22.20", "@babel/helper-plugin-utils": "^7.22.5", @@ -833,7 +795,6 @@ "version": "7.23.3", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.23.3.tgz", "integrity": "sha512-A7LFsKi4U4fomjqXJlZg/u0ft/n8/7n7lpffUP/ZULx/DtV9SGlNKZolHH6PE8Xl1ngCc0M11OaeZptXVkfKSw==", - "dev": true, "dependencies": { "@babel/helper-module-imports": "^7.22.15", "@babel/helper-plugin-utils": "^7.22.5", @@ -850,7 +811,6 @@ "version": "7.23.3", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.23.3.tgz", "integrity": "sha512-vI+0sIaPIO6CNuM9Kk5VmXcMVRiOpDh7w2zZt9GXzmE/9KD70CUEVhvPR/etAeNK/FAEkhxQtXOzVF3EuRL41A==", - "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" }, @@ -865,7 +825,6 @@ "version": "7.23.4", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.23.4.tgz", "integrity": "sha512-0QqbP6B6HOh7/8iNR4CQU2Th/bbRtBp4KS9vcaZd1fZ0wSh5Fyssg0UCIHwxh+ka+pNDREbVLQnHCMHKZfPwfw==", - "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" }, @@ -880,7 +839,6 @@ "version": "7.23.3", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.23.3.tgz", "integrity": "sha512-uM+AN8yCIjDPccsKGlw271xjJtGii+xQIF/uMPS8H15L12jZTsLfF4o5vNO7d/oUguOyfdikHGc/yi9ge4SGIg==", - "dev": true, "dependencies": { "@babel/helper-create-class-features-plugin": "^7.22.15", "@babel/helper-plugin-utils": "^7.22.5" @@ -896,7 +854,6 @@ "version": "7.23.4", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.23.4.tgz", "integrity": "sha512-nsWu/1M+ggti1SOALj3hfx5FXzAY06fwPJsUZD4/A5e1bWi46VUIWtD+kOX6/IdhXGsXBWllLFDSnqSCdUNydQ==", - "dev": true, "dependencies": { "@babel/helper-create-class-features-plugin": "^7.22.15", "@babel/helper-plugin-utils": "^7.22.5", @@ -913,7 +870,6 @@ "version": "7.23.5", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.23.5.tgz", "integrity": "sha512-jvOTR4nicqYC9yzOHIhXG5emiFEOpappSJAl73SDSEDcybD+Puuze8Tnpb9p9qEyYup24tq891gkaygIFvWDqg==", - "dev": true, "dependencies": { "@babel/helper-annotate-as-pure": "^7.22.5", "@babel/helper-compilation-targets": "^7.22.15", @@ -936,7 +892,6 @@ "version": "7.23.3", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.23.3.tgz", "integrity": "sha512-dTj83UVTLw/+nbiHqQSFdwO9CbTtwq1DsDqm3CUEtDrZNET5rT5E6bIdTlOftDTDLMYxvxHNEYO4B9SLl8SLZw==", - "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5", "@babel/template": "^7.22.15" @@ -952,7 +907,6 @@ "version": "7.23.3", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.23.3.tgz", "integrity": "sha512-n225npDqjDIr967cMScVKHXJs7rout1q+tt50inyBCPkyZ8KxeI6d+GIbSBTT/w/9WdlWDOej3V9HE5Lgk57gw==", - "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" }, @@ -967,7 +921,6 @@ "version": "7.23.3", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.23.3.tgz", "integrity": "sha512-vgnFYDHAKzFaTVp+mneDsIEbnJ2Np/9ng9iviHw3P/KVcgONxpNULEW/51Z/BaFojG2GI2GwwXck5uV1+1NOYQ==", - "dev": true, "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.22.15", "@babel/helper-plugin-utils": "^7.22.5" @@ -983,7 +936,6 @@ "version": "7.23.3", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.23.3.tgz", "integrity": "sha512-RrqQ+BQmU3Oyav3J+7/myfvRCq7Tbz+kKLLshUmMwNlDHExbGL7ARhajvoBJEvc+fCguPPu887N+3RRXBVKZUA==", - "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" }, @@ -998,7 +950,6 @@ "version": "7.23.4", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.23.4.tgz", "integrity": "sha512-V6jIbLhdJK86MaLh4Jpghi8ho5fGzt3imHOBu/x0jlBaPYqDoWz4RDXjmMOfnh+JWNaQleEAByZLV0QzBT4YQQ==", - "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5", "@babel/plugin-syntax-dynamic-import": "^7.8.3" @@ -1014,7 +965,6 @@ "version": "7.23.3", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.23.3.tgz", "integrity": "sha512-5fhCsl1odX96u7ILKHBj4/Y8vipoqwsJMh4csSA8qFfxrZDEA4Ssku2DyNvMJSmZNOEBT750LfFPbtrnTP90BQ==", - "dev": true, "dependencies": { "@babel/helper-builder-binary-assignment-operator-visitor": "^7.22.15", "@babel/helper-plugin-utils": "^7.22.5" @@ -1030,7 +980,6 @@ "version": "7.23.4", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.23.4.tgz", "integrity": "sha512-GzuSBcKkx62dGzZI1WVgTWvkkz84FZO5TC5T8dl/Tht/rAla6Dg/Mz9Yhypg+ezVACf/rgDuQt3kbWEv7LdUDQ==", - "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5", "@babel/plugin-syntax-export-namespace-from": "^7.8.3" @@ -1046,7 +995,6 @@ "version": "7.23.6", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.23.6.tgz", "integrity": "sha512-aYH4ytZ0qSuBbpfhuofbg/e96oQ7U2w1Aw/UQmKT+1l39uEhUPoFS3fHevDc1G0OvewyDudfMKY1OulczHzWIw==", - "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5", "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5" @@ -1062,7 +1010,6 @@ "version": "7.23.3", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.23.3.tgz", "integrity": "sha512-I1QXp1LxIvt8yLaib49dRW5Okt7Q4oaxao6tFVKS/anCdEOMtYwWVKoiOA1p34GOWIZjUK0E+zCp7+l1pfQyiw==", - "dev": true, "dependencies": { "@babel/helper-compilation-targets": "^7.22.15", "@babel/helper-function-name": "^7.23.0", @@ -1079,7 +1026,6 @@ "version": "7.23.4", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.23.4.tgz", "integrity": "sha512-81nTOqM1dMwZ/aRXQ59zVubN9wHGqk6UtqRK+/q+ciXmRy8fSolhGVvG09HHRGo4l6fr/c4ZhXUQH0uFW7PZbg==", - "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5", "@babel/plugin-syntax-json-strings": "^7.8.3" @@ -1095,7 +1041,6 @@ "version": "7.23.3", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.23.3.tgz", "integrity": "sha512-wZ0PIXRxnwZvl9AYpqNUxpZ5BiTGrYt7kueGQ+N5FiQ7RCOD4cm8iShd6S6ggfVIWaJf2EMk8eRzAh52RfP4rQ==", - "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" }, @@ -1110,7 +1055,6 @@ "version": "7.23.4", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.23.4.tgz", "integrity": "sha512-Mc/ALf1rmZTP4JKKEhUwiORU+vcfarFVLfcFiolKUo6sewoxSEgl36ak5t+4WamRsNr6nzjZXQjM35WsU+9vbg==", - "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5", "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4" @@ -1126,7 +1070,6 @@ "version": "7.23.3", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.23.3.tgz", "integrity": "sha512-sC3LdDBDi5x96LA+Ytekz2ZPk8i/Ck+DEuDbRAll5rknJ5XRTSaPKEYwomLcs1AA8wg9b3KjIQRsnApj+q51Ag==", - "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" }, @@ -1141,7 +1084,6 @@ "version": "7.23.3", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.23.3.tgz", "integrity": "sha512-vJYQGxeKM4t8hYCKVBlZX/gtIY2I7mRGFNcm85sgXGMTBcoV3QdVtdpbcWEbzbfUIUZKwvgFT82mRvaQIebZzw==", - "dev": true, "dependencies": { "@babel/helper-module-transforms": "^7.23.3", "@babel/helper-plugin-utils": "^7.22.5" @@ -1157,7 +1099,6 @@ "version": "7.23.3", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.23.3.tgz", "integrity": "sha512-aVS0F65LKsdNOtcz6FRCpE4OgsP2OFnW46qNxNIX9h3wuzaNcSQsJysuMwqSibC98HPrf2vCgtxKNwS0DAlgcA==", - "dev": true, "dependencies": { "@babel/helper-module-transforms": "^7.23.3", "@babel/helper-plugin-utils": "^7.22.5", @@ -1174,7 +1115,6 @@ "version": "7.23.3", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.23.3.tgz", "integrity": "sha512-ZxyKGTkF9xT9YJuKQRo19ewf3pXpopuYQd8cDXqNzc3mUNbOME0RKMoZxviQk74hwzfQsEe66dE92MaZbdHKNQ==", - "dev": true, "dependencies": { "@babel/helper-hoist-variables": "^7.22.5", "@babel/helper-module-transforms": "^7.23.3", @@ -1192,7 +1132,6 @@ "version": "7.23.3", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.23.3.tgz", "integrity": "sha512-zHsy9iXX2nIsCBFPud3jKn1IRPWg3Ing1qOZgeKV39m1ZgIdpJqvlWVeiHBZC6ITRG0MfskhYe9cLgntfSFPIg==", - "dev": true, "dependencies": { "@babel/helper-module-transforms": "^7.23.3", "@babel/helper-plugin-utils": "^7.22.5" @@ -1208,7 +1147,6 @@ "version": "7.22.5", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.22.5.tgz", "integrity": "sha512-YgLLKmS3aUBhHaxp5hi1WJTgOUb/NCuDHzGT9z9WTt3YG+CPRhJs6nprbStx6DnWM4dh6gt7SU3sZodbZ08adQ==", - "dev": true, "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.22.5", "@babel/helper-plugin-utils": "^7.22.5" @@ -1224,7 +1162,6 @@ "version": "7.23.3", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.23.3.tgz", "integrity": "sha512-YJ3xKqtJMAT5/TIZnpAR3I+K+WaDowYbN3xyxI8zxx/Gsypwf9B9h0VB+1Nh6ACAAPRS5NSRje0uVv5i79HYGQ==", - "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" }, @@ -1239,7 +1176,6 @@ "version": "7.23.4", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.23.4.tgz", "integrity": "sha512-jHE9EVVqHKAQx+VePv5LLGHjmHSJR76vawFPTdlxR/LVJPfOEGxREQwQfjuZEOPTwG92X3LINSh3M40Rv4zpVA==", - "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5", "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3" @@ -1255,7 +1191,6 @@ "version": "7.23.4", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.23.4.tgz", "integrity": "sha512-mps6auzgwjRrwKEZA05cOwuDc9FAzoyFS4ZsG/8F43bTLf/TgkJg7QXOrPO1JO599iA3qgK9MXdMGOEC8O1h6Q==", - "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5", "@babel/plugin-syntax-numeric-separator": "^7.10.4" @@ -1271,7 +1206,6 @@ "version": "7.23.4", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.23.4.tgz", "integrity": "sha512-9x9K1YyeQVw0iOXJlIzwm8ltobIIv7j2iLyP2jIhEbqPRQ7ScNgwQufU2I0Gq11VjyG4gI4yMXt2VFags+1N3g==", - "dev": true, "dependencies": { "@babel/compat-data": "^7.23.3", "@babel/helper-compilation-targets": "^7.22.15", @@ -1290,7 +1224,6 @@ "version": "7.23.3", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.23.3.tgz", "integrity": "sha512-BwQ8q0x2JG+3lxCVFohg+KbQM7plfpBwThdW9A6TMtWwLsbDA01Ek2Zb/AgDN39BiZsExm4qrXxjk+P1/fzGrA==", - "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5", "@babel/helper-replace-supers": "^7.22.20" @@ -1306,7 +1239,6 @@ "version": "7.23.4", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.23.4.tgz", "integrity": "sha512-XIq8t0rJPHf6Wvmbn9nFxU6ao4c7WhghTR5WyV8SrJfUFzyxhCm4nhC+iAp3HFhbAKLfYpgzhJ6t4XCtVwqO5A==", - "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5", "@babel/plugin-syntax-optional-catch-binding": "^7.8.3" @@ -1322,7 +1254,6 @@ "version": "7.23.4", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.23.4.tgz", "integrity": "sha512-ZU8y5zWOfjM5vZ+asjgAPwDaBjJzgufjES89Rs4Lpq63O300R/kOz30WCLo6BxxX6QVEilwSlpClnG5cZaikTA==", - "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5", "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", @@ -1339,7 +1270,6 @@ "version": "7.23.3", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.23.3.tgz", "integrity": "sha512-09lMt6UsUb3/34BbECKVbVwrT9bO6lILWln237z7sLaWnMsTi7Yc9fhX5DLpkJzAGfaReXI22wP41SZmnAA3Vw==", - "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" }, @@ -1354,7 +1284,6 @@ "version": "7.23.3", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.23.3.tgz", "integrity": "sha512-UzqRcRtWsDMTLrRWFvUBDwmw06tCQH9Rl1uAjfh6ijMSmGYQ+fpdB+cnqRC8EMh5tuuxSv0/TejGL+7vyj+50g==", - "dev": true, "dependencies": { "@babel/helper-create-class-features-plugin": "^7.22.15", "@babel/helper-plugin-utils": "^7.22.5" @@ -1370,7 +1299,6 @@ "version": "7.23.4", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.23.4.tgz", "integrity": "sha512-9G3K1YqTq3F4Vt88Djx1UZ79PDyj+yKRnUy7cZGSMe+a7jkwD259uKKuUzQlPkGam7R+8RJwh5z4xO27fA1o2A==", - "dev": true, "dependencies": { "@babel/helper-annotate-as-pure": "^7.22.5", "@babel/helper-create-class-features-plugin": "^7.22.15", @@ -1388,7 +1316,6 @@ "version": "7.23.3", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.23.3.tgz", "integrity": "sha512-jR3Jn3y7cZp4oEWPFAlRsSWjxKe4PZILGBSd4nis1TsC5qeSpb+nrtihJuDhNI7QHiVbUaiXa0X2RZY3/TI6Nw==", - "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" }, @@ -1403,7 +1330,6 @@ "version": "7.23.3", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.23.3.tgz", "integrity": "sha512-GnvhtVfA2OAtzdX58FJxU19rhoGeQzyVndw3GgtdECQvQFXPEZIOVULHVZGAYmOgmqjXpVpfocAbSjh99V/Fqw==", - "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" }, @@ -1418,7 +1344,6 @@ "version": "7.22.15", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.22.15.tgz", "integrity": "sha512-oKckg2eZFa8771O/5vi7XeTvmM6+O9cxZu+kanTU7tD4sin5nO/G8jGJhq8Hvt2Z0kUoEDRayuZLaUlYl8QuGA==", - "dev": true, "dependencies": { "@babel/helper-annotate-as-pure": "^7.22.5", "@babel/helper-module-imports": "^7.22.15", @@ -1437,7 +1362,6 @@ "version": "7.22.5", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.22.5.tgz", "integrity": "sha512-bDhuzwWMuInwCYeDeMzyi7TaBgRQei6DqxhbyniL7/VG4RSS7HtSL2QbY4eESy1KJqlWt8g3xeEBGPuo+XqC8A==", - "dev": true, "dependencies": { "@babel/plugin-transform-react-jsx": "^7.22.5" }, @@ -1452,7 +1376,6 @@ "version": "7.23.3", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.23.3.tgz", "integrity": "sha512-qMFdSS+TUhB7Q/3HVPnEdYJDQIk57jkntAwSuz9xfSE4n+3I+vHYCli3HoHawN1Z3RfCz/y1zXA/JXjG6cVImQ==", - "dev": true, "dependencies": { "@babel/helper-annotate-as-pure": "^7.22.5", "@babel/helper-plugin-utils": "^7.22.5" @@ -1468,7 +1391,6 @@ "version": "7.23.3", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.23.3.tgz", "integrity": "sha512-KP+75h0KghBMcVpuKisx3XTu9Ncut8Q8TuvGO4IhY+9D5DFEckQefOuIsB/gQ2tG71lCke4NMrtIPS8pOj18BQ==", - "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5", "regenerator-transform": "^0.15.2" @@ -1484,7 +1406,6 @@ "version": "7.23.3", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.23.3.tgz", "integrity": "sha512-QnNTazY54YqgGxwIexMZva9gqbPa15t/x9VS+0fsEFWplwVpXYZivtgl43Z1vMpc1bdPP2PP8siFeVcnFvA3Cg==", - "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" }, @@ -1499,7 +1420,6 @@ "version": "7.23.3", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.23.3.tgz", "integrity": "sha512-ED2fgqZLmexWiN+YNFX26fx4gh5qHDhn1O2gvEhreLW2iI63Sqm4llRLCXALKrCnbN4Jy0VcMQZl/SAzqug/jg==", - "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" }, @@ -1514,7 +1434,6 @@ "version": "7.23.3", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.23.3.tgz", "integrity": "sha512-VvfVYlrlBVu+77xVTOAoxQ6mZbnIq5FM0aGBSFEcIh03qHf+zNqA4DC/3XMUozTg7bZV3e3mZQ0i13VB6v5yUg==", - "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5", "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5" @@ -1530,7 +1449,6 @@ "version": "7.23.3", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.23.3.tgz", "integrity": "sha512-HZOyN9g+rtvnOU3Yh7kSxXrKbzgrm5X4GncPY1QOquu7epga5MxKHVpYu2hvQnry/H+JjckSYRb93iNfsioAGg==", - "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" }, @@ -1545,7 +1463,6 @@ "version": "7.23.3", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.23.3.tgz", "integrity": "sha512-Flok06AYNp7GV2oJPZZcP9vZdszev6vPBkHLwxwSpaIqx75wn6mUd3UFWsSsA0l8nXAKkyCmL/sR02m8RYGeHg==", - "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" }, @@ -1560,7 +1477,6 @@ "version": "7.23.3", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.23.3.tgz", "integrity": "sha512-4t15ViVnaFdrPC74be1gXBSMzXk3B4Us9lP7uLRQHTFpV5Dvt33pn+2MyyNxmN3VTTm3oTrZVMUmuw3oBnQ2oQ==", - "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" }, @@ -1575,7 +1491,6 @@ "version": "7.23.3", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.23.3.tgz", "integrity": "sha512-OMCUx/bU6ChE3r4+ZdylEqAjaQgHAgipgW8nsCfu5pGqDcFytVd91AwRvUJSBZDz0exPGgnjoqhgRYLRjFZc9Q==", - "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" }, @@ -1590,7 +1505,6 @@ "version": "7.23.3", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.23.3.tgz", "integrity": "sha512-KcLIm+pDZkWZQAFJ9pdfmh89EwVfmNovFBcXko8szpBeF8z68kWIPeKlmSOkT9BXJxs2C0uk+5LxoxIv62MROA==", - "dev": true, "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.22.15", "@babel/helper-plugin-utils": "^7.22.5" @@ -1606,7 +1520,6 @@ "version": "7.23.3", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.23.3.tgz", "integrity": "sha512-wMHpNA4x2cIA32b/ci3AfwNgheiva2W0WUKWTK7vBHBhDKfPsc5cFGNWm69WBqpwd86u1qwZ9PWevKqm1A3yAw==", - "dev": true, "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.22.15", "@babel/helper-plugin-utils": "^7.22.5" @@ -1622,7 +1535,6 @@ "version": "7.23.3", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.23.3.tgz", "integrity": "sha512-W7lliA/v9bNR83Qc3q1ip9CQMZ09CcHDbHfbLRDNuAhn1Mvkr1ZNF7hPmztMQvtTGVLJ9m8IZqWsTkXOml8dbw==", - "dev": true, "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.22.15", "@babel/helper-plugin-utils": "^7.22.5" @@ -1638,7 +1550,6 @@ "version": "7.23.7", "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.23.7.tgz", "integrity": "sha512-SY27X/GtTz/L4UryMNJ6p4fH4nsgWbz84y9FE0bQeWJP6O5BhgVCt53CotQKHCOeXJel8VyhlhujhlltKms/CA==", - "dev": true, "dependencies": { "@babel/compat-data": "^7.23.5", "@babel/helper-compilation-targets": "^7.23.6", @@ -1732,7 +1643,6 @@ "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", @@ -1746,7 +1656,6 @@ "version": "7.23.3", "resolved": "https://registry.npmjs.org/@babel/preset-react/-/preset-react-7.23.3.tgz", "integrity": "sha512-tbkHOS9axH6Ysf2OUEqoSZ6T3Fa2SrNH6WTWSPBboxKzdxNc9qOICeLXkNG0ZEwbQ1HY8liwOce4aN/Ceyuq6w==", - "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5", "@babel/helper-validator-option": "^7.22.15", @@ -1765,8 +1674,7 @@ "node_modules/@babel/regjsgen": { "version": "0.8.0", "resolved": "https://registry.npmjs.org/@babel/regjsgen/-/regjsgen-0.8.0.tgz", - "integrity": "sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA==", - "dev": true + "integrity": "sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA==" }, "node_modules/@babel/runtime": { "version": "7.23.2", @@ -1835,7 +1743,6 @@ "version": "0.5.7", "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz", "integrity": "sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==", - "dev": true, "engines": { "node": ">=10.0.0" } @@ -4014,7 +3921,6 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/@webpack-cli/configtest/-/configtest-1.2.0.tgz", "integrity": "sha512-4FB8Tj6xyVkyqjj1OaTqCjXYULB9FMkqQ8yGrZjRDrYh0nOE+7Lhs45WioWQQMV+ceFlE368Ukhe6xdvJM9Egg==", - "dev": true, "peerDependencies": { "webpack": "4.x.x || 5.x.x", "webpack-cli": "4.x.x" @@ -4024,7 +3930,6 @@ "version": "1.5.0", "resolved": "https://registry.npmjs.org/@webpack-cli/info/-/info-1.5.0.tgz", "integrity": "sha512-e8tSXZpw2hPl2uMJY6fsMswaok5FdlGNRTktvFk2sD8RjH0hE2+XistawJx1vmKteh4NmGmNUrp+Tb2w+udPcQ==", - "dev": true, "dependencies": { "envinfo": "^7.7.3" }, @@ -4036,7 +3941,6 @@ "version": "1.7.0", "resolved": "https://registry.npmjs.org/@webpack-cli/serve/-/serve-1.7.0.tgz", "integrity": "sha512-oxnCNGj88fL+xzV+dacXs44HcDwf1ovs3AuEzvP7mqXw7fQntqIhQ1BRmynh4qEKQSSSRSWVyXRjmTbZIX9V2Q==", - "dev": true, "peerDependencies": { "webpack-cli": "4.x.x" }, @@ -4473,7 +4377,6 @@ "version": "0.4.7", "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.7.tgz", "integrity": "sha512-LidDk/tEGDfuHW2DWh/Hgo4rmnw3cduK6ZkOI1NPFceSK3n/yAGeOsNT7FLnSGHkXj3RHGSEVkN3FsCTY6w2CQ==", - "dev": true, "dependencies": { "@babel/compat-data": "^7.22.6", "@babel/helper-define-polyfill-provider": "^0.4.4", @@ -4487,7 +4390,6 @@ "version": "0.8.7", "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.8.7.tgz", "integrity": "sha512-KyDvZYxAzkC0Aj2dAPyDzi2Ym15e5JKZSK+maI7NAwSqofvuFglbSsxE7wUOvTg9oFVnHMzVzBKcqEb4PJgtOA==", - "dev": true, "dependencies": { "@babel/helper-define-polyfill-provider": "^0.4.4", "core-js-compat": "^3.33.1" @@ -4500,7 +4402,6 @@ "version": "0.5.4", "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.5.4.tgz", "integrity": "sha512-S/x2iOCvDaCASLYsOOgWOq4bCfKYVqvO/uxjkaYyZ3rVsVE3CeAI/c84NpyuBBymEgNvHgjEot3a9/Z/kXvqsg==", - "dev": true, "dependencies": { "@babel/helper-define-polyfill-provider": "^0.4.4" }, @@ -4839,7 +4740,6 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", - "dev": true, "dependencies": { "is-plain-object": "^2.0.4", "kind-of": "^6.0.2", @@ -4881,8 +4781,7 @@ "node_modules/colorette": { "version": "2.0.20", "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", - "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", - "dev": true + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==" }, "node_modules/combined-stream": { "version": "1.0.8", @@ -4921,7 +4820,6 @@ "version": "3.35.0", "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.35.0.tgz", "integrity": "sha512-5blwFAddknKeNgsjBzilkdQ0+YK8L1PfqPYq40NOYMYFSS38qj+hpTcLLWwpIwA2A5bje/x5jmVn2tzUMg9IVw==", - "dev": true, "dependencies": { "browserslist": "^4.22.2" }, @@ -5025,7 +4923,6 @@ "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", - "dev": true, "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", @@ -5410,7 +5307,6 @@ "version": "7.11.0", "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.11.0.tgz", "integrity": "sha512-G9/6xF1FPbIw0TtalAMaVPpiq2aDEuKLXM314jPVAO9r2fo2a4BLqMNkmRS7O/xPPZ+COAhGIz3ETvHEV3eUcg==", - "dev": true, "bin": { "envinfo": "dist/cli.js" }, @@ -5554,7 +5450,6 @@ "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" } @@ -5634,7 +5529,6 @@ "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" } @@ -5776,7 +5670,6 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dev": true, "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" @@ -5789,7 +5682,6 @@ "version": "5.0.2", "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", - "dev": true, "bin": { "flat": "cli.js" } @@ -5840,7 +5732,6 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "dev": true, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -6118,7 +6009,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz", "integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==", - "dev": true, "dependencies": { "function-bind": "^1.1.2" }, @@ -6223,7 +6113,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.1.0.tgz", "integrity": "sha512-ASB07uLtnDs1o6EHjKpX34BKYDSqnFerfTOJL2HvMqF70LnxpjkzDB8J44oT9pu4AMPkQwf8jl6szgvNd2tRIg==", - "dev": true, "dependencies": { "pkg-dir": "^4.2.0", "resolve-cwd": "^3.0.0" @@ -6290,7 +6179,6 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/interpret/-/interpret-2.2.0.tgz", "integrity": "sha512-Ju0Bz/cEia55xDwUWEa8+olFpCiQoypjnQySseKtmjNrnps3P+xfpUmGr90T7yjlVJmOtybRvPXhKMbHr+fWnw==", - "dev": true, "engines": { "node": ">= 0.10" } @@ -6430,7 +6318,6 @@ "version": "2.13.1", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==", - "dev": true, "dependencies": { "hasown": "^2.0.0" }, @@ -6552,7 +6439,6 @@ "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" }, @@ -6691,14 +6577,12 @@ "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" }, "node_modules/isobject": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -8832,7 +8716,6 @@ "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" } @@ -8894,7 +8777,6 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dev": true, "dependencies": { "p-locate": "^4.1.0" }, @@ -8921,8 +8803,7 @@ "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 + "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==" }, "node_modules/lodash.defaults": { "version": "4.2.0", @@ -9344,7 +9225,6 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dev": true, "dependencies": { "p-try": "^2.0.0" }, @@ -9359,7 +9239,6 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dev": true, "dependencies": { "p-limit": "^2.2.0" }, @@ -9371,7 +9250,6 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "dev": true, "engines": { "node": ">=6" } @@ -9427,7 +9305,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true, "engines": { "node": ">=8" } @@ -9445,7 +9322,6 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, "engines": { "node": ">=8" } @@ -9453,8 +9329,7 @@ "node_modules/path-parse": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" }, "node_modules/picocolors": { "version": "1.0.0", @@ -9485,7 +9360,6 @@ "version": "4.2.0", "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", - "dev": true, "dependencies": { "find-up": "^4.0.0" }, @@ -9824,7 +9698,6 @@ "version": "0.7.1", "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.7.1.tgz", "integrity": "sha512-/njmZ8s1wVeR6pjTZ+0nCnv8SpZNRMT2D1RLOJQESlYFDBvwpTA4KWJpZ+sBJ4+vhjILRcK7JIFdGCdxEAAitg==", - "dev": true, "dependencies": { "resolve": "^1.9.0" }, @@ -9848,14 +9721,12 @@ "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 + "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==" }, "node_modules/regenerate-unicode-properties": { "version": "10.1.1", "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.1.1.tgz", "integrity": "sha512-X007RyZLsCJVVrjgEFVpLUTZwyOZk3oiL75ZcuYjlIWd6rNJtOjkBwQc5AsRrpbKVkxN6sklw/k/9m2jJYOf8Q==", - "dev": true, "dependencies": { "regenerate": "^1.4.2" }, @@ -9872,7 +9743,6 @@ "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" } @@ -9898,7 +9768,6 @@ "version": "5.3.2", "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-5.3.2.tgz", "integrity": "sha512-RAM5FlZz+Lhmo7db9L298p2vHP5ZywrVXmVXpmAD9GuL5MPH6t9ROw1iA/wfHkQ76Qe7AaPF0nGuim96/IrQMQ==", - "dev": true, "dependencies": { "@babel/regjsgen": "^0.8.0", "regenerate": "^1.4.2", @@ -9915,7 +9784,6 @@ "version": "0.9.1", "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.9.1.tgz", "integrity": "sha512-dQUtn90WanSNl+7mQKcXAgZxvUe7Z0SqXlgzv0za4LwiUhyzBC58yQO3liFoUgu8GiJVInAhJjkj1N0EtQ5nkQ==", - "dev": true, "dependencies": { "jsesc": "~0.5.0" }, @@ -9927,7 +9795,6 @@ "version": "0.5.0", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", "integrity": "sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA==", - "dev": true, "bin": { "jsesc": "bin/jsesc" } @@ -9971,7 +9838,6 @@ "version": "1.22.8", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", - "dev": true, "dependencies": { "is-core-module": "^2.13.0", "path-parse": "^1.0.7", @@ -9988,7 +9854,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", - "dev": true, "dependencies": { "resolve-from": "^5.0.0" }, @@ -10000,7 +9865,6 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", - "dev": true, "engines": { "node": ">=8" } @@ -10179,7 +10043,6 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==", - "dev": true, "dependencies": { "kind-of": "^6.0.2" }, @@ -10196,7 +10059,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, "dependencies": { "shebang-regex": "^3.0.0" }, @@ -10208,7 +10070,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, "engines": { "node": ">=8" } @@ -10467,7 +10328,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "dev": true, "engines": { "node": ">= 0.4" }, @@ -10637,7 +10497,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz", "integrity": "sha512-yY5PpDlfVIU5+y/BSCxAJRBIS1Zc2dDG3Ujq+sR0U+JjUevW2JhocOF+soROYDSaAezOzOKuyyixhD6mBknSmQ==", - "dev": true, "engines": { "node": ">=4" } @@ -10646,7 +10505,6 @@ "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" @@ -10659,7 +10517,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.1.0.tgz", "integrity": "sha512-qxkjQt6qjg/mYscYMC0XKRn3Rh0wFPlfxB0xkt9CfyTvpX1Ra0+rAmdX2QyAobptSEvuy4RtpPRui6XkV+8wjA==", - "dev": true, "engines": { "node": ">=4" } @@ -10668,7 +10525,6 @@ "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" } @@ -10940,7 +10796,6 @@ "version": "4.10.0", "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-4.10.0.tgz", "integrity": "sha512-NLhDfH/h4O6UOy+0LSso42xvYypClINuMNBVVzX4vX98TmTaTUxwRbXdhucbFMd2qLaCTcLq/PdYrvi8onw90w==", - "dev": true, "dependencies": { "@discoveryjs/json-ext": "^0.5.0", "@webpack-cli/configtest": "^1.2.0", @@ -10987,7 +10842,6 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", - "dev": true, "engines": { "node": ">= 10" } @@ -10996,7 +10850,6 @@ "version": "5.10.0", "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.10.0.tgz", "integrity": "sha512-+4zXKdx7UnO+1jaN4l2lHVD+mFvnlZQP/6ljaJVb4SZiwIKeUnrT5l0gkT8z+n4hKpC+jpOv6O9R+gLtag7pSA==", - "dev": true, "dependencies": { "clone-deep": "^4.0.1", "flat": "^5.0.2", @@ -11027,7 +10880,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, "dependencies": { "isexe": "^2.0.0" }, @@ -11091,8 +10943,7 @@ "node_modules/wildcard": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.1.tgz", - "integrity": "sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==", - "dev": true + "integrity": "sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==" }, "node_modules/wrap-ansi": { "version": "7.0.0", diff --git a/reference_documents/forms.py b/reference_documents/forms.py index f40344e3e..4a33ed97f 100644 --- a/reference_documents/forms.py +++ b/reference_documents/forms.py @@ -5,6 +5,7 @@ from crispy_forms_gds.layout import Size from crispy_forms_gds.layout import Submit from django import forms +from django.core.exceptions import ValidationError from common.forms import ValidityPeriodForm from reference_documents import models @@ -12,7 +13,7 @@ from reference_documents.validators import commodity_code_validator -class PreferentialRateEditForm( +class PreferentialRateCreateUpdateForm( ValidityPeriodForm, forms.ModelForm, ): @@ -42,6 +43,18 @@ class Meta: }, ) + def clean_duty_rate(self): + data = self.cleaned_data["duty_rate"] + if len(data) < 1: + raise ValidationError("Duty Rate is not valid - it must have a value") + return data + + def clean_commodity_code(self): + data = self.cleaned_data["commodity_code"] + if len(data) != 10 or not data.isdigit(): + raise ValidationError("Commodity Code is not valid - it must be 10 digits") + return data + def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.init_layout() diff --git a/reference_documents/jinja2/reference_documents/edit_preferential_rates.jinja b/reference_documents/jinja2/reference_documents/edit_preferential_rates.jinja new file mode 100644 index 000000000..1c50f421b --- /dev/null +++ b/reference_documents/jinja2/reference_documents/edit_preferential_rates.jinja @@ -0,0 +1,18 @@ +{% extends "layouts/form.jinja" %} +{% from "components/breadcrumbs/macro.njk" import govukBreadcrumbs %} + +{% set page_title = "Edit Reference Document " ~ object.area_id ~ " details" %} + +{% block breadcrumb %} + {{ govukBreadcrumbs({ + "items": [{"text": "Home", "href": url("home")}, + {"text": "View reference documents", "href": url("reference_documents:index")}, + {"text": page_title}] + }) }} +{% endblock %} + +{% block form %} + {% call django_form() %} + {{ crispy(form) }} + {% endcall %} +{% endblock %} diff --git a/reference_documents/jinja2/reference_documents/preferential_rates/edit.jinja b/reference_documents/jinja2/reference_documents/preferential_rates/edit.jinja deleted file mode 100644 index 400da07a9..000000000 --- a/reference_documents/jinja2/reference_documents/preferential_rates/edit.jinja +++ /dev/null @@ -1,9 +0,0 @@ -{% extends 'layouts/form.jinja' %} - -{% set page_title = "Edit preferential duty rate" %} - -{% block form %} - {% call django_form() %} - {{ crispy(form) }} - {% endcall %} -{% endblock %} \ No newline at end of file diff --git a/reference_documents/tests/test_preferential_quotas_views.py b/reference_documents/tests/test_preferential_quotas_views.py index a7057c2cc..c29e56eb8 100644 --- a/reference_documents/tests/test_preferential_quotas_views.py +++ b/reference_documents/tests/test_preferential_quotas_views.py @@ -1,3 +1,30 @@ import pytest +from django.urls import reverse + +from reference_documents.tests import factories pytestmark = pytest.mark.django_db + + +class TestPreferentialQuotaEditView: + def test_get_without_permissions(self, valid_user_client): + pref_quota = factories.PreferentialQuotaFactory.create() + + response = valid_user_client.get( + reverse( + "reference_documents:preferential_quotas_edit", + kwargs={"pk": pref_quota.pk}, + ), + ) + assert response.status_code == 200 + + def test_get_with_permissions(self, superuser_client): + pref_quota = factories.PreferentialQuotaFactory.create() + + response = superuser_client.get( + reverse( + "reference_documents:preferential_quotas_edit", + kwargs={"pk": pref_quota.pk}, + ), + ) + assert response.status_code == 200 diff --git a/reference_documents/tests/test_preferential_rates_forms.py b/reference_documents/tests/test_preferential_rates_forms.py index a7057c2cc..e4d5412e4 100644 --- a/reference_documents/tests/test_preferential_rates_forms.py +++ b/reference_documents/tests/test_preferential_rates_forms.py @@ -1,3 +1,44 @@ import pytest +from reference_documents.forms import PreferentialRateCreateUpdateForm + pytestmark = pytest.mark.django_db + + +@pytest.mark.reference_documents +class TestPreferentialRateCreateUpdateForm: + def test_validation_valid(self): + form = PreferentialRateCreateUpdateForm( + data={ + "commodity_code": "0100000000", + "duty_rate": "10%", + "start_date_0": "1", + "start_date_1": "1", + "start_date_2": "2024", + "end_date": None, + }, + ) + + assert form.is_valid() + + def test_validation_no_comm_code(self): + form = PreferentialRateCreateUpdateForm( + data={ + "commodity_code": "", + "duty_rate": "", + "start_date_0": "1", + "start_date_1": "1", + "start_date_2": "2024", + "end_date": None, + }, + ) + + assert not form.is_valid() + assert "commodity_code" in form.errors.as_data().keys() + assert "duty_rate" in form.errors.as_data().keys() + assert "start_date" not in form.errors.as_data().keys() + assert "end_date" not in form.errors.as_data().keys() + + +class TestPreferentialRateDeleteForm: + pass diff --git a/reference_documents/tests/test_preferential_rates_views.py b/reference_documents/tests/test_preferential_rates_views.py index a7057c2cc..f7738b77c 100644 --- a/reference_documents/tests/test_preferential_rates_views.py +++ b/reference_documents/tests/test_preferential_rates_views.py @@ -1,3 +1,81 @@ import pytest +from django.contrib.auth.models import Permission +from django.urls import reverse + +from reference_documents.tests import factories pytestmark = pytest.mark.django_db + + +def test_get_without_permissions(valid_user, client): + valid_user.user_permissions.add( + Permission.objects.get(codename="change_preferentialrate"), + ) + client.force_login(valid_user) + pref_rate = factories.PreferentialRateFactory.create() + + response = client.get( + reverse( + "reference_documents:preferential_rates_edit", + kwargs={"pk": pref_rate.pk}, + ), + ) + + assert response.status_code == 200 + + +@pytest.mark.reference_documents +class TestPreferentialRateEditView: + def test_get_without_permissions(self, valid_user, client): + valid_user.user_permissions.add( + Permission.objects.get(codename="change_preferentialrate"), + ) + client.force_login(valid_user) + pref_rate = factories.PreferentialRateFactory.create() + + response = client.get( + reverse( + "reference_documents:preferential_rates_edit", + kwargs={"pk": pref_rate.pk}, + ), + ) + + assert response.status_code == 200 + + def test_get_with_permissions(self, superuser_client): + pref_rate = factories.PreferentialRateFactory.create() + + response = superuser_client.get( + reverse( + "reference_documents:preferential_rates_edit", + kwargs={"pk": pref_rate.pk}, + ), + ) + assert response.status_code == 200 + + def test_ref_doc_create_creates_object_and_redirects(self, valid_user, client): + """Tests that posting the reference document create form adds the new + reference document to the database and redirects to the confirm-create + page.""" + valid_user.user_permissions.add( + Permission.objects.get(codename="change_preferentialrate"), + ) + client.force_login(valid_user) + pref_rate = factories.PreferentialRateFactory.create() + create_url = reverse( + "reference_documents:preferential_rates_edit", + kwargs={"pk": pref_rate.pk}, + ) + response = client.get(create_url) + assert response.status_code == 302 + + # ref_doc = ReferenceDocument.objects.get(title=form_data["title"]) + # assert ref_doc + # assert response.url == reverse( + # "reference_documents:confirm-create", + # kwargs={"pk": ref_doc.pk}, + # ) + + +class TestPreferentialRateDeleteView: + pass diff --git a/reference_documents/views/preferential_quotas.py b/reference_documents/views/preferential_quotas.py index 6a454325b..5007f49de 100644 --- a/reference_documents/views/preferential_quotas.py +++ b/reference_documents/views/preferential_quotas.py @@ -7,7 +7,7 @@ class PreferentialQuotaEditView(PermissionRequiredMixin, UpdateView): - template_name = "preferential_quotas/edit.jinja" + template_name = "reference_documents/preferential_quotas/edit.jinja" permission_required = "reference_documents.edit_reference_document" model = PreferentialQuota fields = [ diff --git a/reference_documents/views/preferential_rates.py b/reference_documents/views/preferential_rates.py index f24e0ff5c..5906691e1 100644 --- a/reference_documents/views/preferential_rates.py +++ b/reference_documents/views/preferential_rates.py @@ -5,16 +5,16 @@ from django.views.generic import DeleteView from django.views.generic import UpdateView -from reference_documents.forms import PreferentialRateEditForm +from reference_documents.forms import PreferentialRateCreateUpdateForm from reference_documents.models import PreferentialRate from reference_documents.models import ReferenceDocumentVersion class PreferentialRateEditView(PermissionRequiredMixin, UpdateView): - form_class = PreferentialRateEditForm - permission_required = "reference_documents.edit_reference_document" + form_class = PreferentialRateCreateUpdateForm + permission_required = "reference_documents.change_preferentialrate" model = PreferentialRate - template_name = "reference_documents/preferential_rates/edit.jinja" + template_name = "reference_documents/edit_preferential_rates.jinja" def get_success_url(self): return reverse( @@ -28,7 +28,7 @@ def form_valid(self, form): class PreferentialRateCreateView(PermissionRequiredMixin, CreateView): - form_class = PreferentialRateEditForm + form_class = PreferentialRateCreateUpdateForm permission_required = "reference_documents.edit_reference_document" model = PreferentialRate template_name = "reference_documents/preferential_rates/create.jinja" From b10aad91b740d8714cdb3b24103fd5e7e049b127 Mon Sep 17 00:00:00 2001 From: Doug Mills Date: Fri, 1 Mar 2024 12:45:29 +0000 Subject: [PATCH 074/118] add basic tests for models --- .../tests/reference_documents/test_forms.py | 61 ------- .../tests/reference_documents/test_views.py | 154 ------------------ .../tests/test_preferential_rates_views.py | 87 ++++------ 3 files changed, 30 insertions(+), 272 deletions(-) delete mode 100644 reference_documents/tests/reference_documents/test_forms.py delete mode 100644 reference_documents/tests/reference_documents/test_views.py diff --git a/reference_documents/tests/reference_documents/test_forms.py b/reference_documents/tests/reference_documents/test_forms.py deleted file mode 100644 index 6c43ed88f..000000000 --- a/reference_documents/tests/reference_documents/test_forms.py +++ /dev/null @@ -1,61 +0,0 @@ -import pytest - -from reference_documents import forms -from reference_documents.tests import factories - -pytestmark = pytest.mark.django_db - - -@pytest.mark.reference_documents -def test_ref_doc_create_update_form_valid_data(): - """Test that ReferenceDocumentCreateUpdateForm is valid when completed - correctly.""" - data = {"title": "Reference document for XY", "area_id": "XY"} - form = forms.ReferenceDocumentCreateUpdateForm(data=data) - - assert form.is_valid() - - -@pytest.mark.reference_documents -def test_ref_doc_create_update_form_invalid_data(): - """Test that ReferenceDocumentCreateUpdateForm is invalid when not complete - correctly.""" - form = forms.ReferenceDocumentCreateUpdateForm(data={}) - assert not form.is_valid() - assert "A Reference Document title is required" in form.errors["title"] - assert "An area ID is required" in form.errors["area_id"] - - factories.ReferenceDocumentFactory.create( - title="Reference document for XY", - area_id="XY", - ) - data = {"title": "Reference document for XY", "area_id": "VWXYZ"} - form = forms.ReferenceDocumentCreateUpdateForm(data=data) - assert not form.is_valid() - assert "The area ID can be at most 4 characters long" in form.errors["area_id"] - assert "A Reference Document with this title already exists" in form.errors["title"] - - -@pytest.mark.reference_documents -def test_ref_doc_delete_form_valid(): - """Test that ReferenceDocumentDeleteForm is valid for a reference document - with no versions.""" - ref_doc = factories.ReferenceDocumentFactory.create( - title="Reference document for XY", - area_id="XY", - ) - form = forms.ReferenceDocumentDeleteForm(data={}, instance=ref_doc) - assert form.is_valid() - - -@pytest.mark.reference_documents -def test_ref_doc_delete_form_invalid(): - """Test that ReferenceDocumentDeleteForm is invalid for a reference document - with versions.""" - ref_doc = factories.ReferenceDocumentFactory.create( - title="Reference document for XY", - area_id="XY", - ) - factories.ReferenceDocumentVersionFactory(reference_document=ref_doc) - form = forms.ReferenceDocumentDeleteForm(data={}, instance=ref_doc) - assert not form.is_valid() diff --git a/reference_documents/tests/reference_documents/test_views.py b/reference_documents/tests/reference_documents/test_views.py deleted file mode 100644 index 536b7dc54..000000000 --- a/reference_documents/tests/reference_documents/test_views.py +++ /dev/null @@ -1,154 +0,0 @@ -import pytest -from bs4 import BeautifulSoup -from django.contrib.auth.models import Permission -from django.urls import reverse - -from reference_documents.models import ReferenceDocument -from reference_documents.tests import factories - -pytestmark = pytest.mark.django_db - - -@pytest.mark.reference_documents -def test_ref_doc_create_creates_object_and_redirects(valid_user, client): - """Tests that posting the reference document create form adds the new - reference document to the database and redirects to the confirm-create - page.""" - valid_user.user_permissions.add( - Permission.objects.get(codename="add_referencedocument"), - ) - client.force_login(valid_user) - create_url = reverse("reference_documents:create") - form_data = { - "title": "Reference document for XY", - "area_id": "XY", - } - response = client.post(create_url, form_data) - assert response.status_code == 302 - - ref_doc = ReferenceDocument.objects.get(title=form_data["title"]) - assert ref_doc - assert response.url == reverse( - "reference_documents:confirm-create", - kwargs={"pk": ref_doc.pk}, - ) - - -@pytest.mark.reference_documents -def test_ref_doc_edit_updates_ref_doc_object(valid_user, client): - """Tests that posting the reference document edit form updates the reference - document and redirects to the confirm-update page.""" - valid_user.user_permissions.add( - Permission.objects.get(codename="change_referencedocument"), - ) - client.force_login(valid_user) - - ref_doc = factories.ReferenceDocumentFactory.create() - - edit_url = reverse( - "reference_documents:update", - kwargs={"pk": ref_doc.pk}, - ) - - new_title = "Updated title for this reference document" - new_area_id = "XY" - form_data = { - "title": new_title, - "area_id": new_area_id, - } - - assert not ref_doc.title == new_title - assert not ref_doc.area_id == new_area_id - - response = client.get(edit_url) - assert response.status_code == 200 - - response = client.post(edit_url, form_data) - assert response.status_code == 302 - assert response.url == reverse( - "reference_documents:confirm-update", - kwargs={"pk": ref_doc.pk}, - ) - - ref_doc.refresh_from_db() - assert ref_doc.title == new_title - assert ref_doc.area_id == new_area_id - - -@pytest.mark.reference_documents -def test_successfully_delete_ref_doc(valid_user, client): - """Tests that posting the reference document delete form deletes the - reference document and redirects to the confirm-delete page.""" - valid_user.user_permissions.add( - Permission.objects.get(codename="delete_referencedocument"), - ) - client.force_login(valid_user) - - ref_doc = factories.ReferenceDocumentFactory.create() - assert ReferenceDocument.objects.filter(pk=ref_doc.pk) - delete_url = reverse( - "reference_documents:delete", - kwargs={"pk": ref_doc.pk}, - ) - - response = client.get(delete_url) - page = BeautifulSoup(response.content, "html.parser") - assert response.status_code == 200 - assert ( - f"Delete Reference Document {ref_doc.area_id}" in page.select("main h1")[0].text - ) - - response = client.post(delete_url) - assert response.status_code == 302 - assert response.url == reverse( - "reference_documents:confirm-delete", - kwargs={"deleted_pk": ref_doc.pk}, - ) - assert not ReferenceDocument.objects.filter(pk=ref_doc.pk) - - -@pytest.mark.reference_documents -def test_delete_ref_doc_with_versions(valid_user, client): - """Test that deleting a reference document with versions does not work.""" - valid_user.user_permissions.add( - Permission.objects.get(codename="delete_referencedocument"), - ) - client.force_login(valid_user) - - ref_doc = factories.ReferenceDocumentFactory.create() - factories.ReferenceDocumentVersionFactory(reference_document=ref_doc) - - delete_url = reverse( - "reference_documents:delete", - kwargs={"pk": ref_doc.pk}, - ) - - response = client.get(delete_url) - assert response.status_code == 200 - - response = client.post(delete_url) - assert response.status_code == 200 - assert ( - f"Reference Document {ref_doc.area_id} cannot be deleted as it has active versions." - in str(response.content) - ) - assert ReferenceDocument.objects.filter(pk=ref_doc.pk) - - -@pytest.mark.reference_documents -def test_ref_doc_crud_without_permission(valid_user_client): - # TODO: potentially update this if the permissions for reference doc behaviour changes - ref_doc = factories.ReferenceDocumentFactory.create() - create_url = reverse("reference_documents:create") - edit_url = reverse("reference_documents:update", kwargs={"pk": ref_doc.pk}) - delete_url = reverse("reference_documents:delete", kwargs={"pk": ref_doc.pk}) - form_data = { - "title": "Reference document for XY", - "area_id": "XY", - } - response = valid_user_client.post(create_url, form_data) - assert response.status_code == 403 - response = valid_user_client.post(edit_url, form_data) - assert response.status_code == 403 - response = valid_user_client.post(delete_url) - assert response.status_code == 403 diff --git a/reference_documents/tests/test_preferential_rates_views.py b/reference_documents/tests/test_preferential_rates_views.py index f7738b77c..6d24852ba 100644 --- a/reference_documents/tests/test_preferential_rates_views.py +++ b/reference_documents/tests/test_preferential_rates_views.py @@ -7,30 +7,36 @@ pytestmark = pytest.mark.django_db -def test_get_without_permissions(valid_user, client): - valid_user.user_permissions.add( - Permission.objects.get(codename="change_preferentialrate"), - ) - client.force_login(valid_user) - pref_rate = factories.PreferentialRateFactory.create() - - response = client.get( - reverse( - "reference_documents:preferential_rates_edit", - kwargs={"pk": pref_rate.pk}, - ), - ) - - assert response.status_code == 200 - - @pytest.mark.reference_documents class TestPreferentialRateEditView: - def test_get_without_permissions(self, valid_user, client): - valid_user.user_permissions.add( - Permission.objects.get(codename="change_preferentialrate"), - ) - client.force_login(valid_user) + @pytest.mark.parametrize( + "has_permissions, user_type, expected_http_status", + [ + (["change_preferentialrate"], "regular", 200), + ([], "regular", 403), + ([], "superuser", 200), + ], + ) + def test_get( + self, + valid_user, + superuser, + client, + has_permissions, + user_type, + expected_http_status, + ): + if user_type == "superuser": + user = superuser + else: + user = valid_user + + for permission in has_permissions: + user.user_permissions.add( + Permission.objects.get(codename=permission), + ) + + client.force_login(user) pref_rate = factories.PreferentialRateFactory.create() response = client.get( @@ -40,42 +46,9 @@ def test_get_without_permissions(self, valid_user, client): ), ) - assert response.status_code == 200 - - def test_get_with_permissions(self, superuser_client): - pref_rate = factories.PreferentialRateFactory.create() - - response = superuser_client.get( - reverse( - "reference_documents:preferential_rates_edit", - kwargs={"pk": pref_rate.pk}, - ), - ) - assert response.status_code == 200 - - def test_ref_doc_create_creates_object_and_redirects(self, valid_user, client): - """Tests that posting the reference document create form adds the new - reference document to the database and redirects to the confirm-create - page.""" - valid_user.user_permissions.add( - Permission.objects.get(codename="change_preferentialrate"), - ) - client.force_login(valid_user) - pref_rate = factories.PreferentialRateFactory.create() - create_url = reverse( - "reference_documents:preferential_rates_edit", - kwargs={"pk": pref_rate.pk}, - ) - response = client.get(create_url) - assert response.status_code == 302 - - # ref_doc = ReferenceDocument.objects.get(title=form_data["title"]) - # assert ref_doc - # assert response.url == reverse( - # "reference_documents:confirm-create", - # kwargs={"pk": ref_doc.pk}, - # ) + assert response.status_code == expected_http_status +@pytest.mark.reference_documents class TestPreferentialRateDeleteView: pass From d33d92aa1abad810ad66560fd377eb50a12e7826 Mon Sep 17 00:00:00 2001 From: Doug Mills Date: Mon, 4 Mar 2024 08:17:11 +0000 Subject: [PATCH 075/118] add basic tests for models --- reference_documents/tests/factories.py | 2 +- .../tests/test_preferential_rates_views.py | 52 +++++++++++++++++++ 2 files changed, 53 insertions(+), 1 deletion(-) diff --git a/reference_documents/tests/factories.py b/reference_documents/tests/factories.py index 960a5964a..08ffb8f48 100644 --- a/reference_documents/tests/factories.py +++ b/reference_documents/tests/factories.py @@ -72,7 +72,7 @@ class Meta: reference_document_version = factory.SubFactory(ReferenceDocumentVersionFactory) - valid_between = ( + valid_between = TaricDateRange( get_random_date( date.today() + timedelta(days=-(365 * 2)), date.today() + timedelta(days=-365), diff --git a/reference_documents/tests/test_preferential_rates_views.py b/reference_documents/tests/test_preferential_rates_views.py index 6d24852ba..09b9eb476 100644 --- a/reference_documents/tests/test_preferential_rates_views.py +++ b/reference_documents/tests/test_preferential_rates_views.py @@ -2,7 +2,9 @@ from django.contrib.auth.models import Permission from django.urls import reverse +from reference_documents.forms import PreferentialRateCreateUpdateForm from reference_documents.tests import factories +from reference_documents.views.preferential_rates import PreferentialRateEditView pytestmark = pytest.mark.django_db @@ -48,6 +50,56 @@ def test_get( assert response.status_code == expected_http_status + def test_success_url(self): + pref_rate = factories.PreferentialRateFactory.create() + + target = PreferentialRateEditView() + target.object = pref_rate + assert target.get_success_url() == reverse( + "reference_documents:version_details", + args=[target.object.reference_document_version.pk], + ) + + def test_form_valid(self): + pref_rate = factories.PreferentialRateFactory.create() + target = PreferentialRateEditView() + target.object = pref_rate + + form = PreferentialRateCreateUpdateForm( + data={ + "start_date_0": 1, + "start_date_1": 1, + "start_date_2": 2024, + "commodity_code": "0100000000", + "duty_rate": "10%", + }, + instance=target.object, + ) + + assert form.is_valid() + assert target.form_valid(form) + + def test_form_invalid(self): + pref_rate = factories.PreferentialRateFactory.create() + target = PreferentialRateEditView() + target.object = pref_rate + + form = PreferentialRateCreateUpdateForm( + data={ + "start_date_0": 1, + "start_date_1": 1, + "start_date_2": 2024, + "commodity_code": "", + "duty_rate": "", + }, + instance=target.object, + ) + + assert not form.is_valid() + + with pytest.raises(ValueError): + target.form_valid(form) + @pytest.mark.reference_documents class TestPreferentialRateDeleteView: From 197fbce04578f9e8f9545e7bdfe5e1337143c3b7 Mon Sep 17 00:00:00 2001 From: Doug Mills Date: Mon, 4 Mar 2024 10:02:56 +0000 Subject: [PATCH 076/118] update edit quota view --- reference_documents/forms.py | 106 ++++++++++++++++++ .../preferential_quotas/edit.jinja | 18 +-- reference_documents/validators.py | 1 + .../views/preferential_quotas.py | 10 +- 4 files changed, 115 insertions(+), 20 deletions(-) diff --git a/reference_documents/forms.py b/reference_documents/forms.py index 4a33ed97f..051db5381 100644 --- a/reference_documents/forms.py +++ b/reference_documents/forms.py @@ -6,11 +6,13 @@ from crispy_forms_gds.layout import Submit from django import forms from django.core.exceptions import ValidationError +from django.core.validators import MinValueValidator from common.forms import ValidityPeriodForm from reference_documents import models from reference_documents.models import PreferentialRate from reference_documents.validators import commodity_code_validator +from reference_documents.validators import order_number_validator class PreferentialRateCreateUpdateForm( @@ -245,3 +247,107 @@ def init_fields(self): def clean(self): return super().clean() + + +class PreferentialQuotaCreateUpdateForm( + ValidityPeriodForm, + forms.ModelForm, +): + class Meta: + model = PreferentialRate + fields = [ + "quota_order_number", + "commodity_code", + "quota_duty_rate", + "volume", + "measurement", + "valid_between", + ] + + commodity_code = forms.CharField( + help_text="Commodity Code", + validators=[commodity_code_validator], + error_messages={ + "invalid": "Commodity code should be 10 digits", + "required": "Commodity code is required", + }, + ) + + quota_duty_rate = forms.CharField( + help_text="Quota Duty Rate", + validators=[], + error_messages={ + "invalid": "Duty rate is invalid", + "required": "Duty rate is required", + }, + ) + + quota_order_number = forms.CharField( + help_text="Quota Order Number", + validators=[order_number_validator], + error_messages={ + "invalid": "Quota Order Number is invalid", + "required": "Quota Order Number is required", + }, + ) + + volume = forms.CharField( + help_text="Volume", + validators=[MinValueValidator(0)], + error_messages={ + "invalid": "Volume invalid", + "required": "Volume is required", + }, + ) + + measurement = forms.CharField( + help_text="Measurement", + validators=[], + error_messages={ + "invalid": "Measurement invalid", + "required": "Measurement is required", + }, + ) + + def clean_quota_duty_rate(self): + data = self.cleaned_data["quota_duty_rate"] + if len(data) < 1: + raise ValidationError("Quota duty Rate is not valid - it must have a value") + return data + + def clean_commodity_code(self): + data = self.cleaned_data["commodity_code"] + if len(data) != 10 or not data.isdigit(): + raise ValidationError("Commodity Code is not valid - it must be 10 digits") + return data + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.init_layout() + self.init_fields() + + def init_layout(self): + self.helper = FormHelper(self) + self.helper.label_size = Size.SMALL + self.helper.legend_size = Size.SMALL + self.helper.layout = Layout( + "quota_order_number", + "commodity_code", + "quota_duty_rate", + "volume", + "measurement", + "start_date", + "end_date", + Submit( + "submit", + "Save", + data_module="govuk-button", + data_prevent_double_click="true", + ), + ) + + def init_fields(self): + pass + + def clean(self): + return super().clean() diff --git a/reference_documents/jinja2/reference_documents/preferential_quotas/edit.jinja b/reference_documents/jinja2/reference_documents/preferential_quotas/edit.jinja index 2950aad1f..53339b8e2 100644 --- a/reference_documents/jinja2/reference_documents/preferential_quotas/edit.jinja +++ b/reference_documents/jinja2/reference_documents/preferential_quotas/edit.jinja @@ -1,15 +1,9 @@ -{% extends "layouts/layout.jinja" %} +{% extends 'layouts/form.jinja' %} {% set page_title = "Edit preferential quota" %} -{% block content %} - -
    - - {{ form.as_p() }} - - -
    - -{% endblock %} - +{% block form %} + {% call django_form() %} + {{ crispy(form) }} + {% endcall %} +{% endblock %} \ No newline at end of file diff --git a/reference_documents/validators.py b/reference_documents/validators.py index 4a35a6a18..20882638d 100644 --- a/reference_documents/validators.py +++ b/reference_documents/validators.py @@ -1,3 +1,4 @@ from django.core.validators import RegexValidator commodity_code_validator = RegexValidator(r"\d{10}") +order_number_validator = RegexValidator(r"^05[0-9]{4}$") diff --git a/reference_documents/views/preferential_quotas.py b/reference_documents/views/preferential_quotas.py index 5007f49de..a602ab1fa 100644 --- a/reference_documents/views/preferential_quotas.py +++ b/reference_documents/views/preferential_quotas.py @@ -3,6 +3,7 @@ from django.urls import reverse from django.views.generic import UpdateView +from reference_documents.forms import PreferentialQuotaCreateUpdateForm from reference_documents.models import PreferentialQuota @@ -10,14 +11,7 @@ class PreferentialQuotaEditView(PermissionRequiredMixin, UpdateView): template_name = "reference_documents/preferential_quotas/edit.jinja" permission_required = "reference_documents.edit_reference_document" model = PreferentialQuota - fields = [ - "quota_order_number", - "commodity_code", - "quota_duty_rate", - "volume", - "measurement", - "valid_between", - ] + form_class = PreferentialQuotaCreateUpdateForm def post(self, request, *args, **kwargs): quota = self.get_object() From fd32c27d07ef4ff9db857892c357c78741bcd5ba Mon Sep 17 00:00:00 2001 From: Doug Mills Date: Mon, 4 Mar 2024 11:52:19 +0000 Subject: [PATCH 077/118] fix views --- reference_documents/views/example_views.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/reference_documents/views/example_views.py b/reference_documents/views/example_views.py index 906f463bc..f17e50a36 100644 --- a/reference_documents/views/example_views.py +++ b/reference_documents/views/example_views.py @@ -9,7 +9,7 @@ class ReferenceDocumentsListView(TemplateView): - template_name = "reference_document_examples/index.jinja" + template_name = "reference_documents/reference_document_examples/index.jinja" def get_context_data(self, *args, **kwargs): context = super().get_context_data(*args, **kwargs) @@ -26,7 +26,7 @@ def get_context_data(self, *args, **kwargs): class ReferenceDocumentsDetailView(TemplateView): - template_name = "reference_document_examples/details.jinja" + template_name = "reference_documents/reference_document_examples/details.jinja" def get_pref_duty_rates(self): """Returns a list of measures associated with the Albania Preferential From 205c0f65e4810d5f521a11af67c9d1ec0ebf0016 Mon Sep 17 00:00:00 2001 From: Matthew McKenzie <97194636+mattjamc@users.noreply.github.com> Date: Mon, 4 Mar 2024 16:12:45 +0000 Subject: [PATCH 078/118] Ref doc versions create and edit urls/templates --- .../confirm_create.jinja | 39 +++++ .../confirm_delete.jinja | 32 ++++ .../confirm_update.jinja | 40 +++++ .../reference_document_versions/create.jinja | 4 + .../reference_document_versions/edit.jinja | 25 +-- .../tests/reference_documents/test_forms.py | 61 ------- .../tests/reference_documents/test_views.py | 154 ------------------ reference_documents/urls.py | 35 +++- 8 files changed, 160 insertions(+), 230 deletions(-) create mode 100644 reference_documents/jinja2/reference_documents/reference_document_versions/confirm_create.jinja create mode 100644 reference_documents/jinja2/reference_documents/reference_document_versions/confirm_delete.jinja create mode 100644 reference_documents/jinja2/reference_documents/reference_document_versions/confirm_update.jinja create mode 100644 reference_documents/jinja2/reference_documents/reference_document_versions/create.jinja delete mode 100644 reference_documents/tests/reference_documents/test_forms.py delete mode 100644 reference_documents/tests/reference_documents/test_views.py diff --git a/reference_documents/jinja2/reference_documents/reference_document_versions/confirm_create.jinja b/reference_documents/jinja2/reference_documents/reference_document_versions/confirm_create.jinja new file mode 100644 index 000000000..26761970f --- /dev/null +++ b/reference_documents/jinja2/reference_documents/reference_document_versions/confirm_create.jinja @@ -0,0 +1,39 @@ +{% extends "layouts/layout.jinja" %} + +{% from "components/panel/macro.njk" import govukPanel %} +{% from "components/button/macro.njk" import govukButton %} +{% from "components/breadcrumbs/macro.njk" import govukBreadcrumbs %} + +{% set page_title = "Reference Document version" ~ object.reference_document ~ " successfully created" %} + +{% block breadcrumb %} + {{ govukBreadcrumbs({ + "items": [{"text": "Home", "href": url("home")}, + {"text": "View reference documents", "href": url("reference_documents:index")}, + {"text": "Create a Reference Document", "href": url("reference_documents:create")}, + {"text": page_title}] + }) }} +{% endblock %} + +{% block content %} +
    +
    + {{ govukPanel({ + "titleText": "Reference Document " ~ object.area_id ~ " has been created", + "text": "This change has taken immediate effect", + "classes": "govuk-!-margin-bottom-7" + }) }} +
    +
    +
    + {{ govukButton({ + "text": "View Reference Document " ~ object.area_id, + "href": url("reference_documents:details", kwargs={"pk":object.pk}), + }) }} + {{ govukButton({ + "text": "Back to View reference documents", + "href": url("reference_documents:index"), + "classes": "govuk-button--secondary" + }) }} +
    +{% endblock %} diff --git a/reference_documents/jinja2/reference_documents/reference_document_versions/confirm_delete.jinja b/reference_documents/jinja2/reference_documents/reference_document_versions/confirm_delete.jinja new file mode 100644 index 000000000..29b53ec0c --- /dev/null +++ b/reference_documents/jinja2/reference_documents/reference_document_versions/confirm_delete.jinja @@ -0,0 +1,32 @@ +{% extends "layouts/layout.jinja" %} + +{% from "components/panel/macro.njk" import govukPanel %} +{% from "components/button/macro.njk" import govukButton %} +{% from "components/breadcrumbs/macro.njk" import govukBreadcrumbs %} + +{% set page_title = "Reference Document " ~ deleted_pk ~ " deleted" %} + +{% block breadcrumb %} + {{ govukBreadcrumbs({ + "items": [{"text": "Home", "href": url("home")}, + {"text": "View reference documents", "href": url("reference_documents:index")}, + {"text": page_title}] + }) }} +{% endblock %} + +{% block content %} +
    +
    + {{ govukPanel({ + "titleText": "Reference Document " ~ deleted_pk ~ " has been deleted", + "text": "This change has taken immediate effect", + "classes": "govuk-!-margin-bottom-7" + }) }} +
    +
    + {{ govukButton({ + "text": "Back to View reference documents", + "href": url("reference_documents:index"), + "classes": "govuk-button--secondary" + }) }} +{% endblock %} diff --git a/reference_documents/jinja2/reference_documents/reference_document_versions/confirm_update.jinja b/reference_documents/jinja2/reference_documents/reference_document_versions/confirm_update.jinja new file mode 100644 index 000000000..66a12d21d --- /dev/null +++ b/reference_documents/jinja2/reference_documents/reference_document_versions/confirm_update.jinja @@ -0,0 +1,40 @@ +{% extends "layouts/layout.jinja" %} + +{% from "components/panel/macro.njk" import govukPanel %} +{% from "components/button/macro.njk" import govukButton %} +{% from "components/breadcrumbs/macro.njk" import govukBreadcrumbs %} + +{% set page_title = "Reference Document " ~ object.reference_document.area_id ~ " version " ~ object.version ~ " successfully updated" %} + +{% block breadcrumb %} + {{ govukBreadcrumbs({ + "items": [{"text": "Home", "href": url("home")}, + {"text": "View reference documents", "href": url("reference_documents:index")}, + {"text": "Reference Document " ~ object.reference_document.area_id}, + {"text": "Version " ~ object.version}, + {"text": page_title}] + }) }} +{% endblock %} + +{% block content %} +
    +
    + {{ govukPanel({ + "titleText": "Reference Document " ~ object.reference_document.area_id ~ " version " ~ object.version ~ " has been updated", + "text": "This change has taken immediate effect", + "classes": "govuk-!-margin-bottom-7" + }) }} +
    +
    +
    + {{ govukButton({ + "text": "View your Reference Document", + "href": url("reference_documents:details", kwargs={"pk":object.pk}), + }) }} + {{ govukButton({ + "text": "Back to View Reference Documents", + "href": url("reference_documents:index"), + "classes": "govuk-button--secondary" + }) }} +
    +{% endblock %} diff --git a/reference_documents/jinja2/reference_documents/reference_document_versions/create.jinja b/reference_documents/jinja2/reference_documents/reference_document_versions/create.jinja new file mode 100644 index 000000000..a1ad598ba --- /dev/null +++ b/reference_documents/jinja2/reference_documents/reference_document_versions/create.jinja @@ -0,0 +1,4 @@ +{% extends "layouts/create.jinja" %} + +{% set page_title = "Create a new version for reference document " ~ reference_document.area_id %} + diff --git a/reference_documents/jinja2/reference_documents/reference_document_versions/edit.jinja b/reference_documents/jinja2/reference_documents/reference_document_versions/edit.jinja index f9bc245a2..f85a6ecb9 100644 --- a/reference_documents/jinja2/reference_documents/reference_document_versions/edit.jinja +++ b/reference_documents/jinja2/reference_documents/reference_document_versions/edit.jinja @@ -1,14 +1,19 @@ -{% extends "layouts/layout.jinja" %} +{% extends "layouts/form.jinja" %} +{% from "components/breadcrumbs/macro.njk" import govukBreadcrumbs %} -{% set page_title = "Preferential duty rates" %} - -{% block content %} - -
    - - {{ form.as_p() }} - -
    +{% set page_title = "Edit Reference Document " ~ object.reference_document.area_id ~ " version " ~ object.version %} +{% block breadcrumb %} + {{ govukBreadcrumbs({ + "items": [{"text": "Home", "href": url("home")}, + {"text": "View reference documents", "href": url("reference_documents:index")}, + {"text": "Reference document " ~ object.reference_document.area_id, "href": url("reference_documents:details", kwargs={"pk":object.reference_document.pk})}, + {"text": page_title}] + }) }} {% endblock %} +{% block form %} + {% call django_form() %} + {{ crispy(form) }} + {% endcall %} +{% endblock %} diff --git a/reference_documents/tests/reference_documents/test_forms.py b/reference_documents/tests/reference_documents/test_forms.py deleted file mode 100644 index 6c43ed88f..000000000 --- a/reference_documents/tests/reference_documents/test_forms.py +++ /dev/null @@ -1,61 +0,0 @@ -import pytest - -from reference_documents import forms -from reference_documents.tests import factories - -pytestmark = pytest.mark.django_db - - -@pytest.mark.reference_documents -def test_ref_doc_create_update_form_valid_data(): - """Test that ReferenceDocumentCreateUpdateForm is valid when completed - correctly.""" - data = {"title": "Reference document for XY", "area_id": "XY"} - form = forms.ReferenceDocumentCreateUpdateForm(data=data) - - assert form.is_valid() - - -@pytest.mark.reference_documents -def test_ref_doc_create_update_form_invalid_data(): - """Test that ReferenceDocumentCreateUpdateForm is invalid when not complete - correctly.""" - form = forms.ReferenceDocumentCreateUpdateForm(data={}) - assert not form.is_valid() - assert "A Reference Document title is required" in form.errors["title"] - assert "An area ID is required" in form.errors["area_id"] - - factories.ReferenceDocumentFactory.create( - title="Reference document for XY", - area_id="XY", - ) - data = {"title": "Reference document for XY", "area_id": "VWXYZ"} - form = forms.ReferenceDocumentCreateUpdateForm(data=data) - assert not form.is_valid() - assert "The area ID can be at most 4 characters long" in form.errors["area_id"] - assert "A Reference Document with this title already exists" in form.errors["title"] - - -@pytest.mark.reference_documents -def test_ref_doc_delete_form_valid(): - """Test that ReferenceDocumentDeleteForm is valid for a reference document - with no versions.""" - ref_doc = factories.ReferenceDocumentFactory.create( - title="Reference document for XY", - area_id="XY", - ) - form = forms.ReferenceDocumentDeleteForm(data={}, instance=ref_doc) - assert form.is_valid() - - -@pytest.mark.reference_documents -def test_ref_doc_delete_form_invalid(): - """Test that ReferenceDocumentDeleteForm is invalid for a reference document - with versions.""" - ref_doc = factories.ReferenceDocumentFactory.create( - title="Reference document for XY", - area_id="XY", - ) - factories.ReferenceDocumentVersionFactory(reference_document=ref_doc) - form = forms.ReferenceDocumentDeleteForm(data={}, instance=ref_doc) - assert not form.is_valid() diff --git a/reference_documents/tests/reference_documents/test_views.py b/reference_documents/tests/reference_documents/test_views.py deleted file mode 100644 index 536b7dc54..000000000 --- a/reference_documents/tests/reference_documents/test_views.py +++ /dev/null @@ -1,154 +0,0 @@ -import pytest -from bs4 import BeautifulSoup -from django.contrib.auth.models import Permission -from django.urls import reverse - -from reference_documents.models import ReferenceDocument -from reference_documents.tests import factories - -pytestmark = pytest.mark.django_db - - -@pytest.mark.reference_documents -def test_ref_doc_create_creates_object_and_redirects(valid_user, client): - """Tests that posting the reference document create form adds the new - reference document to the database and redirects to the confirm-create - page.""" - valid_user.user_permissions.add( - Permission.objects.get(codename="add_referencedocument"), - ) - client.force_login(valid_user) - create_url = reverse("reference_documents:create") - form_data = { - "title": "Reference document for XY", - "area_id": "XY", - } - response = client.post(create_url, form_data) - assert response.status_code == 302 - - ref_doc = ReferenceDocument.objects.get(title=form_data["title"]) - assert ref_doc - assert response.url == reverse( - "reference_documents:confirm-create", - kwargs={"pk": ref_doc.pk}, - ) - - -@pytest.mark.reference_documents -def test_ref_doc_edit_updates_ref_doc_object(valid_user, client): - """Tests that posting the reference document edit form updates the reference - document and redirects to the confirm-update page.""" - valid_user.user_permissions.add( - Permission.objects.get(codename="change_referencedocument"), - ) - client.force_login(valid_user) - - ref_doc = factories.ReferenceDocumentFactory.create() - - edit_url = reverse( - "reference_documents:update", - kwargs={"pk": ref_doc.pk}, - ) - - new_title = "Updated title for this reference document" - new_area_id = "XY" - form_data = { - "title": new_title, - "area_id": new_area_id, - } - - assert not ref_doc.title == new_title - assert not ref_doc.area_id == new_area_id - - response = client.get(edit_url) - assert response.status_code == 200 - - response = client.post(edit_url, form_data) - assert response.status_code == 302 - assert response.url == reverse( - "reference_documents:confirm-update", - kwargs={"pk": ref_doc.pk}, - ) - - ref_doc.refresh_from_db() - assert ref_doc.title == new_title - assert ref_doc.area_id == new_area_id - - -@pytest.mark.reference_documents -def test_successfully_delete_ref_doc(valid_user, client): - """Tests that posting the reference document delete form deletes the - reference document and redirects to the confirm-delete page.""" - valid_user.user_permissions.add( - Permission.objects.get(codename="delete_referencedocument"), - ) - client.force_login(valid_user) - - ref_doc = factories.ReferenceDocumentFactory.create() - assert ReferenceDocument.objects.filter(pk=ref_doc.pk) - delete_url = reverse( - "reference_documents:delete", - kwargs={"pk": ref_doc.pk}, - ) - - response = client.get(delete_url) - page = BeautifulSoup(response.content, "html.parser") - assert response.status_code == 200 - assert ( - f"Delete Reference Document {ref_doc.area_id}" in page.select("main h1")[0].text - ) - - response = client.post(delete_url) - assert response.status_code == 302 - assert response.url == reverse( - "reference_documents:confirm-delete", - kwargs={"deleted_pk": ref_doc.pk}, - ) - assert not ReferenceDocument.objects.filter(pk=ref_doc.pk) - - -@pytest.mark.reference_documents -def test_delete_ref_doc_with_versions(valid_user, client): - """Test that deleting a reference document with versions does not work.""" - valid_user.user_permissions.add( - Permission.objects.get(codename="delete_referencedocument"), - ) - client.force_login(valid_user) - - ref_doc = factories.ReferenceDocumentFactory.create() - factories.ReferenceDocumentVersionFactory(reference_document=ref_doc) - - delete_url = reverse( - "reference_documents:delete", - kwargs={"pk": ref_doc.pk}, - ) - - response = client.get(delete_url) - assert response.status_code == 200 - - response = client.post(delete_url) - assert response.status_code == 200 - assert ( - f"Reference Document {ref_doc.area_id} cannot be deleted as it has active versions." - in str(response.content) - ) - assert ReferenceDocument.objects.filter(pk=ref_doc.pk) - - -@pytest.mark.reference_documents -def test_ref_doc_crud_without_permission(valid_user_client): - # TODO: potentially update this if the permissions for reference doc behaviour changes - ref_doc = factories.ReferenceDocumentFactory.create() - create_url = reverse("reference_documents:create") - edit_url = reverse("reference_documents:update", kwargs={"pk": ref_doc.pk}) - delete_url = reverse("reference_documents:delete", kwargs={"pk": ref_doc.pk}) - form_data = { - "title": "Reference document for XY", - "area_id": "XY", - } - response = valid_user_client.post(create_url, form_data) - assert response.status_code == 403 - response = valid_user_client.post(edit_url, form_data) - assert response.status_code == 403 - response = valid_user_client.post(delete_url) - assert response.status_code == 403 diff --git a/reference_documents/urls.py b/reference_documents/urls.py index bcd2a8175..5f17d7171 100644 --- a/reference_documents/urls.py +++ b/reference_documents/urls.py @@ -68,14 +68,39 @@ ), # reference document version views path( - "reference_document_versions//", + "versions//", reference_document_version_views.ReferenceDocumentVersionDetails.as_view(), - name="version_details", + name="version-details", ), path( - "reference_document_versions/edit//", - reference_document_version_views.ReferenceDocumentVersionEditView.as_view(), - name="reference_document_version_edit", + "/version-create", + reference_document_version_views.ReferenceDocumentVersionCreate.as_view(), + name="version-create", + ), + path( + "/version//edit/", + reference_document_version_views.ReferenceDocumentVersionEdit.as_view(), + name="version-edit", + ), + path( + "/version-delete", + reference_document_version_views.ReferenceDocumentVersionEdit.as_view(), + name="version-delete", + ), + path( + "reference_document_versions//confirm-update/", + reference_document_version_views.ReferenceDocumentVersionConfirmUpdate.as_view(), + name="version-confirm-update", + ), + path( + "reference_document_versions//confirm-create/", + reference_document_version_views.ReferenceDocumentVersionConfirmCreate.as_view(), + name="version-confirm-create", + ), + path( + "reference_document_versions//confirm-delete/", + reference_document_version_views.ReferenceDocumentVersionConfirmDelete.as_view(), + name="version-confirm-delete", ), # Alignment report views path( From 7629f437a3b83f2bfc6903e7079f3be80e5c368d Mon Sep 17 00:00:00 2001 From: Matthew McKenzie <97194636+mattjamc@users.noreply.github.com> Date: Mon, 4 Mar 2024 16:14:33 +0000 Subject: [PATCH 079/118] Ref doc formatting tidy up --- .../jinja2/reference_documents/details.jinja | 19 +++++++++++++------ .../jinja2/reference_documents/index.jinja | 8 +++++--- .../reference_document_versions/details.jinja | 10 ++++++++++ .../views/reference_document_views.py | 4 ++-- 4 files changed, 30 insertions(+), 11 deletions(-) diff --git a/reference_documents/jinja2/reference_documents/details.jinja b/reference_documents/jinja2/reference_documents/details.jinja index 0c301c051..c710e89b8 100644 --- a/reference_documents/jinja2/reference_documents/details.jinja +++ b/reference_documents/jinja2/reference_documents/details.jinja @@ -1,20 +1,27 @@ {% extends "layouts/layout.jinja" %} {% from "components/table/macro.njk" import govukTable %} +{% from "components/breadcrumbs/macro.njk" import govukBreadcrumbs %} -{% set page_title = 'Reference Documents versions Overview' %} +{% set page_title = "Reference Documents for " ~ object.get_area_name_by_area_id() %} {% block breadcrumb %} - {{ breadcrumbs(request, [ - {'text': "Reference Documents"} - ]) }} + {{ govukBreadcrumbs({ + "items": [{"text": "Home", "href": url("home")}, + {"text": "View reference documents", "href": url("reference_documents:index")}, + {"text": "Reference document " ~ object.area_id}], + }) }} {% endblock %} {% block content %}

    - Reference Document Overview + {{ page_title }}

    You will find a list of reference document versions below that can be viewed. - +

    + + Create a new version of this reference document + +

    {{ govukTable({ "head": reference_document_versions_headers, "rows": reference_document_versions }) }}
    diff --git a/reference_documents/jinja2/reference_documents/index.jinja b/reference_documents/jinja2/reference_documents/index.jinja index 0b38d2389..b334b17d5 100644 --- a/reference_documents/jinja2/reference_documents/index.jinja +++ b/reference_documents/jinja2/reference_documents/index.jinja @@ -1,13 +1,15 @@ {% extends "layouts/layout.jinja" %} {% from "components/table/macro.njk" import govukTable %} +{% from "components/breadcrumbs/macro.njk" import govukBreadcrumbs %} {% set page_title = 'Reference Documents Index' %} {% set create_url = "create" %} {% block breadcrumb %} - {{ breadcrumbs(request, [ - {'text': "Reference Documents"} - ]) }} + {{ govukBreadcrumbs({ + "items": [{"text": "Home", "href": url("home")}, + {"text": "View reference documents"}], + }) }} {% endblock %} {% block content %} diff --git a/reference_documents/jinja2/reference_documents/reference_document_versions/details.jinja b/reference_documents/jinja2/reference_documents/reference_document_versions/details.jinja index 77fcdff14..f2ad38533 100644 --- a/reference_documents/jinja2/reference_documents/reference_document_versions/details.jinja +++ b/reference_documents/jinja2/reference_documents/reference_document_versions/details.jinja @@ -2,9 +2,19 @@ {% from "components/table/macro.njk" import govukTable %} {% from "components/tabs/macro.njk" import govukTabs %} +{% from "components/breadcrumbs/macro.njk" import govukBreadcrumbs %} {% set page_title = "Preferential duty rates" %} +{% block breadcrumb %} + {{ govukBreadcrumbs({ + "items": [{"text": "Home", "href": url("home")}, + {"text": "View reference documents", "href": url("reference_documents:index")}, + {"text": "Reference document " ~ object.reference_document.area_id, "href": url("reference_documents:details", kwargs={"pk":object.reference_document.pk})}, + {"text": "Version " ~ object.version}] + }) }} +{% endblock %} + {% set preferential_rates_html %} {% include "includes/tabs/preferential_rates.jinja" %} {% endset %} diff --git a/reference_documents/views/reference_document_views.py b/reference_documents/views/reference_document_views.py index cf5fd8ac4..f02dc7f4f 100644 --- a/reference_documents/views/reference_document_views.py +++ b/reference_documents/views/reference_document_views.py @@ -115,8 +115,8 @@ def get_context_data(self, *args, **kwargs): "text": version.entry_into_force_date, }, { - "html": f'Version details
    ' - f'Edit
    ' + "html": f'Version details
    ' + f'Edit
    ' f'Alignment reports', }, ], From 5aac5409751877bac36978f7532dfc82f417a688 Mon Sep 17 00:00:00 2001 From: Matthew McKenzie <97194636+mattjamc@users.noreply.github.com> Date: Mon, 4 Mar 2024 16:16:06 +0000 Subject: [PATCH 080/118] Ref doc versions create and edit views/form --- reference_documents/forms.py | 51 ++++++++++++ reference_documents/models.py | 9 ++ .../views/reference_document_version_views.py | 82 +++++++++++++++---- 3 files changed, 125 insertions(+), 17 deletions(-) diff --git a/reference_documents/forms.py b/reference_documents/forms.py index f40344e3e..073bafeff 100644 --- a/reference_documents/forms.py +++ b/reference_documents/forms.py @@ -6,6 +6,7 @@ from crispy_forms_gds.layout import Submit from django import forms +from common.forms import DateInputFieldFixed from common.forms import ValidityPeriodForm from reference_documents import models from reference_documents.models import PreferentialRate @@ -232,3 +233,53 @@ def init_fields(self): def clean(self): return super().clean() + + +class ReferenceDocumentVersionsEditCreateForm(forms.ModelForm): + version = forms.CharField( + label="Version number", + error_messages={ + "required": "A version number is required", + "invalid": "Version must be a number", + }, + ) + published_date = DateInputFieldFixed( + label="Published date", + ) + entry_into_force_date = DateInputFieldFixed( + label="Entry into force date", + ) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.helper = FormHelper(self) + self.helper.label_size = Size.SMALL + self.helper.legend_size = Size.SMALL + + self.helper.layout = Layout( + Field( + "reference_document", + type="hidden", + ), + Field.text( + "version", + field_width=Fixed.TEN, + ), + "published_date", + "entry_into_force_date", + Submit( + "submit", + "Save", + data_module="govuk-button", + data_prevent_double_click="true", + ), + ) + + class Meta: + model = models.ReferenceDocumentVersion + fields = [ + "reference_document", + "version", + "published_date", + "entry_into_force_date", + ] diff --git a/reference_documents/models.py b/reference_documents/models.py index de40c3719..804e35aa7 100644 --- a/reference_documents/models.py +++ b/reference_documents/models.py @@ -74,6 +74,15 @@ class ReferenceDocumentVersion(models.Model): editable=False, ) + class Meta: + # TODO: Add violation_error_message to this constraint once we have Django 4.1 + constraints = [ + models.UniqueConstraint( + fields=["version", "reference_document"], + name="unique_versions", + ), + ] + class PreferentialRate(models.Model): commodity_code = models.CharField( diff --git a/reference_documents/views/reference_document_version_views.py b/reference_documents/views/reference_document_version_views.py index e0f9c8aea..19c4ed0d5 100644 --- a/reference_documents/views/reference_document_version_views.py +++ b/reference_documents/views/reference_document_version_views.py @@ -1,19 +1,22 @@ from django.contrib.auth.mixins import PermissionRequiredMixin from django.urls import reverse -from django.urls import reverse_lazy +from django.views.generic import CreateView from django.views.generic import DetailView +from django.views.generic import TemplateView from django.views.generic import UpdateView from commodities.models import GoodsNomenclature from geo_areas.models import GeographicalAreaDescription from quotas.models import QuotaOrderNumber +from reference_documents import forms from reference_documents.models import AlignmentReportCheckStatus +from reference_documents.models import ReferenceDocument from reference_documents.models import ReferenceDocumentVersion class ReferenceDocumentVersionDetails(PermissionRequiredMixin, DetailView): template_name = "reference_documents/reference_document_versions/details.jinja" - permission_required = "reference_documents.view_reference_document" + permission_required = "reference_documents.view_reference_documentversion" model = ReferenceDocumentVersion def get_country_by_area_id(self, area_id): @@ -69,11 +72,12 @@ def get_context_data(self, *args, **kwargs): *args, **kwargs, ) + ref_doc = context["object"].reference_document # title context[ "ref_doc_title" - ] = f'Reference Document for {self.get_country_by_area_id(context["object"].reference_document.area_id)}' + ] = f"Reference Document for {ref_doc.get_area_name_by_area_id()}" context["reference_document_version_duties_headers"] = [ {"text": "Comm Code"}, @@ -224,22 +228,66 @@ def get_context_data(self, *args, **kwargs): return context -class ReferenceDocumentVersionEditView(PermissionRequiredMixin, UpdateView): - template_name = "reference_document_versions/edit.jinja" - permission_required = "reference_documents.edit_reference_document" - model = ReferenceDocumentVersion - fields = ["version", "published_date", "entry_into_force_date"] +class ReferenceDocumentVersionCreate(PermissionRequiredMixin, CreateView): + template_name = "reference_documents/reference_document_versions/create.jinja" + permission_required = "reference_documents.add_referencedocumentversion" + form_class = forms.ReferenceDocumentVersionsEditCreateForm + + def get_initial(self): + initial = super().get_initial() + initial["reference_document"] = ReferenceDocument.objects.all().get( + pk=self.kwargs["pk"], + ) + return initial + + def get_context_data(self, **kwargs): + context_data = super().get_context_data(**kwargs) + print(self.kwargs) + context_data["reference_document"] = ReferenceDocument.objects.all().get( + pk=self.kwargs["pk"], + ) + return context_data + + def get_success_url(self): + return reverse( + "reference_documents:version-confirm-create", + kwargs={"pk": self.object.pk}, + ) - # def post(self, request, *args, **kwargs): - # reference_document_version = self.get_object() - # reference_document_version.save() - # return redirect(reverse("reference_documents:details", args=[reference_document_version.reference_document.pk])) - def form_valid(self, form): - return super(ReferenceDocumentVersionEditView, self).form_valid(form) +class ReferenceDocumentVersionEdit(PermissionRequiredMixin, UpdateView): + model = ReferenceDocumentVersion + permission_required = "reference_documents.change_referencedocumentversion" + template_name = "reference_documents/reference_document_versions/edit.jinja" + form_class = forms.ReferenceDocumentVersionsEditCreateForm def get_success_url(self): - return reverse_lazy( - "reference_documents:details", - args=[self.object.id], + return reverse( + "reference_documents:version-confirm-update", + kwargs={"pk": self.object.pk}, ) + + +class ReferenceDocumentVersionConfirmCreate(DetailView): + template_name = ( + "reference_documents/reference_document_versions/confirm_create.jinja" + ) + model = ReferenceDocument + + +class ReferenceDocumentVersionConfirmUpdate(DetailView): + template_name = ( + "reference_documents/reference_document_versions/confirm_update.jinja" + ) + model = ReferenceDocument + + +class ReferenceDocumentVersionConfirmDelete(TemplateView): + template_name = ( + "reference_documents/reference_document_versions/confirm_delete.jinja" + ) + + def get_context_data(self, **kwargs): + context_data = super().get_context_data(**kwargs) + context_data["deleted_pk"] = self.kwargs["deleted_pk"] + return context_data From b0f2c55e62b8919badafd071b97b4320b0cd9b91 Mon Sep 17 00:00:00 2001 From: Doug Mills Date: Tue, 5 Mar 2024 09:38:12 +0000 Subject: [PATCH 081/118] fix views --- reference_documents/forms.py | 14 +++++-- .../includes/tabs/preferential_quotas.jinja | 5 +-- reference_documents/urls.py | 5 +++ .../views/preferential_quotas.py | 39 +++++++++++++++++++ .../views/reference_document_version_views.py | 3 +- 5 files changed, 59 insertions(+), 7 deletions(-) diff --git a/reference_documents/forms.py b/reference_documents/forms.py index 051db5381..6ea4446ff 100644 --- a/reference_documents/forms.py +++ b/reference_documents/forms.py @@ -1,3 +1,5 @@ +from decimal import Decimal + from crispy_forms_gds.helper import FormHelper from crispy_forms_gds.layout import Field from crispy_forms_gds.layout import Fixed @@ -6,10 +8,10 @@ from crispy_forms_gds.layout import Submit from django import forms from django.core.exceptions import ValidationError -from django.core.validators import MinValueValidator from common.forms import ValidityPeriodForm from reference_documents import models +from reference_documents.models import PreferentialQuota from reference_documents.models import PreferentialRate from reference_documents.validators import commodity_code_validator from reference_documents.validators import order_number_validator @@ -254,7 +256,7 @@ class PreferentialQuotaCreateUpdateForm( forms.ModelForm, ): class Meta: - model = PreferentialRate + model = PreferentialQuota fields = [ "quota_order_number", "commodity_code", @@ -293,7 +295,7 @@ class Meta: volume = forms.CharField( help_text="Volume", - validators=[MinValueValidator(0)], + validators=[], error_messages={ "invalid": "Volume invalid", "required": "Volume is required", @@ -315,6 +317,12 @@ def clean_quota_duty_rate(self): raise ValidationError("Quota duty Rate is not valid - it must have a value") return data + def clean_volume(self): + data = self.cleaned_data["volume"] + if not data.isdigit(): + raise ValidationError("volume is not valid - it must have a value") + return Decimal(data) + def clean_commodity_code(self): data = self.cleaned_data["commodity_code"] if len(data) != 10 or not data.isdigit(): diff --git a/reference_documents/jinja2/includes/tabs/preferential_quotas.jinja b/reference_documents/jinja2/includes/tabs/preferential_quotas.jinja index b22e625f1..b43bf82d1 100644 --- a/reference_documents/jinja2/includes/tabs/preferential_quotas.jinja +++ b/reference_documents/jinja2/includes/tabs/preferential_quotas.jinja @@ -19,7 +19,7 @@ {% endif %}
    {% else %} -

    Order Number {{ value["quota_order_number"] }}

    +

    Order Number {{ value["quota_order_number_text"] }}

    {% endif %} {{ govukTable({ "head": reference_document_version_quotas_headers, @@ -32,8 +32,7 @@ diff --git a/reference_documents/urls.py b/reference_documents/urls.py index bcd2a8175..3bdfa373f 100644 --- a/reference_documents/urls.py +++ b/reference_documents/urls.py @@ -99,6 +99,11 @@ preferential_quotas.PreferentialQuotaEditView.as_view(), name="preferential_quotas_edit", ), + path( + "reference_document_versions//create_preferential_quotas/", + preferential_quotas.PreferentialQuotaCreateView.as_view(), + name="preferential_quotas_create", + ), # Preferential Rates path( "preferential_rates/delete//", diff --git a/reference_documents/views/preferential_quotas.py b/reference_documents/views/preferential_quotas.py index a602ab1fa..0cb68f136 100644 --- a/reference_documents/views/preferential_quotas.py +++ b/reference_documents/views/preferential_quotas.py @@ -1,10 +1,12 @@ from django.contrib.auth.mixins import PermissionRequiredMixin from django.shortcuts import redirect from django.urls import reverse +from django.views.generic import CreateView from django.views.generic import UpdateView from reference_documents.forms import PreferentialQuotaCreateUpdateForm from reference_documents.models import PreferentialQuota +from reference_documents.models import ReferenceDocumentVersion class PreferentialQuotaEditView(PermissionRequiredMixin, UpdateView): @@ -25,6 +27,43 @@ def post(self, request, *args, **kwargs): ) +class PreferentialQuotaCreateView(PermissionRequiredMixin, CreateView): + template_name = "reference_documents/preferential_quotas/edit.jinja" + permission_required = "reference_documents.edit_reference_document" + model = PreferentialQuota + form_class = PreferentialQuotaCreateUpdateForm + + def form_valid(self, form): + instance = form.instance + reference_document_version = ReferenceDocumentVersion.objects.get( + pk=self.kwargs["pk"], + ) + instance.order = len(reference_document_version.preferential_rates.all()) + 1 + instance.reference_document_version = reference_document_version + self.object = instance + return super(PreferentialQuotaCreateView, self).form_valid(form) + + def get_success_url(self): + return ( + reverse( + "reference_documents:version_details", + args=[self.object.reference_document_version.pk], + ) + + "#tariff-quotas" + ) + + # def post(self, request, *args, **kwargs): + # quota = self.get_object() + # quota.save() + # return redirect( + # reverse( + # "reference_documents:version_details", + # args=[quota.reference_document_version.pk], + # ) + # + "#tariff-quotas", + # ) + + class PreferentialQuotaDeleteView(PermissionRequiredMixin, UpdateView): template_name = "preferential_quotas/delete.jinja" permission_required = "reference_documents.edit_reference_document" diff --git a/reference_documents/views/reference_document_version_views.py b/reference_documents/views/reference_document_version_views.py index e0f9c8aea..d229c122f 100644 --- a/reference_documents/views/reference_document_version_views.py +++ b/reference_documents/views/reference_document_version_views.py @@ -216,6 +216,7 @@ def get_context_data(self, *args, **kwargs): reference_document_version_quotas[quota.quota_order_number] = { "data_rows": [row_to_add], "quota_order_number": quota_order_number, + "quota_order_number_text": quota.quota_order_number, } context["reference_document_version_duties"] = reference_document_version_duties @@ -225,7 +226,7 @@ def get_context_data(self, *args, **kwargs): class ReferenceDocumentVersionEditView(PermissionRequiredMixin, UpdateView): - template_name = "reference_document_versions/edit.jinja" + template_name = "reference_documents/reference_document_versions/edit.jinja" permission_required = "reference_documents.edit_reference_document" model = ReferenceDocumentVersion fields = ["version", "published_date", "entry_into_force_date"] From 14e732dd662790798fca6bca346a3bcb1dea2ba3 Mon Sep 17 00:00:00 2001 From: Doug Mills Date: Tue, 5 Mar 2024 10:14:58 +0000 Subject: [PATCH 082/118] fix views --- reference_documents/urls.py | 14 +++++++------- .../views/reference_document_version_views.py | 3 ++- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/reference_documents/urls.py b/reference_documents/urls.py index 50ff7a6d3..511889aac 100644 --- a/reference_documents/urls.py +++ b/reference_documents/urls.py @@ -52,38 +52,38 @@ name="confirm-create", ), path( - f"/confirm-update/", + f"reference_documents//confirm-update/", reference_document_views.ReferenceDocumentConfirmUpdate.as_view(), name="confirm-update", ), path( - f"/delete/", + f"reference_documents//delete/", reference_document_views.ReferenceDocumentDelete.as_view(), name="delete", ), path( - f"/confirm-delete/", + f"reference_documents//confirm-delete/", reference_document_views.ReferenceDocumentConfirmDelete.as_view(), name="confirm-delete", ), # reference document version views path( - "versions//", + "reference_documents_versions//", reference_document_version_views.ReferenceDocumentVersionDetails.as_view(), name="version-details", ), path( - "/version-create", + "reference_documents_versions//create", reference_document_version_views.ReferenceDocumentVersionCreate.as_view(), name="version-create", ), path( - "/version//edit/", + "reference_documents//version//edit/", reference_document_version_views.ReferenceDocumentVersionEdit.as_view(), name="version-edit", ), path( - "/version-delete", + "reference_documents//version-delete", reference_document_version_views.ReferenceDocumentVersionEdit.as_view(), name="version-delete", ), diff --git a/reference_documents/views/reference_document_version_views.py b/reference_documents/views/reference_document_version_views.py index 19c4ed0d5..b710e53ef 100644 --- a/reference_documents/views/reference_document_version_views.py +++ b/reference_documents/views/reference_document_version_views.py @@ -220,6 +220,7 @@ def get_context_data(self, *args, **kwargs): reference_document_version_quotas[quota.quota_order_number] = { "data_rows": [row_to_add], "quota_order_number": quota_order_number, + "quota_order_number_text": quota.quota_order_number, } context["reference_document_version_duties"] = reference_document_version_duties @@ -279,7 +280,7 @@ class ReferenceDocumentVersionConfirmUpdate(DetailView): template_name = ( "reference_documents/reference_document_versions/confirm_update.jinja" ) - model = ReferenceDocument + model = ReferenceDocumentVersion class ReferenceDocumentVersionConfirmDelete(TemplateView): From 89506832b2287442503929f17d136119a818a403 Mon Sep 17 00:00:00 2001 From: Matthew McKenzie <97194636+mattjamc@users.noreply.github.com> Date: Tue, 5 Mar 2024 10:18:46 +0000 Subject: [PATCH 083/118] Update ref doc create breadcrumbs --- .../jinja2/reference_documents/create.jinja | 10 ++++++++++ .../reference_document_versions/create.jinja | 10 ++++++++++ 2 files changed, 20 insertions(+) diff --git a/reference_documents/jinja2/reference_documents/create.jinja b/reference_documents/jinja2/reference_documents/create.jinja index d5506f4c2..a9e199f02 100644 --- a/reference_documents/jinja2/reference_documents/create.jinja +++ b/reference_documents/jinja2/reference_documents/create.jinja @@ -1,4 +1,14 @@ {% extends "layouts/create.jinja" %} +{% from "components/breadcrumbs/macro.njk" import govukBreadcrumbs %} + {% set page_title = "Create a new reference document" %} +{% block breadcrumb %} + {{ govukBreadcrumbs({ + "items": [{"text": "Home", "href": url("home")}, + {"text": "View reference documents", "href": url("reference_documents:index")}, + {"text": page_title}] + }) }} +{% endblock %} + diff --git a/reference_documents/jinja2/reference_documents/reference_document_versions/create.jinja b/reference_documents/jinja2/reference_documents/reference_document_versions/create.jinja index a1ad598ba..88727a086 100644 --- a/reference_documents/jinja2/reference_documents/reference_document_versions/create.jinja +++ b/reference_documents/jinja2/reference_documents/reference_document_versions/create.jinja @@ -1,4 +1,14 @@ {% extends "layouts/create.jinja" %} +{% from "components/breadcrumbs/macro.njk" import govukBreadcrumbs %} + {% set page_title = "Create a new version for reference document " ~ reference_document.area_id %} +{% block breadcrumb %} + {{ govukBreadcrumbs({ + "items": [{"text": "Home", "href": url("home")}, + {"text": "View reference documents", "href": url("reference_documents:index")}, + {"text": "Reference document " ~ reference_document.area_id, "href": url("reference_documents:details", kwargs={"pk":reference_document.pk})}, + {"text": page_title}] + }) }} +{% endblock %} \ No newline at end of file From 85cedb137aec06ac07e3930e27ac46b16c1a82ec Mon Sep 17 00:00:00 2001 From: Matthew McKenzie <97194636+mattjamc@users.noreply.github.com> Date: Tue, 5 Mar 2024 10:19:18 +0000 Subject: [PATCH 084/118] Ref doc version delete --- reference_documents/forms.py | 25 +++++++ .../reference_document_versions/delete.jinja | 73 +++++++++++++++++++ reference_documents/urls.py | 4 +- .../views/reference_document_version_views.py | 34 +++++++++ .../views/reference_document_views.py | 1 + 5 files changed, 135 insertions(+), 2 deletions(-) create mode 100644 reference_documents/jinja2/reference_documents/reference_document_versions/delete.jinja diff --git a/reference_documents/forms.py b/reference_documents/forms.py index f0be1430b..a2a77eacf 100644 --- a/reference_documents/forms.py +++ b/reference_documents/forms.py @@ -402,3 +402,28 @@ class Meta: "published_date", "entry_into_force_date", ] + + +class ReferenceDocumentVersionDeleteForm(forms.Form): + def __init__(self, *args, **kwargs) -> None: + self.instance = kwargs.pop("instance") + super().__init__(*args, **kwargs) + + def clean(self): + cleaned_data = super().clean() + reference_document_version = self.instance + preferential_duty_rates = models.PreferentialRate.objects.all().filter( + reference_document_version=reference_document_version, + ) + print(f"duty rates {preferential_duty_rates}") + tariff_quotas = models.PreferentialQuota.objects.all().filter( + reference_document_version=reference_document_version, + ) + print(f"tariff_quotas {tariff_quotas}") + if preferential_duty_rates or tariff_quotas: + raise forms.ValidationError( + f"Reference Document version {reference_document_version.version} cannot be deleted as it has" + f" current preferential duty rates or tariff quotas.", + ) + + return cleaned_data diff --git a/reference_documents/jinja2/reference_documents/reference_document_versions/delete.jinja b/reference_documents/jinja2/reference_documents/reference_document_versions/delete.jinja new file mode 100644 index 000000000..d5bd0febb --- /dev/null +++ b/reference_documents/jinja2/reference_documents/reference_document_versions/delete.jinja @@ -0,0 +1,73 @@ +{% extends "layouts/layout.jinja" %} + +{% from "components/breadcrumbs/macro.njk" import govukBreadcrumbs %} +{% from "components/warning-text/macro.njk" import govukWarningText %} +{% from "components/button/macro.njk" import govukButton %} +{% from "components/error-summary/macro.njk" import govukErrorSummary %} + +{% set page_title = "Delete Reference Document " ~ object.reference_document.area_id ~ " version " ~ object.version %} + +{% block breadcrumb %} + {{ govukBreadcrumbs({ + "items": [{"text": "Home", "href": url("home")}, + {"text": "View reference documents", "href": url("reference_documents:index")}, + {"text": page_title}] + }) }} +{% endblock %} + +{% block content %} +
    +
    +

    {{ page_title }}

    +
    +
    + +
    +
    +

    Are you sure you want to permanently delete Reference Document version {{ object.reference_document.area_id }} version {{ object.version }}?

    + + {{ govukWarningText({ + "text": "Deleted Reference Documents can not be recovered.", + "iconFallbackText": "Warning" + }) }} + +
    + + + {% set error_list = [] %} + + {% for field, errors in form.errors.items() %} + {% for error in errors.data %} + {% if error.message|length > 1 %} + {{ error_list.append({ + "text": error.message, + "href": "#" ~ (form.prefix ~ "-" if form.prefix else "") ~ field ~ ("_" ~ error.subfield if error.subfield is defined else ""), + }) or "" }} + {% endif %} + {% endfor %} + {% endfor %} + + {% if error_list|length > 0 %} + {{ govukErrorSummary({ + "titleText": "There is a problem", + "errorList": error_list + }) }} + {% endif %} + +
    + {{ govukButton({ + "text": "Delete", + "classes": "govuk-button--warning", + "name": "action", + "value": "delete" + }) }} + {{ govukButton({ + "text": "Cancel", + "href": url("reference_documents:index"), + "classes": "govuk-button--secondary" + }) }} +
    +
    +
    +
    +{% endblock %} diff --git a/reference_documents/urls.py b/reference_documents/urls.py index 5f17d7171..98013055d 100644 --- a/reference_documents/urls.py +++ b/reference_documents/urls.py @@ -83,8 +83,8 @@ name="version-edit", ), path( - "/version-delete", - reference_document_version_views.ReferenceDocumentVersionEdit.as_view(), + "/version//version-delete", + reference_document_version_views.ReferenceDocumentVersionDelete.as_view(), name="version-delete", ), path( diff --git a/reference_documents/views/reference_document_version_views.py b/reference_documents/views/reference_document_version_views.py index 19c4ed0d5..d2d882786 100644 --- a/reference_documents/views/reference_document_version_views.py +++ b/reference_documents/views/reference_document_version_views.py @@ -1,9 +1,12 @@ from django.contrib.auth.mixins import PermissionRequiredMixin +from django.shortcuts import redirect from django.urls import reverse from django.views.generic import CreateView +from django.views.generic import DeleteView from django.views.generic import DetailView from django.views.generic import TemplateView from django.views.generic import UpdateView +from django.views.generic.edit import FormMixin from commodities.models import GoodsNomenclature from geo_areas.models import GeographicalAreaDescription @@ -268,6 +271,37 @@ def get_success_url(self): ) +class ReferenceDocumentVersionDelete(PermissionRequiredMixin, FormMixin, DeleteView): + form_class = forms.ReferenceDocumentVersionDeleteForm + model = ReferenceDocumentVersion + permission_required = "reference_documents.delete_referencedocumentversion" + template_name = "reference_documents/reference_document_versions/delete.jinja" + + # TODO: Update this to get rid of FormMixin with Django 4.2 as no need to overwrite the post anymore + def get_success_url(self) -> str: + return reverse( + "reference_documents:version-confirm-delete", + kwargs={"deleted_pk": self.kwargs["pk"]}, + ) + + def get_form_kwargs(self): + kwargs = super().get_form_kwargs() + kwargs["instance"] = self.get_object() + return kwargs + + def post(self, request, *args, **kwargs): + self.object = self.get_object() + form = self.get_form() + if form.is_valid(): + return self.form_valid(form) + else: + return self.form_invalid(form) + + def form_valid(self, form): + self.object.delete() + return redirect(self.get_success_url()) + + class ReferenceDocumentVersionConfirmCreate(DetailView): template_name = ( "reference_documents/reference_document_versions/confirm_create.jinja" diff --git a/reference_documents/views/reference_document_views.py b/reference_documents/views/reference_document_views.py index f02dc7f4f..c1c30c638 100644 --- a/reference_documents/views/reference_document_views.py +++ b/reference_documents/views/reference_document_views.py @@ -117,6 +117,7 @@ def get_context_data(self, *args, **kwargs): { "html": f'Version details
    ' f'Edit
    ' + f'Delete
    ' f'Alignment reports', }, ], From 6f9cb69d6009b0fc39c95c2014844b135d10ff49 Mon Sep 17 00:00:00 2001 From: Matthew McKenzie <97194636+mattjamc@users.noreply.github.com> Date: Wed, 6 Mar 2024 09:35:37 +0000 Subject: [PATCH 085/118] Delete ref doc version and confirmation pages --- .../confirm_create.jinja | 11 ++++++----- .../confirm_delete.jinja | 13 ++++++++++--- .../confirm_update.jinja | 10 +++++----- .../reference_document_versions/delete.jinja | 5 +++-- reference_documents/urls.py | 4 ++-- .../views/reference_document_version_views.py | 7 ++++++- .../views/reference_document_views.py | 4 ++-- 7 files changed, 34 insertions(+), 20 deletions(-) diff --git a/reference_documents/jinja2/reference_documents/reference_document_versions/confirm_create.jinja b/reference_documents/jinja2/reference_documents/reference_document_versions/confirm_create.jinja index 26761970f..05b3055b9 100644 --- a/reference_documents/jinja2/reference_documents/reference_document_versions/confirm_create.jinja +++ b/reference_documents/jinja2/reference_documents/reference_document_versions/confirm_create.jinja @@ -4,13 +4,14 @@ {% from "components/button/macro.njk" import govukButton %} {% from "components/breadcrumbs/macro.njk" import govukBreadcrumbs %} -{% set page_title = "Reference Document version" ~ object.reference_document ~ " successfully created" %} +{% set page_title = "Reference Document " ~ object.reference_document.area_id ~ " version " ~ object.version ~ " successfully created" %} {% block breadcrumb %} {{ govukBreadcrumbs({ "items": [{"text": "Home", "href": url("home")}, {"text": "View reference documents", "href": url("reference_documents:index")}, - {"text": "Create a Reference Document", "href": url("reference_documents:create")}, + {"text": "Reference Document " ~ object.reference_document.area_id, "href": url("reference_documents:details", kwargs={"pk":object.reference_document.pk})}, + {"text": "Create a new version ", "href": url("reference_documents:version-create", kwargs={"pk":object.reference_document.pk})}, {"text": page_title}] }) }} {% endblock %} @@ -19,7 +20,7 @@
    {{ govukPanel({ - "titleText": "Reference Document " ~ object.area_id ~ " has been created", + "titleText": "Reference Document " ~ object.reference_document.area_id ~ " version " ~ object.version ~ " has been created", "text": "This change has taken immediate effect", "classes": "govuk-!-margin-bottom-7" }) }} @@ -27,8 +28,8 @@
    {{ govukButton({ - "text": "View Reference Document " ~ object.area_id, - "href": url("reference_documents:details", kwargs={"pk":object.pk}), + "text": "View Reference Document " ~ object.reference_document.area_id ~ " version " ~ object.version, + "href": url("reference_documents:version-details", kwargs={"pk":object.pk}), }) }} {{ govukButton({ "text": "Back to View reference documents", diff --git a/reference_documents/jinja2/reference_documents/reference_document_versions/confirm_delete.jinja b/reference_documents/jinja2/reference_documents/reference_document_versions/confirm_delete.jinja index 29b53ec0c..26c4bfbdf 100644 --- a/reference_documents/jinja2/reference_documents/reference_document_versions/confirm_delete.jinja +++ b/reference_documents/jinja2/reference_documents/reference_document_versions/confirm_delete.jinja @@ -4,13 +4,20 @@ {% from "components/button/macro.njk" import govukButton %} {% from "components/breadcrumbs/macro.njk" import govukBreadcrumbs %} -{% set page_title = "Reference Document " ~ deleted_pk ~ " deleted" %} +{% set area_id = request.session['deleted_version']['area_id'] %} +{% set version = request.session['deleted_version']['version'] %} +{% set ref_doc_pk = request.session['deleted_version']['ref_doc_pk'] %} + +{% set page_title = "Reference Document " ~ area_id ~ " version " ~ version ~ " successfully deleted" %} + {% block breadcrumb %} {{ govukBreadcrumbs({ "items": [{"text": "Home", "href": url("home")}, {"text": "View reference documents", "href": url("reference_documents:index")}, - {"text": page_title}] + {"text": "Reference Document " ~ area_id, "href": url("reference_documents:details", kwargs={"pk":ref_doc_pk})}, + {"text": "Delete Reference Document " ~ area_id ~ " version " ~ version}, + {"text": page_title}] }) }} {% endblock %} @@ -18,7 +25,7 @@
    {{ govukPanel({ - "titleText": "Reference Document " ~ deleted_pk ~ " has been deleted", + "titleText": "Reference Document " ~ request.session['deleted_version']['area_id'] ~ " version " ~ request.session['deleted_version']['version'] ~ " has been deleted", "text": "This change has taken immediate effect", "classes": "govuk-!-margin-bottom-7" }) }} diff --git a/reference_documents/jinja2/reference_documents/reference_document_versions/confirm_update.jinja b/reference_documents/jinja2/reference_documents/reference_document_versions/confirm_update.jinja index 66a12d21d..711e17730 100644 --- a/reference_documents/jinja2/reference_documents/reference_document_versions/confirm_update.jinja +++ b/reference_documents/jinja2/reference_documents/reference_document_versions/confirm_update.jinja @@ -10,8 +10,8 @@ {{ govukBreadcrumbs({ "items": [{"text": "Home", "href": url("home")}, {"text": "View reference documents", "href": url("reference_documents:index")}, - {"text": "Reference Document " ~ object.reference_document.area_id}, - {"text": "Version " ~ object.version}, + {"text": "Reference Document " ~ object.reference_document.area_id, "href": url("reference_documents:details", kwargs={"pk":object.reference_document.pk})}, + {"text": "Edit Reference Document " ~ object.reference_document.area_id ~ " version " ~ object.version, "href": url("reference_documents:version-edit", kwargs={"ref_doc_pk": object.reference_document.pk, "pk":object.pk})}, {"text": page_title}] }) }} {% endblock %} @@ -28,11 +28,11 @@
    {{ govukButton({ - "text": "View your Reference Document", - "href": url("reference_documents:details", kwargs={"pk":object.pk}), + "text": "View Reference Document " ~ object.reference_document.area_id ~ " version " ~ object.version, + "href": url("reference_documents:version-details", kwargs={"pk":object.pk}), }) }} {{ govukButton({ - "text": "Back to View Reference Documents", + "text": "Back to View reference documents", "href": url("reference_documents:index"), "classes": "govuk-button--secondary" }) }} diff --git a/reference_documents/jinja2/reference_documents/reference_document_versions/delete.jinja b/reference_documents/jinja2/reference_documents/reference_document_versions/delete.jinja index d5bd0febb..e2c1f83f7 100644 --- a/reference_documents/jinja2/reference_documents/reference_document_versions/delete.jinja +++ b/reference_documents/jinja2/reference_documents/reference_document_versions/delete.jinja @@ -11,7 +11,8 @@ {{ govukBreadcrumbs({ "items": [{"text": "Home", "href": url("home")}, {"text": "View reference documents", "href": url("reference_documents:index")}, - {"text": page_title}] + {"text": "Reference Document " ~ object.reference_document.area_id, "href": url("reference_documents:details", kwargs={"pk":object.reference_document.pk})}, + {"text": page_title}] }) }} {% endblock %} @@ -27,7 +28,7 @@

    Are you sure you want to permanently delete Reference Document version {{ object.reference_document.area_id }} version {{ object.version }}?

    {{ govukWarningText({ - "text": "Deleted Reference Documents can not be recovered.", + "text": "Deleted Reference Document versions can not be recovered.", "iconFallbackText": "Warning" }) }} diff --git a/reference_documents/urls.py b/reference_documents/urls.py index 5e6658ad0..bb6d7b0fd 100644 --- a/reference_documents/urls.py +++ b/reference_documents/urls.py @@ -78,12 +78,12 @@ name="version-create", ), path( - "reference_documents_versions//version//edit/", + "reference_documents_versions//version//edit/", reference_document_version_views.ReferenceDocumentVersionEdit.as_view(), name="version-edit", ), path( - "reference_documents_versions//version//version-delete/", + "reference_documents_versions//version//version-delete/", reference_document_version_views.ReferenceDocumentVersionDelete.as_view(), name="version-delete", ), diff --git a/reference_documents/views/reference_document_version_views.py b/reference_documents/views/reference_document_version_views.py index 4af7f7bee..40bfc7a4c 100644 --- a/reference_documents/views/reference_document_version_views.py +++ b/reference_documents/views/reference_document_version_views.py @@ -294,6 +294,11 @@ def post(self, request, *args, **kwargs): self.object = self.get_object() form = self.get_form() if form.is_valid(): + self.request.session["deleted_version"] = { + "ref_doc_pk": f"{self.object.reference_document.pk}", + "area_id": f"{self.object.reference_document.area_id}", + "version": f"{self.object.version}", + } return self.form_valid(form) else: return self.form_invalid(form) @@ -307,7 +312,7 @@ class ReferenceDocumentVersionConfirmCreate(DetailView): template_name = ( "reference_documents/reference_document_versions/confirm_create.jinja" ) - model = ReferenceDocument + model = ReferenceDocumentVersion class ReferenceDocumentVersionConfirmUpdate(DetailView): diff --git a/reference_documents/views/reference_document_views.py b/reference_documents/views/reference_document_views.py index c1c30c638..0c7d0189d 100644 --- a/reference_documents/views/reference_document_views.py +++ b/reference_documents/views/reference_document_views.py @@ -116,8 +116,8 @@ def get_context_data(self, *args, **kwargs): }, { "html": f'Version details
    ' - f'Edit
    ' - f'Delete
    ' + f'Edit
    ' + f'Delete
    ' f'Alignment reports', }, ], From 12d9533d5d5d6e76bc4372cb3be9f262a1cd08ed Mon Sep 17 00:00:00 2001 From: Matthew McKenzie <97194636+mattjamc@users.noreply.github.com> Date: Wed, 6 Mar 2024 09:54:44 +0000 Subject: [PATCH 086/118] Area ID and version form validation --- reference_documents/forms.py | 17 +++++++++++++++-- .../views/reference_document_version_views.py | 1 - 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/reference_documents/forms.py b/reference_documents/forms.py index 1b7306499..482b44701 100644 --- a/reference_documents/forms.py +++ b/reference_documents/forms.py @@ -11,9 +11,11 @@ from common.forms import DateInputFieldFixed from common.forms import ValidityPeriodForm +from geo_areas.validators import area_id_validator from reference_documents import models from reference_documents.models import PreferentialQuota from reference_documents.models import PreferentialRate +from reference_documents.models import ReferenceDocumentVersion from reference_documents.validators import commodity_code_validator from reference_documents.validators import order_number_validator @@ -99,10 +101,12 @@ class ReferenceDocumentCreateUpdateForm(forms.ModelForm): ) area_id = forms.CharField( label="Area ID", + validators=[area_id_validator], error_messages={ "required": "An area ID is required", "unique": "A Reference Document with this area ID already exists", "max_length": "The area ID can be at most 4 characters long", + "invalid": "Enter the area ID in the correct format.", }, ) @@ -402,6 +406,17 @@ def __init__(self, *args, **kwargs): ), ) + def clean(self): + cleaned_data = super().clean() + ref_doc = cleaned_data.get("reference_document") + latest_version = ReferenceDocumentVersion.objects.filter( + reference_document=ref_doc, + ).latest("created_at") + if float(cleaned_data.get("version")) < latest_version.version: + raise forms.ValidationError( + "New versions of this reference document must be a higher number than previous versions.", + ) + class Meta: model = models.ReferenceDocumentVersion fields = [ @@ -423,11 +438,9 @@ def clean(self): preferential_duty_rates = models.PreferentialRate.objects.all().filter( reference_document_version=reference_document_version, ) - print(f"duty rates {preferential_duty_rates}") tariff_quotas = models.PreferentialQuota.objects.all().filter( reference_document_version=reference_document_version, ) - print(f"tariff_quotas {tariff_quotas}") if preferential_duty_rates or tariff_quotas: raise forms.ValidationError( f"Reference Document version {reference_document_version.version} cannot be deleted as it has" diff --git a/reference_documents/views/reference_document_version_views.py b/reference_documents/views/reference_document_version_views.py index 40bfc7a4c..2653afd07 100644 --- a/reference_documents/views/reference_document_version_views.py +++ b/reference_documents/views/reference_document_version_views.py @@ -246,7 +246,6 @@ def get_initial(self): def get_context_data(self, **kwargs): context_data = super().get_context_data(**kwargs) - print(self.kwargs) context_data["reference_document"] = ReferenceDocument.objects.all().get( pk=self.kwargs["pk"], ) From cd8b173472a503768edacff22cbc091b46dddb12 Mon Sep 17 00:00:00 2001 From: Doug Mills Date: Wed, 6 Mar 2024 10:46:33 +0000 Subject: [PATCH 087/118] update data --- reference_documents/models.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/reference_documents/models.py b/reference_documents/models.py index 804e35aa7..bc879295a 100644 --- a/reference_documents/models.py +++ b/reference_documents/models.py @@ -108,6 +108,27 @@ class PreferentialRate(models.Model): ) +class PreferentialQuotaOrderNumber(models.Model): + quota_order_number = models.CharField( + max_length=6, + db_index=True, + ) + coefficient = models.DecimalField( + max_digits=6, + decimal_places=4, + blank=True, + null=True, + default=None, + ) + main_quota = models.ForeignKey( + "self", + related_name="sub_quotas", + blank=True, + null=True, + on_delete=models.PROTECT, + ) + + class PreferentialQuota(models.Model): quota_order_number = models.CharField( max_length=6, From f0a2fd7fd08202abe171ab56abbe7a5c938d380d Mon Sep 17 00:00:00 2001 From: Matthew McKenzie <97194636+mattjamc@users.noreply.github.com> Date: Wed, 6 Mar 2024 16:55:36 +0000 Subject: [PATCH 088/118] Ref doc versions form and view tests --- reference_documents/forms.py | 29 ++- .../test_reference_document_versions_forms.py | 82 ++++++++ .../test_reference_document_versions_views.py | 185 ++++++++++++++++++ .../tests/test_reference_documents_forms.py | 14 +- 4 files changed, 290 insertions(+), 20 deletions(-) diff --git a/reference_documents/forms.py b/reference_documents/forms.py index 482b44701..746a558a4 100644 --- a/reference_documents/forms.py +++ b/reference_documents/forms.py @@ -105,8 +105,7 @@ class ReferenceDocumentCreateUpdateForm(forms.ModelForm): error_messages={ "required": "An area ID is required", "unique": "A Reference Document with this area ID already exists", - "max_length": "The area ID can be at most 4 characters long", - "invalid": "Enter the area ID in the correct format.", + "invalid": "Enter the area ID in the correct format", }, ) @@ -383,6 +382,12 @@ class ReferenceDocumentVersionsEditCreateForm(forms.ModelForm): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) + self.fields["published_date"].error_messages[ + "required" + ] = "A published date is required" + self.fields["entry_into_force_date"].error_messages[ + "required" + ] = "An entry into force date is required" self.helper = FormHelper(self) self.helper.label_size = Size.SMALL self.helper.legend_size = Size.SMALL @@ -409,13 +414,17 @@ def __init__(self, *args, **kwargs): def clean(self): cleaned_data = super().clean() ref_doc = cleaned_data.get("reference_document") - latest_version = ReferenceDocumentVersion.objects.filter( - reference_document=ref_doc, - ).latest("created_at") - if float(cleaned_data.get("version")) < latest_version.version: - raise forms.ValidationError( - "New versions of this reference document must be a higher number than previous versions.", - ) + if cleaned_data.get("version"): + try: + latest_version = ReferenceDocumentVersion.objects.filter( + reference_document=ref_doc, + ).latest("created_at") + if float(cleaned_data.get("version")) < latest_version.version: + raise forms.ValidationError( + "New versions of this reference document must be a higher number than previous versions", + ) + except ReferenceDocumentVersion.DoesNotExist: + pass class Meta: model = models.ReferenceDocumentVersion @@ -444,7 +453,7 @@ def clean(self): if preferential_duty_rates or tariff_quotas: raise forms.ValidationError( f"Reference Document version {reference_document_version.version} cannot be deleted as it has" - f" current preferential duty rates or tariff quotas.", + f" current preferential duty rates or tariff quotas", ) return cleaned_data diff --git a/reference_documents/tests/test_reference_document_versions_forms.py b/reference_documents/tests/test_reference_document_versions_forms.py index a7057c2cc..0ab7c8141 100644 --- a/reference_documents/tests/test_reference_document_versions_forms.py +++ b/reference_documents/tests/test_reference_document_versions_forms.py @@ -1,3 +1,85 @@ import pytest +from reference_documents import forms +from reference_documents.tests import factories + pytestmark = pytest.mark.django_db + + +@pytest.mark.reference_documents +def test_ref_doc_version_create_update_valid_data(): + """Test that ReferenceDocumentVersionCreateEditForm is valid when completed + correctly.""" + ref_doc = factories.ReferenceDocumentFactory.create() + data = { + "reference_document": ref_doc, + "version": "2.0", + "published_date_0": "11", + "published_date_1": "1", + "published_date_2": "2024", + "entry_into_force_date_0": "1", + "entry_into_force_date_1": "1", + "entry_into_force_date_2": "2024", + } + + form = forms.ReferenceDocumentVersionsEditCreateForm(data=data) + assert form.is_valid() + + +@pytest.mark.reference_documents +def test_ref_doc_version_create_update_invalid_data(): + """Test that ReferenceDocumentVersionCreateEditForm is invalid when not + complete correctly.""" + form = forms.ReferenceDocumentVersionsEditCreateForm(data={}) + assert not form.is_valid() + assert "A version number is required" in form.errors["version"] + assert "A published date is required" in form.errors["published_date"] + assert ( + "An entry into force date is required" in form.errors["entry_into_force_date"] + ) + + # Test that it fails if a version of a higher number already exists + ref_doc = factories.ReferenceDocumentFactory.create() + factories.ReferenceDocumentVersionFactory.create( + reference_document=ref_doc, + version=3.0, + ) + data = { + "reference_document": ref_doc, + "version": "2.0", + "published_date_0": "11", + "published_date_1": "1", + "published_date_2": "2024", + "entry_into_force_date_0": "1", + "entry_into_force_date_1": "1", + "entry_into_force_date_2": "2024", + } + form = forms.ReferenceDocumentVersionsEditCreateForm(data=data) + assert not form.is_valid() + assert ( + "New versions of this reference document must be a higher number than previous versions" + in form.errors["__all__"] + ) + + +@pytest.mark.reference_documents +def test_ref_doc_version_delete_valid(): + """Test that ReferenceDocumentVersionDeleteForm is valid for a reference + document with no versions.""" + version = factories.ReferenceDocumentVersionFactory.create() + form = forms.ReferenceDocumentVersionsEditCreateForm(data={}, instance=version) + assert not form.is_valid() + + +@pytest.mark.reference_documents +def test_ref_doc_version_delete_invalid(): + """Test that ReferenceDocumentVersionDeleteForm is invalid for a reference + document with versions.""" + version = factories.ReferenceDocumentVersionFactory.create() + factories.PreferentialRateFactory.create(reference_document_version=version) + form = forms.ReferenceDocumentVersionDeleteForm(data={}, instance=version) + assert not form.is_valid() + assert ( + f"Reference Document version {version.version} cannot be deleted as it has current preferential duty rates or tariff quotas" + in form.errors["__all__"] + ) diff --git a/reference_documents/tests/test_reference_document_versions_views.py b/reference_documents/tests/test_reference_document_versions_views.py index a7057c2cc..dbfcd55d2 100644 --- a/reference_documents/tests/test_reference_document_versions_views.py +++ b/reference_documents/tests/test_reference_document_versions_views.py @@ -1,3 +1,188 @@ +import datetime + import pytest +from bs4 import BeautifulSoup +from django.contrib.auth.models import Permission +from django.urls import reverse + +from reference_documents.models import ReferenceDocument +from reference_documents.models import ReferenceDocumentVersion +from reference_documents.tests import factories pytestmark = pytest.mark.django_db + + +@pytest.mark.reference_documents +def test_ref_doc_version_create_creates_object_and_redirects(valid_user, client): + """Tests that posting the reference document version create form adds the + new version to the database and redirects to the confirm-create page.""" + valid_user.user_permissions.add( + Permission.objects.get(codename="add_referencedocumentversion"), + ) + client.force_login(valid_user) + ref_doc = factories.ReferenceDocumentFactory.create() + + create_url = reverse( + "reference_documents:version-create", + kwargs={"pk": ref_doc.pk}, + ) + form_data = { + "reference_document": ref_doc.pk, + "version": "2.0", + "published_date_0": "11", + "published_date_1": "1", + "published_date_2": "2024", + "entry_into_force_date_0": "1", + "entry_into_force_date_1": "1", + "entry_into_force_date_2": "2024", + } + response = client.post(create_url, form_data) + assert response.status_code == 302 + + ref_doc = ReferenceDocumentVersion.objects.get( + reference_document=form_data["reference_document"], + ) + assert ref_doc + assert response.url == reverse( + "reference_documents:version-confirm-create", + kwargs={"pk": ref_doc.pk}, + ) + + +@pytest.mark.reference_documents +def test_ref_doc_version_edit_updates_ref_doc_object(client, valid_user): + """Tests that posting the reference document version edit form updates the + reference document and redirects to the confirm-update page.""" + valid_user.user_permissions.add( + Permission.objects.get(codename="change_referencedocumentversion"), + ) + client.force_login(valid_user) + ref_doc_version = factories.ReferenceDocumentVersionFactory.create() + + edit_url = reverse( + "reference_documents:version-edit", + kwargs={ + "pk": ref_doc_version.pk, + "ref_doc_pk": ref_doc_version.reference_document.pk, + }, + ) + form_data = { + "reference_document": ref_doc_version.reference_document.pk, + "version": "6.0", + "published_date_0": "1", + "published_date_1": "1", + "published_date_2": "2024", + "entry_into_force_date_0": "1", + "entry_into_force_date_1": "1", + "entry_into_force_date_2": "2024", + } + response = client.get(edit_url) + assert response.status_code == 200 + assert ref_doc_version.version != 6.0 + + response = client.post(edit_url, form_data) + assert response.status_code == 302 + assert response.url == reverse( + "reference_documents:version-confirm-update", + kwargs={"pk": ref_doc_version.reference_document.pk}, + ) + ref_doc_version.refresh_from_db() + assert ref_doc_version.version == 6.0 + assert ref_doc_version.published_date == datetime.date(2024, 1, 1) + assert ref_doc_version.entry_into_force_date == datetime.date(2024, 1, 1) + + +@pytest.mark.reference_documents +def test_successfully_delete_ref_doc_version(valid_user, client): + """Tests that posting the reference document version delete form deletes the + reference document and redirects to the confirm-delete page.""" + valid_user.user_permissions.add( + Permission.objects.get(codename="delete_referencedocumentversion"), + ) + client.force_login(valid_user) + ref_doc_version = factories.ReferenceDocumentVersionFactory.create() + ref_doc_pk = ref_doc_version.reference_document.pk + area_id = ref_doc_version.reference_document.area_id + assert ReferenceDocumentVersion.objects.filter(pk=ref_doc_version.pk) + delete_url = reverse( + "reference_documents:version-delete", + kwargs={"pk": ref_doc_version.pk, "ref_doc_pk": ref_doc_pk}, + ) + response = client.get(delete_url) + page = BeautifulSoup(response.content, "html.parser") + assert response.status_code == 200 + assert ( + f"Delete Reference Document {area_id} version {ref_doc_version.version}" + in page.select("main h1")[0].text + ) + response = client.post(delete_url) + assert response.status_code == 302 + assert response.url == reverse( + "reference_documents:version-confirm-delete", + kwargs={"deleted_pk": ref_doc_version.pk}, + ) + assert not ReferenceDocumentVersion.objects.filter(pk=ref_doc_version.pk) + + +@pytest.mark.reference_documents +def test_delete_ref_doc_version_invalid(valid_user, client): + """Test that deleting a reference document version with preferential rates + does not work.""" + valid_user.user_permissions.add( + Permission.objects.get(codename="delete_referencedocumentversion"), + ) + client.force_login(valid_user) + + preferential_rate = factories.PreferentialRateFactory.create() + ref_doc_version = preferential_rate.reference_document_version + ref_doc = ref_doc_version.reference_document + + delete_url = reverse( + "reference_documents:version-delete", + kwargs={"pk": ref_doc_version.pk, "ref_doc_pk": ref_doc.pk}, + ) + response = client.get(delete_url) + assert response.status_code == 200 + + response = client.post(delete_url) + assert response.status_code == 200 + assert ( + f"Reference Document version {ref_doc_version.version} cannot be deleted as it has current preferential duty rates or tariff quotas" + in str(response.content) + ) + assert ReferenceDocument.objects.filter(pk=ref_doc.pk) + + +@pytest.mark.reference_documents +def test_ref_doc_crud_without_permission(valid_user_client): + # TODO: potentially update this if the permissions for reference doc behaviour changes + ref_doc_version = factories.ReferenceDocumentVersionFactory.create() + ref_doc = ref_doc_version.reference_document + create_url = reverse( + "reference_documents:version-create", + kwargs={"pk": ref_doc_version.pk}, + ) + edit_url = reverse( + "reference_documents:version-edit", + kwargs={"pk": ref_doc_version.pk, "ref_doc_pk": ref_doc.pk}, + ) + delete_url = reverse( + "reference_documents:version-delete", + kwargs={"pk": ref_doc_version.pk, "ref_doc_pk": ref_doc.pk}, + ) + form_data = { + "reference_document": ref_doc.pk, + "version": "2.0", + "published_date_0": "11", + "published_date_1": "1", + "published_date_2": "2024", + "entry_into_force_date_0": "1", + "entry_into_force_date_1": "1", + "entry_into_force_date_2": "2024", + } + response = valid_user_client.post(create_url, form_data) + assert response.status_code == 403 + response = valid_user_client.post(edit_url, form_data) + assert response.status_code == 403 + response = valid_user_client.post(delete_url) + assert response.status_code == 403 diff --git a/reference_documents/tests/test_reference_documents_forms.py b/reference_documents/tests/test_reference_documents_forms.py index 6c43ed88f..6e27a6ee2 100644 --- a/reference_documents/tests/test_reference_documents_forms.py +++ b/reference_documents/tests/test_reference_documents_forms.py @@ -18,7 +18,7 @@ def test_ref_doc_create_update_form_valid_data(): @pytest.mark.reference_documents def test_ref_doc_create_update_form_invalid_data(): - """Test that ReferenceDocumentCreateUpdateForm is invalid when not complete + """Test that ReferenceDocumentCreateUpdateForm is invalid when not completed correctly.""" form = forms.ReferenceDocumentCreateUpdateForm(data={}) assert not form.is_valid() @@ -32,7 +32,7 @@ def test_ref_doc_create_update_form_invalid_data(): data = {"title": "Reference document for XY", "area_id": "VWXYZ"} form = forms.ReferenceDocumentCreateUpdateForm(data=data) assert not form.is_valid() - assert "The area ID can be at most 4 characters long" in form.errors["area_id"] + assert "Enter the area ID in the correct format" in form.errors["area_id"] assert "A Reference Document with this title already exists" in form.errors["title"] @@ -40,10 +40,7 @@ def test_ref_doc_create_update_form_invalid_data(): def test_ref_doc_delete_form_valid(): """Test that ReferenceDocumentDeleteForm is valid for a reference document with no versions.""" - ref_doc = factories.ReferenceDocumentFactory.create( - title="Reference document for XY", - area_id="XY", - ) + ref_doc = factories.ReferenceDocumentFactory.create() form = forms.ReferenceDocumentDeleteForm(data={}, instance=ref_doc) assert form.is_valid() @@ -52,10 +49,7 @@ def test_ref_doc_delete_form_valid(): def test_ref_doc_delete_form_invalid(): """Test that ReferenceDocumentDeleteForm is invalid for a reference document with versions.""" - ref_doc = factories.ReferenceDocumentFactory.create( - title="Reference document for XY", - area_id="XY", - ) + ref_doc = factories.ReferenceDocumentFactory.create() factories.ReferenceDocumentVersionFactory(reference_document=ref_doc) form = forms.ReferenceDocumentDeleteForm(data={}, instance=ref_doc) assert not form.is_valid() From c0b133b08de4dab83279dbe7826117ef1edaa2d6 Mon Sep 17 00:00:00 2001 From: Matthew McKenzie <97194636+mattjamc@users.noreply.github.com> Date: Thu, 7 Mar 2024 10:38:22 +0000 Subject: [PATCH 089/118] Add model test for get_area_name_by_id --- ...test_reference_document_versions_models.py | 23 +++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/reference_documents/tests/test_reference_document_versions_models.py b/reference_documents/tests/test_reference_document_versions_models.py index f0cacc009..6fa72fd9e 100644 --- a/reference_documents/tests/test_reference_document_versions_models.py +++ b/reference_documents/tests/test_reference_document_versions_models.py @@ -1,6 +1,9 @@ import pytest -from reference_documents.tests.factories import ReferenceDocumentVersionFactory +from common.tests.factories import GeographicalAreaDescriptionFactory +from common.tests.factories import GeographicalAreaFactory +from geo_areas.models import GeographicalAreaDescription +from reference_documents.tests import factories pytestmark = pytest.mark.django_db @@ -8,7 +11,7 @@ @pytest.mark.reference_documents class TestReferenceDocumentVersion: def test_create_with_defaults(self): - target = ReferenceDocumentVersionFactory() + target = factories.ReferenceDocumentVersionFactory() assert target.created_at is not None assert target.updated_at is not None @@ -17,3 +20,19 @@ def test_create_with_defaults(self): assert target.entry_into_force_date is not None assert target.reference_document is not None assert target.status is not None + + +@pytest.mark.reference_documents +def test_get_area_name_by_area_id(): + ref_doc = factories.ReferenceDocumentFactory.create(area_id="BE") + geo_area = GeographicalAreaFactory.create(area_id="BE") + GeographicalAreaDescriptionFactory(described_geographicalarea=geo_area) + + ref_doc_area_name = ref_doc.get_area_name_by_area_id() + geo_area_description = ( + GeographicalAreaDescription.objects.latest_approved() + .filter(described_geographicalarea__area_id=geo_area.area_id) + .order_by("-validity_start") + .first() + ) + assert ref_doc_area_name == geo_area_description.description From 0efc1f12167b46fe4aa6143a4eea14bc2ed14105 Mon Sep 17 00:00:00 2001 From: Doug Mills Date: Thu, 7 Mar 2024 15:06:22 +0000 Subject: [PATCH 090/118] Update data model --- reference_documents/checks/base.py | 7 + reference_documents/checks/check_runner.py | 34 +++- .../preferential_quota_order_numbers.py | 1 + reference_documents/checks/utils.py | 2 +- .../commands/ref_doc_csv_importer.py | 53 +++-- .../migrations/0003_auto_20240307_0848.py | 109 +++++++++++ ...erentialrate_reference_document_version.py | 26 +++ ...alquota_preferential_quota_order_number.py | 26 +++ ...alquota_preferential_quota_order_number.py | 29 +++ ...rtcheck_preferential_quota_order_number.py | 28 +++ reference_documents/models.py | 90 ++++----- .../views/reference_document_version_views.py | 183 +++++++++++------- .../views/reference_document_views.py | 8 +- 13 files changed, 455 insertions(+), 141 deletions(-) create mode 100644 reference_documents/checks/preferential_quota_order_numbers.py create mode 100644 reference_documents/migrations/0003_auto_20240307_0848.py create mode 100644 reference_documents/migrations/0004_preferentialrate_reference_document_version.py create mode 100644 reference_documents/migrations/0005_alter_preferentialquota_preferential_quota_order_number.py create mode 100644 reference_documents/migrations/0006_alter_preferentialquota_preferential_quota_order_number.py create mode 100644 reference_documents/migrations/0007_alignmentreportcheck_preferential_quota_order_number.py diff --git a/reference_documents/checks/base.py b/reference_documents/checks/base.py index 2f1c6ceb0..058bb427e 100644 --- a/reference_documents/checks/base.py +++ b/reference_documents/checks/base.py @@ -8,6 +8,7 @@ from geo_areas.models import GeographicalArea from geo_areas.models import GeographicalAreaDescription from reference_documents.models import PreferentialQuota +from reference_documents.models import PreferentialQuotaOrderNumber from reference_documents.models import PreferentialRate @@ -25,6 +26,12 @@ def __init__(self, preferential_quota: PreferentialQuota): self.preferential_quota = preferential_quota +class BasePreferentialQuotaOrderNumberCheck(BaseCheck): + def __init__(self, preferential_quota_order_number: PreferentialQuotaOrderNumber): + super().__init__() + self.preferential_quota_order_number = preferential_quota_order_number + + class BasePreferentialRateCheck(BaseCheck): def __init__(self, preferential_rate: PreferentialRate): super().__init__() diff --git a/reference_documents/checks/check_runner.py b/reference_documents/checks/check_runner.py index 43d6c1223..cd0ba0cdb 100644 --- a/reference_documents/checks/check_runner.py +++ b/reference_documents/checks/check_runner.py @@ -1,8 +1,10 @@ from reference_documents.checks.base import BasePreferentialQuotaCheck +from reference_documents.checks.base import BasePreferentialQuotaOrderNumberCheck from reference_documents.checks.base import BasePreferentialRateCheck +from reference_documents.checks.preferential_quota_order_numbers import * # noqa from reference_documents.checks.preferential_quotas import * # noqa from reference_documents.checks.preferential_rates import * # noqa -from reference_documents.checks.utils import utils +from reference_documents.checks.utils import Utils from reference_documents.models import AlignmentReport from reference_documents.models import AlignmentReportCheck from reference_documents.models import ReferenceDocumentVersion @@ -17,18 +19,35 @@ def __init__(self, reference_document_version: ReferenceDocumentVersion): @staticmethod def get_checks_for(check_class): - return utils().get_child_checks(check_class) + return Utils().get_child_checks(check_class) def run(self): for check in Checks.get_checks_for(BasePreferentialRateCheck): for pref_rate in self.reference_document_version.preferential_rates.all(): self.capture_check_result(check(pref_rate), pref_rate=pref_rate) - for check in Checks.get_checks_for(BasePreferentialQuotaCheck): - for pref_quota in self.reference_document_version.preferential_quotas.all(): - self.capture_check_result(check(pref_quota), pref_quota=pref_quota) - - def capture_check_result(self, check, pref_rate=None, pref_quota=None): + for check in Checks.get_checks_for(BasePreferentialQuotaOrderNumberCheck): + for ( + pref_quota_order_number + ) in self.reference_document_version.preferential_quota_order_numbers.all(): + self.capture_check_result( + check(pref_quota_order_number), + pref_quota_order_number=pref_quota_order_number, + ) + for sub_check in Checks.get_checks_for(BasePreferentialQuotaCheck): + for pref_quota in pref_quota_order_number.preferential_quotas.all(): + self.capture_check_result( + sub_check(pref_quota), + pref_quota=pref_quota, + ) + + def capture_check_result( + self, + check, + pref_rate=None, + pref_quota=None, + pref_quota_order_number=None, + ): status, message = check.run_check() kwargs = { @@ -36,6 +55,7 @@ def capture_check_result(self, check, pref_rate=None, pref_quota=None): "check_name": check.__class__.__name__, "preferential_rate": pref_rate, "preferential_quota": pref_quota, + "preferential_quota_order_number": pref_quota_order_number, "status": status, "message": message, } diff --git a/reference_documents/checks/preferential_quota_order_numbers.py b/reference_documents/checks/preferential_quota_order_numbers.py new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/reference_documents/checks/preferential_quota_order_numbers.py @@ -0,0 +1 @@ + diff --git a/reference_documents/checks/utils.py b/reference_documents/checks/utils.py index 242f15289..d688cb1af 100644 --- a/reference_documents/checks/utils.py +++ b/reference_documents/checks/utils.py @@ -1,7 +1,7 @@ from reference_documents.checks.base import BaseCheck -class utils: +class Utils: def get_child_checks(self, check_class: BaseCheck.__class__): result = [] diff --git a/reference_documents/management/commands/ref_doc_csv_importer.py b/reference_documents/management/commands/ref_doc_csv_importer.py index fb89ed0f2..24049a218 100644 --- a/reference_documents/management/commands/ref_doc_csv_importer.py +++ b/reference_documents/management/commands/ref_doc_csv_importer.py @@ -4,7 +4,9 @@ import pandas as pd from django.core.management import BaseCommand +from common.util import TaricDateRange from reference_documents.models import PreferentialQuota +from reference_documents.models import PreferentialQuotaOrderNumber from reference_documents.models import PreferentialRate from reference_documents.models import ReferenceDocument from reference_documents.models import ReferenceDocumentVersion @@ -32,6 +34,7 @@ def handle(self, *args, **options): # TODO: remove all ref doc data - temp while testing PreferentialQuota.objects.all().delete() PreferentialRate.objects.all().delete() + PreferentialQuotaOrderNumber.objects.all().delete() ReferenceDocumentVersion.objects.all().delete() ReferenceDocument.objects.all().delete() @@ -83,41 +86,59 @@ def add_pt_quota_if_no_exists( comm_code = df_row["Standardised Commodity Code"] comm_code = comm_code + ("0" * (len(comm_code) - 10)) - quota_duty_rate = df_row["Quota Duty Rate"] - volume = df_row["Quota Volume"].replace(",", "") - units = df_row["Units"] - quota = reference_document_version.preferential_quotas.filter( + # no data contains valid dates, just create a single record - can be edited later in UI + quota_definition_valid_between = None + + if reference_document_version.entry_into_force_date: + order_number_valid_between = TaricDateRange( + reference_document_version.entry_into_force_date, + ) + else: + order_number_valid_between = None + + # Check order number + order_number_record = ( + reference_document_version.preferential_quota_order_numbers.filter( + quota_order_number=order_number, + ).first() + ) + + if not order_number_record: + # add a new one + order_number_record = PreferentialQuotaOrderNumber.objects.create( + quota_order_number=order_number, + reference_document_version_id=reference_document_version.id, + valid_between=order_number_valid_between, + coefficient=None, + main_order_number=None, + ) + + order_number_record.save() + + # check quota definition + quota = order_number_record.preferential_quotas.filter( commodity_code=comm_code, - quota_order_number=order_number, ).first() if not quota: # add a new one quota = PreferentialQuota.objects.create( commodity_code=comm_code, - quota_order_number=order_number, + preferential_quota_order_number=order_number_record, quota_duty_rate=quota_duty_rate, order=order, - reference_document_version=reference_document_version, volume=volume, - valid_between=None, + valid_between=quota_definition_valid_between, measurement=units, ) quota.save() def add_pt_duty_if_no_exist(self, df_row, df_row_index, reference_document_version): - # 'Commodity Code', 'Preferential Duty Rate', 'Staging', 'Validity', - # 'Notes', 'description', 'area_id', 'sid', - # 'TAP_measure__geographical_area__description', - # 'measure__geographical_area__sid', 'Document Date', 'Document Version', - # 'Date Processed', 'Standardised Commodity Code', 'Valid From', - # 'Valid To', 'Valid Date Difference' - # check for existing entry for comm code comm_code = df_row["Standardised Commodity Code"] comm_code = comm_code + ("0" * (len(comm_code) - 10)) @@ -158,8 +179,8 @@ def create_ref_docs_and_versions(self): ) .first() ) - # Create records + # Create records if not ref_doc: ref_doc = ReferenceDocument.objects.create( title=f"Reference document for {area}", diff --git a/reference_documents/migrations/0003_auto_20240307_0848.py b/reference_documents/migrations/0003_auto_20240307_0848.py new file mode 100644 index 000000000..993618183 --- /dev/null +++ b/reference_documents/migrations/0003_auto_20240307_0848.py @@ -0,0 +1,109 @@ +# Generated by Django 3.2.24 on 2024-03-07 08:48 + +import django.db.models.deletion +from django.db import migrations +from django.db import models + +import common.fields + + +class Migration(migrations.Migration): + dependencies = [ + ("reference_documents", "0002_alter_referencedocument_area_id"), + ] + + operations = [ + migrations.CreateModel( + name="PreferentialQuotaOrderNumber", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("quota_order_number", models.CharField(db_index=True, max_length=6)), + ( + "coefficient", + models.DecimalField( + blank=True, + decimal_places=4, + default=None, + max_digits=6, + null=True, + ), + ), + ( + "valid_between", + common.fields.TaricDateRangeField( + blank=True, + db_index=True, + default=None, + null=True, + ), + ), + ], + ), + migrations.RemoveField( + model_name="preferentialquota", + name="coefficient", + ), + migrations.RemoveField( + model_name="preferentialquota", + name="main_quota", + ), + migrations.RemoveField( + model_name="preferentialquota", + name="quota_order_number", + ), + migrations.RemoveField( + model_name="preferentialquota", + name="reference_document_version", + ), + migrations.RemoveField( + model_name="preferentialrate", + name="reference_document_version", + ), + migrations.AddConstraint( + model_name="referencedocumentversion", + constraint=models.UniqueConstraint( + fields=("version", "reference_document"), + name="unique_versions", + ), + ), + migrations.AddField( + model_name="preferentialquotaordernumber", + name="main_order_number", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.PROTECT, + related_name="sub_order_number", + to="reference_documents.preferentialquotaordernumber", + ), + ), + migrations.AddField( + model_name="preferentialquotaordernumber", + name="reference_document_version", + field=models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, + related_name="preferential_quota_order_numbers", + to="reference_documents.referencedocumentversion", + ), + ), + migrations.AddField( + model_name="preferentialquota", + name="preferential_quota_order_number", + field=models.ForeignKey( + blank=True, + default=None, + null=True, + on_delete=django.db.models.deletion.PROTECT, + related_name="preferential_quota_order_number", + to="reference_documents.preferentialquotaordernumber", + ), + ), + ] diff --git a/reference_documents/migrations/0004_preferentialrate_reference_document_version.py b/reference_documents/migrations/0004_preferentialrate_reference_document_version.py new file mode 100644 index 000000000..67aab22a9 --- /dev/null +++ b/reference_documents/migrations/0004_preferentialrate_reference_document_version.py @@ -0,0 +1,26 @@ +# Generated by Django 3.2.24 on 2024-03-07 08:52 + +import django.db.models.deletion +from django.db import migrations +from django.db import models + + +class Migration(migrations.Migration): + dependencies = [ + ("reference_documents", "0003_auto_20240307_0848"), + ] + + operations = [ + migrations.AddField( + model_name="preferentialrate", + name="reference_document_version", + field=models.ForeignKey( + blank=True, + default=None, + null=True, + on_delete=django.db.models.deletion.PROTECT, + related_name="preferential_rates", + to="reference_documents.referencedocumentversion", + ), + ), + ] diff --git a/reference_documents/migrations/0005_alter_preferentialquota_preferential_quota_order_number.py b/reference_documents/migrations/0005_alter_preferentialquota_preferential_quota_order_number.py new file mode 100644 index 000000000..8fa1f6978 --- /dev/null +++ b/reference_documents/migrations/0005_alter_preferentialquota_preferential_quota_order_number.py @@ -0,0 +1,26 @@ +# Generated by Django 3.2.24 on 2024-03-07 09:16 + +import django.db.models.deletion +from django.db import migrations +from django.db import models + + +class Migration(migrations.Migration): + dependencies = [ + ("reference_documents", "0004_preferentialrate_reference_document_version"), + ] + + operations = [ + migrations.AlterField( + model_name="preferentialquota", + name="preferential_quota_order_number", + field=models.ForeignKey( + blank=True, + default=None, + null=True, + on_delete=django.db.models.deletion.PROTECT, + related_name="preferential_quota_order_numbers", + to="reference_documents.preferentialquotaordernumber", + ), + ), + ] diff --git a/reference_documents/migrations/0006_alter_preferentialquota_preferential_quota_order_number.py b/reference_documents/migrations/0006_alter_preferentialquota_preferential_quota_order_number.py new file mode 100644 index 000000000..1c85d8624 --- /dev/null +++ b/reference_documents/migrations/0006_alter_preferentialquota_preferential_quota_order_number.py @@ -0,0 +1,29 @@ +# Generated by Django 3.2.24 on 2024-03-07 09:17 + +import django.db.models.deletion +from django.db import migrations +from django.db import models + + +class Migration(migrations.Migration): + dependencies = [ + ( + "reference_documents", + "0005_alter_preferentialquota_preferential_quota_order_number", + ), + ] + + operations = [ + migrations.AlterField( + model_name="preferentialquota", + name="preferential_quota_order_number", + field=models.ForeignKey( + blank=True, + default=None, + null=True, + on_delete=django.db.models.deletion.PROTECT, + related_name="preferential_quotas", + to="reference_documents.preferentialquotaordernumber", + ), + ), + ] diff --git a/reference_documents/migrations/0007_alignmentreportcheck_preferential_quota_order_number.py b/reference_documents/migrations/0007_alignmentreportcheck_preferential_quota_order_number.py new file mode 100644 index 000000000..77a69c488 --- /dev/null +++ b/reference_documents/migrations/0007_alignmentreportcheck_preferential_quota_order_number.py @@ -0,0 +1,28 @@ +# Generated by Django 3.2.24 on 2024-03-07 14:54 + +import django.db.models.deletion +from django.db import migrations +from django.db import models + + +class Migration(migrations.Migration): + dependencies = [ + ( + "reference_documents", + "0006_alter_preferentialquota_preferential_quota_order_number", + ), + ] + + operations = [ + migrations.AddField( + model_name="alignmentreportcheck", + name="preferential_quota_order_number", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.PROTECT, + related_name="preferential_quota_order_number_checks", + to="reference_documents.preferentialquotaordernumber", + ), + ), + ] diff --git a/reference_documents/models.py b/reference_documents/models.py index bc879295a..e734e9936 100644 --- a/reference_documents/models.py +++ b/reference_documents/models.py @@ -84,31 +84,12 @@ class Meta: ] -class PreferentialRate(models.Model): - commodity_code = models.CharField( - max_length=10, - db_index=True, - ) - duty_rate = models.CharField( - max_length=255, - ) - order = models.IntegerField() - +class PreferentialQuotaOrderNumber(models.Model): reference_document_version = models.ForeignKey( "reference_documents.ReferenceDocumentVersion", on_delete=models.PROTECT, - related_name="preferential_rates", - ) - - valid_between = TaricDateRangeField( - db_index=True, - null=True, - blank=True, - default=None, + related_name="preferential_quota_order_numbers", ) - - -class PreferentialQuotaOrderNumber(models.Model): quota_order_number = models.CharField( max_length=6, db_index=True, @@ -120,19 +101,29 @@ class PreferentialQuotaOrderNumber(models.Model): null=True, default=None, ) - main_quota = models.ForeignKey( + main_order_number = models.ForeignKey( "self", - related_name="sub_quotas", + related_name="sub_order_number", blank=True, null=True, on_delete=models.PROTECT, ) + valid_between = TaricDateRangeField( + db_index=True, + null=True, + blank=True, + default=None, + ) class PreferentialQuota(models.Model): - quota_order_number = models.CharField( - max_length=6, - db_index=True, + preferential_quota_order_number = models.ForeignKey( + "reference_documents.PreferentialQuotaOrderNumber", + on_delete=models.PROTECT, + related_name="preferential_quotas", + null=True, + blank=True, + default=None, ) commodity_code = models.CharField( max_length=10, @@ -144,39 +135,40 @@ class PreferentialQuota(models.Model): volume = models.CharField( max_length=255, ) - coefficient = models.DecimalField( - max_digits=6, - decimal_places=4, - blank=True, - null=True, - default=None, - ) - - main_quota = models.ForeignKey( - "self", - related_name="sub_quotas", - blank=True, - null=True, - on_delete=models.PROTECT, - ) - valid_between = TaricDateRangeField( db_index=True, null=True, blank=True, default=None, ) - measurement = models.CharField( max_length=255, ) - order = models.IntegerField() + +class PreferentialRate(models.Model): reference_document_version = models.ForeignKey( "reference_documents.ReferenceDocumentVersion", on_delete=models.PROTECT, - related_name="preferential_quotas", + related_name="preferential_rates", + null=True, + blank=True, + default=None, + ) + commodity_code = models.CharField( + max_length=10, + db_index=True, + ) + duty_rate = models.CharField( + max_length=255, + ) + order = models.IntegerField() + valid_between = TaricDateRangeField( + db_index=True, + null=True, + blank=True, + default=None, ) @@ -219,6 +211,14 @@ class AlignmentReportCheck(models.Model): null=True, ) + preferential_quota_order_number = models.ForeignKey( + "reference_documents.PreferentialQuotaOrderNumber", + on_delete=models.PROTECT, + related_name="preferential_quota_order_number_checks", + blank=True, + null=True, + ) + preferential_rate = models.ForeignKey( "reference_documents.PreferentialRate", on_delete=models.PROTECT, diff --git a/reference_documents/views/reference_document_version_views.py b/reference_documents/views/reference_document_version_views.py index 2653afd07..96f626b3b 100644 --- a/reference_documents/views/reference_document_version_views.py +++ b/reference_documents/views/reference_document_version_views.py @@ -13,6 +13,7 @@ from quotas.models import QuotaOrderNumber from reference_documents import forms from reference_documents.models import AlignmentReportCheckStatus +from reference_documents.models import PreferentialQuotaOrderNumber from reference_documents.models import ReferenceDocument from reference_documents.models import ReferenceDocumentVersion @@ -34,14 +35,18 @@ def get_country_by_area_id(self, area_id): else: return f"{area_id} (unknown description)" - def get_tap_comm_code(self, duty): - if duty.reference_document_version.entry_into_force_date is not None: - contains_date = duty.reference_document_version.entry_into_force_date + def get_tap_comm_code( + self, + ref_doc_version: ReferenceDocumentVersion, + comm_code: str, + ): + if ref_doc_version.entry_into_force_date is not None: + contains_date = ref_doc_version.entry_into_force_date else: - contains_date = duty.reference_document_version.published_date + contains_date = ref_doc_version.published_date goods = GoodsNomenclature.objects.latest_approved().filter( - item_id=duty.commodity_code, + item_id=comm_code, valid_between__contains=contains_date, suffix=80, ) @@ -51,17 +56,27 @@ def get_tap_comm_code(self, duty): return goods.first() - def get_tap_order_number(self, quota): + def get_tap_order_number( + self, + ref_doc_quota_order_number: PreferentialQuotaOrderNumber, + ): # todo: This needs to consider the validity period(s) # may need to handle in the pre processing of the data e.g. where the volume defines multiple periods - if quota.reference_document_version.entry_into_force_date is not None: - contains_date = quota.reference_document_version.entry_into_force_date + if ( + ref_doc_quota_order_number.reference_document_version.entry_into_force_date + is not None + ): + contains_date = ( + ref_doc_quota_order_number.reference_document_version.entry_into_force_date + ) else: - contains_date = quota.reference_document_version.published_date + contains_date = ( + ref_doc_quota_order_number.reference_document_version.published_date + ) quota_order_number = QuotaOrderNumber.objects.latest_approved().filter( - order_number=quota.quota_order_number, + order_number=ref_doc_quota_order_number.quota_order_number, valid_between__contains=contains_date, ) @@ -104,9 +119,11 @@ def get_context_data(self, *args, **kwargs): latest_alignment_report = context["object"].alignment_reports.last() - for duty in context["object"].preferential_rates.order_by("commodity_code"): + for preferential_rate in context["object"].preferential_rates.order_by( + "commodity_code", + ): failure_count = ( - duty.preferential_rate_checks.all() + preferential_rate.preferential_rate_checks.all() .filter( alignment_report=latest_alignment_report, status=AlignmentReportCheckStatus.FAIL, @@ -115,7 +132,7 @@ def get_context_data(self, *args, **kwargs): ) check_count = ( - duty.preferential_rate_checks.all() + preferential_rate.preferential_rate_checks.all() .filter( alignment_report=latest_alignment_report, ) @@ -129,12 +146,15 @@ def get_context_data(self, *args, **kwargs): else: checks_output = f'
    PASS
    ' - comm_code = self.get_tap_comm_code(duty) + comm_code = self.get_tap_comm_code( + preferential_rate.reference_document_version, + preferential_rate.commodity_code, + ) if comm_code: comm_code_link = f'{comm_code.item_id}' else: - comm_code_link = f"{duty.commodity_code}" + comm_code_link = f"{preferential_rate.commodity_code}" reference_document_version_duties.append( [ @@ -142,25 +162,29 @@ def get_context_data(self, *args, **kwargs): "html": comm_code_link, }, { - "text": duty.duty_rate, + "text": preferential_rate.duty_rate, }, { - "text": duty.valid_between, + "text": preferential_rate.valid_between, }, { "html": checks_output, }, { - "html": f"Edit " - f"Delete", + "html": f"Edit " + f"Delete", }, ], ) # order numbers - for quota in context["object"].preferential_quotas.order_by("order"): + for ref_doc_order_number in context[ + "object" + ].preferential_quota_order_numbers.order_by("quota_order_number"): + tap_quota_order_number = self.get_tap_order_number(ref_doc_order_number) + failure_count = ( - quota.preferential_quota_checks.all() + ref_doc_order_number.preferential_quota_order_number_checks.all() .filter( alignment_report=latest_alignment_report, status=AlignmentReportCheckStatus.FAIL, @@ -169,62 +193,85 @@ def get_context_data(self, *args, **kwargs): ) check_count = ( - quota.preferential_quota_checks.all() + ref_doc_order_number.preferential_quota_order_number_checks.all() .filter( alignment_report=latest_alignment_report, ) .count() ) - if failure_count > 0: - checks_output = f'
    FAIL
    ' - elif check_count == 0: - checks_output = f"N/A" - else: - checks_output = f'
    PASS
    ' + reference_document_version_quotas[ + ref_doc_order_number.quota_order_number + ] = { + "data_rows": [], + "quota_order_number": tap_quota_order_number, + "quota_order_number_text": ref_doc_order_number.quota_order_number, + "failure_count": failure_count, + "check_count": check_count, + } - quota_order_number = self.get_tap_order_number(quota) + # Add Data Rows + for quota in ref_doc_order_number.preferential_quotas.order_by( + "commodity_code", + ): + failure_count = ( + quota.preferential_quota_checks.all() + .filter( + alignment_report=latest_alignment_report, + status=AlignmentReportCheckStatus.FAIL, + ) + .count() + ) - comm_code = self.get_tap_comm_code(quota) - if comm_code: - comm_code_link = f'{comm_code.structure_code}' - else: - comm_code_link = f"{quota.commodity_code}" - - row_to_add = [ - { - "html": comm_code_link, - }, - { - "text": quota.quota_duty_rate, - }, - { - "text": f"{quota.volume} {quota.measurement}", - }, - { - "text": quota.valid_between, - }, - { - "html": checks_output, - }, - { - "html": f"Edit " - f"Delete", - }, - ] - - if quota.quota_order_number in reference_document_version_quotas.keys(): - reference_document_version_quotas[quota.quota_order_number][ - "data_rows" - ].append( - row_to_add, + check_count = ( + quota.preferential_quota_checks.all() + .filter( + alignment_report=latest_alignment_report, + ) + .count() ) - else: - reference_document_version_quotas[quota.quota_order_number] = { - "data_rows": [row_to_add], - "quota_order_number": quota_order_number, - "quota_order_number_text": quota.quota_order_number, - } + + if failure_count > 0: + checks_output = f'
    FAIL
    ' + elif check_count == 0: + checks_output = f"N/A" + else: + checks_output = f'
    PASS
    ' + + comm_code = self.get_tap_comm_code( + quota.preferential_quota_order_number.reference_document_version, + quota.commodity_code, + ) + if comm_code: + comm_code_link = f'{comm_code.structure_code}' + else: + comm_code_link = f"{quota.commodity_code}" + + row_to_add = [ + { + "html": comm_code_link, + }, + { + "text": quota.quota_duty_rate, + }, + { + "text": f"{quota.volume} {quota.measurement}", + }, + { + "text": quota.valid_between, + }, + { + "html": checks_output, + }, + { + "html": f"Edit " + f"Delete", + }, + ] + + reference_document_version_quotas[ + ref_doc_order_number.quota_order_number + ]["data_rows"].append(row_to_add) context["reference_document_version_duties"] = reference_document_version_duties context["reference_document_version_quotas"] = reference_document_version_quotas diff --git a/reference_documents/views/reference_document_views.py b/reference_documents/views/reference_document_views.py index 0c7d0189d..9c7ca5697 100644 --- a/reference_documents/views/reference_document_views.py +++ b/reference_documents/views/reference_document_views.py @@ -54,7 +54,7 @@ def get_context_data(self, **kwargs): "text": reference.reference_document_versions.last().preferential_rates.count(), }, { - "text": reference.reference_document_versions.last().preferential_quotas.count(), + "text": reference.reference_document_versions.last().preferential_quota_order_numbers.count(), }, { "html": f'Details
    ' @@ -69,7 +69,7 @@ def get_context_data(self, **kwargs): {"text": "Latest Version"}, {"text": "Country"}, {"text": "Duties"}, - {"text": "Quotas"}, + {"text": "Order Numbers"}, {"text": "Actions"}, ] return context @@ -89,7 +89,7 @@ def get_context_data(self, *args, **kwargs): context["reference_document_versions_headers"] = [ {"text": "Version"}, {"text": "Duties"}, - {"text": "Quotas"}, + {"text": "Order Numbers"}, {"text": "EIF date"}, {"text": "Actions"}, ] @@ -109,7 +109,7 @@ def get_context_data(self, *args, **kwargs): "text": version.preferential_rates.count(), }, { - "text": version.preferential_quotas.count(), + "text": version.preferential_quota_order_numbers.count(), }, { "text": version.entry_into_force_date, From 22268d4258aa614be55c258ad82812280ff62ee8 Mon Sep 17 00:00:00 2001 From: Matthew McKenzie <97194636+mattjamc@users.noreply.github.com> Date: Thu, 7 Mar 2024 17:27:51 +0000 Subject: [PATCH 091/118] Preferential rate style and consistency tidy --- reference_documents/forms.py | 133 +++--------------- .../edit_preferential_rates.jinja | 18 --- .../preferential_rates/delete.jinja | 72 +++++++++- .../preferential_rates/edit.jinja | 21 +++ .../reference_document_versions/details.jinja | 2 +- .../tests/test_preferential_rates_views.py | 2 +- .../views/preferential_rates.py | 40 ++---- 7 files changed, 118 insertions(+), 170 deletions(-) delete mode 100644 reference_documents/jinja2/reference_documents/edit_preferential_rates.jinja create mode 100644 reference_documents/jinja2/reference_documents/preferential_rates/edit.jinja diff --git a/reference_documents/forms.py b/reference_documents/forms.py index 746a558a4..743d07b3a 100644 --- a/reference_documents/forms.py +++ b/reference_documents/forms.py @@ -24,16 +24,8 @@ class PreferentialRateCreateUpdateForm( ValidityPeriodForm, forms.ModelForm, ): - class Meta: - model = PreferentialRate - fields = [ - "commodity_code", - "duty_rate", - "valid_between", - ] - commodity_code = forms.CharField( - help_text="Commodity Code", + help_text="Enter the 10 digit commodity code", validators=[commodity_code_validator], error_messages={ "invalid": "Commodity code should be 10 digits", @@ -42,7 +34,6 @@ class Meta: ) duty_rate = forms.CharField( - help_text="Duty Rate", validators=[], error_messages={ "invalid": "Duty rate is invalid", @@ -50,30 +41,20 @@ class Meta: }, ) - def clean_duty_rate(self): - data = self.cleaned_data["duty_rate"] - if len(data) < 1: - raise ValidationError("Duty Rate is not valid - it must have a value") - return data - - def clean_commodity_code(self): - data = self.cleaned_data["commodity_code"] - if len(data) != 10 or not data.isdigit(): - raise ValidationError("Commodity Code is not valid - it must be 10 digits") - return data - def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - self.init_layout() - self.init_fields() - - def init_layout(self): self.helper = FormHelper(self) self.helper.label_size = Size.SMALL self.helper.legend_size = Size.SMALL self.helper.layout = Layout( - "commodity_code", - "duty_rate", + Field.text( + "commodity_code", + field_width=Fixed.TEN, + ), + Field.text( + "duty_rate", + field_width=Fixed.TEN, + ), "start_date", "end_date", Submit( @@ -84,11 +65,13 @@ def init_layout(self): ), ) - def init_fields(self): - pass - - def clean(self): - return super().clean() + class Meta: + model = PreferentialRate + fields = [ + "commodity_code", + "duty_rate", + "valid_between", + ] class ReferenceDocumentCreateUpdateForm(forms.ModelForm): @@ -163,68 +146,6 @@ def clean(self): return cleaned_data -class PreferentialRateCreateForm( - ValidityPeriodForm, - forms.ModelForm, -): - class Meta: - model = PreferentialRate - fields = [ - "commodity_code", - "duty_rate", - "valid_between", - "reference_document_version", - ] - - commodity_code = forms.CharField( - help_text="Commodity Code", - validators=[commodity_code_validator], - error_messages={ - "invalid": "Commodity code should be 10 digits", - "required": "Enter the commodity code", - }, - ) - - duty_rate = forms.CharField( - help_text="Duty Rate", - validators=[], - error_messages={ - "invalid": "Duty rate is invalid", - "required": "This is required", - }, - ) - - reference_document_version = forms.CharField(widget=forms.HiddenInput()) - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.init_layout() - self.init_fields() - - def init_layout(self): - self.helper = FormHelper(self) - self.helper.label_size = Size.SMALL - self.helper.legend_size = Size.SMALL - self.helper.layout = Layout( - "commodity_code", - "duty_rate", - "start_date", - "end_date", - Submit( - "submit", - "Create", - data_module="govuk-button", - data_prevent_double_click="true", - ), - ) - - def init_fields(self): - pass - - def clean(self): - return super().clean() - - class PreferentialRateDeleteForm(forms.ModelForm): class Meta: model = PreferentialRate @@ -232,10 +153,6 @@ class Meta: def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - self.init_layout() - self.init_fields() - - def init_layout(self): self.helper = FormHelper(self) self.helper.label_size = Size.SMALL self.helper.legend_size = Size.SMALL @@ -248,12 +165,6 @@ def init_layout(self): ), ) - def init_fields(self): - pass - - def clean(self): - return super().clean() - class PreferentialQuotaCreateUpdateForm( ValidityPeriodForm, @@ -335,10 +246,6 @@ def clean_commodity_code(self): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - self.init_layout() - self.init_fields() - - def init_layout(self): self.helper = FormHelper(self) self.helper.label_size = Size.SMALL self.helper.legend_size = Size.SMALL @@ -358,12 +265,6 @@ def init_layout(self): ), ) - def init_fields(self): - pass - - def clean(self): - return super().clean() - class ReferenceDocumentVersionsEditCreateForm(forms.ModelForm): version = forms.CharField( @@ -447,7 +348,7 @@ def clean(self): preferential_duty_rates = models.PreferentialRate.objects.all().filter( reference_document_version=reference_document_version, ) - tariff_quotas = models.PreferentialQuota.objects.all().filter( + tariff_quotas = models.PreferentialQuotaOrderNumber.objects.all().filter( reference_document_version=reference_document_version, ) if preferential_duty_rates or tariff_quotas: diff --git a/reference_documents/jinja2/reference_documents/edit_preferential_rates.jinja b/reference_documents/jinja2/reference_documents/edit_preferential_rates.jinja deleted file mode 100644 index 1c50f421b..000000000 --- a/reference_documents/jinja2/reference_documents/edit_preferential_rates.jinja +++ /dev/null @@ -1,18 +0,0 @@ -{% extends "layouts/form.jinja" %} -{% from "components/breadcrumbs/macro.njk" import govukBreadcrumbs %} - -{% set page_title = "Edit Reference Document " ~ object.area_id ~ " details" %} - -{% block breadcrumb %} - {{ govukBreadcrumbs({ - "items": [{"text": "Home", "href": url("home")}, - {"text": "View reference documents", "href": url("reference_documents:index")}, - {"text": page_title}] - }) }} -{% endblock %} - -{% block form %} - {% call django_form() %} - {{ crispy(form) }} - {% endcall %} -{% endblock %} diff --git a/reference_documents/jinja2/reference_documents/preferential_rates/delete.jinja b/reference_documents/jinja2/reference_documents/preferential_rates/delete.jinja index c98fe3e01..b86a40c4f 100644 --- a/reference_documents/jinja2/reference_documents/preferential_rates/delete.jinja +++ b/reference_documents/jinja2/reference_documents/preferential_rates/delete.jinja @@ -1,11 +1,75 @@ {% extends "layouts/layout.jinja" %} +{% from "components/breadcrumbs/macro.njk" import govukBreadcrumbs %} +{% from "components/warning-text/macro.njk" import govukWarningText %} +{% from "components/button/macro.njk" import govukButton %} +{% from "components/error-summary/macro.njk" import govukErrorSummary %} + {% set page_title = "Delete preferential duty rate" %} +{% block breadcrumb %} + {{ govukBreadcrumbs({ + "items": [{"text": "Home", "href": url("home")}, + {"text": "View reference documents", "href": url("reference_documents:index")}, + {"text": "Reference document " ~ object.reference_document_version.reference_document.area_id, "href": url("reference_documents:details", kwargs={"pk":object.reference_document_version.reference_document.pk})}, + {"text": "Version " ~ object.reference_document_version.version, "href": url("reference_documents:version-details", kwargs={"pk":object.reference_document_version.pk})}, + {"text": page_title}] + }) }} +{% endblock %} + {% block content %} -

    Are you sure that you want to delete this Preferential rate

    +
    +
    +

    {{ page_title }}

    +
    +
    + +
    +
    +

    Are you sure you want to permanently delete preferential duty rate for commodity {{ object.commodity_code }} reference document {{ object.reference_document_version.reference_document.area_id }}?

    + + {{ govukWarningText({ + "text": "Deleted preferential rates cannot be recovered.", + "iconFallbackText": "Warning" + }) }} +
    - - + + + {% set error_list = [] %} + + {% for field, errors in form.errors.items() %} + {% for error in errors.data %} + {% if error.message|length > 1 %} + {{ error_list.append({ + "text": error.message, + "href": "#" ~ (form.prefix ~ "-" if form.prefix else "") ~ field ~ ("_" ~ error.subfield if error.subfield is defined else ""), + }) or "" }} + {% endif %} + {% endfor %} + {% endfor %} + + {% if error_list|length > 0 %} + {{ govukErrorSummary({ + "titleText": "There is a problem", + "errorList": error_list + }) }} + {% endif %} + +
    + {{ govukButton({ + "text": "Delete", + "classes": "govuk-button--warning", + "name": "action", + "value": "delete" + }) }} + {{ govukButton({ + "text": "Cancel", + "href": url("reference_documents:index"), + "classes": "govuk-button--secondary" + }) }} +
    -{% endblock %} \ No newline at end of file +
    +
    +{% endblock %} diff --git a/reference_documents/jinja2/reference_documents/preferential_rates/edit.jinja b/reference_documents/jinja2/reference_documents/preferential_rates/edit.jinja new file mode 100644 index 000000000..25024d084 --- /dev/null +++ b/reference_documents/jinja2/reference_documents/preferential_rates/edit.jinja @@ -0,0 +1,21 @@ +{% extends "layouts/form.jinja" %} +{% from "components/breadcrumbs/macro.njk" import govukBreadcrumbs %} + +{% set page_title = "Edit preferential rate" %} + +{% block breadcrumb %} + {{ govukBreadcrumbs({ + "items": [{"text": "Home", "href": url("home")}, + {"text": "View reference documents", "href": url("reference_documents:index")}, + {"text": "Reference document " ~ object.reference_document_version.reference_document.area_id, "href": url("reference_documents:details", kwargs={"pk":object.reference_document_version.reference_document.pk})}, + {"text": "Version " ~ object.reference_document_version.version, "href": url("reference_documents:version-details", kwargs={"pk":object.reference_document_version.pk})}, + {"text": page_title}] + }) }} +{% endblock %} + +{% block form %} +

    Editing preferential rate for reference document {{ object.reference_document_version.reference_document.area_id }} version {{ object.reference_document_version.version }}.

    + {% call django_form() %} + {{ crispy(form) }} + {% endcall %} +{% endblock %} diff --git a/reference_documents/jinja2/reference_documents/reference_document_versions/details.jinja b/reference_documents/jinja2/reference_documents/reference_document_versions/details.jinja index f2ad38533..0591f51b9 100644 --- a/reference_documents/jinja2/reference_documents/reference_document_versions/details.jinja +++ b/reference_documents/jinja2/reference_documents/reference_document_versions/details.jinja @@ -4,7 +4,7 @@ {% from "components/tabs/macro.njk" import govukTabs %} {% from "components/breadcrumbs/macro.njk" import govukBreadcrumbs %} -{% set page_title = "Preferential duty rates" %} +{% set page_title = "Reference document " ~ object.reference_document.area_id ~ " version " ~ object.version %} {% block breadcrumb %} {{ govukBreadcrumbs({ diff --git a/reference_documents/tests/test_preferential_rates_views.py b/reference_documents/tests/test_preferential_rates_views.py index 09b9eb476..62d6548d6 100644 --- a/reference_documents/tests/test_preferential_rates_views.py +++ b/reference_documents/tests/test_preferential_rates_views.py @@ -56,7 +56,7 @@ def test_success_url(self): target = PreferentialRateEditView() target.object = pref_rate assert target.get_success_url() == reverse( - "reference_documents:version_details", + "reference_documents:version-details", args=[target.object.reference_document_version.pk], ) diff --git a/reference_documents/views/preferential_rates.py b/reference_documents/views/preferential_rates.py index 5906691e1..b3c3f8e03 100644 --- a/reference_documents/views/preferential_rates.py +++ b/reference_documents/views/preferential_rates.py @@ -1,11 +1,12 @@ from django.contrib.auth.mixins import PermissionRequiredMixin -from django.http import HttpResponseRedirect from django.urls import reverse from django.views.generic import CreateView from django.views.generic import DeleteView from django.views.generic import UpdateView +from django.views.generic.edit import FormMixin from reference_documents.forms import PreferentialRateCreateUpdateForm +from reference_documents.forms import PreferentialRateDeleteForm from reference_documents.models import PreferentialRate from reference_documents.models import ReferenceDocumentVersion @@ -14,28 +15,24 @@ class PreferentialRateEditView(PermissionRequiredMixin, UpdateView): form_class = PreferentialRateCreateUpdateForm permission_required = "reference_documents.change_preferentialrate" model = PreferentialRate - template_name = "reference_documents/edit_preferential_rates.jinja" + template_name = "reference_documents/preferential_rates/edit.jinja" def get_success_url(self): return reverse( - "reference_documents:version_details", + "reference_documents:version-details", args=[self.object.reference_document_version.pk], ) - def form_valid(self, form): - form.save() - return super(PreferentialRateEditView, self).form_valid(form) - class PreferentialRateCreateView(PermissionRequiredMixin, CreateView): form_class = PreferentialRateCreateUpdateForm - permission_required = "reference_documents.edit_reference_document" + permission_required = "reference_documents.add_preferentialrate" model = PreferentialRate template_name = "reference_documents/preferential_rates/create.jinja" def get_success_url(self): return reverse( - "reference_documents:version_details", + "reference_documents:version-details", args=[self.object.reference_document_version.pk], ) @@ -50,31 +47,14 @@ def form_valid(self, form): return super(PreferentialRateCreateView, self).form_valid(form) -class PreferentialRateDeleteView(PermissionRequiredMixin, DeleteView): +class PreferentialRateDeleteView(PermissionRequiredMixin, FormMixin, DeleteView): template_name = "reference_documents/preferential_rates/delete.jinja" - permission_required = "reference_documents.edit_reference_document" + permission_required = "reference_documents.delete_preferentialrate" model = PreferentialRate + form_class = PreferentialRateDeleteForm def get_success_url(self): return reverse( - "reference_documents:version_details", + "reference_documents:version-details", args=[self.object.reference_document_version.pk], ) - - def form_valid(self, form): - instance = form.instance - success_url = reverse( - "reference_documents:version_details", - args=[instance.reference_document_version.pk], - ) - instance.delete() - return HttpResponseRedirect(success_url) - - def post(self, request, *args, **kwargs): - object = PreferentialRate.objects.all().get(pk=kwargs["pk"]) - success_url = reverse( - "reference_documents:version_details", - args=[object.reference_document_version.pk], - ) - object.delete() - return HttpResponseRedirect(success_url) From 343e36399a5aab199fd48e15630a11c51358d2ec Mon Sep 17 00:00:00 2001 From: Doug Mills Date: Fri, 8 Mar 2024 08:37:08 +0000 Subject: [PATCH 092/118] separate out the context from reference document version details --- .../views/reference_document_version_views.py | 168 ++++++++++-------- 1 file changed, 90 insertions(+), 78 deletions(-) diff --git a/reference_documents/views/reference_document_version_views.py b/reference_documents/views/reference_document_version_views.py index 96f626b3b..e5fb45fdf 100644 --- a/reference_documents/views/reference_document_version_views.py +++ b/reference_documents/views/reference_document_version_views.py @@ -9,7 +9,6 @@ from django.views.generic.edit import FormMixin from commodities.models import GoodsNomenclature -from geo_areas.models import GeographicalAreaDescription from quotas.models import QuotaOrderNumber from reference_documents import forms from reference_documents.models import AlignmentReportCheckStatus @@ -18,46 +17,15 @@ from reference_documents.models import ReferenceDocumentVersion -class ReferenceDocumentVersionDetails(PermissionRequiredMixin, DetailView): - template_name = "reference_documents/reference_document_versions/details.jinja" - permission_required = "reference_documents.view_reference_documentversion" - model = ReferenceDocumentVersion - - def get_country_by_area_id(self, area_id): - description = ( - GeographicalAreaDescription.objects.latest_approved() - .filter(described_geographicalarea__area_id=area_id) - .order_by("-validity_start") - .first() - ) - if description: - return description.description - else: - return f"{area_id} (unknown description)" +class ReferenceDocumentVersionContext: + def __init__(self, reference_document_version: ReferenceDocumentVersion): + self.reference_document_version = reference_document_version - def get_tap_comm_code( - self, - ref_doc_version: ReferenceDocumentVersion, - comm_code: str, - ): - if ref_doc_version.entry_into_force_date is not None: - contains_date = ref_doc_version.entry_into_force_date - else: - contains_date = ref_doc_version.published_date - - goods = GoodsNomenclature.objects.latest_approved().filter( - item_id=comm_code, - valid_between__contains=contains_date, - suffix=80, - ) - - if len(goods) == 0: - return None - - return goods.first() + def alignment_report(self): + return self.reference_document_version.alignment_reports.last() + @staticmethod def get_tap_order_number( - self, ref_doc_quota_order_number: PreferentialQuotaOrderNumber, ): # todo: This needs to consider the validity period(s) @@ -85,19 +53,29 @@ def get_tap_order_number( return quota_order_number.first() - def get_context_data(self, *args, **kwargs): - context = super(ReferenceDocumentVersionDetails, self).get_context_data( - *args, - **kwargs, + @staticmethod + def get_tap_comm_code( + ref_doc_version: ReferenceDocumentVersion, + comm_code: str, + ): + if ref_doc_version.entry_into_force_date is not None: + contains_date = ref_doc_version.entry_into_force_date + else: + contains_date = ref_doc_version.published_date + + goods = GoodsNomenclature.objects.latest_approved().filter( + item_id=comm_code, + valid_between__contains=contains_date, + suffix=80, ) - ref_doc = context["object"].reference_document - # title - context[ - "ref_doc_title" - ] = f"Reference Document for {ref_doc.get_area_name_by_area_id()}" + if len(goods) == 0: + return None - context["reference_document_version_duties_headers"] = [ + return goods.first() + + def duties_headers(self): + return [ {"text": "Comm Code"}, {"text": "Duty Rate"}, {"text": "Validity"}, @@ -105,7 +83,8 @@ def get_context_data(self, *args, **kwargs): {"text": "Actions"}, ] - context["reference_document_version_quotas_headers"] = [ + def quotas_headers(self): + return [ {"text": "Comm Code"}, {"text": "Rate"}, {"text": "Volume"}, @@ -114,18 +93,17 @@ def get_context_data(self, *args, **kwargs): {"text": "Actions"}, ] - reference_document_version_duties = [] - reference_document_version_quotas = {} - - latest_alignment_report = context["object"].alignment_reports.last() - - for preferential_rate in context["object"].preferential_rates.order_by( + def duties_row_data(self): + rows = [] + for ( + preferential_rate + ) in self.reference_document_version.preferential_rates.order_by( "commodity_code", ): failure_count = ( preferential_rate.preferential_rate_checks.all() .filter( - alignment_report=latest_alignment_report, + alignment_report=self.alignment_report(), status=AlignmentReportCheckStatus.FAIL, ) .count() @@ -134,7 +112,7 @@ def get_context_data(self, *args, **kwargs): check_count = ( preferential_rate.preferential_rate_checks.all() .filter( - alignment_report=latest_alignment_report, + alignment_report=self.alignment_report(), ) .count() ) @@ -146,7 +124,7 @@ def get_context_data(self, *args, **kwargs): else: checks_output = f'
    PASS
    ' - comm_code = self.get_tap_comm_code( + comm_code = ReferenceDocumentVersionContext.get_tap_comm_code( preferential_rate.reference_document_version, preferential_rate.commodity_code, ) @@ -156,7 +134,7 @@ def get_context_data(self, *args, **kwargs): else: comm_code_link = f"{preferential_rate.commodity_code}" - reference_document_version_duties.append( + rows.append( [ { "html": comm_code_link, @@ -176,17 +154,25 @@ def get_context_data(self, *args, **kwargs): }, ], ) - - # order numbers - for ref_doc_order_number in context[ - "object" - ].preferential_quota_order_numbers.order_by("quota_order_number"): - tap_quota_order_number = self.get_tap_order_number(ref_doc_order_number) + return rows + + def quotas_data_orders_and_rows(self): + data = {} + for ( + ref_doc_order_number + ) in self.reference_document_version.preferential_quota_order_numbers.order_by( + "quota_order_number", + ): + tap_quota_order_number = ( + ReferenceDocumentVersionContext.get_tap_order_number( + ref_doc_order_number, + ) + ) failure_count = ( ref_doc_order_number.preferential_quota_order_number_checks.all() .filter( - alignment_report=latest_alignment_report, + alignment_report=self.alignment_report(), status=AlignmentReportCheckStatus.FAIL, ) .count() @@ -195,14 +181,12 @@ def get_context_data(self, *args, **kwargs): check_count = ( ref_doc_order_number.preferential_quota_order_number_checks.all() .filter( - alignment_report=latest_alignment_report, + alignment_report=self.alignment_report(), ) .count() ) - reference_document_version_quotas[ - ref_doc_order_number.quota_order_number - ] = { + data[ref_doc_order_number.quota_order_number] = { "data_rows": [], "quota_order_number": tap_quota_order_number, "quota_order_number_text": ref_doc_order_number.quota_order_number, @@ -217,7 +201,7 @@ def get_context_data(self, *args, **kwargs): failure_count = ( quota.preferential_quota_checks.all() .filter( - alignment_report=latest_alignment_report, + alignment_report=self.alignment_report(), status=AlignmentReportCheckStatus.FAIL, ) .count() @@ -226,7 +210,7 @@ def get_context_data(self, *args, **kwargs): check_count = ( quota.preferential_quota_checks.all() .filter( - alignment_report=latest_alignment_report, + alignment_report=self.alignment_report(), ) .count() ) @@ -238,7 +222,7 @@ def get_context_data(self, *args, **kwargs): else: checks_output = f'
    PASS
    ' - comm_code = self.get_tap_comm_code( + comm_code = ReferenceDocumentVersionContext.get_tap_comm_code( quota.preferential_quota_order_number.reference_document_version, quota.commodity_code, ) @@ -269,12 +253,40 @@ def get_context_data(self, *args, **kwargs): }, ] - reference_document_version_quotas[ - ref_doc_order_number.quota_order_number - ]["data_rows"].append(row_to_add) + data[ref_doc_order_number.quota_order_number]["data_rows"].append( + row_to_add, + ) + + return data - context["reference_document_version_duties"] = reference_document_version_duties - context["reference_document_version_quotas"] = reference_document_version_quotas + +class ReferenceDocumentVersionDetails(PermissionRequiredMixin, DetailView): + template_name = "reference_documents/reference_document_versions/details.jinja" + permission_required = "reference_documents.view_reference_documentversion" + model = ReferenceDocumentVersion + + def get_context_data(self, *args, **kwargs): + context = super(ReferenceDocumentVersionDetails, self).get_context_data( + *args, + **kwargs, + ) + + # title + context[ + "ref_doc_title" + ] = f"Reference Document for {context['object'].reference_document.get_area_name_by_area_id()}" + + context_data = ReferenceDocumentVersionContext(context["object"]) + context[ + "reference_document_version_duties_headers" + ] = context_data.duties_headers() + context[ + "reference_document_version_quotas_headers" + ] = context_data.quotas_headers() + context["reference_document_version_duties"] = context_data.duties_row_data() + context[ + "reference_document_version_quotas" + ] = context_data.quotas_data_orders_and_rows() return context From 7161b012289e4aad8704f2b29f126a0b7da5beb1 Mon Sep 17 00:00:00 2001 From: Doug Mills Date: Fri, 8 Mar 2024 08:58:52 +0000 Subject: [PATCH 093/118] separate out the context from reference document version details --- .../views/reference_document_version_views.py | 120 +++++++++--------- 1 file changed, 62 insertions(+), 58 deletions(-) diff --git a/reference_documents/views/reference_document_version_views.py b/reference_documents/views/reference_document_version_views.py index e5fb45fdf..3eb945cc7 100644 --- a/reference_documents/views/reference_document_version_views.py +++ b/reference_documents/views/reference_document_version_views.py @@ -194,70 +194,74 @@ def quotas_data_orders_and_rows(self): "check_count": check_count, } - # Add Data Rows - for quota in ref_doc_order_number.preferential_quotas.order_by( - "commodity_code", - ): - failure_count = ( - quota.preferential_quota_checks.all() - .filter( - alignment_report=self.alignment_report(), - status=AlignmentReportCheckStatus.FAIL, - ) - .count() - ) - - check_count = ( - quota.preferential_quota_checks.all() - .filter( - alignment_report=self.alignment_report(), - ) - .count() - ) + # Add the rows from the order number + self.order_number_rows(data, ref_doc_order_number) - if failure_count > 0: - checks_output = f'
    FAIL
    ' - elif check_count == 0: - checks_output = f"N/A" - else: - checks_output = f'
    PASS
    ' + return data - comm_code = ReferenceDocumentVersionContext.get_tap_comm_code( - quota.preferential_quota_order_number.reference_document_version, - quota.commodity_code, + def order_number_rows(self, data, ref_doc_order_number): + # Add Data Rows + for quota in ref_doc_order_number.preferential_quotas.order_by( + "commodity_code", + ): + failure_count = ( + quota.preferential_quota_checks.all() + .filter( + alignment_report=self.alignment_report(), + status=AlignmentReportCheckStatus.FAIL, ) - if comm_code: - comm_code_link = f'{comm_code.structure_code}' - else: - comm_code_link = f"{quota.commodity_code}" - - row_to_add = [ - { - "html": comm_code_link, - }, - { - "text": quota.quota_duty_rate, - }, - { - "text": f"{quota.volume} {quota.measurement}", - }, - { - "text": quota.valid_between, - }, - { - "html": checks_output, - }, - { - "html": f"Edit " - f"Delete", - }, - ] + .count() + ) - data[ref_doc_order_number.quota_order_number]["data_rows"].append( - row_to_add, + check_count = ( + quota.preferential_quota_checks.all() + .filter( + alignment_report=self.alignment_report(), ) + .count() + ) - return data + if failure_count > 0: + checks_output = f'
    FAIL
    ' + elif check_count == 0: + checks_output = f"N/A" + else: + checks_output = f'
    PASS
    ' + + comm_code = ReferenceDocumentVersionContext.get_tap_comm_code( + quota.preferential_quota_order_number.reference_document_version, + quota.commodity_code, + ) + if comm_code: + comm_code_link = f'{comm_code.structure_code}' + else: + comm_code_link = f"{quota.commodity_code}" + + row_to_add = [ + { + "html": comm_code_link, + }, + { + "text": quota.quota_duty_rate, + }, + { + "text": f"{quota.volume} {quota.measurement}", + }, + { + "text": quota.valid_between, + }, + { + "html": checks_output, + }, + { + "html": f"Edit " + f"Delete", + }, + ] + + data[ref_doc_order_number.quota_order_number]["data_rows"].append( + row_to_add, + ) class ReferenceDocumentVersionDetails(PermissionRequiredMixin, DetailView): From 32df83d05a7e709419a498346c297b4072e83979 Mon Sep 17 00:00:00 2001 From: Doug Mills Date: Fri, 8 Mar 2024 14:09:41 +0000 Subject: [PATCH 094/118] separate out the context from reference document version details --- reference_documents/forms.py | 360 ------------------ .../forms/preferential_quota_forms.py | 113 ++++++ .../preferential_quota_order_number_forms.py | 102 +++++ .../forms/preferential_rate_forms.py | 85 +++++ .../forms/reference_document_forms.py | 83 ++++ .../forms/reference_document_version_forms.py | 106 ++++++ .../includes/tabs/preferential_quotas.jinja | 1 + .../delete.jinja | 75 ++++ .../edit.jinja | 9 + reference_documents/urls.py | 134 +++++-- reference_documents/views/example_views.py | 4 +- .../preferential_quota_order_number_views.py | 72 ++++ ..._quotas.py => preferential_quota_views.py} | 4 +- ...al_rates.py => preferential_rate_views.py} | 6 +- .../views/reference_document_version_views.py | 13 +- .../views/reference_document_views.py | 13 +- 16 files changed, 775 insertions(+), 405 deletions(-) delete mode 100644 reference_documents/forms.py create mode 100644 reference_documents/forms/preferential_quota_forms.py create mode 100644 reference_documents/forms/preferential_quota_order_number_forms.py create mode 100644 reference_documents/forms/preferential_rate_forms.py create mode 100644 reference_documents/forms/reference_document_forms.py create mode 100644 reference_documents/forms/reference_document_version_forms.py create mode 100644 reference_documents/jinja2/reference_documents/preferential_quota_order_numbers/delete.jinja create mode 100644 reference_documents/jinja2/reference_documents/preferential_quota_order_numbers/edit.jinja create mode 100644 reference_documents/views/preferential_quota_order_number_views.py rename reference_documents/views/{preferential_quotas.py => preferential_quota_views.py} (95%) rename reference_documents/views/{preferential_rates.py => preferential_rate_views.py} (92%) diff --git a/reference_documents/forms.py b/reference_documents/forms.py deleted file mode 100644 index 743d07b3a..000000000 --- a/reference_documents/forms.py +++ /dev/null @@ -1,360 +0,0 @@ -from decimal import Decimal - -from crispy_forms_gds.helper import FormHelper -from crispy_forms_gds.layout import Field -from crispy_forms_gds.layout import Fixed -from crispy_forms_gds.layout import Layout -from crispy_forms_gds.layout import Size -from crispy_forms_gds.layout import Submit -from django import forms -from django.core.exceptions import ValidationError - -from common.forms import DateInputFieldFixed -from common.forms import ValidityPeriodForm -from geo_areas.validators import area_id_validator -from reference_documents import models -from reference_documents.models import PreferentialQuota -from reference_documents.models import PreferentialRate -from reference_documents.models import ReferenceDocumentVersion -from reference_documents.validators import commodity_code_validator -from reference_documents.validators import order_number_validator - - -class PreferentialRateCreateUpdateForm( - ValidityPeriodForm, - forms.ModelForm, -): - commodity_code = forms.CharField( - help_text="Enter the 10 digit commodity code", - validators=[commodity_code_validator], - error_messages={ - "invalid": "Commodity code should be 10 digits", - "required": "Enter the commodity code", - }, - ) - - duty_rate = forms.CharField( - validators=[], - error_messages={ - "invalid": "Duty rate is invalid", - "required": "This is required", - }, - ) - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.helper = FormHelper(self) - self.helper.label_size = Size.SMALL - self.helper.legend_size = Size.SMALL - self.helper.layout = Layout( - Field.text( - "commodity_code", - field_width=Fixed.TEN, - ), - Field.text( - "duty_rate", - field_width=Fixed.TEN, - ), - "start_date", - "end_date", - Submit( - "submit", - "Save", - data_module="govuk-button", - data_prevent_double_click="true", - ), - ) - - class Meta: - model = PreferentialRate - fields = [ - "commodity_code", - "duty_rate", - "valid_between", - ] - - -class ReferenceDocumentCreateUpdateForm(forms.ModelForm): - title = forms.CharField( - label="Reference Document title", - error_messages={ - "required": "A Reference Document title is required", - "unique": "A Reference Document with this title already exists", - }, - ) - area_id = forms.CharField( - label="Area ID", - validators=[area_id_validator], - error_messages={ - "required": "An area ID is required", - "unique": "A Reference Document with this area ID already exists", - "invalid": "Enter the area ID in the correct format", - }, - ) - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - - self.fields[ - "title" - ].help_text = "For example, 'Reference document for XX' where XX is the Area ID" - self.fields["area_id"].help_text = "Two character ID for the area referenced" - - self.helper = FormHelper(self) - self.helper.label_size = Size.SMALL - self.helper.legend_size = Size.SMALL - - self.helper.layout = Layout( - Field.text( - "title", - field_width=Fixed.TWENTY, - ), - Field.text( - "area_id", - field_width=Fixed.TEN, - ), - Submit( - "submit", - "Save", - data_module="govuk-button", - data_prevent_double_click="true", - ), - ) - - class Meta: - model = models.ReferenceDocument - fields = ["title", "area_id"] - - -class ReferenceDocumentDeleteForm(forms.Form): - def __init__(self, *args, **kwargs) -> None: - self.instance = kwargs.pop("instance") - super().__init__(*args, **kwargs) - - def clean(self): - cleaned_data = super().clean() - reference_document = self.instance - versions = models.ReferenceDocumentVersion.objects.all().filter( - reference_document=reference_document, - ) - if versions: - raise forms.ValidationError( - f"Reference Document {reference_document.area_id} cannot be deleted as it has" - f" active versions.", - ) - - return cleaned_data - - -class PreferentialRateDeleteForm(forms.ModelForm): - class Meta: - model = PreferentialRate - fields = [] - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.helper = FormHelper(self) - self.helper.label_size = Size.SMALL - self.helper.legend_size = Size.SMALL - self.helper.layout = Layout( - Submit( - "submit", - "Confirm Delete", - data_module="govuk-button", - data_prevent_double_click="true", - ), - ) - - -class PreferentialQuotaCreateUpdateForm( - ValidityPeriodForm, - forms.ModelForm, -): - class Meta: - model = PreferentialQuota - fields = [ - "quota_order_number", - "commodity_code", - "quota_duty_rate", - "volume", - "measurement", - "valid_between", - ] - - commodity_code = forms.CharField( - help_text="Commodity Code", - validators=[commodity_code_validator], - error_messages={ - "invalid": "Commodity code should be 10 digits", - "required": "Commodity code is required", - }, - ) - - quota_duty_rate = forms.CharField( - help_text="Quota Duty Rate", - validators=[], - error_messages={ - "invalid": "Duty rate is invalid", - "required": "Duty rate is required", - }, - ) - - quota_order_number = forms.CharField( - help_text="Quota Order Number", - validators=[order_number_validator], - error_messages={ - "invalid": "Quota Order Number is invalid", - "required": "Quota Order Number is required", - }, - ) - - volume = forms.CharField( - help_text="Volume", - validators=[], - error_messages={ - "invalid": "Volume invalid", - "required": "Volume is required", - }, - ) - - measurement = forms.CharField( - help_text="Measurement", - validators=[], - error_messages={ - "invalid": "Measurement invalid", - "required": "Measurement is required", - }, - ) - - def clean_quota_duty_rate(self): - data = self.cleaned_data["quota_duty_rate"] - if len(data) < 1: - raise ValidationError("Quota duty Rate is not valid - it must have a value") - return data - - def clean_volume(self): - data = self.cleaned_data["volume"] - if not data.isdigit(): - raise ValidationError("volume is not valid - it must have a value") - return Decimal(data) - - def clean_commodity_code(self): - data = self.cleaned_data["commodity_code"] - if len(data) != 10 or not data.isdigit(): - raise ValidationError("Commodity Code is not valid - it must be 10 digits") - return data - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.helper = FormHelper(self) - self.helper.label_size = Size.SMALL - self.helper.legend_size = Size.SMALL - self.helper.layout = Layout( - "quota_order_number", - "commodity_code", - "quota_duty_rate", - "volume", - "measurement", - "start_date", - "end_date", - Submit( - "submit", - "Save", - data_module="govuk-button", - data_prevent_double_click="true", - ), - ) - - -class ReferenceDocumentVersionsEditCreateForm(forms.ModelForm): - version = forms.CharField( - label="Version number", - error_messages={ - "required": "A version number is required", - "invalid": "Version must be a number", - }, - ) - published_date = DateInputFieldFixed( - label="Published date", - ) - entry_into_force_date = DateInputFieldFixed( - label="Entry into force date", - ) - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.fields["published_date"].error_messages[ - "required" - ] = "A published date is required" - self.fields["entry_into_force_date"].error_messages[ - "required" - ] = "An entry into force date is required" - self.helper = FormHelper(self) - self.helper.label_size = Size.SMALL - self.helper.legend_size = Size.SMALL - - self.helper.layout = Layout( - Field( - "reference_document", - type="hidden", - ), - Field.text( - "version", - field_width=Fixed.TEN, - ), - "published_date", - "entry_into_force_date", - Submit( - "submit", - "Save", - data_module="govuk-button", - data_prevent_double_click="true", - ), - ) - - def clean(self): - cleaned_data = super().clean() - ref_doc = cleaned_data.get("reference_document") - if cleaned_data.get("version"): - try: - latest_version = ReferenceDocumentVersion.objects.filter( - reference_document=ref_doc, - ).latest("created_at") - if float(cleaned_data.get("version")) < latest_version.version: - raise forms.ValidationError( - "New versions of this reference document must be a higher number than previous versions", - ) - except ReferenceDocumentVersion.DoesNotExist: - pass - - class Meta: - model = models.ReferenceDocumentVersion - fields = [ - "reference_document", - "version", - "published_date", - "entry_into_force_date", - ] - - -class ReferenceDocumentVersionDeleteForm(forms.Form): - def __init__(self, *args, **kwargs) -> None: - self.instance = kwargs.pop("instance") - super().__init__(*args, **kwargs) - - def clean(self): - cleaned_data = super().clean() - reference_document_version = self.instance - preferential_duty_rates = models.PreferentialRate.objects.all().filter( - reference_document_version=reference_document_version, - ) - tariff_quotas = models.PreferentialQuotaOrderNumber.objects.all().filter( - reference_document_version=reference_document_version, - ) - if preferential_duty_rates or tariff_quotas: - raise forms.ValidationError( - f"Reference Document version {reference_document_version.version} cannot be deleted as it has" - f" current preferential duty rates or tariff quotas", - ) - - return cleaned_data diff --git a/reference_documents/forms/preferential_quota_forms.py b/reference_documents/forms/preferential_quota_forms.py new file mode 100644 index 000000000..9caa20915 --- /dev/null +++ b/reference_documents/forms/preferential_quota_forms.py @@ -0,0 +1,113 @@ +from decimal import Decimal + +from crispy_forms_gds.helper import FormHelper +from crispy_forms_gds.layout import Layout +from crispy_forms_gds.layout import Size +from crispy_forms_gds.layout import Submit +from django import forms +from django.core.exceptions import ValidationError + +from common.forms import ValidityPeriodForm +from reference_documents.models import PreferentialQuota +from reference_documents.validators import commodity_code_validator +from reference_documents.validators import order_number_validator + + +class PreferentialQuotaCreateUpdateForm( + ValidityPeriodForm, + forms.ModelForm, +): + class Meta: + model = PreferentialQuota + fields = [ + "quota_order_number", + "commodity_code", + "quota_duty_rate", + "volume", + "measurement", + "valid_between", + ] + + commodity_code = forms.CharField( + help_text="Commodity Code", + validators=[commodity_code_validator], + error_messages={ + "invalid": "Commodity code should be 10 digits", + "required": "Commodity code is required", + }, + ) + + quota_duty_rate = forms.CharField( + help_text="Quota Duty Rate", + validators=[], + error_messages={ + "invalid": "Duty rate is invalid", + "required": "Duty rate is required", + }, + ) + + quota_order_number = forms.CharField( + help_text="Quota Order Number", + validators=[order_number_validator], + error_messages={ + "invalid": "Quota Order Number is invalid", + "required": "Quota Order Number is required", + }, + ) + + volume = forms.CharField( + help_text="Volume", + validators=[], + error_messages={ + "invalid": "Volume invalid", + "required": "Volume is required", + }, + ) + + measurement = forms.CharField( + help_text="Measurement", + validators=[], + error_messages={ + "invalid": "Measurement invalid", + "required": "Measurement is required", + }, + ) + + def clean_quota_duty_rate(self): + data = self.cleaned_data["quota_duty_rate"] + if len(data) < 1: + raise ValidationError("Quota duty Rate is not valid - it must have a value") + return data + + def clean_volume(self): + data = self.cleaned_data["volume"] + if not data.isdigit(): + raise ValidationError("volume is not valid - it must have a value") + return Decimal(data) + + def clean_commodity_code(self): + data = self.cleaned_data["commodity_code"] + if len(data) != 10 or not data.isdigit(): + raise ValidationError("Commodity Code is not valid - it must be 10 digits") + return data + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.helper = FormHelper(self) + self.helper.label_size = Size.SMALL + self.helper.legend_size = Size.SMALL + self.helper.layout = Layout( + "quota_order_number", + "commodity_code", + "quota_duty_rate", + "volume", + "measurement", + "start_date", + "end_date", + Submit( + "submit", + "Save", + data_module="govuk-button", + data_prevent_double_click="true", + ), + ) diff --git a/reference_documents/forms/preferential_quota_order_number_forms.py b/reference_documents/forms/preferential_quota_order_number_forms.py new file mode 100644 index 000000000..ef163f183 --- /dev/null +++ b/reference_documents/forms/preferential_quota_order_number_forms.py @@ -0,0 +1,102 @@ +from crispy_forms_gds.helper import FormHelper +from crispy_forms_gds.layout import Field +from crispy_forms_gds.layout import Layout +from crispy_forms_gds.layout import Size +from crispy_forms_gds.layout import Submit +from django import forms + +from common.forms import ValidityPeriodForm +from reference_documents.models import PreferentialQuotaOrderNumber +from reference_documents.models import PreferentialRate + + +class PreferentialQuotaOrderNumberCreateUpdateForm( + ValidityPeriodForm, + forms.ModelForm, +): + class Meta: + model = PreferentialQuotaOrderNumber + + quota_order_number = forms.CharField( + validators=[], + error_messages={ + "invalid": "Quota Order number is invalid", + "required": "Quota Order number is required", + }, + max_length=6, + widget=forms.TextInput( + attrs={ + "style": "max-width: 12em", + "title": "Enter a six digit number ", + }, + ), + ) + + coefficient = forms.CharField( + validators=[], + error_messages={ + "invalid": "Coefficient is invalid", + "required": "Coefficient is required", + }, + widget=forms.TextInput(attrs={"style": "max-width: 6em"}), + ) + + main_order_number = forms.ModelChoiceField( + queryset=PreferentialQuotaOrderNumber.objects.all(), + validators=[], + error_messages={ + "invalid": "Main Order number is invalid", + }, + required=False, + to_field_name="main_order_number_id", + widget=forms.Select(attrs={"class": "form-control"}), + ) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.helper = FormHelper(self) + self.helper.label_size = Size.SMALL + self.helper.legend_size = Size.SMALL + self.helper.layout = Layout( + Field.text( + "quota_order_number", + ), + "coefficient", + "main_order_number", + "start_date", + "end_date", + Submit( + "submit", + "Save", + data_module="govuk-button", + data_prevent_double_click="true", + ), + ) + + class Meta: + model = PreferentialRate + fields = [ + "commodity_code", + "duty_rate", + "valid_between", + ] + + +class PreferentialQuotaOrderNumberDeleteForm(forms.ModelForm): + class Meta: + model = PreferentialQuotaOrderNumber + fields = [] + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.helper = FormHelper(self) + self.helper.label_size = Size.SMALL + self.helper.legend_size = Size.SMALL + self.helper.layout = Layout( + Submit( + "submit", + "Confirm Delete", + data_module="govuk-button", + data_prevent_double_click="true", + ), + ) diff --git a/reference_documents/forms/preferential_rate_forms.py b/reference_documents/forms/preferential_rate_forms.py new file mode 100644 index 000000000..af8f7d5c0 --- /dev/null +++ b/reference_documents/forms/preferential_rate_forms.py @@ -0,0 +1,85 @@ +from crispy_forms_gds.helper import FormHelper +from crispy_forms_gds.layout import Field +from crispy_forms_gds.layout import Fixed +from crispy_forms_gds.layout import Layout +from crispy_forms_gds.layout import Size +from crispy_forms_gds.layout import Submit +from django import forms + +from common.forms import ValidityPeriodForm +from reference_documents.models import PreferentialRate +from reference_documents.validators import commodity_code_validator + + +class PreferentialRateCreateUpdateForm( + ValidityPeriodForm, + forms.ModelForm, +): + commodity_code = forms.CharField( + help_text="Enter the 10 digit commodity code", + validators=[commodity_code_validator], + error_messages={ + "invalid": "Commodity code should be 10 digits", + "required": "Enter the commodity code", + }, + ) + + duty_rate = forms.CharField( + validators=[], + error_messages={ + "invalid": "Duty rate is invalid", + "required": "This is required", + }, + ) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.helper = FormHelper(self) + self.helper.label_size = Size.SMALL + self.helper.legend_size = Size.SMALL + self.helper.layout = Layout( + Field.text( + "commodity_code", + field_width=Fixed.TEN, + ), + Field.text( + "duty_rate", + field_width=Fixed.TEN, + ), + "start_date", + "end_date", + Submit( + "submit", + "Save", + data_module="govuk-button", + data_prevent_double_click="true", + ), + ) + + class Meta: + model = PreferentialRate + fields = [ + "commodity_code", + "duty_rate", + "valid_between", + ] + + +class PreferentialRateDeleteForm(forms.ModelForm): + class Meta: + model = PreferentialRate + fields = [] + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.helper = FormHelper(self) + self.helper.label_size = Size.SMALL + self.helper.legend_size = Size.SMALL + self.helper.layout = Layout( + Submit( + "submit", + "Confirm Delete", + data_module="govuk-button", + data_prevent_double_click="true", + ), + ) diff --git a/reference_documents/forms/reference_document_forms.py b/reference_documents/forms/reference_document_forms.py new file mode 100644 index 000000000..3f9e231b3 --- /dev/null +++ b/reference_documents/forms/reference_document_forms.py @@ -0,0 +1,83 @@ +from crispy_forms_gds.helper import FormHelper +from crispy_forms_gds.layout import Field +from crispy_forms_gds.layout import Fixed +from crispy_forms_gds.layout import Layout +from crispy_forms_gds.layout import Size +from crispy_forms_gds.layout import Submit +from django import forms + +from geo_areas.validators import area_id_validator +from reference_documents.models import ReferenceDocument +from reference_documents.models import ReferenceDocumentVersion + + +class ReferenceDocumentCreateUpdateForm(forms.ModelForm): + title = forms.CharField( + label="Reference Document title", + error_messages={ + "required": "A Reference Document title is required", + "unique": "A Reference Document with this title already exists", + }, + ) + area_id = forms.CharField( + label="Area ID", + validators=[area_id_validator], + error_messages={ + "required": "An area ID is required", + "unique": "A Reference Document with this area ID already exists", + "invalid": "Enter the area ID in the correct format", + }, + ) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + self.fields[ + "title" + ].help_text = "For example, 'Reference document for XX' where XX is the Area ID" + self.fields["area_id"].help_text = "Two character ID for the area referenced" + + self.helper = FormHelper(self) + self.helper.label_size = Size.SMALL + self.helper.legend_size = Size.SMALL + + self.helper.layout = Layout( + Field.text( + "title", + field_width=Fixed.TWENTY, + ), + Field.text( + "area_id", + field_width=Fixed.TEN, + ), + Submit( + "submit", + "Save", + data_module="govuk-button", + data_prevent_double_click="true", + ), + ) + + class Meta: + model = ReferenceDocument + fields = ["title", "area_id"] + + +class ReferenceDocumentDeleteForm(forms.Form): + def __init__(self, *args, **kwargs) -> None: + self.instance = kwargs.pop("instance") + super().__init__(*args, **kwargs) + + def clean(self): + cleaned_data = super().clean() + reference_document = self.instance + versions = ReferenceDocumentVersion.objects.all().filter( + reference_document=reference_document, + ) + if versions: + raise forms.ValidationError( + f"Reference Document {reference_document.area_id} cannot be deleted as it has" + f" active versions.", + ) + + return cleaned_data diff --git a/reference_documents/forms/reference_document_version_forms.py b/reference_documents/forms/reference_document_version_forms.py new file mode 100644 index 000000000..14d52eb10 --- /dev/null +++ b/reference_documents/forms/reference_document_version_forms.py @@ -0,0 +1,106 @@ +from crispy_forms_gds.helper import FormHelper +from crispy_forms_gds.layout import Field +from crispy_forms_gds.layout import Fixed +from crispy_forms_gds.layout import Layout +from crispy_forms_gds.layout import Size +from crispy_forms_gds.layout import Submit +from django import forms + +from common.forms import DateInputFieldFixed +from reference_documents.models import PreferentialQuotaOrderNumber +from reference_documents.models import PreferentialRate +from reference_documents.models import ReferenceDocumentVersion + + +class ReferenceDocumentVersionsEditCreateForm(forms.ModelForm): + version = forms.CharField( + label="Version number", + error_messages={ + "required": "A version number is required", + "invalid": "Version must be a number", + }, + ) + published_date = DateInputFieldFixed( + label="Published date", + ) + entry_into_force_date = DateInputFieldFixed( + label="Entry into force date", + ) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.fields["published_date"].error_messages[ + "required" + ] = "A published date is required" + self.fields["entry_into_force_date"].error_messages[ + "required" + ] = "An entry into force date is required" + self.helper = FormHelper(self) + self.helper.label_size = Size.SMALL + self.helper.legend_size = Size.SMALL + + self.helper.layout = Layout( + Field( + "reference_document", + type="hidden", + ), + Field.text( + "version", + field_width=Fixed.TEN, + ), + "published_date", + "entry_into_force_date", + Submit( + "submit", + "Save", + data_module="govuk-button", + data_prevent_double_click="true", + ), + ) + + def clean(self): + cleaned_data = super().clean() + ref_doc = cleaned_data.get("reference_document") + if cleaned_data.get("version"): + try: + latest_version = ReferenceDocumentVersion.objects.filter( + reference_document=ref_doc, + ).latest("created_at") + if float(cleaned_data.get("version")) < latest_version.version: + raise forms.ValidationError( + "New versions of this reference document must be a higher number than previous versions", + ) + except ReferenceDocumentVersion.DoesNotExist: + pass + + class Meta: + model = ReferenceDocumentVersion + fields = [ + "reference_document", + "version", + "published_date", + "entry_into_force_date", + ] + + +class ReferenceDocumentVersionDeleteForm(forms.Form): + def __init__(self, *args, **kwargs) -> None: + self.instance = kwargs.pop("instance") + super().__init__(*args, **kwargs) + + def clean(self): + cleaned_data = super().clean() + reference_document_version = self.instance + preferential_duty_rates = PreferentialRate.objects.all().filter( + reference_document_version=reference_document_version, + ) + tariff_quotas = PreferentialQuotaOrderNumber.objects.all().filter( + reference_document_version=reference_document_version, + ) + if preferential_duty_rates or tariff_quotas: + raise forms.ValidationError( + f"Reference Document version {reference_document_version.version} cannot be deleted as it has" + f" current preferential duty rates or tariff quotas", + ) + + return cleaned_data diff --git a/reference_documents/jinja2/includes/tabs/preferential_quotas.jinja b/reference_documents/jinja2/includes/tabs/preferential_quotas.jinja index b43bf82d1..080a3dfea 100644 --- a/reference_documents/jinja2/includes/tabs/preferential_quotas.jinja +++ b/reference_documents/jinja2/includes/tabs/preferential_quotas.jinja @@ -32,6 +32,7 @@ diff --git a/reference_documents/jinja2/reference_documents/preferential_quotas/bulk_create.jinja b/reference_documents/jinja2/reference_documents/preferential_quotas/bulk_create.jinja new file mode 100644 index 000000000..5f263a3c7 --- /dev/null +++ b/reference_documents/jinja2/reference_documents/preferential_quotas/bulk_create.jinja @@ -0,0 +1,21 @@ +{% extends 'layouts/form.jinja' %} + +{% from "components/breadcrumbs/macro.njk" import govukBreadcrumbs %} + +{% set page_title = "Bulk create preferential quotas" %} + +{% block breadcrumb %} + {{ govukBreadcrumbs({ + "items": [{"text": "Home", "href": url("home")}, + {"text": "View reference documents", "href": url("reference_documents:index")}, + {"text": "Reference document " ~ reference_document_version.reference_document.area_id, "href": url("reference_documents:details", kwargs={"pk":reference_document_version.reference_document.pk})}, + {"text": "Version " ~ reference_document_version.version, "href": url("reference_documents:version-details", kwargs={"pk":reference_document_version.pk})}, + {"text": page_title}] + }) }} +{% endblock %} + +{% block form %} + {% call django_form() %} + {{ crispy(form) }} + {% endcall %} +{% endblock %} \ No newline at end of file diff --git a/reference_documents/jinja2/reference_documents/preferential_quotas/create.jinja b/reference_documents/jinja2/reference_documents/preferential_quotas/create.jinja new file mode 100644 index 000000000..91ace147c --- /dev/null +++ b/reference_documents/jinja2/reference_documents/preferential_quotas/create.jinja @@ -0,0 +1,9 @@ +{% extends 'layouts/form.jinja' %} + +{% set page_title = "Create preferential quota" %} + +{% block form %} + {% call django_form() %} + {{ crispy(form) }} + {% endcall %} +{% endblock %} \ No newline at end of file diff --git a/reference_documents/urls.py b/reference_documents/urls.py index ee2917701..8b36b1165 100644 --- a/reference_documents/urls.py +++ b/reference_documents/urls.py @@ -16,6 +16,9 @@ from reference_documents.views.preferential_quota_order_number_views import ( PreferentialQuotaOrderNumberEditView, ) +from reference_documents.views.preferential_quota_views import ( + PreferentialQuotaBulkCreateView, +) from reference_documents.views.preferential_quota_views import ( PreferentialQuotaCreateView, ) @@ -183,6 +186,11 @@ PreferentialQuotaCreateView.as_view(), name="preferential_quotas_create", ), + path( + "reference_document_versions//bulk_create_preferential_quotas/", + PreferentialQuotaBulkCreateView.as_view(), + name="preferential_quotas_bulk_create", + ), # Preferential Rates path( "preferential_rates/delete//", diff --git a/reference_documents/views/preferential_quota_views.py b/reference_documents/views/preferential_quota_views.py index ffd4494b7..2e81c8b2a 100644 --- a/reference_documents/views/preferential_quota_views.py +++ b/reference_documents/views/preferential_quota_views.py @@ -4,6 +4,9 @@ from django.views.generic import CreateView from django.views.generic import UpdateView +from reference_documents.forms.preferential_quota_forms import ( + PreferentialQuotaBulkCreate, +) from reference_documents.forms.preferential_quota_forms import ( PreferentialQuotaCreateUpdateForm, ) @@ -48,7 +51,7 @@ def form_valid(self, form): def get_success_url(self): return ( reverse( - "reference_documents:version_details", + "reference_documents:version-details", args=[self.object.reference_document_version.pk], ) + "#tariff-quotas" @@ -66,6 +69,49 @@ def get_success_url(self): # ) +class PreferentialQuotaBulkCreateView(PermissionRequiredMixin, CreateView): + template_name = "reference_documents/preferential_quotas/bulk_create.jinja" + permission_required = "reference_documents.add_preferentialquota" + model = PreferentialQuota + form_class = PreferentialQuotaBulkCreate + queryset = ReferenceDocumentVersion.objects.all() + + def get_form_kwargs(self): + kwargs = super().get_form_kwargs() + print(f"get object: {self.get_object()}") + kwargs["reference_document_version"] = self.get_object() + return kwargs + + def get_context_data(self, **kwargs): + context_data = super().get_context_data(**kwargs) + context_data[ + "reference_document_version" + ] = ReferenceDocumentVersion.objects.all().get( + pk=self.kwargs["pk"], + ) + return context_data + + def form_valid(self, form): + instance = form.instance + reference_document_version = ReferenceDocumentVersion.objects.get( + pk=self.kwargs["pk"], + ) + instance.order = len(reference_document_version.preferential_rates.all()) + 1 + instance.reference_document_version = reference_document_version + self.object = instance + self.object = form.save() + return redirect(self.get_success_url()) + + def get_success_url(self): + return ( + reverse( + "reference_documents:version-details", + args=[self.object.reference_document_version.pk], + ) + + "#tariff-quotas" + ) + + class PreferentialQuotaDeleteView(PermissionRequiredMixin, UpdateView): template_name = "preferential_quotas/delete.jinja" permission_required = "reference_documents.edit_reference_document" From e6007c75c4fa5e36fbd1d904d25570ce6c019e30 Mon Sep 17 00:00:00 2001 From: Doug Mills Date: Mon, 11 Mar 2024 11:39:44 +0000 Subject: [PATCH 098/118] add / edit order number --- .../includes/tabs/preferential_quotas.jinja | 24 +++++++++++++++---- .../preferential_quota_order_number_views.py | 18 ++++++++------ .../views/reference_document_version_views.py | 1 + 3 files changed, 32 insertions(+), 11 deletions(-) diff --git a/reference_documents/jinja2/includes/tabs/preferential_quotas.jinja b/reference_documents/jinja2/includes/tabs/preferential_quotas.jinja index 080a3dfea..dadae92ad 100644 --- a/reference_documents/jinja2/includes/tabs/preferential_quotas.jinja +++ b/reference_documents/jinja2/includes/tabs/preferential_quotas.jinja @@ -21,10 +21,26 @@ {% else %}

    Order Number {{ value["quota_order_number_text"] }}

    {% endif %} - {{ govukTable({ - "head": reference_document_version_quotas_headers, - "rows": value['data_rows'] - }) }} +
    + Valid between: {{ value['ref_doc_order_number'].valid_between }} + {% if value['ref_doc_order_number'].main_order_number %} + Sub Quota to {{ value['ref_doc_order_number'].main_order_number.quota_order_number }} + {% endif %} +
    +
    + Edit + Delete +
    + {% if value['data_rows'] != [] %} + {{ govukTable({ + "head": reference_document_version_quotas_headers, + "rows": value['data_rows'] + }) }} + {% else %} +
    + No Quota definitions defined +
    + {% endif %} {% endfor %}
    diff --git a/reference_documents/views/preferential_quota_order_number_views.py b/reference_documents/views/preferential_quota_order_number_views.py index 997cf2468..5391a76a3 100644 --- a/reference_documents/views/preferential_quota_order_number_views.py +++ b/reference_documents/views/preferential_quota_order_number_views.py @@ -1,23 +1,27 @@ from django.contrib.auth.mixins import PermissionRequiredMixin from django.urls import reverse from django.views.generic import CreateView +from django.views.generic import DeleteView from django.views.generic import UpdateView from reference_documents.forms.preferential_quota_order_number_forms import ( PreferentialQuotaOrderNumberCreateUpdateForm, ) -from reference_documents.models import PreferentialQuota +from reference_documents.models import PreferentialQuotaOrderNumber from reference_documents.models import ReferenceDocumentVersion class PreferentialQuotaOrderNumberEditView(PermissionRequiredMixin, UpdateView): template_name = "reference_documents/preferential_quota_order_numbers/edit.jinja" permission_required = "reference_documents.edit_reference_document" - model = PreferentialQuota + model = PreferentialQuotaOrderNumber form_class = PreferentialQuotaOrderNumberCreateUpdateForm def get_form_kwargs(self): kwargs = super(PreferentialQuotaOrderNumberEditView, self).get_form_kwargs() + kwargs["reference_document_version"] = PreferentialQuotaOrderNumber.objects.get( + id=self.kwargs["pk"], + ).reference_document_version return kwargs def get_success_url(self): @@ -30,7 +34,7 @@ def get_success_url(self): class PreferentialQuotaOrderNumberCreateView(PermissionRequiredMixin, CreateView): template_name = "reference_documents/preferential_quota_order_numbers/edit.jinja" permission_required = "reference_documents.edit_reference_document" - model = PreferentialQuota + model = PreferentialQuotaOrderNumber form_class = PreferentialQuotaOrderNumberCreateUpdateForm def get_form_kwargs(self): @@ -53,7 +57,7 @@ def form_valid(self, form): def get_success_url(self): return ( reverse( - "reference_documents:version_details", + "reference_documents:version-details", args=[self.object.reference_document_version.pk], ) + "#tariff-quotas" @@ -71,7 +75,7 @@ def get_success_url(self): # ) -class PreferentialQuotaOrderNumberDeleteView(PermissionRequiredMixin, UpdateView): - template_name = "preferential_quota_order_numbers/delete.jinja" +class PreferentialQuotaOrderNumberDeleteView(PermissionRequiredMixin, DeleteView): + template_name = "reference_documents/preferential_quota_order_numbers/delete.jinja" permission_required = "reference_documents.edit_reference_document" - model = PreferentialQuota + model = PreferentialQuotaOrderNumber diff --git a/reference_documents/views/reference_document_version_views.py b/reference_documents/views/reference_document_version_views.py index 4bb9b711f..84e9c4948 100644 --- a/reference_documents/views/reference_document_version_views.py +++ b/reference_documents/views/reference_document_version_views.py @@ -194,6 +194,7 @@ def quotas_data_orders_and_rows(self): data[ref_doc_order_number.quota_order_number] = { "data_rows": [], "quota_order_number": tap_quota_order_number, + "ref_doc_order_number": ref_doc_order_number, "quota_order_number_text": ref_doc_order_number.quota_order_number, "failure_count": failure_count, "check_count": check_count, From 21133237e5956b512b41bb1a791c8e572702be0d Mon Sep 17 00:00:00 2001 From: Matthew McKenzie <97194636+mattjamc@users.noreply.github.com> Date: Mon, 11 Mar 2024 16:04:04 +0000 Subject: [PATCH 099/118] Bulk add preferential quotas for a commodity code list --- .../forms/preferential_quota_forms.py | 22 +++++++++--- reference_documents/models.py | 6 ++++ .../views/preferential_quota_views.py | 36 +++++++++++++------ 3 files changed, 49 insertions(+), 15 deletions(-) diff --git a/reference_documents/forms/preferential_quota_forms.py b/reference_documents/forms/preferential_quota_forms.py index 8c5611fcc..a138a93f9 100644 --- a/reference_documents/forms/preferential_quota_forms.py +++ b/reference_documents/forms/preferential_quota_forms.py @@ -103,8 +103,10 @@ def __init__(self, *args, **kwargs): class PreferentialQuotaBulkCreate(ValidityPeriodForm, forms.ModelForm): - commodity_code = forms.CharField( - validators=[commodity_code_validator], + commodity_codes = forms.CharField( + label="Commodity codes", + widget=forms.Textarea, + # validators=[commodity_code_validator], error_messages={ "invalid": "Commodity code should be 10 digits", "required": "Commodity code is required", @@ -160,8 +162,7 @@ def __init__(self, reference_document_version, *args, **kwargs): self.helper.layout = Layout( "preferential_quota_order_number", Field.text( - "commodity_code", - field_width=Fixed.TEN, + "commodity_codes", ), Field.text( "quota_duty_rate", @@ -185,11 +186,22 @@ def __init__(self, reference_document_version, *args, **kwargs): ), ) + def clean(self): + cleaned_data = super().clean() + commodity_codes = cleaned_data.get("commodity_codes").splitlines() + for commodity_code in commodity_codes: + try: + commodity_code_validator(commodity_code) + except ValidationError: + self.add_error( + "commodity_codes", + "Ensure all commodity codes are 10 digits and each on a new line", + ) + class Meta: model = PreferentialQuota fields = [ "preferential_quota_order_number", - "commodity_code", "quota_duty_rate", "volume", "measurement", diff --git a/reference_documents/models.py b/reference_documents/models.py index 60367868f..f74e58dad 100644 --- a/reference_documents/models.py +++ b/reference_documents/models.py @@ -83,6 +83,12 @@ class Meta: ), ] + def preferential_quotas(self): + order_numbers = self.preferential_quota_order_numbers.all() + return PreferentialQuota.objects.all().filter( + preferential_quota_order_number__in=order_numbers, + ) + class PreferentialQuotaOrderNumber(models.Model): reference_document_version = models.ForeignKey( diff --git a/reference_documents/views/preferential_quota_views.py b/reference_documents/views/preferential_quota_views.py index 2e81c8b2a..cb836d4ac 100644 --- a/reference_documents/views/preferential_quota_views.py +++ b/reference_documents/views/preferential_quota_views.py @@ -2,6 +2,7 @@ from django.shortcuts import redirect from django.urls import reverse from django.views.generic import CreateView +from django.views.generic import FormView from django.views.generic import UpdateView from reference_documents.forms.preferential_quota_forms import ( @@ -69,7 +70,7 @@ def get_success_url(self): # ) -class PreferentialQuotaBulkCreateView(PermissionRequiredMixin, CreateView): +class PreferentialQuotaBulkCreateView(PermissionRequiredMixin, FormView): template_name = "reference_documents/preferential_quotas/bulk_create.jinja" permission_required = "reference_documents.add_preferentialquota" model = PreferentialQuota @@ -78,8 +79,11 @@ class PreferentialQuotaBulkCreateView(PermissionRequiredMixin, CreateView): def get_form_kwargs(self): kwargs = super().get_form_kwargs() - print(f"get object: {self.get_object()}") - kwargs["reference_document_version"] = self.get_object() + kwargs[ + "reference_document_version" + ] = ReferenceDocumentVersion.objects.all().get( + pk=self.kwargs["pk"], + ) return kwargs def get_context_data(self, **kwargs): @@ -92,21 +96,33 @@ def get_context_data(self, **kwargs): return context_data def form_valid(self, form): - instance = form.instance - reference_document_version = ReferenceDocumentVersion.objects.get( + commodity_codes = form.cleaned_data["commodity_codes"].splitlines() + self.reference_document_version = ReferenceDocumentVersion.objects.all().get( pk=self.kwargs["pk"], ) - instance.order = len(reference_document_version.preferential_rates.all()) + 1 - instance.reference_document_version = reference_document_version - self.object = instance - self.object = form.save() + for commodity_code in commodity_codes: + instance = form.save(commit=False) + instance.order = ( + len(self.reference_document_version.preferential_quotas()) + 1 + ) + instance.commodity_code = commodity_code + instance = PreferentialQuota( + commodity_code=instance.commodity_code, + quota_duty_rate=instance.quota_duty_rate, + volume=instance.volume, + valid_between=instance.valid_between, + measurement=instance.measurement, + order=instance.order, + preferential_quota_order_number=instance.preferential_quota_order_number, + ) + instance.save() return redirect(self.get_success_url()) def get_success_url(self): return ( reverse( "reference_documents:version-details", - args=[self.object.reference_document_version.pk], + args=[self.reference_document_version.pk], ) + "#tariff-quotas" ) From 0fbeeb484bcbf005407515065b00fcf932f6a0fb Mon Sep 17 00:00:00 2001 From: Doug Mills Date: Tue, 12 Mar 2024 09:09:07 +0000 Subject: [PATCH 100/118] add / edit order number --- .../forms/preferential_quota_forms.py | 60 ++++++++++--------- .../preferential_quota_order_number_forms.py | 25 ++++++++ .../views/preferential_quota_views.py | 26 ++++---- 3 files changed, 73 insertions(+), 38 deletions(-) diff --git a/reference_documents/forms/preferential_quota_forms.py b/reference_documents/forms/preferential_quota_forms.py index 8c5611fcc..58c29301a 100644 --- a/reference_documents/forms/preferential_quota_forms.py +++ b/reference_documents/forms/preferential_quota_forms.py @@ -11,7 +11,6 @@ from reference_documents.models import PreferentialQuota from reference_documents.models import PreferentialQuotaOrderNumber from reference_documents.validators import commodity_code_validator -from reference_documents.validators import order_number_validator class PreferentialQuotaCreateUpdateForm( @@ -29,6 +28,31 @@ class Meta: "valid_between", ] + def __init__(self, reference_document_version, *args, **kwargs): + super().__init__(*args, **kwargs) + self.fields[ + "quota_order_number" + ].queryset = reference_document_version.preferential_quota_order_numbers.all() + self.reference_document_version = reference_document_version + self.helper = FormHelper(self) + self.helper.label_size = Size.SMALL + self.helper.legend_size = Size.SMALL + self.helper.layout = Layout( + "quota_order_number", + "commodity_code", + "quota_duty_rate", + "volume", + "measurement", + "start_date", + "end_date", + Submit( + "submit", + "Save", + data_module="govuk-button", + data_prevent_double_click="true", + ), + ) + commodity_code = forms.CharField( help_text="Commodity Code", validators=[commodity_code_validator], @@ -47,13 +71,16 @@ class Meta: }, ) - quota_order_number = forms.CharField( - help_text="Quota Order Number", - validators=[order_number_validator], + quota_order_number = forms.ModelChoiceField( + label="Quota Order Number", + help_text="Select Quota order number", + queryset=PreferentialQuotaOrderNumber.objects.all(), + validators=[], error_messages={ - "invalid": "Quota Order Number is invalid", - "required": "Quota Order Number is required", + "invalid": "Quota Order number is invalid", }, + required=False, + widget=forms.Select(attrs={"class": "form-control"}), ) volume = forms.CharField( @@ -80,27 +107,6 @@ def clean_quota_duty_rate(self): raise ValidationError("Quota duty Rate is not valid - it must have a value") return data - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.helper = FormHelper(self) - self.helper.label_size = Size.SMALL - self.helper.legend_size = Size.SMALL - self.helper.layout = Layout( - "quota_order_number", - "commodity_code", - "quota_duty_rate", - "volume", - "measurement", - "start_date", - "end_date", - Submit( - "submit", - "Save", - data_module="govuk-button", - data_prevent_double_click="true", - ), - ) - class PreferentialQuotaBulkCreate(ValidityPeriodForm, forms.ModelForm): commodity_code = forms.CharField( diff --git a/reference_documents/forms/preferential_quota_order_number_forms.py b/reference_documents/forms/preferential_quota_order_number_forms.py index f4cee9abe..a92218c05 100644 --- a/reference_documents/forms/preferential_quota_order_number_forms.py +++ b/reference_documents/forms/preferential_quota_order_number_forms.py @@ -4,6 +4,7 @@ from crispy_forms_gds.layout import Size from crispy_forms_gds.layout import Submit from django import forms +from django.core.exceptions import ValidationError from common.forms import ValidityPeriodForm from reference_documents.models import PreferentialQuotaOrderNumber @@ -27,6 +28,7 @@ def __init__(self, reference_document_version, *args, **kwargs): self.fields[ "main_order_number" ].queryset = reference_document_version.preferential_quota_order_numbers.all() + self.reference_document_version = reference_document_version self.helper = FormHelper(self) self.helper.label_size = Size.SMALL self.helper.legend_size = Size.SMALL @@ -46,6 +48,29 @@ def __init__(self, reference_document_version, *args, **kwargs): ), ) + def clean(self): + cleaned_data = super().clean() + coefficient = cleaned_data.get("coefficient") + main_order_number = cleaned_data.get("main_order_number") + + # cant have one without the other + if coefficient and not main_order_number: + raise ValidationError( + "Coefficient specified without main order number", + ) + elif not coefficient and main_order_number: + raise ValidationError( + "Main order number specified without coefficient", + ) + + def clean_quota_order_number(self): + data = self.cleaned_data["quota_order_number"] + if self.reference_document_version.preferential_quota_order_numbers.filter( + quota_order_number=data, + ).exists(): + raise ValidationError("Quota Order Number Already Exists") + return data + quota_order_number = forms.CharField( label="Order number", help_text="Enter a six digit number", diff --git a/reference_documents/views/preferential_quota_views.py b/reference_documents/views/preferential_quota_views.py index 2e81c8b2a..76aa7941c 100644 --- a/reference_documents/views/preferential_quota_views.py +++ b/reference_documents/views/preferential_quota_views.py @@ -11,6 +11,7 @@ PreferentialQuotaCreateUpdateForm, ) from reference_documents.models import PreferentialQuota +from reference_documents.models import PreferentialQuotaOrderNumber from reference_documents.models import ReferenceDocumentVersion @@ -20,6 +21,13 @@ class PreferentialQuotaEditView(PermissionRequiredMixin, UpdateView): model = PreferentialQuota form_class = PreferentialQuotaCreateUpdateForm + def get_form_kwargs(self): + kwargs = super(PreferentialQuotaEditView, self).get_form_kwargs() + kwargs["reference_document_version"] = PreferentialQuotaOrderNumber.objects.get( + id=self.kwargs["pk"], + ).reference_document_version + return kwargs + def post(self, request, *args, **kwargs): quota = self.get_object() quota.save() @@ -38,6 +46,13 @@ class PreferentialQuotaCreateView(PermissionRequiredMixin, CreateView): model = PreferentialQuota form_class = PreferentialQuotaCreateUpdateForm + def get_form_kwargs(self): + kwargs = super(PreferentialQuotaCreateView, self).get_form_kwargs() + kwargs["reference_document_version"] = ReferenceDocumentVersion.objects.get( + id=self.kwargs["pk"], + ) + return kwargs + def form_valid(self, form): instance = form.instance reference_document_version = ReferenceDocumentVersion.objects.get( @@ -57,17 +72,6 @@ def get_success_url(self): + "#tariff-quotas" ) - # def post(self, request, *args, **kwargs): - # quota = self.get_object() - # quota.save() - # return redirect( - # reverse( - # "reference_documents:version_details", - # args=[quota.reference_document_version.pk], - # ) - # + "#tariff-quotas", - # ) - class PreferentialQuotaBulkCreateView(PermissionRequiredMixin, CreateView): template_name = "reference_documents/preferential_quotas/bulk_create.jinja" From 8b39d1a4a2477e323ab5657448da619bcb3c0f68 Mon Sep 17 00:00:00 2001 From: Matthew McKenzie <97194636+mattjamc@users.noreply.github.com> Date: Tue, 12 Mar 2024 17:00:40 +0000 Subject: [PATCH 101/118] Bulk create quotas for multiple validity period volume combos --- .../forms/preferential_quota_forms.py | 27 ++++++--- .../preferential_quotas/bulk_create.jinja | 8 +++ .../views/preferential_quota_views.py | 55 +++++++++++++------ 3 files changed, 66 insertions(+), 24 deletions(-) diff --git a/reference_documents/forms/preferential_quota_forms.py b/reference_documents/forms/preferential_quota_forms.py index a138a93f9..af23f8a5e 100644 --- a/reference_documents/forms/preferential_quota_forms.py +++ b/reference_documents/forms/preferential_quota_forms.py @@ -1,5 +1,7 @@ from crispy_forms_gds.helper import FormHelper +from crispy_forms_gds.layout import Div from crispy_forms_gds.layout import Field +from crispy_forms_gds.layout import Fieldset from crispy_forms_gds.layout import Fixed from crispy_forms_gds.layout import Layout from crispy_forms_gds.layout import Size @@ -136,6 +138,7 @@ class PreferentialQuotaBulkCreate(ValidityPeriodForm, forms.ModelForm): "invalid": "Volume invalid", "required": "Volume is required", }, + help_text="
    ", ) measurement = forms.CharField( @@ -156,6 +159,7 @@ def __init__(self, reference_document_version, *args, **kwargs): self.fields[ "preferential_quota_order_number" ].label_from_instance = lambda obj: f"{obj.quota_order_number}" + self.fields["end_date"].help_text = "" self.helper = FormHelper(self) self.helper.label_size = Size.SMALL self.helper.legend_size = Size.SMALL @@ -168,16 +172,25 @@ def __init__(self, reference_document_version, *args, **kwargs): "quota_duty_rate", field_width=Fixed.TEN, ), - Field.text( - "volume", - field_width=Fixed.TEN, - ), Field.text( "measurement", field_width=Fixed.TEN, ), - "start_date", - "end_date", + Fieldset( + Div( + Field("start_date"), + ), + Div( + Field("end_date"), + ), + Div( + Field( + "volume", + field_width=Fixed.TEN, + ), + ), + style="display: grid; grid-template-columns: 2fr 2fr 1fr", + ), Submit( "submit", "Save", @@ -203,7 +216,5 @@ class Meta: fields = [ "preferential_quota_order_number", "quota_duty_rate", - "volume", "measurement", - "valid_between", ] diff --git a/reference_documents/jinja2/reference_documents/preferential_quotas/bulk_create.jinja b/reference_documents/jinja2/reference_documents/preferential_quotas/bulk_create.jinja index 5f263a3c7..3e703c929 100644 --- a/reference_documents/jinja2/reference_documents/preferential_quotas/bulk_create.jinja +++ b/reference_documents/jinja2/reference_documents/preferential_quotas/bulk_create.jinja @@ -18,4 +18,12 @@ {% call django_form() %} {{ crispy(form) }} {% endcall %} + + {{ govukButton({ + "text": "Add new", + "attributes": {"id": "add-new"}, + "classes": "govuk-button--secondary", + "value": "1", + "name": form.prefix ~ "-ADD", + }) }} {% endblock %} \ No newline at end of file diff --git a/reference_documents/views/preferential_quota_views.py b/reference_documents/views/preferential_quota_views.py index cb836d4ac..c99f1691a 100644 --- a/reference_documents/views/preferential_quota_views.py +++ b/reference_documents/views/preferential_quota_views.py @@ -1,3 +1,5 @@ +from datetime import date + from django.contrib.auth.mixins import PermissionRequiredMixin from django.shortcuts import redirect from django.urls import reverse @@ -5,6 +7,7 @@ from django.views.generic import FormView from django.views.generic import UpdateView +from common.util import TaricDateRange from reference_documents.forms.preferential_quota_forms import ( PreferentialQuotaBulkCreate, ) @@ -73,7 +76,7 @@ def get_success_url(self): class PreferentialQuotaBulkCreateView(PermissionRequiredMixin, FormView): template_name = "reference_documents/preferential_quotas/bulk_create.jinja" permission_required = "reference_documents.add_preferentialquota" - model = PreferentialQuota + # model = PreferentialQuota form_class = PreferentialQuotaBulkCreate queryset = ReferenceDocumentVersion.objects.all() @@ -95,27 +98,47 @@ def get_context_data(self, **kwargs): ) return context_data + @staticmethod + def get_validity_and_volume_data(data): + entries = [] + for entry in range(len(data.getlist("volume"))): + start_date = date( + day=int(data.getlist("start_date_0")[entry]), + month=int(data.getlist("start_date_1")[entry]), + year=int(data.getlist("start_date_2")[entry]), + ) + end_date = date( + day=int(data.getlist("end_date_0")[entry]), + month=int(data.getlist("end_date_1")[entry]), + year=int(data.getlist("end_date_2")[entry]), + ) + valid_between = TaricDateRange(start_date, end_date) + volume = data.getlist("volume")[entry] + entries.append({f"valid_between": valid_between, f"volume": volume}) + return entries + def form_valid(self, form): + dates_and_volumes = self.get_validity_and_volume_data(form.data) commodity_codes = form.cleaned_data["commodity_codes"].splitlines() self.reference_document_version = ReferenceDocumentVersion.objects.all().get( pk=self.kwargs["pk"], ) for commodity_code in commodity_codes: - instance = form.save(commit=False) - instance.order = ( - len(self.reference_document_version.preferential_quotas()) + 1 - ) - instance.commodity_code = commodity_code - instance = PreferentialQuota( - commodity_code=instance.commodity_code, - quota_duty_rate=instance.quota_duty_rate, - volume=instance.volume, - valid_between=instance.valid_between, - measurement=instance.measurement, - order=instance.order, - preferential_quota_order_number=instance.preferential_quota_order_number, - ) - instance.save() + for entry in dates_and_volumes: + instance = form.save(commit=False) + instance.order = ( + len(self.reference_document_version.preferential_quotas()) + 1 + ) + instance = PreferentialQuota( + commodity_code=commodity_code, + quota_duty_rate=instance.quota_duty_rate, + volume=entry["volume"], + valid_between=entry["valid_between"], + measurement=instance.measurement, + order=instance.order, + preferential_quota_order_number=instance.preferential_quota_order_number, + ) + instance.save() return redirect(self.get_success_url()) def get_success_url(self): From 44aaf01e0cebb09a4783c319ea03b64129241c3f Mon Sep 17 00:00:00 2001 From: Doug Mills Date: Fri, 15 Mar 2024 14:14:00 +0000 Subject: [PATCH 102/118] WIP commit - quota and order number updates --- .../forms/preferential_quota_forms.py | 90 ++++++++++++++++--- .../preferential_quota_order_number_forms.py | 46 +++++++--- .../forms/preferential_rate_forms.py | 1 + .../includes/tabs/preferential_quotas.jinja | 6 +- .../confirm_delete.jinja | 39 ++++++++ .../delete.jinja | 69 +++++++------- .../preferential_quotas/delete.jinja | 78 ++++++++++++++++ reference_documents/models.py | 1 + .../scss/_reference_documents.scss | 3 + reference_documents/urls.py | 11 ++- .../preferential_quota_order_number_views.py | 61 +++++++++---- .../views/preferential_quota_views.py | 66 +++++++++++--- .../views/reference_document_version_views.py | 4 +- 13 files changed, 386 insertions(+), 89 deletions(-) create mode 100644 reference_documents/jinja2/reference_documents/preferential_quota_order_numbers/confirm_delete.jinja create mode 100644 reference_documents/jinja2/reference_documents/preferential_quotas/delete.jinja diff --git a/reference_documents/forms/preferential_quota_forms.py b/reference_documents/forms/preferential_quota_forms.py index 58c29301a..4c20349e9 100644 --- a/reference_documents/forms/preferential_quota_forms.py +++ b/reference_documents/forms/preferential_quota_forms.py @@ -28,21 +28,45 @@ class Meta: "valid_between", ] - def __init__(self, reference_document_version, *args, **kwargs): + def __init__( + self, + reference_document_version, + preferential_quota_order_number, + *args, + **kwargs, + ): super().__init__(*args, **kwargs) + + if preferential_quota_order_number: + self.initial["quota_order_number"] = preferential_quota_order_number + self.fields[ "quota_order_number" ].queryset = reference_document_version.preferential_quota_order_numbers.all() + self.reference_document_version = reference_document_version + self.quota_order_number = preferential_quota_order_number self.helper = FormHelper(self) self.helper.label_size = Size.SMALL self.helper.legend_size = Size.SMALL self.helper.layout = Layout( "quota_order_number", - "commodity_code", - "quota_duty_rate", - "volume", - "measurement", + Field.text( + "commodity_code", + field_width=Fixed.TEN, + ), + Field.text( + "quota_duty_rate", + field_width=Fixed.THIRTY, + ), + Field.text( + "volume", + field_width=Fixed.TWENTY, + ), + Field.text( + "measurement", + field_width=Fixed.TWENTY, + ), "start_date", "end_date", Submit( @@ -53,12 +77,37 @@ def __init__(self, reference_document_version, *args, **kwargs): ), ) + def clean_quota_duty_rate(self): + data = self.cleaned_data["quota_duty_rate"] + if len(data) < 1: + raise ValidationError("Quota duty Rate is not valid - it must have a value") + return data + + # def clean(self, ): + # data = self.cleaned_data["quota_duty_rate"] + # pass + + # def clean_validity_period( + # self, + # cleaned_data, + # valid_between_field_name="valid_between", + # start_date_field_name="start_date", + # end_date_field_name="end_date", + # ): + # super().clean_validity_period(cleaned_data, "valid_between", "start_date", "end_date") + # print(cleaned_data[valid_between_field_name]) + # data = self.cleaned_data["quota_duty_rate"] + # if data is None: + # raise ValidationError("Validity range must have an end date") + # return data + commodity_code = forms.CharField( - help_text="Commodity Code", + max_length=10, + help_text="Enter the 10 digit commodity code", validators=[commodity_code_validator], error_messages={ "invalid": "Commodity code should be 10 digits", - "required": "Commodity code is required", + "required": "Enter the commodity code", }, ) @@ -101,12 +150,6 @@ def __init__(self, reference_document_version, *args, **kwargs): }, ) - def clean_quota_duty_rate(self): - data = self.cleaned_data["quota_duty_rate"] - if len(data) < 1: - raise ValidationError("Quota duty Rate is not valid - it must have a value") - return data - class PreferentialQuotaBulkCreate(ValidityPeriodForm, forms.ModelForm): commodity_code = forms.CharField( @@ -201,3 +244,24 @@ class Meta: "measurement", "valid_between", ] + + +class PreferentialQuotaDeleteForm(forms.Form): + def __init__(self, *args, **kwargs) -> None: + self.instance = kwargs.pop("instance") + super().__init__(*args, **kwargs) + self.helper = FormHelper(self) + self.helper.label_size = Size.SMALL + self.helper.legend_size = Size.SMALL + self.helper.layout = Layout( + Submit( + "submit", + "Confirm Delete", + data_module="govuk-button", + data_prevent_double_click="true", + ), + ) + + class Meta: + model = PreferentialQuotaOrderNumber + fields = [] diff --git a/reference_documents/forms/preferential_quota_order_number_forms.py b/reference_documents/forms/preferential_quota_order_number_forms.py index a92218c05..2b32287be 100644 --- a/reference_documents/forms/preferential_quota_order_number_forms.py +++ b/reference_documents/forms/preferential_quota_order_number_forms.py @@ -7,6 +7,7 @@ from django.core.exceptions import ValidationError from common.forms import ValidityPeriodForm +from reference_documents.models import PreferentialQuota from reference_documents.models import PreferentialQuotaOrderNumber @@ -48,6 +49,14 @@ def __init__(self, reference_document_version, *args, **kwargs): ), ) + def clean_coefficient(self): + cleaned_data = super().clean() + coefficient = cleaned_data.get("coefficient") + if coefficient == "": + return None + + return coefficient + def clean(self): cleaned_data = super().clean() coefficient = cleaned_data.get("coefficient") @@ -65,10 +74,12 @@ def clean(self): def clean_quota_order_number(self): data = self.cleaned_data["quota_order_number"] - if self.reference_document_version.preferential_quota_order_numbers.filter( - quota_order_number=data, - ).exists(): - raise ValidationError("Quota Order Number Already Exists") + if self.instance._state.adding: + if self.reference_document_version.preferential_quota_order_numbers.filter( + quota_order_number=data, + ).exists(): + raise ValidationError("Quota Order Number Already Exists") + return data quota_order_number = forms.CharField( @@ -111,12 +122,9 @@ def clean_quota_order_number(self): ) -class PreferentialQuotaOrderNumberDeleteForm(forms.ModelForm): - class Meta: - model = PreferentialQuotaOrderNumber - fields = [] - - def __init__(self, *args, **kwargs): +class PreferentialQuotaOrderNumberDeleteForm(forms.Form): + def __init__(self, *args, **kwargs) -> None: + self.instance = kwargs.pop("instance") super().__init__(*args, **kwargs) self.helper = FormHelper(self) self.helper.label_size = Size.SMALL @@ -129,3 +137,21 @@ def __init__(self, *args, **kwargs): data_prevent_double_click="true", ), ) + + def clean(self): + cleaned_data = super().clean() + quota_order_number = self.instance + versions = PreferentialQuota.objects.all().filter( + preferential_quota_order_number=quota_order_number, + ) + if versions: + raise forms.ValidationError( + f"Quota Order Number {quota_order_number} cannot be deleted as it has" + f" associated Preferential Quotas.", + ) + + return cleaned_data + + class Meta: + model = PreferentialQuotaOrderNumber + fields = [] diff --git a/reference_documents/forms/preferential_rate_forms.py b/reference_documents/forms/preferential_rate_forms.py index af8f7d5c0..dc74c851f 100644 --- a/reference_documents/forms/preferential_rate_forms.py +++ b/reference_documents/forms/preferential_rate_forms.py @@ -16,6 +16,7 @@ class PreferentialRateCreateUpdateForm( forms.ModelForm, ): commodity_code = forms.CharField( + max_length=10, help_text="Enter the 10 digit commodity code", validators=[commodity_code_validator], error_messages={ diff --git a/reference_documents/jinja2/includes/tabs/preferential_quotas.jinja b/reference_documents/jinja2/includes/tabs/preferential_quotas.jinja index 80c1baa89..54f53ac72 100644 --- a/reference_documents/jinja2/includes/tabs/preferential_quotas.jinja +++ b/reference_documents/jinja2/includes/tabs/preferential_quotas.jinja @@ -28,8 +28,10 @@ {% endif %}
    {% if value['data_rows'] != [] %} {{ govukTable({ diff --git a/reference_documents/jinja2/reference_documents/preferential_quota_order_numbers/confirm_delete.jinja b/reference_documents/jinja2/reference_documents/preferential_quota_order_numbers/confirm_delete.jinja new file mode 100644 index 000000000..26c4bfbdf --- /dev/null +++ b/reference_documents/jinja2/reference_documents/preferential_quota_order_numbers/confirm_delete.jinja @@ -0,0 +1,39 @@ +{% extends "layouts/layout.jinja" %} + +{% from "components/panel/macro.njk" import govukPanel %} +{% from "components/button/macro.njk" import govukButton %} +{% from "components/breadcrumbs/macro.njk" import govukBreadcrumbs %} + +{% set area_id = request.session['deleted_version']['area_id'] %} +{% set version = request.session['deleted_version']['version'] %} +{% set ref_doc_pk = request.session['deleted_version']['ref_doc_pk'] %} + +{% set page_title = "Reference Document " ~ area_id ~ " version " ~ version ~ " successfully deleted" %} + + +{% block breadcrumb %} + {{ govukBreadcrumbs({ + "items": [{"text": "Home", "href": url("home")}, + {"text": "View reference documents", "href": url("reference_documents:index")}, + {"text": "Reference Document " ~ area_id, "href": url("reference_documents:details", kwargs={"pk":ref_doc_pk})}, + {"text": "Delete Reference Document " ~ area_id ~ " version " ~ version}, + {"text": page_title}] + }) }} +{% endblock %} + +{% block content %} +
    +
    + {{ govukPanel({ + "titleText": "Reference Document " ~ request.session['deleted_version']['area_id'] ~ " version " ~ request.session['deleted_version']['version'] ~ " has been deleted", + "text": "This change has taken immediate effect", + "classes": "govuk-!-margin-bottom-7" + }) }} +
    +
    + {{ govukButton({ + "text": "Back to View reference documents", + "href": url("reference_documents:index"), + "classes": "govuk-button--secondary" + }) }} +{% endblock %} diff --git a/reference_documents/jinja2/reference_documents/preferential_quota_order_numbers/delete.jinja b/reference_documents/jinja2/reference_documents/preferential_quota_order_numbers/delete.jinja index e554f790e..ee695f41d 100644 --- a/reference_documents/jinja2/reference_documents/preferential_quota_order_numbers/delete.jinja +++ b/reference_documents/jinja2/reference_documents/preferential_quota_order_numbers/delete.jinja @@ -8,7 +8,7 @@ {% set page_title = "todo : Delete preferential quota order number" %} {% block breadcrumb %} - {{ govukBreadcrumbs({ + {{ govukBreadcrumbs({ "items": [{"text": "Home", "href": url("home")}, {"text": "View reference documents", "href": url("reference_documents:index")}, {"text": "Reference document " ~ object.reference_document_version.reference_document.area_id, "href": url("reference_documents:details", kwargs={"pk":object.reference_document_version.reference_document.pk})}, @@ -18,58 +18,61 @@ {% endblock %} {% block content %} -
    -
    -

    {{ page_title }}

    -
    -
    +
    +
    +

    {{ page_title }}

    +
    +
    -
    -
    -

    Are you sure you want to permanently delete preferential duty rate for commodity {{ object.commodity_code }} reference document {{ object.reference_document_version.reference_document.area_id }}?

    +
    +
    +

    + Are you sure you want to permanently delete preferential quota order number {{ object.quota_order_number }}, + reference document {{ object.reference_document_version.reference_document.area_id }}? +

    - {{ govukWarningText({ - "text": "Deleted preferential rates cannot be recovered.", - "iconFallbackText": "Warning" - }) }} + {{ govukWarningText({ + "text": "Deleted preferential quota order number cannot be recovered.", + "iconFallbackText": "Warning" + }) }} -
    - + + - {% set error_list = [] %} + {% set error_list = [] %} - {% for field, errors in form.errors.items() %} - {% for error in errors.data %} - {% if error.message|length > 1 %} - {{ error_list.append({ + {% for field, errors in form.errors.items() %} + {% for error in errors.data %} + {% if error.message|length > 1 %} + {{ error_list.append({ "text": error.message, "href": "#" ~ (form.prefix ~ "-" if form.prefix else "") ~ field ~ ("_" ~ error.subfield if error.subfield is defined else ""), }) or "" }} - {% endif %} - {% endfor %} - {% endfor %} + {% endif %} + {% endfor %} + {% endfor %} - {% if error_list|length > 0 %} - {{ govukErrorSummary({ + {% if error_list|length > 0 %} + {{ govukErrorSummary({ "titleText": "There is a problem", "errorList": error_list }) }} - {% endif %} + {% endif %} -
    - {{ govukButton({ +
    + {{ govukButton({ "text": "Delete", "classes": "govuk-button--warning", "name": "action", "value": "delete" }) }} - {{ govukButton({ + {{ govukButton({ "text": "Cancel", "href": url("reference_documents:index"), "classes": "govuk-button--secondary" }) }} -
    - -
    -
    +
    + +
    +
    {% endblock %} diff --git a/reference_documents/jinja2/reference_documents/preferential_quotas/delete.jinja b/reference_documents/jinja2/reference_documents/preferential_quotas/delete.jinja new file mode 100644 index 000000000..9617e0e50 --- /dev/null +++ b/reference_documents/jinja2/reference_documents/preferential_quotas/delete.jinja @@ -0,0 +1,78 @@ +{% extends "layouts/layout.jinja" %} + +{% from "components/breadcrumbs/macro.njk" import govukBreadcrumbs %} +{% from "components/warning-text/macro.njk" import govukWarningText %} +{% from "components/button/macro.njk" import govukButton %} +{% from "components/error-summary/macro.njk" import govukErrorSummary %} + +{% set page_title = "todo : Delete preferential quota order number" %} + +{% block breadcrumb %} + {{ govukBreadcrumbs({ + "items": [{"text": "Home", "href": url("home")}, + {"text": "View reference documents", "href": url("reference_documents:index")}, + {"text": "Reference document " ~ object.preferential_quota_order_number.reference_document_version.reference_document.area_id, "href": url("reference_documents:details", kwargs={"pk":object.preferential_quota_order_number.reference_document_version.reference_document.pk})}, + {"text": "Version " ~ object.preferential_quota_order_number.reference_document_version.version, "href": url("reference_documents:version-details", kwargs={"pk":object.preferential_quota_order_number.reference_document_version.pk})}, + {"text": page_title}] + }) }} +{% endblock %} + +{% block content %} +
    +
    +

    {{ page_title }}

    +
    +
    + +
    +
    +

    + Are you sure you want to permanently delete preferential quota {{ object.commodity_code }} for order number {{ object.preferential_quota_order_number.quota_order_number }}, + reference document {{ object.preferential_quota_order_number.reference_document_version.reference_document.area_id }}? +

    + + {{ govukWarningText({ + "text": "Deleted preferential quota order number cannot be recovered.", + "iconFallbackText": "Warning" + }) }} + +
    + + + {% set error_list = [] %} + + {% for field, errors in form.errors.items() %} + {% for error in errors.data %} + {% if error.message|length > 1 %} + {{ error_list.append({ + "text": error.message, + "href": "#" ~ (form.prefix ~ "-" if form.prefix else "") ~ field ~ ("_" ~ error.subfield if error.subfield is defined else ""), + }) or "" }} + {% endif %} + {% endfor %} + {% endfor %} + + {% if error_list|length > 0 %} + {{ govukErrorSummary({ + "titleText": "There is a problem", + "errorList": error_list + }) }} + {% endif %} + +
    + {{ govukButton({ + "text": "Delete", + "classes": "govuk-button--warning", + "name": "action", + "value": "delete" + }) }} + {{ govukButton({ + "text": "Cancel", + "href": url("reference_documents:index"), + "classes": "govuk-button--secondary" + }) }} +
    +
    +
    +
    +{% endblock %} diff --git a/reference_documents/models.py b/reference_documents/models.py index 60367868f..bb6dac4a8 100644 --- a/reference_documents/models.py +++ b/reference_documents/models.py @@ -90,6 +90,7 @@ class PreferentialQuotaOrderNumber(models.Model): on_delete=models.PROTECT, related_name="preferential_quota_order_numbers", ) + quota_order_number = models.CharField( max_length=6, db_index=True, diff --git a/reference_documents/static/reference_documents/scss/_reference_documents.scss b/reference_documents/static/reference_documents/scss/_reference_documents.scss index 59f254e15..4b746e19c 100644 --- a/reference_documents/static/reference_documents/scss/_reference_documents.scss +++ b/reference_documents/static/reference_documents/scss/_reference_documents.scss @@ -5,4 +5,7 @@ .check-failing { color: #671111; font-weight: bold; +} +.order_number_link { + padding-right: 10px; } \ No newline at end of file diff --git a/reference_documents/urls.py b/reference_documents/urls.py index 8b36b1165..194109700 100644 --- a/reference_documents/urls.py +++ b/reference_documents/urls.py @@ -172,7 +172,7 @@ ), # Preferential Quotas path( - "preferential_quotas/delete//", + "preferential_quotas/delete///", PreferentialQuotaDeleteView.as_view(), name="preferential_quotas_delete", ), @@ -182,10 +182,15 @@ name="preferential_quotas_edit", ), path( - "preferential_quota_order_numbers//create_preferential_quotas/", + "preferential_quota_order_numbers//create_preferential_quotas/", PreferentialQuotaCreateView.as_view(), name="preferential_quotas_create", ), + path( + "preferential_quota_order_numbers//create_preferential_quotas_for_order//", + PreferentialQuotaCreateView.as_view(), + name="preferential_quotas_create_for_order", + ), path( "reference_document_versions//bulk_create_preferential_quotas/", PreferentialQuotaBulkCreateView.as_view(), @@ -209,7 +214,7 @@ ), # Preferential rate Quota order number path( - "preferential_quota_order_numbers/delete//", + "preferential_quota_order_numbers/delete///", PreferentialQuotaOrderNumberDeleteView.as_view(), name="preferential_quota_order_number_delete", ), diff --git a/reference_documents/views/preferential_quota_order_number_views.py b/reference_documents/views/preferential_quota_order_number_views.py index 5391a76a3..a01381c42 100644 --- a/reference_documents/views/preferential_quota_order_number_views.py +++ b/reference_documents/views/preferential_quota_order_number_views.py @@ -1,12 +1,17 @@ from django.contrib.auth.mixins import PermissionRequiredMixin +from django.shortcuts import redirect from django.urls import reverse from django.views.generic import CreateView from django.views.generic import DeleteView from django.views.generic import UpdateView +from django.views.generic.edit import FormMixin from reference_documents.forms.preferential_quota_order_number_forms import ( PreferentialQuotaOrderNumberCreateUpdateForm, ) +from reference_documents.forms.preferential_quota_order_number_forms import ( + PreferentialQuotaOrderNumberDeleteForm, +) from reference_documents.models import PreferentialQuotaOrderNumber from reference_documents.models import ReferenceDocumentVersion @@ -25,10 +30,13 @@ def get_form_kwargs(self): return kwargs def get_success_url(self): - reverse( - "reference_documents:version_details", - args=[self.get_object().reference_document_version.pk], - ) + "#tariff-quotas", + return ( + reverse( + "reference_documents:version-details", + args=[self.get_object().reference_document_version.pk], + ) + + "#tariff-quotas" + ) class PreferentialQuotaOrderNumberCreateView(PermissionRequiredMixin, CreateView): @@ -42,6 +50,7 @@ def get_form_kwargs(self): kwargs["reference_document_version"] = ReferenceDocumentVersion.objects.get( id=self.kwargs["pk"], ) + return kwargs def form_valid(self, form): @@ -63,19 +72,39 @@ def get_success_url(self): + "#tariff-quotas" ) - # def post(self, request, *args, **kwargs): - # quota = self.get_object() - # quota.save() - # return redirect( - # reverse( - # "reference_documents:version_details", - # args=[quota.reference_document_version.pk], - # ) - # + "#tariff-quotas", - # ) - -class PreferentialQuotaOrderNumberDeleteView(PermissionRequiredMixin, DeleteView): +class PreferentialQuotaOrderNumberDeleteView( + PermissionRequiredMixin, + FormMixin, + DeleteView, +): + form_class = PreferentialQuotaOrderNumberDeleteForm template_name = "reference_documents/preferential_quota_order_numbers/delete.jinja" permission_required = "reference_documents.edit_reference_document" model = PreferentialQuotaOrderNumber + + def get_success_url(self) -> str: + return reverse( + "reference_documents:version-details", + kwargs={"pk": self.kwargs["version_pk"]}, + ) + + def get_form_kwargs(self): + kwargs = super().get_form_kwargs() + kwargs["instance"] = self.get_object() + return kwargs + + def post(self, request, *args, **kwargs): + self.object = self.get_object() + form = self.get_form() + if form.is_valid(): + self.request.session["deleted_version"] = { + "quota_order_number": f"{self.object.quota_order_number}", + } + return self.form_valid(form) + else: + return self.form_invalid(form) + + def form_valid(self, form): + self.object.delete() + return redirect(self.get_success_url()) diff --git a/reference_documents/views/preferential_quota_views.py b/reference_documents/views/preferential_quota_views.py index 76aa7941c..af0acce57 100644 --- a/reference_documents/views/preferential_quota_views.py +++ b/reference_documents/views/preferential_quota_views.py @@ -3,6 +3,8 @@ from django.urls import reverse from django.views.generic import CreateView from django.views.generic import UpdateView +from django.views.generic.edit import DeleteView +from django.views.generic.edit import FormMixin from reference_documents.forms.preferential_quota_forms import ( PreferentialQuotaBulkCreate, @@ -10,8 +12,10 @@ from reference_documents.forms.preferential_quota_forms import ( PreferentialQuotaCreateUpdateForm, ) +from reference_documents.forms.preferential_quota_forms import ( + PreferentialQuotaDeleteForm, +) from reference_documents.models import PreferentialQuota -from reference_documents.models import PreferentialQuotaOrderNumber from reference_documents.models import ReferenceDocumentVersion @@ -23,9 +27,12 @@ class PreferentialQuotaEditView(PermissionRequiredMixin, UpdateView): def get_form_kwargs(self): kwargs = super(PreferentialQuotaEditView, self).get_form_kwargs() - kwargs["reference_document_version"] = PreferentialQuotaOrderNumber.objects.get( + kwargs["reference_document_version"] = PreferentialQuota.objects.get( + id=self.kwargs["pk"], + ).preferential_quota_order_number.reference_document_version + kwargs["preferential_quota_order_number"] = PreferentialQuota.objects.get( id=self.kwargs["pk"], - ).reference_document_version + ).preferential_quota_order_number return kwargs def post(self, request, *args, **kwargs): @@ -33,15 +40,17 @@ def post(self, request, *args, **kwargs): quota.save() return redirect( reverse( - "reference_documents:version_details", - args=[quota.reference_document_version.pk], + "reference_documents:version-details", + args=[ + quota.preferential_quota_order_number.reference_document_version.pk, + ], ) + "#tariff-quotas", ) class PreferentialQuotaCreateView(PermissionRequiredMixin, CreateView): - template_name = "reference_documents/preferential_quotas/edit.jinja" + template_name = "reference_documents/preferential_quotas/create.jinja" permission_required = "reference_documents.edit_reference_document" model = PreferentialQuota form_class = PreferentialQuotaCreateUpdateForm @@ -49,14 +58,24 @@ class PreferentialQuotaCreateView(PermissionRequiredMixin, CreateView): def get_form_kwargs(self): kwargs = super(PreferentialQuotaCreateView, self).get_form_kwargs() kwargs["reference_document_version"] = ReferenceDocumentVersion.objects.get( - id=self.kwargs["pk"], + id=self.kwargs["version_pk"], ) + + if "order_pk" in self.kwargs.keys(): + kwargs["preferential_quota_order_number"] = kwargs[ + "reference_document_version" + ].preferential_quota_order_numbers.get( + id=self.kwargs["order_pk"], + ) + else: + kwargs["preferential_quota_order_number"] = None + return kwargs def form_valid(self, form): instance = form.instance reference_document_version = ReferenceDocumentVersion.objects.get( - pk=self.kwargs["pk"], + pk=self.kwargs["version_pk"], ) instance.order = len(reference_document_version.preferential_rates.all()) + 1 instance.reference_document_version = reference_document_version @@ -116,7 +135,34 @@ def get_success_url(self): ) -class PreferentialQuotaDeleteView(PermissionRequiredMixin, UpdateView): - template_name = "preferential_quotas/delete.jinja" +class PreferentialQuotaDeleteView(PermissionRequiredMixin, FormMixin, DeleteView): + form_class = PreferentialQuotaDeleteForm + template_name = "reference_documents/preferential_quotas/delete.jinja" permission_required = "reference_documents.edit_reference_document" model = PreferentialQuota + + def get_success_url(self) -> str: + return reverse( + "reference_documents:version-details", + kwargs={"pk": self.kwargs["version_pk"]}, + ) + + def get_form_kwargs(self): + kwargs = super().get_form_kwargs() + kwargs["instance"] = self.get_object() + return kwargs + + def post(self, request, *args, **kwargs): + self.object = self.get_object() + form = self.get_form() + if form.is_valid(): + self.request.session["deleted_version"] = { + "quota_commodity_code": f"{self.object.commodity_code}", + } + return self.form_valid(form) + else: + return self.form_invalid(form) + + def form_valid(self, form): + self.object.delete() + return redirect(self.get_success_url()) diff --git a/reference_documents/views/reference_document_version_views.py b/reference_documents/views/reference_document_version_views.py index 84e9c4948..f16d3772c 100644 --- a/reference_documents/views/reference_document_version_views.py +++ b/reference_documents/views/reference_document_version_views.py @@ -155,7 +155,7 @@ def duties_row_data(self): }, { "html": f"Edit " - f"Delete", + f"Delete", }, ], ) @@ -261,7 +261,7 @@ def order_number_rows(self, data, ref_doc_order_number): }, { "html": f"Edit " - f"Delete", + f"Delete", }, ] From 26685e27ef0662e6de9e100f141ad8e7bff72c41 Mon Sep 17 00:00:00 2001 From: Doug Mills Date: Mon, 18 Mar 2024 09:44:30 +0000 Subject: [PATCH 103/118] WIP commit - quota and order number updates --- reference_documents/forms/preferential_quota_forms.py | 5 +++++ reference_documents/views/preferential_quota_views.py | 8 +------- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/reference_documents/forms/preferential_quota_forms.py b/reference_documents/forms/preferential_quota_forms.py index 4c20349e9..16e7aa547 100644 --- a/reference_documents/forms/preferential_quota_forms.py +++ b/reference_documents/forms/preferential_quota_forms.py @@ -7,6 +7,7 @@ from django import forms from django.core.exceptions import ValidationError +from common.forms import DateInputFieldFixed from common.forms import ValidityPeriodForm from reference_documents.models import PreferentialQuota from reference_documents.models import PreferentialQuotaOrderNumber @@ -150,6 +151,10 @@ def clean_quota_duty_rate(self): }, ) + end_date = DateInputFieldFixed( + label="End date", + ) + class PreferentialQuotaBulkCreate(ValidityPeriodForm, forms.ModelForm): commodity_code = forms.CharField( diff --git a/reference_documents/views/preferential_quota_views.py b/reference_documents/views/preferential_quota_views.py index af0acce57..496308308 100644 --- a/reference_documents/views/preferential_quota_views.py +++ b/reference_documents/views/preferential_quota_views.py @@ -73,13 +73,7 @@ def get_form_kwargs(self): return kwargs def form_valid(self, form): - instance = form.instance - reference_document_version = ReferenceDocumentVersion.objects.get( - pk=self.kwargs["version_pk"], - ) - instance.order = len(reference_document_version.preferential_rates.all()) + 1 - instance.reference_document_version = reference_document_version - self.object = instance + form.save() return super(PreferentialQuotaCreateView, self).form_valid(form) def get_success_url(self): From 5235106fcd3a3388399dc34522d0ca77044adc10 Mon Sep 17 00:00:00 2001 From: Doug Mills Date: Mon, 18 Mar 2024 12:10:37 +0000 Subject: [PATCH 104/118] quota and order number views + test fixes --- .../forms/preferential_quota_forms.py | 34 +++++++------------ reference_documents/tests/factories.py | 32 +++++++++++++---- .../tests/test_preferential_quotas_models.py | 5 +-- .../tests/test_preferential_rates_forms.py | 4 ++- .../tests/test_preferential_rates_views.py | 6 ++-- .../test_reference_document_versions_forms.py | 17 ++++++---- .../test_reference_document_versions_views.py | 5 +-- .../tests/test_reference_documents_forms.py | 17 ++++++---- .../views/preferential_quota_views.py | 5 ++- 9 files changed, 76 insertions(+), 49 deletions(-) diff --git a/reference_documents/forms/preferential_quota_forms.py b/reference_documents/forms/preferential_quota_forms.py index 16e7aa547..c82a77f44 100644 --- a/reference_documents/forms/preferential_quota_forms.py +++ b/reference_documents/forms/preferential_quota_forms.py @@ -21,7 +21,7 @@ class PreferentialQuotaCreateUpdateForm( class Meta: model = PreferentialQuota fields = [ - "quota_order_number", + "preferential_quota_order_number", "commodity_code", "quota_duty_rate", "volume", @@ -39,10 +39,12 @@ def __init__( super().__init__(*args, **kwargs) if preferential_quota_order_number: - self.initial["quota_order_number"] = preferential_quota_order_number + self.initial[ + "preferential_quota_order_number" + ] = preferential_quota_order_number self.fields[ - "quota_order_number" + "preferential_quota_order_number" ].queryset = reference_document_version.preferential_quota_order_numbers.all() self.reference_document_version = reference_document_version @@ -51,7 +53,7 @@ def __init__( self.helper.label_size = Size.SMALL self.helper.legend_size = Size.SMALL self.helper.layout = Layout( - "quota_order_number", + "preferential_quota_order_number", Field.text( "commodity_code", field_width=Fixed.TEN, @@ -84,23 +86,13 @@ def clean_quota_duty_rate(self): raise ValidationError("Quota duty Rate is not valid - it must have a value") return data - # def clean(self, ): - # data = self.cleaned_data["quota_duty_rate"] - # pass - - # def clean_validity_period( - # self, - # cleaned_data, - # valid_between_field_name="valid_between", - # start_date_field_name="start_date", - # end_date_field_name="end_date", - # ): - # super().clean_validity_period(cleaned_data, "valid_between", "start_date", "end_date") - # print(cleaned_data[valid_between_field_name]) - # data = self.cleaned_data["quota_duty_rate"] - # if data is None: - # raise ValidationError("Validity range must have an end date") - # return data + def clean_preferential_quota_order_number(self): + data = self.cleaned_data["preferential_quota_order_number"] + if not data: + raise ValidationError( + "Quota Order Number is not valid - it must have a value", + ) + return data commodity_code = forms.CharField( max_length=10, diff --git a/reference_documents/tests/factories.py b/reference_documents/tests/factories.py index 08ffb8f48..0ac84f4bd 100644 --- a/reference_documents/tests/factories.py +++ b/reference_documents/tests/factories.py @@ -110,6 +110,28 @@ class Params: ) +class PreferentialQuotaOrderNumberFactory(factory.django.DjangoModelFactory): + class Meta: + model = "reference_documents.PreferentialQuotaOrderNumber" + + quota_order_number = FuzzyText(prefix="054", length=3, chars=string.digits) + + coefficient = None + main_order_number = None + reference_document_version = factory.SubFactory(ReferenceDocumentVersionFactory) + + valid_between = TaricDateRange( + get_random_date( + date.today() + timedelta(days=-(365 * 2)), + date.today() + timedelta(days=-365), + ), + get_random_date( + date.today() + timedelta(days=-364), + date.today(), + ), + ) + + class PreferentialQuotaFactory(factory.django.DjangoModelFactory): class Meta: model = "reference_documents.PreferentialQuota" @@ -118,17 +140,15 @@ class Meta: quota_duty_rate = FuzzyText(length=2, chars=string.digits, suffix="%") - quota_order_number = FuzzyText(prefix="054", length=3, chars=string.digits) + preferential_quota_order_number = factory.SubFactory( + PreferentialQuotaOrderNumberFactory, + ) volume = FuzzyDecimal(100.0, 10000.0, 1) - coefficient = None - - main_quota = None - measurement = "tonnes" + order = FuzzyInteger(0, 100, 1) - reference_document_version = factory.SubFactory(ReferenceDocumentVersionFactory) valid_between = TaricDateRange( get_random_date( diff --git a/reference_documents/tests/test_preferential_quotas_models.py b/reference_documents/tests/test_preferential_quotas_models.py index 44aa20c4a..448a31eee 100644 --- a/reference_documents/tests/test_preferential_quotas_models.py +++ b/reference_documents/tests/test_preferential_quotas_models.py @@ -10,13 +10,10 @@ class TestPreferentialQuota: def test_create_with_defaults(self): target = PreferentialQuotaFactory() - assert target.quota_order_number is not None + assert target.preferential_quota_order_number is not None assert target.commodity_code is not None assert target.quota_duty_rate is not None assert target.volume is not None - assert target.coefficient is None - assert target.main_quota is None assert target.valid_between is not None assert target.measurement is not None assert target.order is not None - assert target.reference_document_version is not None diff --git a/reference_documents/tests/test_preferential_rates_forms.py b/reference_documents/tests/test_preferential_rates_forms.py index e4d5412e4..227d50dab 100644 --- a/reference_documents/tests/test_preferential_rates_forms.py +++ b/reference_documents/tests/test_preferential_rates_forms.py @@ -1,6 +1,8 @@ import pytest -from reference_documents.forms import PreferentialRateCreateUpdateForm +from reference_documents.forms.preferential_rate_forms import ( + PreferentialRateCreateUpdateForm, +) pytestmark = pytest.mark.django_db diff --git a/reference_documents/tests/test_preferential_rates_views.py b/reference_documents/tests/test_preferential_rates_views.py index 62d6548d6..333e0190a 100644 --- a/reference_documents/tests/test_preferential_rates_views.py +++ b/reference_documents/tests/test_preferential_rates_views.py @@ -2,9 +2,11 @@ from django.contrib.auth.models import Permission from django.urls import reverse -from reference_documents.forms import PreferentialRateCreateUpdateForm +from reference_documents.forms.preferential_rate_forms import ( + PreferentialRateCreateUpdateForm, +) from reference_documents.tests import factories -from reference_documents.views.preferential_rates import PreferentialRateEditView +from reference_documents.views.preferential_rate_views import PreferentialRateEditView pytestmark = pytest.mark.django_db diff --git a/reference_documents/tests/test_reference_document_versions_forms.py b/reference_documents/tests/test_reference_document_versions_forms.py index 0ab7c8141..80235af0c 100644 --- a/reference_documents/tests/test_reference_document_versions_forms.py +++ b/reference_documents/tests/test_reference_document_versions_forms.py @@ -1,6 +1,11 @@ import pytest -from reference_documents import forms +from reference_documents.forms.reference_document_version_forms import ( + ReferenceDocumentVersionDeleteForm, +) +from reference_documents.forms.reference_document_version_forms import ( + ReferenceDocumentVersionsEditCreateForm, +) from reference_documents.tests import factories pytestmark = pytest.mark.django_db @@ -22,7 +27,7 @@ def test_ref_doc_version_create_update_valid_data(): "entry_into_force_date_2": "2024", } - form = forms.ReferenceDocumentVersionsEditCreateForm(data=data) + form = ReferenceDocumentVersionsEditCreateForm(data=data) assert form.is_valid() @@ -30,7 +35,7 @@ def test_ref_doc_version_create_update_valid_data(): def test_ref_doc_version_create_update_invalid_data(): """Test that ReferenceDocumentVersionCreateEditForm is invalid when not complete correctly.""" - form = forms.ReferenceDocumentVersionsEditCreateForm(data={}) + form = ReferenceDocumentVersionsEditCreateForm(data={}) assert not form.is_valid() assert "A version number is required" in form.errors["version"] assert "A published date is required" in form.errors["published_date"] @@ -54,7 +59,7 @@ def test_ref_doc_version_create_update_invalid_data(): "entry_into_force_date_1": "1", "entry_into_force_date_2": "2024", } - form = forms.ReferenceDocumentVersionsEditCreateForm(data=data) + form = ReferenceDocumentVersionsEditCreateForm(data=data) assert not form.is_valid() assert ( "New versions of this reference document must be a higher number than previous versions" @@ -67,7 +72,7 @@ def test_ref_doc_version_delete_valid(): """Test that ReferenceDocumentVersionDeleteForm is valid for a reference document with no versions.""" version = factories.ReferenceDocumentVersionFactory.create() - form = forms.ReferenceDocumentVersionsEditCreateForm(data={}, instance=version) + form = ReferenceDocumentVersionsEditCreateForm(data={}, instance=version) assert not form.is_valid() @@ -77,7 +82,7 @@ def test_ref_doc_version_delete_invalid(): document with versions.""" version = factories.ReferenceDocumentVersionFactory.create() factories.PreferentialRateFactory.create(reference_document_version=version) - form = forms.ReferenceDocumentVersionDeleteForm(data={}, instance=version) + form = ReferenceDocumentVersionDeleteForm(data={}, instance=version) assert not form.is_valid() assert ( f"Reference Document version {version.version} cannot be deleted as it has current preferential duty rates or tariff quotas" diff --git a/reference_documents/tests/test_reference_document_versions_views.py b/reference_documents/tests/test_reference_document_versions_views.py index dbfcd55d2..3b7e66e66 100644 --- a/reference_documents/tests/test_reference_document_versions_views.py +++ b/reference_documents/tests/test_reference_document_versions_views.py @@ -26,6 +26,7 @@ def test_ref_doc_version_create_creates_object_and_redirects(valid_user, client) "reference_documents:version-create", kwargs={"pk": ref_doc.pk}, ) + form_data = { "reference_document": ref_doc.pk, "version": "2.0", @@ -40,7 +41,7 @@ def test_ref_doc_version_create_creates_object_and_redirects(valid_user, client) assert response.status_code == 302 ref_doc = ReferenceDocumentVersion.objects.get( - reference_document=form_data["reference_document"], + reference_document=ref_doc, ) assert ref_doc assert response.url == reverse( @@ -84,7 +85,7 @@ def test_ref_doc_version_edit_updates_ref_doc_object(client, valid_user): assert response.status_code == 302 assert response.url == reverse( "reference_documents:version-confirm-update", - kwargs={"pk": ref_doc_version.reference_document.pk}, + kwargs={"pk": ref_doc_version.pk}, ) ref_doc_version.refresh_from_db() assert ref_doc_version.version == 6.0 diff --git a/reference_documents/tests/test_reference_documents_forms.py b/reference_documents/tests/test_reference_documents_forms.py index 6e27a6ee2..b06b876e6 100644 --- a/reference_documents/tests/test_reference_documents_forms.py +++ b/reference_documents/tests/test_reference_documents_forms.py @@ -1,6 +1,11 @@ import pytest -from reference_documents import forms +from reference_documents.forms.reference_document_forms import ( + ReferenceDocumentCreateUpdateForm, +) +from reference_documents.forms.reference_document_forms import ( + ReferenceDocumentDeleteForm, +) from reference_documents.tests import factories pytestmark = pytest.mark.django_db @@ -11,7 +16,7 @@ def test_ref_doc_create_update_form_valid_data(): """Test that ReferenceDocumentCreateUpdateForm is valid when completed correctly.""" data = {"title": "Reference document for XY", "area_id": "XY"} - form = forms.ReferenceDocumentCreateUpdateForm(data=data) + form = ReferenceDocumentCreateUpdateForm(data=data) assert form.is_valid() @@ -20,7 +25,7 @@ def test_ref_doc_create_update_form_valid_data(): def test_ref_doc_create_update_form_invalid_data(): """Test that ReferenceDocumentCreateUpdateForm is invalid when not completed correctly.""" - form = forms.ReferenceDocumentCreateUpdateForm(data={}) + form = ReferenceDocumentCreateUpdateForm(data={}) assert not form.is_valid() assert "A Reference Document title is required" in form.errors["title"] assert "An area ID is required" in form.errors["area_id"] @@ -30,7 +35,7 @@ def test_ref_doc_create_update_form_invalid_data(): area_id="XY", ) data = {"title": "Reference document for XY", "area_id": "VWXYZ"} - form = forms.ReferenceDocumentCreateUpdateForm(data=data) + form = ReferenceDocumentCreateUpdateForm(data=data) assert not form.is_valid() assert "Enter the area ID in the correct format" in form.errors["area_id"] assert "A Reference Document with this title already exists" in form.errors["title"] @@ -41,7 +46,7 @@ def test_ref_doc_delete_form_valid(): """Test that ReferenceDocumentDeleteForm is valid for a reference document with no versions.""" ref_doc = factories.ReferenceDocumentFactory.create() - form = forms.ReferenceDocumentDeleteForm(data={}, instance=ref_doc) + form = ReferenceDocumentDeleteForm(data={}, instance=ref_doc) assert form.is_valid() @@ -51,5 +56,5 @@ def test_ref_doc_delete_form_invalid(): with versions.""" ref_doc = factories.ReferenceDocumentFactory.create() factories.ReferenceDocumentVersionFactory(reference_document=ref_doc) - form = forms.ReferenceDocumentDeleteForm(data={}, instance=ref_doc) + form = ReferenceDocumentDeleteForm(data={}, instance=ref_doc) assert not form.is_valid() diff --git a/reference_documents/views/preferential_quota_views.py b/reference_documents/views/preferential_quota_views.py index 496308308..6a0a6ba85 100644 --- a/reference_documents/views/preferential_quota_views.py +++ b/reference_documents/views/preferential_quota_views.py @@ -73,6 +73,7 @@ def get_form_kwargs(self): return kwargs def form_valid(self, form): + form.instance.order = 1 form.save() return super(PreferentialQuotaCreateView, self).form_valid(form) @@ -80,7 +81,9 @@ def get_success_url(self): return ( reverse( "reference_documents:version-details", - args=[self.object.reference_document_version.pk], + args=[ + self.object.preferential_quota_order_number.reference_document_version.pk, + ], ) + "#tariff-quotas" ) From 531695ce590db68189e4d49214b367678a0d2210 Mon Sep 17 00:00:00 2001 From: Doug Mills Date: Mon, 18 Mar 2024 12:12:14 +0000 Subject: [PATCH 105/118] quota and order number views + test fixes --- .../test_reference_document_versions_views.py | 50 +++++++++---------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/reference_documents/tests/test_reference_document_versions_views.py b/reference_documents/tests/test_reference_document_versions_views.py index 3b7e66e66..431db70af 100644 --- a/reference_documents/tests/test_reference_document_versions_views.py +++ b/reference_documents/tests/test_reference_document_versions_views.py @@ -37,14 +37,14 @@ def test_ref_doc_version_create_creates_object_and_redirects(valid_user, client) "entry_into_force_date_1": "1", "entry_into_force_date_2": "2024", } - response = client.post(create_url, form_data) - assert response.status_code == 302 + resp = client.post(create_url, form_data) + assert resp.status_code == 302 ref_doc = ReferenceDocumentVersion.objects.get( reference_document=ref_doc, ) assert ref_doc - assert response.url == reverse( + assert resp.url == reverse( "reference_documents:version-confirm-create", kwargs={"pk": ref_doc.pk}, ) @@ -77,13 +77,13 @@ def test_ref_doc_version_edit_updates_ref_doc_object(client, valid_user): "entry_into_force_date_1": "1", "entry_into_force_date_2": "2024", } - response = client.get(edit_url) - assert response.status_code == 200 + resp = client.get(edit_url) + assert resp.status_code == 200 assert ref_doc_version.version != 6.0 - response = client.post(edit_url, form_data) - assert response.status_code == 302 - assert response.url == reverse( + resp = client.post(edit_url, form_data) + assert resp.status_code == 302 + assert resp.url == reverse( "reference_documents:version-confirm-update", kwargs={"pk": ref_doc_version.pk}, ) @@ -109,16 +109,16 @@ def test_successfully_delete_ref_doc_version(valid_user, client): "reference_documents:version-delete", kwargs={"pk": ref_doc_version.pk, "ref_doc_pk": ref_doc_pk}, ) - response = client.get(delete_url) - page = BeautifulSoup(response.content, "html.parser") - assert response.status_code == 200 + resp = client.get(delete_url) + page = BeautifulSoup(resp.content, "html.parser") + assert resp.status_code == 200 assert ( f"Delete Reference Document {area_id} version {ref_doc_version.version}" in page.select("main h1")[0].text ) - response = client.post(delete_url) - assert response.status_code == 302 - assert response.url == reverse( + resp = client.post(delete_url) + assert resp.status_code == 302 + assert resp.url == reverse( "reference_documents:version-confirm-delete", kwargs={"deleted_pk": ref_doc_version.pk}, ) @@ -142,14 +142,14 @@ def test_delete_ref_doc_version_invalid(valid_user, client): "reference_documents:version-delete", kwargs={"pk": ref_doc_version.pk, "ref_doc_pk": ref_doc.pk}, ) - response = client.get(delete_url) - assert response.status_code == 200 + resp = client.get(delete_url) + assert resp.status_code == 200 - response = client.post(delete_url) - assert response.status_code == 200 + client.post(delete_url) + assert resp.status_code == 200 assert ( f"Reference Document version {ref_doc_version.version} cannot be deleted as it has current preferential duty rates or tariff quotas" - in str(response.content) + in str(resp.content) ) assert ReferenceDocument.objects.filter(pk=ref_doc.pk) @@ -181,9 +181,9 @@ def test_ref_doc_crud_without_permission(valid_user_client): "entry_into_force_date_1": "1", "entry_into_force_date_2": "2024", } - response = valid_user_client.post(create_url, form_data) - assert response.status_code == 403 - response = valid_user_client.post(edit_url, form_data) - assert response.status_code == 403 - response = valid_user_client.post(delete_url) - assert response.status_code == 403 + resp = valid_user_client.post(create_url, form_data) + assert resp.status_code == 403 + resp = valid_user_client.post(edit_url, form_data) + assert resp.status_code == 403 + resp = valid_user_client.post(delete_url) + assert resp.status_code == 403 From 4c27d3abffe6ef396d1b58bcbab272f56b657317 Mon Sep 17 00:00:00 2001 From: Matthew McKenzie <97194636+mattjamc@users.noreply.github.com> Date: Mon, 18 Mar 2024 15:39:02 +0000 Subject: [PATCH 106/118] Bulk create quotas redo without validityperiodform --- common/forms.py | 26 +++ .../common/js/addNewQuotaDefinitionForm.js | 37 ++++ common/static/common/js/application.js | 2 + .../forms/preferential_quota_forms.py | 182 +++++++++++++++--- .../views/preferential_quota_views.py | 56 ++---- 5 files changed, 234 insertions(+), 69 deletions(-) create mode 100644 common/static/common/js/addNewQuotaDefinitionForm.js diff --git a/common/forms.py b/common/forms.py index fd3d7d6b8..f3c5a04c0 100644 --- a/common/forms.py +++ b/common/forms.py @@ -338,6 +338,32 @@ def compress(self, data_list): return None +class DateInputFieldTakesParameters(DateInputField): + def __init__(self, day, month, year, **kwargs): + error_messages = { + "required": "Enter the day, month and year", + "incomplete": "Enter the day, month and year", + } + fields = (day, month, year) + + forms.MultiValueField.__init__( + self, + error_messages=error_messages, + fields=fields, + **kwargs, + ) + + def compress(self, data_list): + day, month, year = data_list or [None, None, None] + if day and month and year: + try: + return date(day=int(day), month=int(month), year=int(year)) + except ValueError as e: + raise ValidationError(str(e).capitalize()) from e + else: + return None + + class GovukDateRangeField(DateRangeField): base_field = DateInputFieldFixed diff --git a/common/static/common/js/addNewQuotaDefinitionForm.js b/common/static/common/js/addNewQuotaDefinitionForm.js new file mode 100644 index 000000000..4e2d1f39d --- /dev/null +++ b/common/static/common/js/addNewQuotaDefinitionForm.js @@ -0,0 +1,37 @@ +const addNewForm = (event) => { + event.preventDefault(); + + let numForms = document.querySelectorAll(".quota-definition-row").length; + let fieldset = document.querySelector(".quota-definition-row"); + let formset = fieldset.parentNode; + let newForm = fieldset.cloneNode(true); + + newForm.innerHTML = newForm.innerHTML.replaceAll('name="volume_0"', 'name="volume_' + numForms + '"'); + newForm.innerHTML = newForm.innerHTML.replaceAll('name="start_date_0_0"', 'name="start_date_' + numForms + '_0"'); + newForm.innerHTML = newForm.innerHTML.replaceAll('name="start_date_0_1"', 'name="start_date_' + numForms + '_1"'); + newForm.innerHTML = newForm.innerHTML.replaceAll('name="start_date_0_2"', 'name="start_date_' + numForms + '_2"'); + newForm.innerHTML = newForm.innerHTML.replaceAll('name="end_date_0_0"', 'name="end_date_' + numForms + '_0"'); + newForm.innerHTML = newForm.innerHTML.replaceAll('name="end_date_0_1"', 'name="end_date_' + numForms + '_1"'); + newForm.innerHTML = newForm.innerHTML.replaceAll('name="end_date_0_2"', 'name="end_date_' + numForms + '_2"'); + + let formFields = newForm.querySelectorAll("input"); + for (let field of formFields.values()) { + field.value = ""; + } + + let submitButton = document.getElementById("submit-id-submit"); + formset.insertBefore(newForm, submitButton); + + let addNewButton = document.querySelector("#add-new-definition"); + addNewButton.scrollIntoView(false); + } + +const initAddNewDefinition = () => { + const btn = document.querySelector("#add-new-definition"); + + if (btn) { + btn.addEventListener("click", addNewForm); + } + } + +export { initAddNewDefinition } diff --git a/common/static/common/js/application.js b/common/static/common/js/application.js index 82ef1e73f..2c9c9732e 100644 --- a/common/static/common/js/application.js +++ b/common/static/common/js/application.js @@ -5,6 +5,7 @@ require.context('govuk-frontend/govuk/assets'); import showHideCheckboxes from './showHideCheckboxes'; import { initAutocomplete } from './autocomplete'; import { initAutocompleteProgressiveEnhancement } from './autocompleteProgressiveEnhancement'; +import { initAddNewDefinition } from './addNewQuotaDefinitionForm'; import { initAddNewEnhancement } from './addNewForm'; import { initCopyToNextDuties } from './copyDuties'; import { initAll } from 'govuk-frontend'; @@ -20,6 +21,7 @@ showHideCheckboxes(); // Initialise accessible-autocomplete components without a `name` attr in order // to avoid the "dummy" autocomplete field being submitted as part of the form // to the server. +initAddNewDefinition(); initAddNewEnhancement(); initAutocomplete(false); initCopyToNextDuties(); diff --git a/reference_documents/forms/preferential_quota_forms.py b/reference_documents/forms/preferential_quota_forms.py index da10dd2df..00b55e6b5 100644 --- a/reference_documents/forms/preferential_quota_forms.py +++ b/reference_documents/forms/preferential_quota_forms.py @@ -1,3 +1,5 @@ +from datetime import date + from crispy_forms_gds.helper import FormHelper from crispy_forms_gds.layout import Div from crispy_forms_gds.layout import Field @@ -9,7 +11,11 @@ from django import forms from django.core.exceptions import ValidationError +from common.forms import DateInputFieldFixed +from common.forms import DateInputFieldTakesParameters +from common.forms import GovukDateRangeField from common.forms import ValidityPeriodForm +from common.util import TaricDateRange from reference_documents.models import PreferentialQuota from reference_documents.models import PreferentialQuotaOrderNumber from reference_documents.validators import commodity_code_validator @@ -110,7 +116,7 @@ def clean_quota_duty_rate(self): return data -class PreferentialQuotaBulkCreate(ValidityPeriodForm, forms.ModelForm): +class PreferentialQuotaBulkCreate(forms.Form): commodity_codes = forms.CharField( label="Commodity codes", widget=forms.Textarea, @@ -138,15 +144,6 @@ class PreferentialQuotaBulkCreate(ValidityPeriodForm, forms.ModelForm): }, ) - volume = forms.CharField( - validators=[], - error_messages={ - "invalid": "Volume invalid", - "required": "Volume is required", - }, - help_text="
    ", - ) - measurement = forms.CharField( validators=[], error_messages={ @@ -155,8 +152,56 @@ class PreferentialQuotaBulkCreate(ValidityPeriodForm, forms.ModelForm): }, ) + def get_variant_index(self, post_data): + result = [0] + if "data" in post_data.keys(): + for key in post_data["data"].keys(): + if key.startswith("start_date_"): + variant_index = int(key.replace("start_date_", "").split("_")[0]) + result.append(variant_index) + result = list(set(result)) + result.sort() + return result + def __init__(self, reference_document_version, *args, **kwargs): super().__init__(*args, **kwargs) + self.variant_indices = self.get_variant_index(kwargs) + self.fields["start_date_0"] = DateInputFieldFixed( + label="Start date", + required=True, + ) + self.fields["end_date_0"] = DateInputFieldFixed(label="End date", required=True) + self.fields["volume_0"] = forms.CharField( + error_messages={ + "invalid": "Volume invalid", + "required": "Volume is required", + }, + help_text="
    ", + ) + for index in self.variant_indices: + self.fields[f"start_date_{index}_0"] = forms.CharField() + self.fields[f"start_date_{index}_1"] = forms.CharField() + self.fields[f"start_date_{index}_2"] = forms.CharField() + self.fields[f"start_date_{index}"] = DateInputFieldTakesParameters( + day=self.fields[f"start_date_{index}_0"], + month=self.fields[f"start_date_{index}_1"], + year=self.fields[f"start_date_{index}_2"], + label="Start date", + ) + self.fields[f"end_date_{index}_0"] = forms.CharField() + self.fields[f"end_date_{index}_1"] = forms.CharField() + self.fields[f"end_date_{index}_2"] = forms.CharField() + self.fields[f"end_date_{index}"] = DateInputFieldTakesParameters( + day=self.fields[f"end_date_{index}_0"], + month=self.fields[f"end_date_{index}_1"], + year=self.fields[f"end_date_{index}_2"], + label="End date", + ) + self.fields[f"valid_between_{index}"] = GovukDateRangeField() + self.fields[f"volume_{index}"] = forms.CharField( + label="Volume", + help_text="
    ", + ) self.fields["preferential_quota_order_number"].queryset = ( PreferentialQuotaOrderNumber.objects.all() .filter(reference_document_version=reference_document_version) @@ -165,7 +210,7 @@ def __init__(self, reference_document_version, *args, **kwargs): self.fields[ "preferential_quota_order_number" ].label_from_instance = lambda obj: f"{obj.quota_order_number}" - self.fields["end_date"].help_text = "" + self.fields["end_date_0"].help_text = "" self.helper = FormHelper(self) self.helper.label_size = Size.SMALL self.helper.legend_size = Size.SMALL @@ -184,18 +229,24 @@ def __init__(self, reference_document_version, *args, **kwargs): ), Fieldset( Div( - Field("start_date"), + Field( + "start_date_0", + ), ), Div( - Field("end_date"), + Field( + "end_date_0", + ), ), Div( Field( - "volume", + "volume_0", + label="Volume", field_width=Fixed.TEN, ), ), style="display: grid; grid-template-columns: 2fr 2fr 1fr", + css_class="quota-definition-row", ), Submit( "submit", @@ -204,23 +255,98 @@ def __init__(self, reference_document_version, *args, **kwargs): data_prevent_double_click="true", ), ) + for index in self.variant_indices[1:]: + self.helper.layout.insert( + -1, + Fieldset( + Div( + Field( + f"start_date_{index}", + ), + ), + Div( + Field( + f"end_date_{index}", + ), + ), + Div( + Field( + f"volume_{index}", + field_width=Fixed.TEN, + ), + ), + style="display: grid; grid-template-columns: 2fr 2fr 1fr", + css_class="quota-definition-row", + ), + ) def clean(self): cleaned_data = super().clean() - commodity_codes = cleaned_data.get("commodity_codes").splitlines() - for commodity_code in commodity_codes: - try: - commodity_code_validator(commodity_code) - except ValidationError: + # Clean commodity codes + commodity_codes = cleaned_data.get("commodity_codes") + if commodity_codes: + for commodity_code in commodity_codes.splitlines(): + try: + commodity_code_validator(commodity_code) + except ValidationError: + self.add_error( + "commodity_codes", + "Ensure all commodity codes are 10 digits and each on a new line", + ) + # Clean validity periods + for index in self.variant_indices: + self.clean_validity_period( + self, + cleaned_data, + valid_between_field_name=f"valid_between_{index}", + start_date_field_name=f"start_date_{index}", + end_date_field_name=f"end_date_{index}", + ) + + @staticmethod + def clean_validity_period( + self, + cleaned_data, + valid_between_field_name, + start_date_field_name, + end_date_field_name, + ): + start_date = cleaned_data.pop(start_date_field_name, None) + end_date = cleaned_data.pop(end_date_field_name, None) + + # Data may not be present, e.g. if the user skips ahead in the sidebar + valid_between = self.initial.get(valid_between_field_name) + if end_date and start_date and end_date < start_date: + if valid_between: + if start_date != valid_between.lower: + self.add_error( + start_date_field_name, + "The start date must be the same as or before the end date.", + ) + if end_date != self.initial[valid_between_field_name].upper: + self.add_error( + end_date_field_name, + "The end date must be the same as or after the start date.", + ) + else: self.add_error( - "commodity_codes", - "Ensure all commodity codes are 10 digits and each on a new line", + end_date_field_name, + "The end date must be the same as or after the start date.", ) + cleaned_data[valid_between_field_name] = TaricDateRange(start_date, end_date) - class Meta: - model = PreferentialQuota - fields = [ - "preferential_quota_order_number", - "quota_duty_rate", - "measurement", - ] + if start_date: + day, month, year = (start_date.day, start_date.month, start_date.year) + self.fields[start_date_field_name].initial = date( + day=int(day), + month=int(month), + year=int(year), + ) + + if end_date: + day, month, year = (end_date.day, end_date.month, end_date.year) + self.fields[end_date_field_name].initial = date( + day=int(day), + month=int(month), + year=int(year), + ) diff --git a/reference_documents/views/preferential_quota_views.py b/reference_documents/views/preferential_quota_views.py index 398dde170..19908e476 100644 --- a/reference_documents/views/preferential_quota_views.py +++ b/reference_documents/views/preferential_quota_views.py @@ -1,5 +1,3 @@ -from datetime import date - from django.contrib.auth.mixins import PermissionRequiredMixin from django.shortcuts import redirect from django.urls import reverse @@ -7,7 +5,6 @@ from django.views.generic import FormView from django.views.generic import UpdateView -from common.util import TaricDateRange from reference_documents.forms.preferential_quota_forms import ( PreferentialQuotaBulkCreate, ) @@ -80,7 +77,6 @@ def get_success_url(self): class PreferentialQuotaBulkCreateView(PermissionRequiredMixin, FormView): template_name = "reference_documents/preferential_quotas/bulk_create.jinja" permission_required = "reference_documents.add_preferentialquota" - # model = PreferentialQuota form_class = PreferentialQuotaBulkCreate queryset = ReferenceDocumentVersion.objects.all() @@ -102,54 +98,32 @@ def get_context_data(self, **kwargs): ) return context_data - @staticmethod - def get_validity_and_volume_data(data): - entries = [] - for entry in range(len(data.getlist("volume"))): - start_date = date( - day=int(data.getlist("start_date_0")[entry]), - month=int(data.getlist("start_date_1")[entry]), - year=int(data.getlist("start_date_2")[entry]), - ) - end_date = date( - day=int(data.getlist("end_date_0")[entry]), - month=int(data.getlist("end_date_1")[entry]), - year=int(data.getlist("end_date_2")[entry]), - ) - valid_between = TaricDateRange(start_date, end_date) - volume = data.getlist("volume")[entry] - entries.append({f"valid_between": valid_between, f"volume": volume}) - return entries - def form_valid(self, form): - dates_and_volumes = self.get_validity_and_volume_data(form.data) + cleaned_data = form.cleaned_data commodity_codes = form.cleaned_data["commodity_codes"].splitlines() - self.reference_document_version = ReferenceDocumentVersion.objects.all().get( + reference_document_version = ReferenceDocumentVersion.objects.all().get( pk=self.kwargs["pk"], ) for commodity_code in commodity_codes: - for entry in dates_and_volumes: - instance = form.save(commit=False) - instance.order = ( - len(self.reference_document_version.preferential_quotas()) + 1 - ) - instance = PreferentialQuota( + for index in form.variant_indices: + PreferentialQuota.objects.create( commodity_code=commodity_code, - quota_duty_rate=instance.quota_duty_rate, - volume=entry["volume"], - valid_between=entry["valid_between"], - measurement=instance.measurement, - order=instance.order, - preferential_quota_order_number=instance.preferential_quota_order_number, + quota_duty_rate=cleaned_data["quota_duty_rate"], + volume=cleaned_data[f"volume_{index}"], + valid_between=cleaned_data[f"valid_between_{index}"], + measurement=cleaned_data["measurement"], + order=len(reference_document_version.preferential_quotas()) + 1, + preferential_quota_order_number=cleaned_data[ + "preferential_quota_order_number" + ], ) - instance.save() - return redirect(self.get_success_url()) + return redirect(self.get_success_url(reference_document_version)) - def get_success_url(self): + def get_success_url(self, reference_document_version): return ( reverse( "reference_documents:version-details", - args=[self.reference_document_version.pk], + args=[reference_document_version.pk], ) + "#tariff-quotas" ) From d135a45cd732e566e0bd58d412fbe2c60cb8690c Mon Sep 17 00:00:00 2001 From: Doug Mills Date: Tue, 19 Mar 2024 08:35:48 +0000 Subject: [PATCH 107/118] test fixes --- .../tests/test_reference_document_versions_views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/reference_documents/tests/test_reference_document_versions_views.py b/reference_documents/tests/test_reference_document_versions_views.py index 431db70af..ddb5f652d 100644 --- a/reference_documents/tests/test_reference_document_versions_views.py +++ b/reference_documents/tests/test_reference_document_versions_views.py @@ -145,7 +145,7 @@ def test_delete_ref_doc_version_invalid(valid_user, client): resp = client.get(delete_url) assert resp.status_code == 200 - client.post(delete_url) + resp = client.post(delete_url) assert resp.status_code == 200 assert ( f"Reference Document version {ref_doc_version.version} cannot be deleted as it has current preferential duty rates or tariff quotas" From cbe694f0e5dbd87b84838098b464a02fb21b2152 Mon Sep 17 00:00:00 2001 From: Matthew McKenzie <97194636+mattjamc@users.noreply.github.com> Date: Tue, 19 Mar 2024 16:10:41 +0000 Subject: [PATCH 108/118] Add js remove button to additional quota definition forms --- .../common/js/addNewQuotaDefinitionForm.js | 38 ++++++++++++++----- .../common/js/removeQuotaDefinitionForm.js | 19 ++++++++++ .../forms/preferential_quota_forms.py | 29 ++++++++------ .../preferential_quotas/bulk_create.jinja | 8 ---- .../views/preferential_quota_views.py | 27 ++++++------- 5 files changed, 77 insertions(+), 44 deletions(-) create mode 100644 common/static/common/js/removeQuotaDefinitionForm.js diff --git a/common/static/common/js/addNewQuotaDefinitionForm.js b/common/static/common/js/addNewQuotaDefinitionForm.js index 4e2d1f39d..ef48201e2 100644 --- a/common/static/common/js/addNewQuotaDefinitionForm.js +++ b/common/static/common/js/addNewQuotaDefinitionForm.js @@ -1,29 +1,47 @@ +import { removeQuotaDefinitionForm } from "./removeQuotaDefinitionForm.js" + +let formCounter = 1 + const addNewForm = (event) => { event.preventDefault(); - let numForms = document.querySelectorAll(".quota-definition-row").length; let fieldset = document.querySelector(".quota-definition-row"); let formset = fieldset.parentNode; let newForm = fieldset.cloneNode(true); - newForm.innerHTML = newForm.innerHTML.replaceAll('name="volume_0"', 'name="volume_' + numForms + '"'); - newForm.innerHTML = newForm.innerHTML.replaceAll('name="start_date_0_0"', 'name="start_date_' + numForms + '_0"'); - newForm.innerHTML = newForm.innerHTML.replaceAll('name="start_date_0_1"', 'name="start_date_' + numForms + '_1"'); - newForm.innerHTML = newForm.innerHTML.replaceAll('name="start_date_0_2"', 'name="start_date_' + numForms + '_2"'); - newForm.innerHTML = newForm.innerHTML.replaceAll('name="end_date_0_0"', 'name="end_date_' + numForms + '_0"'); - newForm.innerHTML = newForm.innerHTML.replaceAll('name="end_date_0_1"', 'name="end_date_' + numForms + '_1"'); - newForm.innerHTML = newForm.innerHTML.replaceAll('name="end_date_0_2"', 'name="end_date_' + numForms + '_2"'); + let new_class = "quota-definition-row-" + formCounter + newForm.classList.add(new_class) + + newForm.innerHTML = newForm.innerHTML.replaceAll('name="volume_0"', 'name="volume_' + formCounter + '"'); + newForm.innerHTML = newForm.innerHTML.replaceAll('name="start_date_0_0"', 'name="start_date_' + formCounter + '_0"'); + newForm.innerHTML = newForm.innerHTML.replaceAll('name="start_date_0_1"', 'name="start_date_' + formCounter + '_1"'); + newForm.innerHTML = newForm.innerHTML.replaceAll('name="start_date_0_2"', 'name="start_date_' + formCounter + '_2"'); + newForm.innerHTML = newForm.innerHTML.replaceAll('name="end_date_0_0"', 'name="end_date_' + formCounter + '_0"'); + newForm.innerHTML = newForm.innerHTML.replaceAll('name="end_date_0_1"', 'name="end_date_' + formCounter + '_1"'); + newForm.innerHTML = newForm.innerHTML.replaceAll('name="end_date_0_2"', 'name="end_date_' + formCounter + '_2"'); + + let removeFormID = "form-remove_" + formCounter; + newForm.insertAdjacentHTML("beforeend", ``); + let remove_button = newForm.lastChild + remove_button.addEventListener("click", removeQuotaDefinitionForm); let formFields = newForm.querySelectorAll("input"); for (let field of formFields.values()) { field.value = ""; } - let submitButton = document.getElementById("submit-id-submit"); - formset.insertBefore(newForm, submitButton); + let buttonGroup = document.querySelector('.govuk-button-group'); + formset.insertBefore(newForm, buttonGroup); + + let numForms = document.querySelectorAll(".quota-definition-row").length; let addNewButton = document.querySelector("#add-new-definition"); addNewButton.scrollIntoView(false); + formCounter += 1; + if (numForms >= 10) { + addNewButton.style.display = "none"; + } + } const initAddNewDefinition = () => { diff --git a/common/static/common/js/removeQuotaDefinitionForm.js b/common/static/common/js/removeQuotaDefinitionForm.js new file mode 100644 index 000000000..74ea0a282 --- /dev/null +++ b/common/static/common/js/removeQuotaDefinitionForm.js @@ -0,0 +1,19 @@ +const removeQuotaDefinitionForm = (event) => { + event.preventDefault(); + let remove_id = event.target.id; + let remove_number = remove_id.split('_').at(-1) + + let fieldset_id = 'quota-definition-row-' + remove_number + + let fieldset_to_remove = document.querySelector(".quota-definition-row-" + remove_number) + fieldset_to_remove.remove() + + let addNewButton = document.querySelector("#add-new-definition"); + let numForms = document.querySelectorAll(".quota-definition-row").length; + if (numForms < 10) { + addNewButton.style.display = "inline" + } + + } + +export { removeQuotaDefinitionForm } diff --git a/reference_documents/forms/preferential_quota_forms.py b/reference_documents/forms/preferential_quota_forms.py index ef19ffe31..07dce30cb 100644 --- a/reference_documents/forms/preferential_quota_forms.py +++ b/reference_documents/forms/preferential_quota_forms.py @@ -1,6 +1,7 @@ from datetime import date from crispy_forms_gds.helper import FormHelper +from crispy_forms_gds.layout import Button from crispy_forms_gds.layout import Div from crispy_forms_gds.layout import Field from crispy_forms_gds.layout import Fieldset @@ -159,7 +160,7 @@ class PreferentialQuotaBulkCreate(forms.Form): commodity_codes = forms.CharField( label="Commodity codes", widget=forms.Textarea, - # validators=[commodity_code_validator], + help_text="Enter one or more commodity codes with each one on a new line.", error_messages={ "invalid": "Commodity code should be 10 digits", "required": "Commodity code is required", @@ -192,6 +193,9 @@ class PreferentialQuotaBulkCreate(forms.Form): ) def get_variant_index(self, post_data): + """Looks through post data to see how many validity date / volume + combinations have been submitted and returns the index value of each + combination in a list.""" result = [0] if "data" in post_data.keys(): for key in post_data["data"].keys(): @@ -217,6 +221,7 @@ def __init__(self, reference_document_version, *args, **kwargs): }, help_text="
    ", ) + # Add frontend dynamically added fields to the backend Django form for index in self.variant_indices: self.fields[f"start_date_{index}_0"] = forms.CharField() self.fields[f"start_date_{index}_1"] = forms.CharField() @@ -249,7 +254,6 @@ def __init__(self, reference_document_version, *args, **kwargs): self.fields[ "preferential_quota_order_number" ].label_from_instance = lambda obj: f"{obj.quota_order_number}" - self.fields["end_date_0"].help_text = "" self.helper = FormHelper(self) self.helper.label_size = Size.SMALL self.helper.legend_size = Size.SMALL @@ -287,13 +291,18 @@ def __init__(self, reference_document_version, *args, **kwargs): style="display: grid; grid-template-columns: 2fr 2fr 1fr", css_class="quota-definition-row", ), - Submit( - "submit", - "Save", - data_module="govuk-button", - data_prevent_double_click="true", + Div( + Submit( + "submit", + "Save", + data_module="govuk-button", + data_prevent_double_click="true", + ), + Button.secondary("", "Add new", css_id="add-new-definition"), + css_class="govuk-button-group", ), ) + # Add dynamically added fields to Django form layout so they do not disappear in the event the form is invalid and reloads for index in self.variant_indices[1:]: self.helper.layout.insert( -1, @@ -334,16 +343,14 @@ def clean(self): ) # Clean validity periods for index in self.variant_indices: - self.clean_validity_period( - self, + self.custom_clean_validity_period( cleaned_data, valid_between_field_name=f"valid_between_{index}", start_date_field_name=f"start_date_{index}", end_date_field_name=f"end_date_{index}", ) - @staticmethod - def clean_validity_period( + def custom_clean_validity_period( self, cleaned_data, valid_between_field_name, diff --git a/reference_documents/jinja2/reference_documents/preferential_quotas/bulk_create.jinja b/reference_documents/jinja2/reference_documents/preferential_quotas/bulk_create.jinja index 3e703c929..5f263a3c7 100644 --- a/reference_documents/jinja2/reference_documents/preferential_quotas/bulk_create.jinja +++ b/reference_documents/jinja2/reference_documents/preferential_quotas/bulk_create.jinja @@ -18,12 +18,4 @@ {% call django_form() %} {{ crispy(form) }} {% endcall %} - - {{ govukButton({ - "text": "Add new", - "attributes": {"id": "add-new"}, - "classes": "govuk-button--secondary", - "value": "1", - "name": form.prefix ~ "-ADD", - }) }} {% endblock %} \ No newline at end of file diff --git a/reference_documents/views/preferential_quota_views.py b/reference_documents/views/preferential_quota_views.py index 8d3a84b0b..5cb08cbdd 100644 --- a/reference_documents/views/preferential_quota_views.py +++ b/reference_documents/views/preferential_quota_views.py @@ -96,30 +96,27 @@ class PreferentialQuotaBulkCreateView(PermissionRequiredMixin, FormView): form_class = PreferentialQuotaBulkCreate queryset = ReferenceDocumentVersion.objects.all() - def get_form_kwargs(self): - kwargs = super().get_form_kwargs() - kwargs[ - "reference_document_version" - ] = ReferenceDocumentVersion.objects.all().get( + def get_reference_document_version(self): + return ReferenceDocumentVersion.objects.all().get( pk=self.kwargs["pk"], ) + + def get_form_kwargs(self): + kwargs = super().get_form_kwargs() + kwargs["reference_document_version"] = self.get_reference_document_version() return kwargs def get_context_data(self, **kwargs): context_data = super().get_context_data(**kwargs) context_data[ "reference_document_version" - ] = ReferenceDocumentVersion.objects.all().get( - pk=self.kwargs["pk"], - ) + ] = self.get_reference_document_version() return context_data def form_valid(self, form): cleaned_data = form.cleaned_data - commodity_codes = form.cleaned_data["commodity_codes"].splitlines() - reference_document_version = ReferenceDocumentVersion.objects.all().get( - pk=self.kwargs["pk"], - ) + commodity_codes = set(form.cleaned_data["commodity_codes"].splitlines()) + reference_document_version = self.get_reference_document_version() for commodity_code in commodity_codes: for index in form.variant_indices: PreferentialQuota.objects.create( @@ -133,13 +130,13 @@ def form_valid(self, form): "preferential_quota_order_number" ], ) - return redirect(self.get_success_url(reference_document_version)) + return redirect(self.get_success_url()) - def get_success_url(self, reference_document_version): + def get_success_url(self): return ( reverse( "reference_documents:version-details", - args=[reference_document_version.pk], + args=[self.get_reference_document_version().pk], ) + "#tariff-quotas" ) From 2193470601232b59bdffc8cf328fbf6f5b67981a Mon Sep 17 00:00:00 2001 From: Doug Mills Date: Wed, 20 Mar 2024 10:51:09 +0000 Subject: [PATCH 109/118] tested quota order number and quota forms --- .../forms/preferential_quota_forms.py | 28 ++- .../preferential_quota_order_number_forms.py | 25 +- ...t_preferential_quota_order_number_forms.py | 236 ++++++++++++++++++ ..._preferential_quota_order_number_models.py | 19 ++ ...t_preferential_quota_order_number_views.py | 30 +++ .../tests/test_preferential_quotas_forms.py | 131 ++++++++++ 6 files changed, 452 insertions(+), 17 deletions(-) create mode 100644 reference_documents/tests/test_preferential_quota_order_number_forms.py create mode 100644 reference_documents/tests/test_preferential_quota_order_number_models.py create mode 100644 reference_documents/tests/test_preferential_quota_order_number_views.py diff --git a/reference_documents/forms/preferential_quota_forms.py b/reference_documents/forms/preferential_quota_forms.py index ef19ffe31..adb9a764c 100644 --- a/reference_documents/forms/preferential_quota_forms.py +++ b/reference_documents/forms/preferential_quota_forms.py @@ -88,17 +88,27 @@ def __init__( ) def clean_quota_duty_rate(self): - data = self.cleaned_data["quota_duty_rate"] - if len(data) < 1: - raise ValidationError("Quota duty Rate is not valid - it must have a value") + error_message = "Quota duty Rate is not valid - it must have a value" + + if "quota_duty_rate" in self.cleaned_data.keys(): + data = self.cleaned_data["quota_duty_rate"] + if len(data) < 1: + raise ValidationError(error_message) + else: + raise ValidationError(error_message) + return data def clean_preferential_quota_order_number(self): - data = self.cleaned_data["preferential_quota_order_number"] - if not data: - raise ValidationError( - "Quota Order Number is not valid - it must have a value", - ) + error_message = "Quota Order Number is not valid - it must have a value" + + if "preferential_quota_order_number" in self.cleaned_data.keys(): + data = self.cleaned_data["preferential_quota_order_number"] + if not data: + raise ValidationError(error_message) + else: + raise ValidationError(error_message) + return data commodity_code = forms.CharField( @@ -408,5 +418,5 @@ def __init__(self, *args, **kwargs) -> None: ) class Meta: - model = PreferentialQuotaOrderNumber + model = PreferentialQuota fields = [] diff --git a/reference_documents/forms/preferential_quota_order_number_forms.py b/reference_documents/forms/preferential_quota_order_number_forms.py index 2b32287be..63b12549c 100644 --- a/reference_documents/forms/preferential_quota_order_number_forms.py +++ b/reference_documents/forms/preferential_quota_order_number_forms.py @@ -50,24 +50,30 @@ def __init__(self, reference_document_version, *args, **kwargs): ) def clean_coefficient(self): - cleaned_data = super().clean() - coefficient = cleaned_data.get("coefficient") - if coefficient == "": + coefficient = self.cleaned_data["coefficient"] + + if coefficient == "" or coefficient is None: return None - return coefficient + try: + coefficient = float(coefficient) + return coefficient + except ValueError: + raise ValidationError( + "Coefficient not a valid number", + ) def clean(self): cleaned_data = super().clean() coefficient = cleaned_data.get("coefficient") - main_order_number = cleaned_data.get("main_order_number") + main_order_number_id = cleaned_data.get("main_order_number_id") # cant have one without the other - if coefficient and not main_order_number: + if coefficient and not main_order_number_id: raise ValidationError( "Coefficient specified without main order number", ) - elif not coefficient and main_order_number: + elif not coefficient and main_order_number_id: raise ValidationError( "Main order number specified without coefficient", ) @@ -80,7 +86,10 @@ def clean_quota_order_number(self): ).exists(): raise ValidationError("Quota Order Number Already Exists") - return data + if not data.isnumeric(): + raise ValidationError("Quota Order Number is not numeric") + else: + return data quota_order_number = forms.CharField( label="Order number", diff --git a/reference_documents/tests/test_preferential_quota_order_number_forms.py b/reference_documents/tests/test_preferential_quota_order_number_forms.py new file mode 100644 index 000000000..d32aa2677 --- /dev/null +++ b/reference_documents/tests/test_preferential_quota_order_number_forms.py @@ -0,0 +1,236 @@ +import pytest +from django.core.exceptions import ValidationError + +from reference_documents.forms.preferential_quota_order_number_forms import ( + PreferentialQuotaOrderNumberCreateUpdateForm, +) +from reference_documents.forms.preferential_quota_order_number_forms import ( + PreferentialQuotaOrderNumberDeleteForm, +) +from reference_documents.models import PreferentialQuotaOrderNumber +from reference_documents.tests import factories + +pytestmark = pytest.mark.django_db + + +@pytest.mark.reference_documents +class TestPreferentialQuotaOrderNumberCreateUpdateForm: + def test_init(self): + pref_quota_order_number = factories.PreferentialQuotaOrderNumberFactory() + + target = PreferentialQuotaOrderNumberCreateUpdateForm( + pref_quota_order_number.reference_document_version, + instance=pref_quota_order_number, + ) + + # it sets initial values + assert ( + target.reference_document_version + == pref_quota_order_number.reference_document_version + ) + assert target.Meta.model == PreferentialQuotaOrderNumber + assert target.Meta.fields == [ + "quota_order_number", + "coefficient", + "main_order_number", + "valid_between", + ] + + def test_clean_coefficient_pass_valid(self): + pref_quota_order_number = factories.PreferentialQuotaOrderNumberFactory() + + data = { + "coefficient": "1.6", + } + + target = PreferentialQuotaOrderNumberCreateUpdateForm( + pref_quota_order_number.reference_document_version, + instance=pref_quota_order_number, + data=data, + ) + + assert not target.is_valid() + assert target.instance.coefficient == 1.6 + + def test_clean_coefficient_fail_invalid(self): + pref_quota_order_number = factories.PreferentialQuotaOrderNumberFactory() + + data = { + "coefficient": "zz", + } + + target = PreferentialQuotaOrderNumberCreateUpdateForm( + pref_quota_order_number.reference_document_version, + instance=pref_quota_order_number, + data=data, + ) + + assert not target.is_valid() + assert target.errors["coefficient"] == ["Coefficient not a valid number"] + + def test_clean_coefficient_pass_blank(self): + pref_quota_order_number = factories.PreferentialQuotaOrderNumberFactory() + + data = { + "coefficient": "", + } + + target = PreferentialQuotaOrderNumberCreateUpdateForm( + pref_quota_order_number.reference_document_version, + instance=pref_quota_order_number, + data=data, + ) + + assert not target.is_valid() + assert "coefficient" not in target.errors.keys() + + def test_clean_coefficient_pass_not_provided(self): + pref_quota_order_number = factories.PreferentialQuotaOrderNumberFactory() + + data = {} + + target = PreferentialQuotaOrderNumberCreateUpdateForm( + pref_quota_order_number.reference_document_version, + instance=pref_quota_order_number, + data=data, + ) + + assert not target.is_valid() + assert "coefficient" not in target.errors.keys() + + def test_clean_quota_order_number_valid_adding(self): + pref_quota_order_number = factories.PreferentialQuotaOrderNumberFactory() + + data = { + "quota_order_number": "054333", + } + + target = PreferentialQuotaOrderNumberCreateUpdateForm( + pref_quota_order_number.reference_document_version, + data=data, + ) + + assert not target.is_valid() + assert "quota_order_number" not in target.errors.keys() + + def test_clean_quota_order_number_invalid_already_exists_adding(self): + pref_quota_order_number = factories.PreferentialQuotaOrderNumberFactory( + quota_order_number="054333", + ) + + data = { + "quota_order_number": "054333", + } + + target = PreferentialQuotaOrderNumberCreateUpdateForm( + pref_quota_order_number.reference_document_version, + data=data, + ) + + assert not target.is_valid() + assert "quota_order_number" in target.errors.keys() + + def test_clean_quota_order_number_invalid_order_number_adding(self): + ref_doc_ver = factories.ReferenceDocumentVersionFactory() + + data = { + "quota_order_number": "zzaabb", + } + + target = PreferentialQuotaOrderNumberCreateUpdateForm( + ref_doc_ver, + data=data, + ) + + assert not target.is_valid() + assert "quota_order_number" in target.errors.keys() + + def test_clean_coefficient_no_main_order(self): + factories.PreferentialQuotaOrderNumberFactory() + pref_quota_order_number = factories.PreferentialQuotaOrderNumberFactory() + + data = { + "quota_order_number": pref_quota_order_number.quota_order_number, + "coefficient": "1.0", + "valid_between": pref_quota_order_number.valid_between, + } + + target = PreferentialQuotaOrderNumberCreateUpdateForm( + pref_quota_order_number.reference_document_version, + data=data, + ) + + target.is_valid() + + with pytest.raises(ValidationError) as ve: + target.clean() + assert "Coefficient specified without main order number" in str(ve.value) + + def test_clean_main_order_no_coefficient(self): + pref_quota_order_number_main = factories.PreferentialQuotaOrderNumberFactory() + pref_quota_order_number = factories.PreferentialQuotaOrderNumberFactory() + + data = { + "main_order_number_id": pref_quota_order_number_main.id, + "quota_order_number": pref_quota_order_number.quota_order_number, + "valid_between": pref_quota_order_number.valid_between, + } + + target = PreferentialQuotaOrderNumberCreateUpdateForm( + pref_quota_order_number.reference_document_version, + data=data, + ) + + target.is_valid() + + with pytest.raises(ValidationError) as ve: + target.clean() + + assert "Main order number specified without coefficient" in str(ve.value) + + +@pytest.mark.reference_documents +class TestPreferentialQuotaOrderNumberDeleteForm: + def test_init(self): + pref_quota_order_number = factories.PreferentialQuotaOrderNumberFactory() + + target = PreferentialQuotaOrderNumberDeleteForm( + instance=pref_quota_order_number, + ) + + assert target.instance == pref_quota_order_number + assert target.Meta.fields == [] + assert target.Meta.model == PreferentialQuotaOrderNumber + + def test_clean_with_child_records(self): + pref_quota = factories.PreferentialQuotaFactory() + + target = PreferentialQuotaOrderNumberDeleteForm( + instance=pref_quota.preferential_quota_order_number, + data={}, + ) + + assert not target.is_valid() + + with pytest.raises(ValidationError) as ve: + target.clean() + + expected_string = ( + f"Quota Order Number {pref_quota.preferential_quota_order_number} " + f"cannot be deleted as it has associated Preferential Quotas." + ) + + assert expected_string in str(ve) + + def test_clean_with_no_child_records(self): + pref_quota_order_number = factories.PreferentialQuotaOrderNumberFactory() + + target = PreferentialQuotaOrderNumberDeleteForm( + instance=pref_quota_order_number, + data={}, + ) + + assert target.is_valid() + target.clean() + + assert len(target.errors) == 0 diff --git a/reference_documents/tests/test_preferential_quota_order_number_models.py b/reference_documents/tests/test_preferential_quota_order_number_models.py new file mode 100644 index 000000000..448a31eee --- /dev/null +++ b/reference_documents/tests/test_preferential_quota_order_number_models.py @@ -0,0 +1,19 @@ +import pytest + +from reference_documents.tests.factories import PreferentialQuotaFactory + +pytestmark = pytest.mark.django_db + + +@pytest.mark.reference_documents +class TestPreferentialQuota: + def test_create_with_defaults(self): + target = PreferentialQuotaFactory() + + assert target.preferential_quota_order_number is not None + assert target.commodity_code is not None + assert target.quota_duty_rate is not None + assert target.volume is not None + assert target.valid_between is not None + assert target.measurement is not None + assert target.order is not None diff --git a/reference_documents/tests/test_preferential_quota_order_number_views.py b/reference_documents/tests/test_preferential_quota_order_number_views.py new file mode 100644 index 000000000..c29e56eb8 --- /dev/null +++ b/reference_documents/tests/test_preferential_quota_order_number_views.py @@ -0,0 +1,30 @@ +import pytest +from django.urls import reverse + +from reference_documents.tests import factories + +pytestmark = pytest.mark.django_db + + +class TestPreferentialQuotaEditView: + def test_get_without_permissions(self, valid_user_client): + pref_quota = factories.PreferentialQuotaFactory.create() + + response = valid_user_client.get( + reverse( + "reference_documents:preferential_quotas_edit", + kwargs={"pk": pref_quota.pk}, + ), + ) + assert response.status_code == 200 + + def test_get_with_permissions(self, superuser_client): + pref_quota = factories.PreferentialQuotaFactory.create() + + response = superuser_client.get( + reverse( + "reference_documents:preferential_quotas_edit", + kwargs={"pk": pref_quota.pk}, + ), + ) + assert response.status_code == 200 diff --git a/reference_documents/tests/test_preferential_quotas_forms.py b/reference_documents/tests/test_preferential_quotas_forms.py index a7057c2cc..4cec7c9fd 100644 --- a/reference_documents/tests/test_preferential_quotas_forms.py +++ b/reference_documents/tests/test_preferential_quotas_forms.py @@ -1,3 +1,134 @@ import pytest +from django.core.exceptions import ValidationError + +from reference_documents.forms.preferential_quota_forms import ( + PreferentialQuotaCreateUpdateForm, +) +from reference_documents.forms.preferential_quota_forms import ( + PreferentialQuotaDeleteForm, +) +from reference_documents.models import PreferentialQuota +from reference_documents.tests import factories pytestmark = pytest.mark.django_db + + +@pytest.mark.reference_documents +class TestPreferentialQuotaCreateUpdateForm: + def test_init(self): + pref_quota = factories.PreferentialQuotaFactory() + + target = PreferentialQuotaCreateUpdateForm( + pref_quota.preferential_quota_order_number.reference_document_version, + pref_quota.preferential_quota_order_number, + instance=pref_quota, + ) + + # it sets initial values + assert ( + target.initial["preferential_quota_order_number"] + == pref_quota.preferential_quota_order_number + ) + assert ( + target.reference_document_version + == pref_quota.preferential_quota_order_number.reference_document_version + ) + assert target.Meta.fields == [ + "preferential_quota_order_number", + "commodity_code", + "quota_duty_rate", + "volume", + "measurement", + "valid_between", + ] + + def test_clean_quota_duty_rate_pass(self): + pref_quota = factories.PreferentialQuotaFactory() + + data = { + "quota_duty_rate": "10%", + } + + target = PreferentialQuotaCreateUpdateForm( + pref_quota.preferential_quota_order_number.reference_document_version, + pref_quota.preferential_quota_order_number, + instance=pref_quota, + data=data, + ) + + assert not target.is_valid() + assert target.clean_quota_duty_rate() == "10%" + + def test_clean_quota_duty_rate_fail(self): + pref_quota = factories.PreferentialQuotaFactory() + + data = { + "quota_duty_rate": "", + } + + target = PreferentialQuotaCreateUpdateForm( + pref_quota.preferential_quota_order_number.reference_document_version, + pref_quota.preferential_quota_order_number, + instance=pref_quota, + data=data, + ) + + assert not target.is_valid() + + with pytest.raises(ValidationError) as ve: + target.clean_quota_duty_rate() + + assert "Quota duty Rate is not valid - it must have a value" in str(ve) + + def test_clean_preferential_quota_order_number_pass(self): + pref_quota = factories.PreferentialQuotaFactory() + + data = { + "preferential_quota_order_number": pref_quota.preferential_quota_order_number.pk, + } + + target = PreferentialQuotaCreateUpdateForm( + pref_quota.preferential_quota_order_number.reference_document_version, + pref_quota.preferential_quota_order_number, + instance=pref_quota, + data=data, + ) + + assert not target.is_valid() + + assert target.clean_preferential_quota_order_number() is not None + + def test_preferential_quota_order_number_fail(self): + pref_quota = factories.PreferentialQuotaFactory() + + data = { + "preferential_quota_order_number": None, + } + + target = PreferentialQuotaCreateUpdateForm( + pref_quota.preferential_quota_order_number.reference_document_version, + pref_quota.preferential_quota_order_number, + instance=pref_quota, + data=data, + ) + + assert not target.is_valid() + + with pytest.raises(ValidationError) as ve: + target.clean_preferential_quota_order_number() + + assert "Quota Order Number is not valid - it must have a value" in str(ve) + + +@pytest.mark.reference_documents +class TestPreferentialQuotaDeleteForm: + def test_init(self): + pref_quota = factories.PreferentialQuotaFactory() + + target = PreferentialQuotaDeleteForm( + instance=pref_quota, + ) + + assert target.instance == pref_quota + assert target.Meta.fields == [] + assert target.Meta.model == PreferentialQuota From f7b702bc15de376a375fb51a3f5aee650ed9f2e5 Mon Sep 17 00:00:00 2001 From: Matthew McKenzie <97194636+mattjamc@users.noreply.github.com> Date: Wed, 20 Mar 2024 17:33:41 +0000 Subject: [PATCH 110/118] Preferential quota bulk create tests --- .../tests/test_preferential_quotas_forms.py | 106 ++++++++++ .../tests/test_preferential_quotas_views.py | 189 ++++++++++++++++++ reference_documents/validators.py | 2 +- 3 files changed, 296 insertions(+), 1 deletion(-) diff --git a/reference_documents/tests/test_preferential_quotas_forms.py b/reference_documents/tests/test_preferential_quotas_forms.py index a7057c2cc..49e6de67c 100644 --- a/reference_documents/tests/test_preferential_quotas_forms.py +++ b/reference_documents/tests/test_preferential_quotas_forms.py @@ -1,3 +1,109 @@ import pytest +from reference_documents.forms.preferential_quota_forms import ( + PreferentialQuotaBulkCreate, +) +from reference_documents.tests import factories + pytestmark = pytest.mark.django_db + + +@pytest.mark.reference_documents +def test_preferential_quota_bulk_create_valid_data(): + """Test that preferential quota bulk create is valid when completed + correctly.""" + ref_doc_version = factories.ReferenceDocumentVersionFactory.create() + preferential_quota_order_number = ( + factories.PreferentialQuotaOrderNumberFactory.create( + reference_document_version=ref_doc_version, + ) + ) + data = { + "preferential_quota_order_number": preferential_quota_order_number.pk, + "commodity_codes": "1234567890\r\n2345678901", + "quota_duty_rate": "5%", + "measurement": "KG", + "start_date_0_0": "1", + "start_date_0_1": "1", + "start_date_0_2": "2023", + "end_date_0_0": "31", + "end_date_0_1": "12", + "end_date_0_2": "2023", + "volume_0": "500", + "start_date_1_0": "1", + "start_date_1_1": "1", + "start_date_1_2": "2024", + "end_date_1_0": "31", + "end_date_1_1": "12", + "end_date_1_2": "2024", + "volume_1": "400", + "start_date_2_0": "1", + "start_date_2_1": "1", + "start_date_2_2": "2025", + "end_date_2_0": "31", + "end_date_2_1": "12", + "end_date_2_2": "2025", + "volume_2": "300", + } + + form = PreferentialQuotaBulkCreate( + data=data, + reference_document_version=ref_doc_version, + ) + assert form.is_valid() + + +@pytest.mark.reference_documents +def test_preferential_quota_bulk_create_invalid_data(): + """Test that preferential quota bulk create is invalid when completed + incorrectly.""" + ref_doc_version = factories.ReferenceDocumentVersionFactory.create() + preferential_quota_order_number = ( + factories.PreferentialQuotaOrderNumberFactory.create( + reference_document_version=ref_doc_version, + ) + ) + data = { + "preferential_quota_order_number": preferential_quota_order_number, + "commodity_codes": "1234567890\r\n2345678901\r\n12345678910", + "quota_duty_rate": "", + "measurement": "", + "start_date_0_0": "1", + "start_date_0_1": "1", + "start_date_0_2": "2023", + "end_date_0_0": "31", + "end_date_0_1": "12", + "end_date_0_2": "2023", + "volume_0": "500", + "start_date_1_0": "1", + "start_date_1_1": "1", + "start_date_1_2": "2024", + "end_date_1_0": "31", + "end_date_1_1": "12", + "end_date_1_2": "2022", + "volume_1": "400", + "start_date_2_0": "", + "start_date_2_1": "1", + "start_date_2_2": "2025", + "end_date_2_0": "31", + "end_date_2_1": "12", + "end_date_2_2": "2025", + "volume_2": "300", + } + + form = PreferentialQuotaBulkCreate( + data=data, + reference_document_version=ref_doc_version, + ) + assert not form.is_valid() + assert ( + "Ensure all commodity codes are 10 digits and each on a new line" + in form.errors["commodity_codes"] + ) + assert "Duty rate is required" in form.errors["quota_duty_rate"] + assert "Measurement is required" in form.errors["measurement"] + assert ( + "The end date must be the same as or after the start date." + in form.errors["end_date_1"] + ) + assert "Enter the day, month and year" in form.errors["start_date_2"] diff --git a/reference_documents/tests/test_preferential_quotas_views.py b/reference_documents/tests/test_preferential_quotas_views.py index c29e56eb8..7ff386ce6 100644 --- a/reference_documents/tests/test_preferential_quotas_views.py +++ b/reference_documents/tests/test_preferential_quotas_views.py @@ -1,4 +1,6 @@ import pytest +from bs4 import BeautifulSoup +from django.contrib.auth.models import Permission from django.urls import reverse from reference_documents.tests import factories @@ -28,3 +30,190 @@ def test_get_with_permissions(self, superuser_client): ), ) assert response.status_code == 200 + + +@pytest.mark.reference_documents +def test_quota_bulk_create_creates_object_and_redirects(valid_user, client): + """Test that posting the bulk create from creates all preferential quotas + and redirects.""" + valid_user.user_permissions.add( + Permission.objects.get(codename="add_preferentialquota"), + ) + client.force_login(valid_user) + + ref_doc_version = factories.ReferenceDocumentVersionFactory.create() + preferential_quota_order_number = ( + factories.PreferentialQuotaOrderNumberFactory.create( + reference_document_version=ref_doc_version, + ) + ) + assert not ref_doc_version.preferential_quotas() + + data = { + "preferential_quota_order_number": preferential_quota_order_number.pk, + "commodity_codes": "1234567890\r\n2345678901", + "quota_duty_rate": "5%", + "measurement": "KG", + "start_date_0_0": "1", + "start_date_0_1": "1", + "start_date_0_2": "2023", + "end_date_0_0": "31", + "end_date_0_1": "12", + "end_date_0_2": "2023", + "volume_0": "500", + "start_date_1_0": "1", + "start_date_1_1": "1", + "start_date_1_2": "2024", + "end_date_1_0": "31", + "end_date_1_1": "12", + "end_date_1_2": "2024", + "volume_1": "400", + "start_date_2_0": "1", + "start_date_2_1": "1", + "start_date_2_2": "2025", + "end_date_2_0": "31", + "end_date_2_1": "12", + "end_date_2_2": "2025", + "volume_2": "300", + } + + create_url = reverse( + "reference_documents:preferential_quotas_bulk_create", + kwargs={"pk": ref_doc_version.pk}, + ) + response = client.get(create_url) + assert response.status_code == 200 + + response = client.post(create_url, data) + assert response.status_code == 302 + new_preferential_quotas = ref_doc_version.preferential_quotas() + assert len(new_preferential_quotas) == 6 + assert ( + response.url + == reverse( + "reference_documents:version-details", + args=[ref_doc_version.pk], + ) + + "#tariff-quotas" + ) + + +@pytest.mark.reference_documents +def test_quota_bulk_create_invalid(valid_user, client): + """Test that posting the bulk create form with invalid data fails and + reloads the form with errors.""" + valid_user.user_permissions.add( + Permission.objects.get(codename="add_preferentialquota"), + ) + client.force_login(valid_user) + + ref_doc_version = factories.ReferenceDocumentVersionFactory.create() + preferential_quota_order_number = ( + factories.PreferentialQuotaOrderNumberFactory.create( + reference_document_version=ref_doc_version, + ) + ) + assert not ref_doc_version.preferential_quotas() + + data = { + "preferential_quota_order_number": preferential_quota_order_number.pk, + "commodity_codes": "1234567890\r\n2345678901\r\n12345678910", + "quota_duty_rate": "", + "measurement": "", + "start_date_0_0": "1", + "start_date_0_1": "1", + "start_date_0_2": "2023", + "end_date_0_0": "31", + "end_date_0_1": "12", + "end_date_0_2": "2023", + "volume_0": "500", + "start_date_1_0": "1", + "start_date_1_1": "1", + "start_date_1_2": "2024", + "end_date_1_0": "31", + "end_date_1_1": "12", + "end_date_1_2": "2022", + "volume_1": "400", + "start_date_2_0": "", + "start_date_2_1": "1", + "start_date_2_2": "2025", + "end_date_2_0": "31", + "end_date_2_1": "12", + "end_date_2_2": "2025", + "volume_2": "300", + } + + create_url = reverse( + "reference_documents:preferential_quotas_bulk_create", + kwargs={"pk": ref_doc_version.pk}, + ) + + response = client.post(create_url, data) + assert response.status_code == 200 + soup = BeautifulSoup(response.content.decode(response.charset), "html.parser") + error_messages = soup.select("ul.govuk-list.govuk-error-summary__list a") + + assert "Duty rate is required" == error_messages[0].text + assert "Measurement is required" in error_messages[1].text + assert "Enter the day, month and year" in error_messages[2].text + assert ( + "Ensure all commodity codes are 10 digits and each on a new line" + in error_messages[3].text + ) + assert ( + "The end date must be the same as or after the start date" + in error_messages[4].text + ) + + new_preferential_quotas = ref_doc_version.preferential_quotas() + assert len(new_preferential_quotas) == 0 + + +@pytest.mark.reference_documents +def test_quota_bulk_create_without_permission(valid_user_client): + """Test that posting the bulk create form without relevant user permissions + does not work.""" + ref_doc_version = factories.ReferenceDocumentVersionFactory.create() + preferential_quota_order_number = ( + factories.PreferentialQuotaOrderNumberFactory.create( + reference_document_version=ref_doc_version, + ) + ) + assert not ref_doc_version.preferential_quotas() + + data = { + "preferential_quota_order_number": preferential_quota_order_number.pk, + "commodity_codes": "1234567890\r\n2345678901", + "quota_duty_rate": "5%", + "measurement": "KG", + "start_date_0_0": "1", + "start_date_0_1": "1", + "start_date_0_2": "2023", + "end_date_0_0": "31", + "end_date_0_1": "12", + "end_date_0_2": "2023", + "volume_0": "500", + "start_date_1_0": "1", + "start_date_1_1": "1", + "start_date_1_2": "2024", + "end_date_1_0": "31", + "end_date_1_1": "12", + "end_date_1_2": "2024", + "volume_1": "400", + "start_date_2_0": "1", + "start_date_2_1": "1", + "start_date_2_2": "2025", + "end_date_2_0": "31", + "end_date_2_1": "12", + "end_date_2_2": "2025", + "volume_2": "300", + } + + create_url = reverse( + "reference_documents:preferential_quotas_bulk_create", + kwargs={"pk": ref_doc_version.pk}, + ) + + response = valid_user_client.post(create_url, data) + assert response.status_code == 403 + assert not ref_doc_version.preferential_quotas() diff --git a/reference_documents/validators.py b/reference_documents/validators.py index 20882638d..25ea727b0 100644 --- a/reference_documents/validators.py +++ b/reference_documents/validators.py @@ -1,4 +1,4 @@ from django.core.validators import RegexValidator -commodity_code_validator = RegexValidator(r"\d{10}") +commodity_code_validator = RegexValidator(r"^\d{10}$") order_number_validator = RegexValidator(r"^05[0-9]{4}$") From accb89447d372bddef472ec2b8772a229555ae84 Mon Sep 17 00:00:00 2001 From: Matthew McKenzie <97194636+mattjamc@users.noreply.github.com> Date: Thu, 21 Mar 2024 16:09:25 +0000 Subject: [PATCH 111/118] Content standardisations --- .../forms/preferential_quota_forms.py | 2 +- .../forms/reference_document_forms.py | 10 ++-- .../forms/reference_document_version_forms.py | 4 +- .../alignment_reports/details.jinja | 4 +- .../reference_documents/confirm_create.jinja | 8 +-- .../reference_documents/confirm_delete.jinja | 4 +- .../reference_documents/confirm_update.jinja | 10 ++-- .../jinja2/reference_documents/delete.jinja | 6 +- .../jinja2/reference_documents/details.jinja | 2 +- .../jinja2/reference_documents/index.jinja | 6 +- .../jinja2/reference_documents/overview.jinja | 6 +- .../confirm_delete.jinja | 8 +-- .../alignment_reports.jinja | 4 +- .../confirm_create.jinja | 8 +-- .../confirm_delete.jinja | 8 +-- .../confirm_update.jinja | 10 ++-- .../reference_document_versions/delete.jinja | 8 +-- .../reference_document_versions/edit.jinja | 2 +- .../jinja2/reference_documents/update.jinja | 2 +- .../tests/test_preferential_quotas_forms.py | 6 +- .../tests/test_preferential_rates_views.py | 8 +-- .../test_reference_document_versions_forms.py | 12 ++-- .../test_reference_document_versions_views.py | 4 +- .../tests/test_reference_documents_forms.py | 4 +- .../tests/test_reference_documents_views.py | 8 +-- reference_documents/urls.py | 56 +++++++++---------- .../preferential_quota_order_number_views.py | 12 ++-- .../views/preferential_quota_views.py | 18 +++--- .../views/preferential_rate_views.py | 8 +-- .../views/reference_document_version_views.py | 8 +-- .../views/reference_document_views.py | 6 +- 31 files changed, 129 insertions(+), 133 deletions(-) diff --git a/reference_documents/forms/preferential_quota_forms.py b/reference_documents/forms/preferential_quota_forms.py index 98135f332..e489b1c33 100644 --- a/reference_documents/forms/preferential_quota_forms.py +++ b/reference_documents/forms/preferential_quota_forms.py @@ -166,7 +166,7 @@ def clean_preferential_quota_order_number(self): ) -class PreferentialQuotaBulkCreate(forms.Form): +class PreferentialQuotaBulkCreateForm(forms.Form): commodity_codes = forms.CharField( label="Commodity codes", widget=forms.Textarea, diff --git a/reference_documents/forms/reference_document_forms.py b/reference_documents/forms/reference_document_forms.py index 3f9e231b3..d0e1b7d69 100644 --- a/reference_documents/forms/reference_document_forms.py +++ b/reference_documents/forms/reference_document_forms.py @@ -13,10 +13,10 @@ class ReferenceDocumentCreateUpdateForm(forms.ModelForm): title = forms.CharField( - label="Reference Document title", + label="Reference document title", error_messages={ - "required": "A Reference Document title is required", - "unique": "A Reference Document with this title already exists", + "required": "A reference document title is required", + "unique": "A reference document with this title already exists", }, ) area_id = forms.CharField( @@ -24,7 +24,7 @@ class ReferenceDocumentCreateUpdateForm(forms.ModelForm): validators=[area_id_validator], error_messages={ "required": "An area ID is required", - "unique": "A Reference Document with this area ID already exists", + "unique": "A reference document with this area ID already exists", "invalid": "Enter the area ID in the correct format", }, ) @@ -76,7 +76,7 @@ def clean(self): ) if versions: raise forms.ValidationError( - f"Reference Document {reference_document.area_id} cannot be deleted as it has" + f"Reference document {reference_document.area_id} cannot be deleted as it has" f" active versions.", ) diff --git a/reference_documents/forms/reference_document_version_forms.py b/reference_documents/forms/reference_document_version_forms.py index 14d52eb10..8517290c4 100644 --- a/reference_documents/forms/reference_document_version_forms.py +++ b/reference_documents/forms/reference_document_version_forms.py @@ -12,7 +12,7 @@ from reference_documents.models import ReferenceDocumentVersion -class ReferenceDocumentVersionsEditCreateForm(forms.ModelForm): +class ReferenceDocumentVersionsCreateUpdateForm(forms.ModelForm): version = forms.CharField( label="Version number", error_messages={ @@ -99,7 +99,7 @@ def clean(self): ) if preferential_duty_rates or tariff_quotas: raise forms.ValidationError( - f"Reference Document version {reference_document_version.version} cannot be deleted as it has" + f"Reference document version {reference_document_version.version} cannot be deleted as it has" f" current preferential duty rates or tariff quotas", ) diff --git a/reference_documents/jinja2/reference_documents/alignment_reports/details.jinja b/reference_documents/jinja2/reference_documents/alignment_reports/details.jinja index 59de1a733..0678d4e55 100644 --- a/reference_documents/jinja2/reference_documents/alignment_reports/details.jinja +++ b/reference_documents/jinja2/reference_documents/alignment_reports/details.jinja @@ -1,11 +1,11 @@ {% extends "layouts/layout.jinja" %} {% from "components/table/macro.njk" import govukTable %} -{% set page_title = 'Reference Documents version details' %} +{% set page_title = 'Reference documents version details' %} {% block breadcrumb %} {{ breadcrumbs(request, [ - {'text': "Reference Document Version Overview"} + {'text': "Reference document Version Overview"} ]) }} {% endblock %} diff --git a/reference_documents/jinja2/reference_documents/confirm_create.jinja b/reference_documents/jinja2/reference_documents/confirm_create.jinja index 3ea829afe..c5306136c 100644 --- a/reference_documents/jinja2/reference_documents/confirm_create.jinja +++ b/reference_documents/jinja2/reference_documents/confirm_create.jinja @@ -4,13 +4,13 @@ {% from "components/button/macro.njk" import govukButton %} {% from "components/breadcrumbs/macro.njk" import govukBreadcrumbs %} -{% set page_title = "Reference Document " ~ object.area_id ~ " successfully created" %} +{% set page_title = "Reference document " ~ object.area_id ~ " successfully created" %} {% block breadcrumb %} {{ govukBreadcrumbs({ "items": [{"text": "Home", "href": url("home")}, {"text": "View reference documents", "href": url("reference_documents:index")}, - {"text": "Create a Reference Document", "href": url("reference_documents:create")}, + {"text": "Create a reference document", "href": url("reference_documents:create")}, {"text": page_title}] }) }} {% endblock %} @@ -19,7 +19,7 @@
    {{ govukPanel({ - "titleText": "Reference Document " ~ object.area_id ~ " has been created", + "titleText": "Reference document " ~ object.area_id ~ " has been created", "text": "This change has taken immediate effect", "classes": "govuk-!-margin-bottom-7" }) }} @@ -27,7 +27,7 @@
    {{ govukButton({ - "text": "View Reference Document " ~ object.area_id, + "text": "View reference document " ~ object.area_id, "href": url("reference_documents:details", kwargs={"pk":object.pk}), }) }} {{ govukButton({ diff --git a/reference_documents/jinja2/reference_documents/confirm_delete.jinja b/reference_documents/jinja2/reference_documents/confirm_delete.jinja index 29b53ec0c..caa3ce4f3 100644 --- a/reference_documents/jinja2/reference_documents/confirm_delete.jinja +++ b/reference_documents/jinja2/reference_documents/confirm_delete.jinja @@ -4,7 +4,7 @@ {% from "components/button/macro.njk" import govukButton %} {% from "components/breadcrumbs/macro.njk" import govukBreadcrumbs %} -{% set page_title = "Reference Document " ~ deleted_pk ~ " deleted" %} +{% set page_title = "Reference document " ~ deleted_pk ~ " deleted" %} {% block breadcrumb %} {{ govukBreadcrumbs({ @@ -18,7 +18,7 @@
    {{ govukPanel({ - "titleText": "Reference Document " ~ deleted_pk ~ " has been deleted", + "titleText": "Reference document " ~ deleted_pk ~ " has been deleted", "text": "This change has taken immediate effect", "classes": "govuk-!-margin-bottom-7" }) }} diff --git a/reference_documents/jinja2/reference_documents/confirm_update.jinja b/reference_documents/jinja2/reference_documents/confirm_update.jinja index 414e23298..bfe2cef44 100644 --- a/reference_documents/jinja2/reference_documents/confirm_update.jinja +++ b/reference_documents/jinja2/reference_documents/confirm_update.jinja @@ -4,13 +4,13 @@ {% from "components/button/macro.njk" import govukButton %} {% from "components/breadcrumbs/macro.njk" import govukBreadcrumbs %} -{% set page_title = "Reference Document " ~ object.area_id ~ " successfully updated" %} +{% set page_title = "Reference document " ~ object.area_id ~ " successfully updated" %} {% block breadcrumb %} {{ govukBreadcrumbs({ "items": [{"text": "Home", "href": url("home")}, {"text": "View reference documents", "href": url("reference_documents:index")}, - {"text": "Edit Reference Document " ~ object.area_id, "href": url("reference_documents:update", kwargs={"pk":object.pk})}, + {"text": "Edit reference document " ~ object.area_id, "href": url("reference_documents:edit", kwargs={"pk":object.pk})}, {"text": page_title}] }) }} {% endblock %} @@ -19,7 +19,7 @@
    {{ govukPanel({ - "titleText": "Reference Document " ~ object.area_id ~ " has been updated", + "titleText": "Reference document " ~ object.area_id ~ " has been updated", "text": "This change has taken immediate effect", "classes": "govuk-!-margin-bottom-7" }) }} @@ -27,11 +27,11 @@
    {{ govukButton({ - "text": "View your Reference Document", + "text": "View your reference document", "href": url("reference_documents:details", kwargs={"pk":object.pk}), }) }} {{ govukButton({ - "text": "Back to View Reference Documents", + "text": "Back to view reference documents", "href": url("reference_documents:index"), "classes": "govuk-button--secondary" }) }} diff --git a/reference_documents/jinja2/reference_documents/delete.jinja b/reference_documents/jinja2/reference_documents/delete.jinja index 78abd9d31..ebb6f7f37 100644 --- a/reference_documents/jinja2/reference_documents/delete.jinja +++ b/reference_documents/jinja2/reference_documents/delete.jinja @@ -5,7 +5,7 @@ {% from "components/button/macro.njk" import govukButton %} {% from "components/error-summary/macro.njk" import govukErrorSummary %} -{% set page_title = "Delete Reference Document " ~ object.area_id %} +{% set page_title = "Delete reference document " ~ object.area_id %} {% block breadcrumb %} {{ govukBreadcrumbs({ @@ -24,10 +24,10 @@
    -

    Are you sure you want to permanently delete Reference Document {{ object.area_id }}?

    +

    Are you sure you want to permanently delete reference document {{ object.area_id }}?

    {{ govukWarningText({ - "text": "Deleted Reference Documents can not be recovered.", + "text": "Deleted reference documents can not be recovered.", "iconFallbackText": "Warning" }) }} diff --git a/reference_documents/jinja2/reference_documents/details.jinja b/reference_documents/jinja2/reference_documents/details.jinja index c710e89b8..9287649a8 100644 --- a/reference_documents/jinja2/reference_documents/details.jinja +++ b/reference_documents/jinja2/reference_documents/details.jinja @@ -2,7 +2,7 @@ {% from "components/table/macro.njk" import govukTable %} {% from "components/breadcrumbs/macro.njk" import govukBreadcrumbs %} -{% set page_title = "Reference Documents for " ~ object.get_area_name_by_area_id() %} +{% set page_title = "Reference documents for " ~ object.get_area_name_by_area_id() %} {% block breadcrumb %} {{ govukBreadcrumbs({ diff --git a/reference_documents/jinja2/reference_documents/index.jinja b/reference_documents/jinja2/reference_documents/index.jinja index b334b17d5..994e93b6b 100644 --- a/reference_documents/jinja2/reference_documents/index.jinja +++ b/reference_documents/jinja2/reference_documents/index.jinja @@ -2,7 +2,7 @@ {% from "components/table/macro.njk" import govukTable %} {% from "components/breadcrumbs/macro.njk" import govukBreadcrumbs %} -{% set page_title = 'Reference Documents Index' %} +{% set page_title = 'Reference documents index' %} {% set create_url = "create" %} {% block breadcrumb %} @@ -14,12 +14,12 @@ {% block content %}

    - Reference Documents + Reference documents

    You will find a list of reference documents below that can be viewed.

    - Create a new Reference Document + Create a new reference document

    diff --git a/reference_documents/jinja2/reference_documents/overview.jinja b/reference_documents/jinja2/reference_documents/overview.jinja index fc442ceb7..141a42f82 100644 --- a/reference_documents/jinja2/reference_documents/overview.jinja +++ b/reference_documents/jinja2/reference_documents/overview.jinja @@ -1,17 +1,17 @@ {% extends "layouts/layout.jinja" %} {% from "components/table/macro.njk" import govukTable %} -{% set page_title = 'Reference Documents versions Overview' %} +{% set page_title = 'Reference documents versions overview' %} {% block breadcrumb %} {{ breadcrumbs(request, [ - {'text': "Reference Documents"} + {'text': "Reference documents"} ]) }} {% endblock %} {% block content %}

    - Reference Document Overview + Reference document overview

    You will find a list of reference document versions below that can be viewed. diff --git a/reference_documents/jinja2/reference_documents/preferential_quota_order_numbers/confirm_delete.jinja b/reference_documents/jinja2/reference_documents/preferential_quota_order_numbers/confirm_delete.jinja index 26c4bfbdf..1df30e6cd 100644 --- a/reference_documents/jinja2/reference_documents/preferential_quota_order_numbers/confirm_delete.jinja +++ b/reference_documents/jinja2/reference_documents/preferential_quota_order_numbers/confirm_delete.jinja @@ -8,15 +8,15 @@ {% set version = request.session['deleted_version']['version'] %} {% set ref_doc_pk = request.session['deleted_version']['ref_doc_pk'] %} -{% set page_title = "Reference Document " ~ area_id ~ " version " ~ version ~ " successfully deleted" %} +{% set page_title = "Reference document " ~ area_id ~ " version " ~ version ~ " successfully deleted" %} {% block breadcrumb %} {{ govukBreadcrumbs({ "items": [{"text": "Home", "href": url("home")}, {"text": "View reference documents", "href": url("reference_documents:index")}, - {"text": "Reference Document " ~ area_id, "href": url("reference_documents:details", kwargs={"pk":ref_doc_pk})}, - {"text": "Delete Reference Document " ~ area_id ~ " version " ~ version}, + {"text": "Reference document " ~ area_id, "href": url("reference_documents:details", kwargs={"pk":ref_doc_pk})}, + {"text": "Delete reference document " ~ area_id ~ " version " ~ version}, {"text": page_title}] }) }} {% endblock %} @@ -25,7 +25,7 @@
    {{ govukPanel({ - "titleText": "Reference Document " ~ request.session['deleted_version']['area_id'] ~ " version " ~ request.session['deleted_version']['version'] ~ " has been deleted", + "titleText": "Reference document " ~ request.session['deleted_version']['area_id'] ~ " version " ~ request.session['deleted_version']['version'] ~ " has been deleted", "text": "This change has taken immediate effect", "classes": "govuk-!-margin-bottom-7" }) }} diff --git a/reference_documents/jinja2/reference_documents/reference_document_versions/alignment_reports.jinja b/reference_documents/jinja2/reference_documents/reference_document_versions/alignment_reports.jinja index 948ea8916..3243186c1 100644 --- a/reference_documents/jinja2/reference_documents/reference_document_versions/alignment_reports.jinja +++ b/reference_documents/jinja2/reference_documents/reference_document_versions/alignment_reports.jinja @@ -1,11 +1,11 @@ {% extends "layouts/layout.jinja" %} {% from "components/table/macro.njk" import govukTable %} -{% set page_title = 'Reference Documents version details' %} +{% set page_title = 'Reference documents version details' %} {% block breadcrumb %} {{ breadcrumbs(request, [ - {'text': "Reference Document Version Overview"} + {'text': "Reference document version overview"} ]) }} {% endblock %} diff --git a/reference_documents/jinja2/reference_documents/reference_document_versions/confirm_create.jinja b/reference_documents/jinja2/reference_documents/reference_document_versions/confirm_create.jinja index 05b3055b9..97969ad31 100644 --- a/reference_documents/jinja2/reference_documents/reference_document_versions/confirm_create.jinja +++ b/reference_documents/jinja2/reference_documents/reference_document_versions/confirm_create.jinja @@ -4,13 +4,13 @@ {% from "components/button/macro.njk" import govukButton %} {% from "components/breadcrumbs/macro.njk" import govukBreadcrumbs %} -{% set page_title = "Reference Document " ~ object.reference_document.area_id ~ " version " ~ object.version ~ " successfully created" %} +{% set page_title = "Reference document " ~ object.reference_document.area_id ~ " version " ~ object.version ~ " successfully created" %} {% block breadcrumb %} {{ govukBreadcrumbs({ "items": [{"text": "Home", "href": url("home")}, {"text": "View reference documents", "href": url("reference_documents:index")}, - {"text": "Reference Document " ~ object.reference_document.area_id, "href": url("reference_documents:details", kwargs={"pk":object.reference_document.pk})}, + {"text": "Reference document " ~ object.reference_document.area_id, "href": url("reference_documents:details", kwargs={"pk":object.reference_document.pk})}, {"text": "Create a new version ", "href": url("reference_documents:version-create", kwargs={"pk":object.reference_document.pk})}, {"text": page_title}] }) }} @@ -20,7 +20,7 @@
    {{ govukPanel({ - "titleText": "Reference Document " ~ object.reference_document.area_id ~ " version " ~ object.version ~ " has been created", + "titleText": "Reference document " ~ object.reference_document.area_id ~ " version " ~ object.version ~ " has been created", "text": "This change has taken immediate effect", "classes": "govuk-!-margin-bottom-7" }) }} @@ -28,7 +28,7 @@
    {{ govukButton({ - "text": "View Reference Document " ~ object.reference_document.area_id ~ " version " ~ object.version, + "text": "View reference document " ~ object.reference_document.area_id ~ " version " ~ object.version, "href": url("reference_documents:version-details", kwargs={"pk":object.pk}), }) }} {{ govukButton({ diff --git a/reference_documents/jinja2/reference_documents/reference_document_versions/confirm_delete.jinja b/reference_documents/jinja2/reference_documents/reference_document_versions/confirm_delete.jinja index 26c4bfbdf..1df30e6cd 100644 --- a/reference_documents/jinja2/reference_documents/reference_document_versions/confirm_delete.jinja +++ b/reference_documents/jinja2/reference_documents/reference_document_versions/confirm_delete.jinja @@ -8,15 +8,15 @@ {% set version = request.session['deleted_version']['version'] %} {% set ref_doc_pk = request.session['deleted_version']['ref_doc_pk'] %} -{% set page_title = "Reference Document " ~ area_id ~ " version " ~ version ~ " successfully deleted" %} +{% set page_title = "Reference document " ~ area_id ~ " version " ~ version ~ " successfully deleted" %} {% block breadcrumb %} {{ govukBreadcrumbs({ "items": [{"text": "Home", "href": url("home")}, {"text": "View reference documents", "href": url("reference_documents:index")}, - {"text": "Reference Document " ~ area_id, "href": url("reference_documents:details", kwargs={"pk":ref_doc_pk})}, - {"text": "Delete Reference Document " ~ area_id ~ " version " ~ version}, + {"text": "Reference document " ~ area_id, "href": url("reference_documents:details", kwargs={"pk":ref_doc_pk})}, + {"text": "Delete reference document " ~ area_id ~ " version " ~ version}, {"text": page_title}] }) }} {% endblock %} @@ -25,7 +25,7 @@
    {{ govukPanel({ - "titleText": "Reference Document " ~ request.session['deleted_version']['area_id'] ~ " version " ~ request.session['deleted_version']['version'] ~ " has been deleted", + "titleText": "Reference document " ~ request.session['deleted_version']['area_id'] ~ " version " ~ request.session['deleted_version']['version'] ~ " has been deleted", "text": "This change has taken immediate effect", "classes": "govuk-!-margin-bottom-7" }) }} diff --git a/reference_documents/jinja2/reference_documents/reference_document_versions/confirm_update.jinja b/reference_documents/jinja2/reference_documents/reference_document_versions/confirm_update.jinja index 711e17730..681a96173 100644 --- a/reference_documents/jinja2/reference_documents/reference_document_versions/confirm_update.jinja +++ b/reference_documents/jinja2/reference_documents/reference_document_versions/confirm_update.jinja @@ -4,14 +4,14 @@ {% from "components/button/macro.njk" import govukButton %} {% from "components/breadcrumbs/macro.njk" import govukBreadcrumbs %} -{% set page_title = "Reference Document " ~ object.reference_document.area_id ~ " version " ~ object.version ~ " successfully updated" %} +{% set page_title = "Reference document " ~ object.reference_document.area_id ~ " version " ~ object.version ~ " successfully updated" %} {% block breadcrumb %} {{ govukBreadcrumbs({ "items": [{"text": "Home", "href": url("home")}, {"text": "View reference documents", "href": url("reference_documents:index")}, - {"text": "Reference Document " ~ object.reference_document.area_id, "href": url("reference_documents:details", kwargs={"pk":object.reference_document.pk})}, - {"text": "Edit Reference Document " ~ object.reference_document.area_id ~ " version " ~ object.version, "href": url("reference_documents:version-edit", kwargs={"ref_doc_pk": object.reference_document.pk, "pk":object.pk})}, + {"text": "Reference document " ~ object.reference_document.area_id, "href": url("reference_documents:details", kwargs={"pk":object.reference_document.pk})}, + {"text": "Edit reference document " ~ object.reference_document.area_id ~ " version " ~ object.version, "href": url("reference_documents:version-edit", kwargs={"ref_doc_pk": object.reference_document.pk, "pk":object.pk})}, {"text": page_title}] }) }} {% endblock %} @@ -20,7 +20,7 @@
    {{ govukPanel({ - "titleText": "Reference Document " ~ object.reference_document.area_id ~ " version " ~ object.version ~ " has been updated", + "titleText": "Reference document " ~ object.reference_document.area_id ~ " version " ~ object.version ~ " has been updated", "text": "This change has taken immediate effect", "classes": "govuk-!-margin-bottom-7" }) }} @@ -28,7 +28,7 @@
    {{ govukButton({ - "text": "View Reference Document " ~ object.reference_document.area_id ~ " version " ~ object.version, + "text": "View reference document " ~ object.reference_document.area_id ~ " version " ~ object.version, "href": url("reference_documents:version-details", kwargs={"pk":object.pk}), }) }} {{ govukButton({ diff --git a/reference_documents/jinja2/reference_documents/reference_document_versions/delete.jinja b/reference_documents/jinja2/reference_documents/reference_document_versions/delete.jinja index e2c1f83f7..298942d2d 100644 --- a/reference_documents/jinja2/reference_documents/reference_document_versions/delete.jinja +++ b/reference_documents/jinja2/reference_documents/reference_document_versions/delete.jinja @@ -5,13 +5,13 @@ {% from "components/button/macro.njk" import govukButton %} {% from "components/error-summary/macro.njk" import govukErrorSummary %} -{% set page_title = "Delete Reference Document " ~ object.reference_document.area_id ~ " version " ~ object.version %} +{% set page_title = "Delete reference document " ~ object.reference_document.area_id ~ " version " ~ object.version %} {% block breadcrumb %} {{ govukBreadcrumbs({ "items": [{"text": "Home", "href": url("home")}, {"text": "View reference documents", "href": url("reference_documents:index")}, - {"text": "Reference Document " ~ object.reference_document.area_id, "href": url("reference_documents:details", kwargs={"pk":object.reference_document.pk})}, + {"text": "Reference document " ~ object.reference_document.area_id, "href": url("reference_documents:details", kwargs={"pk":object.reference_document.pk})}, {"text": page_title}] }) }} {% endblock %} @@ -25,10 +25,10 @@
    -

    Are you sure you want to permanently delete Reference Document version {{ object.reference_document.area_id }} version {{ object.version }}?

    +

    Are you sure you want to permanently delete reference document version {{ object.reference_document.area_id }} version {{ object.version }}?

    {{ govukWarningText({ - "text": "Deleted Reference Document versions can not be recovered.", + "text": "Deleted reference document versions can not be recovered.", "iconFallbackText": "Warning" }) }} diff --git a/reference_documents/jinja2/reference_documents/reference_document_versions/edit.jinja b/reference_documents/jinja2/reference_documents/reference_document_versions/edit.jinja index f85a6ecb9..747b29495 100644 --- a/reference_documents/jinja2/reference_documents/reference_document_versions/edit.jinja +++ b/reference_documents/jinja2/reference_documents/reference_document_versions/edit.jinja @@ -1,7 +1,7 @@ {% extends "layouts/form.jinja" %} {% from "components/breadcrumbs/macro.njk" import govukBreadcrumbs %} -{% set page_title = "Edit Reference Document " ~ object.reference_document.area_id ~ " version " ~ object.version %} +{% set page_title = "Edit reference document " ~ object.reference_document.area_id ~ " version " ~ object.version %} {% block breadcrumb %} {{ govukBreadcrumbs({ diff --git a/reference_documents/jinja2/reference_documents/update.jinja b/reference_documents/jinja2/reference_documents/update.jinja index 1c50f421b..121b96e22 100644 --- a/reference_documents/jinja2/reference_documents/update.jinja +++ b/reference_documents/jinja2/reference_documents/update.jinja @@ -1,7 +1,7 @@ {% extends "layouts/form.jinja" %} {% from "components/breadcrumbs/macro.njk" import govukBreadcrumbs %} -{% set page_title = "Edit Reference Document " ~ object.area_id ~ " details" %} +{% set page_title = "Edit reference document " ~ object.area_id ~ " details" %} {% block breadcrumb %} {{ govukBreadcrumbs({ diff --git a/reference_documents/tests/test_preferential_quotas_forms.py b/reference_documents/tests/test_preferential_quotas_forms.py index 407448384..ce3951b87 100644 --- a/reference_documents/tests/test_preferential_quotas_forms.py +++ b/reference_documents/tests/test_preferential_quotas_forms.py @@ -2,7 +2,7 @@ from django.core.exceptions import ValidationError from reference_documents.forms.preferential_quota_forms import ( - PreferentialQuotaBulkCreate, + PreferentialQuotaBulkCreateForm, ) from reference_documents.forms.preferential_quota_forms import ( PreferentialQuotaCreateUpdateForm, @@ -175,7 +175,7 @@ def test_preferential_quota_bulk_create_valid_data(): "volume_2": "300", } - form = PreferentialQuotaBulkCreate( + form = PreferentialQuotaBulkCreateForm( data=data, reference_document_version=ref_doc_version, ) @@ -220,7 +220,7 @@ def test_preferential_quota_bulk_create_invalid_data(): "volume_2": "300", } - form = PreferentialQuotaBulkCreate( + form = PreferentialQuotaBulkCreateForm( data=data, reference_document_version=ref_doc_version, ) diff --git a/reference_documents/tests/test_preferential_rates_views.py b/reference_documents/tests/test_preferential_rates_views.py index 333e0190a..a3cad0b61 100644 --- a/reference_documents/tests/test_preferential_rates_views.py +++ b/reference_documents/tests/test_preferential_rates_views.py @@ -6,7 +6,7 @@ PreferentialRateCreateUpdateForm, ) from reference_documents.tests import factories -from reference_documents.views.preferential_rate_views import PreferentialRateEditView +from reference_documents.views.preferential_rate_views import PreferentialRateEdit pytestmark = pytest.mark.django_db @@ -55,7 +55,7 @@ def test_get( def test_success_url(self): pref_rate = factories.PreferentialRateFactory.create() - target = PreferentialRateEditView() + target = PreferentialRateEdit() target.object = pref_rate assert target.get_success_url() == reverse( "reference_documents:version-details", @@ -64,7 +64,7 @@ def test_success_url(self): def test_form_valid(self): pref_rate = factories.PreferentialRateFactory.create() - target = PreferentialRateEditView() + target = PreferentialRateEdit() target.object = pref_rate form = PreferentialRateCreateUpdateForm( @@ -83,7 +83,7 @@ def test_form_valid(self): def test_form_invalid(self): pref_rate = factories.PreferentialRateFactory.create() - target = PreferentialRateEditView() + target = PreferentialRateEdit() target.object = pref_rate form = PreferentialRateCreateUpdateForm( diff --git a/reference_documents/tests/test_reference_document_versions_forms.py b/reference_documents/tests/test_reference_document_versions_forms.py index 80235af0c..c8840efb4 100644 --- a/reference_documents/tests/test_reference_document_versions_forms.py +++ b/reference_documents/tests/test_reference_document_versions_forms.py @@ -4,7 +4,7 @@ ReferenceDocumentVersionDeleteForm, ) from reference_documents.forms.reference_document_version_forms import ( - ReferenceDocumentVersionsEditCreateForm, + ReferenceDocumentVersionsCreateUpdateForm, ) from reference_documents.tests import factories @@ -27,7 +27,7 @@ def test_ref_doc_version_create_update_valid_data(): "entry_into_force_date_2": "2024", } - form = ReferenceDocumentVersionsEditCreateForm(data=data) + form = ReferenceDocumentVersionsCreateUpdateForm(data=data) assert form.is_valid() @@ -35,7 +35,7 @@ def test_ref_doc_version_create_update_valid_data(): def test_ref_doc_version_create_update_invalid_data(): """Test that ReferenceDocumentVersionCreateEditForm is invalid when not complete correctly.""" - form = ReferenceDocumentVersionsEditCreateForm(data={}) + form = ReferenceDocumentVersionsCreateUpdateForm(data={}) assert not form.is_valid() assert "A version number is required" in form.errors["version"] assert "A published date is required" in form.errors["published_date"] @@ -59,7 +59,7 @@ def test_ref_doc_version_create_update_invalid_data(): "entry_into_force_date_1": "1", "entry_into_force_date_2": "2024", } - form = ReferenceDocumentVersionsEditCreateForm(data=data) + form = ReferenceDocumentVersionsCreateUpdateForm(data=data) assert not form.is_valid() assert ( "New versions of this reference document must be a higher number than previous versions" @@ -72,7 +72,7 @@ def test_ref_doc_version_delete_valid(): """Test that ReferenceDocumentVersionDeleteForm is valid for a reference document with no versions.""" version = factories.ReferenceDocumentVersionFactory.create() - form = ReferenceDocumentVersionsEditCreateForm(data={}, instance=version) + form = ReferenceDocumentVersionsCreateUpdateForm(data={}, instance=version) assert not form.is_valid() @@ -85,6 +85,6 @@ def test_ref_doc_version_delete_invalid(): form = ReferenceDocumentVersionDeleteForm(data={}, instance=version) assert not form.is_valid() assert ( - f"Reference Document version {version.version} cannot be deleted as it has current preferential duty rates or tariff quotas" + f"Reference document version {version.version} cannot be deleted as it has current preferential duty rates or tariff quotas" in form.errors["__all__"] ) diff --git a/reference_documents/tests/test_reference_document_versions_views.py b/reference_documents/tests/test_reference_document_versions_views.py index ddb5f652d..4c6d9ebd6 100644 --- a/reference_documents/tests/test_reference_document_versions_views.py +++ b/reference_documents/tests/test_reference_document_versions_views.py @@ -113,7 +113,7 @@ def test_successfully_delete_ref_doc_version(valid_user, client): page = BeautifulSoup(resp.content, "html.parser") assert resp.status_code == 200 assert ( - f"Delete Reference Document {area_id} version {ref_doc_version.version}" + f"Delete reference document {area_id} version {ref_doc_version.version}" in page.select("main h1")[0].text ) resp = client.post(delete_url) @@ -148,7 +148,7 @@ def test_delete_ref_doc_version_invalid(valid_user, client): resp = client.post(delete_url) assert resp.status_code == 200 assert ( - f"Reference Document version {ref_doc_version.version} cannot be deleted as it has current preferential duty rates or tariff quotas" + f"Reference document version {ref_doc_version.version} cannot be deleted as it has current preferential duty rates or tariff quotas" in str(resp.content) ) assert ReferenceDocument.objects.filter(pk=ref_doc.pk) diff --git a/reference_documents/tests/test_reference_documents_forms.py b/reference_documents/tests/test_reference_documents_forms.py index b06b876e6..a7800bc04 100644 --- a/reference_documents/tests/test_reference_documents_forms.py +++ b/reference_documents/tests/test_reference_documents_forms.py @@ -27,7 +27,7 @@ def test_ref_doc_create_update_form_invalid_data(): correctly.""" form = ReferenceDocumentCreateUpdateForm(data={}) assert not form.is_valid() - assert "A Reference Document title is required" in form.errors["title"] + assert "A reference document title is required" in form.errors["title"] assert "An area ID is required" in form.errors["area_id"] factories.ReferenceDocumentFactory.create( @@ -38,7 +38,7 @@ def test_ref_doc_create_update_form_invalid_data(): form = ReferenceDocumentCreateUpdateForm(data=data) assert not form.is_valid() assert "Enter the area ID in the correct format" in form.errors["area_id"] - assert "A Reference Document with this title already exists" in form.errors["title"] + assert "A reference document with this title already exists" in form.errors["title"] @pytest.mark.reference_documents diff --git a/reference_documents/tests/test_reference_documents_views.py b/reference_documents/tests/test_reference_documents_views.py index 536b7dc54..c9706aaa7 100644 --- a/reference_documents/tests/test_reference_documents_views.py +++ b/reference_documents/tests/test_reference_documents_views.py @@ -46,7 +46,7 @@ def test_ref_doc_edit_updates_ref_doc_object(valid_user, client): ref_doc = factories.ReferenceDocumentFactory.create() edit_url = reverse( - "reference_documents:update", + "reference_documents:edit", kwargs={"pk": ref_doc.pk}, ) @@ -95,7 +95,7 @@ def test_successfully_delete_ref_doc(valid_user, client): page = BeautifulSoup(response.content, "html.parser") assert response.status_code == 200 assert ( - f"Delete Reference Document {ref_doc.area_id}" in page.select("main h1")[0].text + f"Delete reference document {ref_doc.area_id}" in page.select("main h1")[0].text ) response = client.post(delete_url) @@ -129,7 +129,7 @@ def test_delete_ref_doc_with_versions(valid_user, client): response = client.post(delete_url) assert response.status_code == 200 assert ( - f"Reference Document {ref_doc.area_id} cannot be deleted as it has active versions." + f"Reference document {ref_doc.area_id} cannot be deleted as it has active versions." in str(response.content) ) assert ReferenceDocument.objects.filter(pk=ref_doc.pk) @@ -140,7 +140,7 @@ def test_ref_doc_crud_without_permission(valid_user_client): # TODO: potentially update this if the permissions for reference doc behaviour changes ref_doc = factories.ReferenceDocumentFactory.create() create_url = reverse("reference_documents:create") - edit_url = reverse("reference_documents:update", kwargs={"pk": ref_doc.pk}) + edit_url = reverse("reference_documents:edit", kwargs={"pk": ref_doc.pk}) delete_url = reverse("reference_documents:delete", kwargs={"pk": ref_doc.pk}) form_data = { "title": "Reference document for XY", diff --git a/reference_documents/urls.py b/reference_documents/urls.py index 194109700..157d89fb7 100644 --- a/reference_documents/urls.py +++ b/reference_documents/urls.py @@ -8,27 +8,23 @@ from reference_documents.views.example_views import ExampleReferenceDocumentsDetailView from reference_documents.views.example_views import ExampleReferenceDocumentsListView from reference_documents.views.preferential_quota_order_number_views import ( - PreferentialQuotaOrderNumberCreateView, + PreferentialQuotaOrderNumberCreate, ) from reference_documents.views.preferential_quota_order_number_views import ( - PreferentialQuotaOrderNumberDeleteView, + PreferentialQuotaOrderNumberDelete, ) from reference_documents.views.preferential_quota_order_number_views import ( - PreferentialQuotaOrderNumberEditView, + PreferentialQuotaOrderNumberEdit, ) from reference_documents.views.preferential_quota_views import ( - PreferentialQuotaBulkCreateView, -) -from reference_documents.views.preferential_quota_views import ( - PreferentialQuotaCreateView, -) -from reference_documents.views.preferential_quota_views import ( - PreferentialQuotaDeleteView, -) -from reference_documents.views.preferential_quota_views import PreferentialQuotaEditView -from reference_documents.views.preferential_rate_views import PreferentialRateCreateView -from reference_documents.views.preferential_rate_views import PreferentialRateDeleteView -from reference_documents.views.preferential_rate_views import PreferentialRateEditView + PreferentialQuotaBulkCreate, +) +from reference_documents.views.preferential_quota_views import PreferentialQuotaCreate +from reference_documents.views.preferential_quota_views import PreferentialQuotaDelete +from reference_documents.views.preferential_quota_views import PreferentialQuotaEdit +from reference_documents.views.preferential_rate_views import PreferentialRateCreate +from reference_documents.views.preferential_rate_views import PreferentialRateDelete +from reference_documents.views.preferential_rate_views import PreferentialRateEdit from reference_documents.views.reference_document_version_views import ( ReferenceDocumentVersionConfirmCreate, ) @@ -62,8 +58,8 @@ from reference_documents.views.reference_document_views import ReferenceDocumentCreate from reference_documents.views.reference_document_views import ReferenceDocumentDelete from reference_documents.views.reference_document_views import ReferenceDocumentDetails +from reference_documents.views.reference_document_views import ReferenceDocumentEdit from reference_documents.views.reference_document_views import ReferenceDocumentList -from reference_documents.views.reference_document_views import ReferenceDocumentUpdate app_name = "reference_documents" @@ -99,9 +95,9 @@ name="details", ), path( - "reference_documents//update/", - ReferenceDocumentUpdate.as_view(), - name="update", + "reference_documents//edit/", + ReferenceDocumentEdit.as_view(), + name="edit", ), path( f"/confirm-create/", @@ -173,59 +169,59 @@ # Preferential Quotas path( "preferential_quotas/delete///", - PreferentialQuotaDeleteView.as_view(), + PreferentialQuotaDelete.as_view(), name="preferential_quotas_delete", ), path( "preferential_quotas/edit//", - PreferentialQuotaEditView.as_view(), + PreferentialQuotaEdit.as_view(), name="preferential_quotas_edit", ), path( "preferential_quota_order_numbers//create_preferential_quotas/", - PreferentialQuotaCreateView.as_view(), + PreferentialQuotaCreate.as_view(), name="preferential_quotas_create", ), path( "preferential_quota_order_numbers//create_preferential_quotas_for_order//", - PreferentialQuotaCreateView.as_view(), + PreferentialQuotaCreate.as_view(), name="preferential_quotas_create_for_order", ), path( "reference_document_versions//bulk_create_preferential_quotas/", - PreferentialQuotaBulkCreateView.as_view(), + PreferentialQuotaBulkCreate.as_view(), name="preferential_quotas_bulk_create", ), # Preferential Rates path( "preferential_rates/delete//", - PreferentialRateDeleteView.as_view(), + PreferentialRateDelete.as_view(), name="preferential_rates_delete", ), path( "preferential_rates/edit//", - PreferentialRateEditView.as_view(), + PreferentialRateEdit.as_view(), name="preferential_rates_edit", ), path( "reference_document_versions//create_preferential_rates/", - PreferentialRateCreateView.as_view(), + PreferentialRateCreate.as_view(), name="preferential_rates_create", ), # Preferential rate Quota order number path( "preferential_quota_order_numbers/delete///", - PreferentialQuotaOrderNumberDeleteView.as_view(), + PreferentialQuotaOrderNumberDelete.as_view(), name="preferential_quota_order_number_delete", ), path( "preferential_quota_order_numbers/edit//", - PreferentialQuotaOrderNumberEditView.as_view(), + PreferentialQuotaOrderNumberEdit.as_view(), name="preferential_quota_order_number_edit", ), path( "reference_document_versions//create_preferential_quota_order_number/", - PreferentialQuotaOrderNumberCreateView.as_view(), + PreferentialQuotaOrderNumberCreate.as_view(), name="preferential_quota_order_number_create", ), ] diff --git a/reference_documents/views/preferential_quota_order_number_views.py b/reference_documents/views/preferential_quota_order_number_views.py index a01381c42..d24f50bf7 100644 --- a/reference_documents/views/preferential_quota_order_number_views.py +++ b/reference_documents/views/preferential_quota_order_number_views.py @@ -16,14 +16,14 @@ from reference_documents.models import ReferenceDocumentVersion -class PreferentialQuotaOrderNumberEditView(PermissionRequiredMixin, UpdateView): +class PreferentialQuotaOrderNumberEdit(PermissionRequiredMixin, UpdateView): template_name = "reference_documents/preferential_quota_order_numbers/edit.jinja" permission_required = "reference_documents.edit_reference_document" model = PreferentialQuotaOrderNumber form_class = PreferentialQuotaOrderNumberCreateUpdateForm def get_form_kwargs(self): - kwargs = super(PreferentialQuotaOrderNumberEditView, self).get_form_kwargs() + kwargs = super(PreferentialQuotaOrderNumberEdit, self).get_form_kwargs() kwargs["reference_document_version"] = PreferentialQuotaOrderNumber.objects.get( id=self.kwargs["pk"], ).reference_document_version @@ -39,14 +39,14 @@ def get_success_url(self): ) -class PreferentialQuotaOrderNumberCreateView(PermissionRequiredMixin, CreateView): +class PreferentialQuotaOrderNumberCreate(PermissionRequiredMixin, CreateView): template_name = "reference_documents/preferential_quota_order_numbers/edit.jinja" permission_required = "reference_documents.edit_reference_document" model = PreferentialQuotaOrderNumber form_class = PreferentialQuotaOrderNumberCreateUpdateForm def get_form_kwargs(self): - kwargs = super(PreferentialQuotaOrderNumberCreateView, self).get_form_kwargs() + kwargs = super(PreferentialQuotaOrderNumberCreate, self).get_form_kwargs() kwargs["reference_document_version"] = ReferenceDocumentVersion.objects.get( id=self.kwargs["pk"], ) @@ -61,7 +61,7 @@ def form_valid(self, form): instance.order = len(reference_document_version.preferential_rates.all()) + 1 instance.reference_document_version = reference_document_version self.object = instance - return super(PreferentialQuotaOrderNumberCreateView, self).form_valid(form) + return super(PreferentialQuotaOrderNumberCreate, self).form_valid(form) def get_success_url(self): return ( @@ -73,7 +73,7 @@ def get_success_url(self): ) -class PreferentialQuotaOrderNumberDeleteView( +class PreferentialQuotaOrderNumberDelete( PermissionRequiredMixin, FormMixin, DeleteView, diff --git a/reference_documents/views/preferential_quota_views.py b/reference_documents/views/preferential_quota_views.py index 5cb08cbdd..5abde88ce 100644 --- a/reference_documents/views/preferential_quota_views.py +++ b/reference_documents/views/preferential_quota_views.py @@ -8,7 +8,7 @@ from django.views.generic.edit import FormMixin from reference_documents.forms.preferential_quota_forms import ( - PreferentialQuotaBulkCreate, + PreferentialQuotaBulkCreateForm, ) from reference_documents.forms.preferential_quota_forms import ( PreferentialQuotaCreateUpdateForm, @@ -20,14 +20,14 @@ from reference_documents.models import ReferenceDocumentVersion -class PreferentialQuotaEditView(PermissionRequiredMixin, UpdateView): +class PreferentialQuotaEdit(PermissionRequiredMixin, UpdateView): template_name = "reference_documents/preferential_quotas/edit.jinja" permission_required = "reference_documents.edit_reference_document" model = PreferentialQuota form_class = PreferentialQuotaCreateUpdateForm def get_form_kwargs(self): - kwargs = super(PreferentialQuotaEditView, self).get_form_kwargs() + kwargs = super(PreferentialQuotaEdit, self).get_form_kwargs() kwargs["reference_document_version"] = PreferentialQuota.objects.get( id=self.kwargs["pk"], ).preferential_quota_order_number.reference_document_version @@ -50,14 +50,14 @@ def post(self, request, *args, **kwargs): ) -class PreferentialQuotaCreateView(PermissionRequiredMixin, CreateView): +class PreferentialQuotaCreate(PermissionRequiredMixin, CreateView): template_name = "reference_documents/preferential_quotas/create.jinja" permission_required = "reference_documents.edit_reference_document" model = PreferentialQuota form_class = PreferentialQuotaCreateUpdateForm def get_form_kwargs(self): - kwargs = super(PreferentialQuotaCreateView, self).get_form_kwargs() + kwargs = super(PreferentialQuotaCreate, self).get_form_kwargs() kwargs["reference_document_version"] = ReferenceDocumentVersion.objects.get( id=self.kwargs["version_pk"], ) @@ -76,7 +76,7 @@ def get_form_kwargs(self): def form_valid(self, form): form.instance.order = 1 form.save() - return super(PreferentialQuotaCreateView, self).form_valid(form) + return super(PreferentialQuotaCreate, self).form_valid(form) def get_success_url(self): return ( @@ -90,10 +90,10 @@ def get_success_url(self): ) -class PreferentialQuotaBulkCreateView(PermissionRequiredMixin, FormView): +class PreferentialQuotaBulkCreate(PermissionRequiredMixin, FormView): template_name = "reference_documents/preferential_quotas/bulk_create.jinja" permission_required = "reference_documents.add_preferentialquota" - form_class = PreferentialQuotaBulkCreate + form_class = PreferentialQuotaBulkCreateForm queryset = ReferenceDocumentVersion.objects.all() def get_reference_document_version(self): @@ -142,7 +142,7 @@ def get_success_url(self): ) -class PreferentialQuotaDeleteView(PermissionRequiredMixin, FormMixin, DeleteView): +class PreferentialQuotaDelete(PermissionRequiredMixin, FormMixin, DeleteView): form_class = PreferentialQuotaDeleteForm template_name = "reference_documents/preferential_quotas/delete.jinja" permission_required = "reference_documents.edit_reference_document" diff --git a/reference_documents/views/preferential_rate_views.py b/reference_documents/views/preferential_rate_views.py index b083756c6..05afbb1fb 100644 --- a/reference_documents/views/preferential_rate_views.py +++ b/reference_documents/views/preferential_rate_views.py @@ -13,7 +13,7 @@ from reference_documents.models import ReferenceDocumentVersion -class PreferentialRateEditView(PermissionRequiredMixin, UpdateView): +class PreferentialRateEdit(PermissionRequiredMixin, UpdateView): form_class = PreferentialRateCreateUpdateForm permission_required = "reference_documents.change_preferentialrate" model = PreferentialRate @@ -26,7 +26,7 @@ def get_success_url(self): ) -class PreferentialRateCreateView(PermissionRequiredMixin, CreateView): +class PreferentialRateCreate(PermissionRequiredMixin, CreateView): form_class = PreferentialRateCreateUpdateForm permission_required = "reference_documents.add_preferentialrate" model = PreferentialRate @@ -46,10 +46,10 @@ def form_valid(self, form): instance.order = len(reference_document_version.preferential_rates.all()) + 1 instance.reference_document_version = reference_document_version form.save() - return super(PreferentialRateCreateView, self).form_valid(form) + return super(PreferentialRateCreate, self).form_valid(form) -class PreferentialRateDeleteView(PermissionRequiredMixin, FormMixin, DeleteView): +class PreferentialRateDelete(PermissionRequiredMixin, FormMixin, DeleteView): template_name = "reference_documents/preferential_rates/delete.jinja" permission_required = "reference_documents.delete_preferentialrate" model = PreferentialRate diff --git a/reference_documents/views/reference_document_version_views.py b/reference_documents/views/reference_document_version_views.py index f16d3772c..b77c71304 100644 --- a/reference_documents/views/reference_document_version_views.py +++ b/reference_documents/views/reference_document_version_views.py @@ -14,7 +14,7 @@ ReferenceDocumentVersionDeleteForm, ) from reference_documents.forms.reference_document_version_forms import ( - ReferenceDocumentVersionsEditCreateForm, + ReferenceDocumentVersionsCreateUpdateForm, ) from reference_documents.models import AlignmentReportCheckStatus from reference_documents.models import PreferentialQuotaOrderNumber @@ -284,7 +284,7 @@ def get_context_data(self, *args, **kwargs): # title context[ "ref_doc_title" - ] = f"Reference Document for {context['object'].reference_document.get_area_name_by_area_id()}" + ] = f"Reference document for {context['object'].reference_document.get_area_name_by_area_id()}" context_data = ReferenceDocumentVersionContext(context["object"]) context[ @@ -304,7 +304,7 @@ def get_context_data(self, *args, **kwargs): class ReferenceDocumentVersionCreate(PermissionRequiredMixin, CreateView): template_name = "reference_documents/reference_document_versions/create.jinja" permission_required = "reference_documents.add_referencedocumentversion" - form_class = ReferenceDocumentVersionsEditCreateForm + form_class = ReferenceDocumentVersionsCreateUpdateForm def get_initial(self): initial = super().get_initial() @@ -331,7 +331,7 @@ class ReferenceDocumentVersionEdit(PermissionRequiredMixin, UpdateView): model = ReferenceDocumentVersion permission_required = "reference_documents.change_referencedocumentversion" template_name = "reference_documents/reference_document_versions/edit.jinja" - form_class = ReferenceDocumentVersionsEditCreateForm + form_class = ReferenceDocumentVersionsCreateUpdateForm def get_success_url(self): return reverse( diff --git a/reference_documents/views/reference_document_views.py b/reference_documents/views/reference_document_views.py index e9523af9c..44c419399 100644 --- a/reference_documents/views/reference_document_views.py +++ b/reference_documents/views/reference_document_views.py @@ -42,7 +42,7 @@ def get_context_data(self, **kwargs): {"text": 0}, { "html": f'Details
    ' - f"Edit
    " + f"Edit
    " f"Delete", }, ], @@ -63,7 +63,7 @@ def get_context_data(self, **kwargs): }, { "html": f'Details
    ' - f"Edit
    " + f"Edit
    " f"Delete", }, ], @@ -145,7 +145,7 @@ def get_success_url(self): ) -class ReferenceDocumentUpdate(PermissionRequiredMixin, UpdateView): +class ReferenceDocumentEdit(PermissionRequiredMixin, UpdateView): model = models.ReferenceDocument permission_required = "reference_documents.change_referencedocument" template_name = "reference_documents/update.jinja" From cf9d80cdec67f81c265eb7ea1465b551317d3e15 Mon Sep 17 00:00:00 2001 From: Matthew McKenzie <97194636+mattjamc@users.noreply.github.com> Date: Thu, 21 Mar 2024 16:28:36 +0000 Subject: [PATCH 112/118] Add preferential quota bulk create for specific order numbers --- .../forms/preferential_quota_forms.py | 14 +++++++++++++- .../jinja2/includes/tabs/preferential_quotas.jinja | 4 ++-- reference_documents/urls.py | 5 +++++ .../views/preferential_quota_views.py | 8 ++++++++ 4 files changed, 28 insertions(+), 3 deletions(-) diff --git a/reference_documents/forms/preferential_quota_forms.py b/reference_documents/forms/preferential_quota_forms.py index e489b1c33..868bf2c1e 100644 --- a/reference_documents/forms/preferential_quota_forms.py +++ b/reference_documents/forms/preferential_quota_forms.py @@ -216,9 +216,21 @@ def get_variant_index(self, post_data): result.sort() return result - def __init__(self, reference_document_version, *args, **kwargs): + def __init__( + self, + reference_document_version, + preferential_quota_order_number=None, + *args, + **kwargs, + ): super().__init__(*args, **kwargs) self.variant_indices = self.get_variant_index(kwargs) + # Populate order number box if already specified by the URL + if preferential_quota_order_number: + self.initial[ + "preferential_quota_order_number" + ] = preferential_quota_order_number + # Add fields for the first mandatory date and volume fieldset self.fields["start_date_0"] = DateInputFieldFixed( label="Start date", required=True, diff --git a/reference_documents/jinja2/includes/tabs/preferential_quotas.jinja b/reference_documents/jinja2/includes/tabs/preferential_quotas.jinja index 54f53ac72..3e160f206 100644 --- a/reference_documents/jinja2/includes/tabs/preferential_quotas.jinja +++ b/reference_documents/jinja2/includes/tabs/preferential_quotas.jinja @@ -30,8 +30,8 @@ {% if value['data_rows'] != [] %} {{ govukTable({ diff --git a/reference_documents/urls.py b/reference_documents/urls.py index 157d89fb7..99c32397c 100644 --- a/reference_documents/urls.py +++ b/reference_documents/urls.py @@ -192,6 +192,11 @@ PreferentialQuotaBulkCreate.as_view(), name="preferential_quotas_bulk_create", ), + path( + "reference_document_versions//bulk_create_preferential_quotas//", + PreferentialQuotaBulkCreate.as_view(), + name="preferential_quotas_bulk_create_for_order", + ), # Preferential Rates path( "preferential_rates/delete//", diff --git a/reference_documents/views/preferential_quota_views.py b/reference_documents/views/preferential_quota_views.py index 5abde88ce..0a9441b8e 100644 --- a/reference_documents/views/preferential_quota_views.py +++ b/reference_documents/views/preferential_quota_views.py @@ -104,6 +104,14 @@ def get_reference_document_version(self): def get_form_kwargs(self): kwargs = super().get_form_kwargs() kwargs["reference_document_version"] = self.get_reference_document_version() + if "order_pk" in self.kwargs.keys(): + kwargs["preferential_quota_order_number"] = kwargs[ + "reference_document_version" + ].preferential_quota_order_numbers.get( + id=self.kwargs["order_pk"], + ) + else: + kwargs["preferential_quota_order_number"] = None return kwargs def get_context_data(self, **kwargs): From 416f7b1a7ee505dcfe13aa2c5404b573eec6836c Mon Sep 17 00:00:00 2001 From: Doug Mills Date: Mon, 25 Mar 2024 12:33:03 +0000 Subject: [PATCH 113/118] models 100% tested, forms 100% tested --- .../preferential_quota_order_number_forms.py | 7 +- reference_documents/tests.py | 1 - ...t_preferential_quota_order_number_forms.py | 4 +- ..._preferential_quota_order_number_models.py | 25 +- ...t_preferential_quota_order_number_views.py | 303 +++++++++++++++++- .../tests/test_preferential_quotas_models.py | 20 +- .../tests/test_preferential_quotas_views.py | 3 +- .../tests/test_preferential_rates_forms.py | 15 +- .../tests/test_preferential_rates_models.py | 16 +- ...test_reference_document_versions_models.py | 24 +- .../tests/test_reference_documents_models.py | 27 +- .../views/reference_document_views.py | 54 ++-- 12 files changed, 410 insertions(+), 89 deletions(-) delete mode 100644 reference_documents/tests.py diff --git a/reference_documents/forms/preferential_quota_order_number_forms.py b/reference_documents/forms/preferential_quota_order_number_forms.py index 63b12549c..7d16fa971 100644 --- a/reference_documents/forms/preferential_quota_order_number_forms.py +++ b/reference_documents/forms/preferential_quota_order_number_forms.py @@ -1,3 +1,6 @@ +from decimal import Decimal +from decimal import InvalidOperation + from crispy_forms_gds.helper import FormHelper from crispy_forms_gds.layout import Field from crispy_forms_gds.layout import Layout @@ -56,9 +59,9 @@ def clean_coefficient(self): return None try: - coefficient = float(coefficient) + coefficient = Decimal(coefficient) return coefficient - except ValueError: + except InvalidOperation: raise ValidationError( "Coefficient not a valid number", ) diff --git a/reference_documents/tests.py b/reference_documents/tests.py deleted file mode 100644 index a39b155ac..000000000 --- a/reference_documents/tests.py +++ /dev/null @@ -1 +0,0 @@ -# Create your tests here. diff --git a/reference_documents/tests/test_preferential_quota_order_number_forms.py b/reference_documents/tests/test_preferential_quota_order_number_forms.py index d32aa2677..44181c3aa 100644 --- a/reference_documents/tests/test_preferential_quota_order_number_forms.py +++ b/reference_documents/tests/test_preferential_quota_order_number_forms.py @@ -1,3 +1,5 @@ +from decimal import Decimal + import pytest from django.core.exceptions import ValidationError @@ -50,7 +52,7 @@ def test_clean_coefficient_pass_valid(self): ) assert not target.is_valid() - assert target.instance.coefficient == 1.6 + assert target.instance.coefficient == Decimal("1.6") def test_clean_coefficient_fail_invalid(self): pref_quota_order_number = factories.PreferentialQuotaOrderNumberFactory() diff --git a/reference_documents/tests/test_preferential_quota_order_number_models.py b/reference_documents/tests/test_preferential_quota_order_number_models.py index 448a31eee..f1f7bb5a8 100644 --- a/reference_documents/tests/test_preferential_quota_order_number_models.py +++ b/reference_documents/tests/test_preferential_quota_order_number_models.py @@ -1,19 +1,22 @@ import pytest -from reference_documents.tests.factories import PreferentialQuotaFactory +from reference_documents.models import PreferentialQuotaOrderNumber +from reference_documents.tests.factories import PreferentialQuotaOrderNumberFactory pytestmark = pytest.mark.django_db @pytest.mark.reference_documents -class TestPreferentialQuota: - def test_create_with_defaults(self): - target = PreferentialQuotaFactory() +class TestPreferentialQuotaOrderNumber: + def test_init(self): + target = PreferentialQuotaOrderNumber() - assert target.preferential_quota_order_number is not None - assert target.commodity_code is not None - assert target.quota_duty_rate is not None - assert target.volume is not None - assert target.valid_between is not None - assert target.measurement is not None - assert target.order is not None + assert target.quota_order_number == "" + assert target.coefficient is None + assert target.main_order_number is None + assert target.valid_between is None + + def test_str(self): + target = PreferentialQuotaOrderNumberFactory.create() + + assert str(target) == f"{target.quota_order_number}" diff --git a/reference_documents/tests/test_preferential_quota_order_number_views.py b/reference_documents/tests/test_preferential_quota_order_number_views.py index c29e56eb8..e9849a0ab 100644 --- a/reference_documents/tests/test_preferential_quota_order_number_views.py +++ b/reference_documents/tests/test_preferential_quota_order_number_views.py @@ -1,30 +1,311 @@ +from datetime import date +from decimal import Decimal + import pytest from django.urls import reverse +from reference_documents.models import PreferentialQuotaOrderNumber from reference_documents.tests import factories pytestmark = pytest.mark.django_db -class TestPreferentialQuotaEditView: +@pytest.mark.reference_documents +class TestPreferentialQuotaOrderNumberEditView: + def test_get_without_permissions(self, valid_user_client): + pref_quota_order_number = factories.PreferentialQuotaOrderNumberFactory() + + resp = valid_user_client.get( + reverse( + "reference_documents:preferential_quota_order_number_edit", + kwargs={"pk": pref_quota_order_number.pk}, + ), + ) + assert resp.status_code == 403 + + def test_get_with_permissions(self, superuser_client): + pref_quota_order_number = factories.PreferentialQuotaOrderNumberFactory() + + resp = superuser_client.get( + reverse( + "reference_documents:preferential_quota_order_number_edit", + kwargs={"pk": pref_quota_order_number.pk}, + ), + ) + assert resp.status_code == 200 + + def test_post_with_permission_pass_not_sub_quota(self, superuser_client): + factories.PreferentialQuotaOrderNumberFactory.create() + pref_quota_order_number = factories.PreferentialQuotaOrderNumberFactory.create() + + post_data = { + "quota_order_number": "012345", + "start_date_0": 1, + "start_date_1": 1, + "start_date_2": 2022, + "end_date_0": 1, + "end_date_1": 1, + "end_date_2": 2023, + "coefficient": "", + "main_order_number_id": "", + "reference_document_version_id": pref_quota_order_number.reference_document_version.pk, + } + + resp = superuser_client.post( + reverse( + "reference_documents:preferential_quota_order_number_edit", + kwargs={"pk": pref_quota_order_number.pk}, + ), + post_data, + ) + + assert resp.status_code == 302 + + pref_quota_order_number.refresh_from_db() + + # check the update was applied + assert pref_quota_order_number.valid_between.lower == date(2022, 1, 1) + assert pref_quota_order_number.valid_between.upper == date(2023, 1, 1) + assert pref_quota_order_number.coefficient is None + assert pref_quota_order_number.quota_order_number == "012345" + assert pref_quota_order_number.main_order_number_id is None + + def test_post_with_permission_pass_with_sub_quota(self, superuser_client): + pref_quota_order_number_main = ( + factories.PreferentialQuotaOrderNumberFactory.create() + ) + pref_quota_order_number = factories.PreferentialQuotaOrderNumberFactory.create() + + post_data = { + "quota_order_number": "012345", + "start_date_0": 1, + "start_date_1": 1, + "start_date_2": 2022, + "end_date_0": 1, + "end_date_1": 1, + "end_date_2": 2023, + "coefficient": "1.2", + "main_order_number_id": pref_quota_order_number_main.pk, + "reference_document_version_id": pref_quota_order_number.reference_document_version.pk, + } + + resp = superuser_client.post( + reverse( + "reference_documents:preferential_quota_order_number_edit", + kwargs={"pk": pref_quota_order_number.pk}, + ), + post_data, + ) + + assert resp.status_code == 302 + + pref_quota_order_number.refresh_from_db() + + # check the update was applied + assert pref_quota_order_number.valid_between.lower == date(2022, 1, 1) + assert pref_quota_order_number.valid_between.upper == date(2023, 1, 1) + assert pref_quota_order_number.coefficient == Decimal("1.2") + assert pref_quota_order_number.quota_order_number == "012345" + assert pref_quota_order_number.main_order_number_id == None + + +@pytest.mark.reference_documents +class TestPreferentialQuotaOrderNumberCreateView: + def test_get_without_permissions(self, valid_user_client): + ref_doc_version = factories.ReferenceDocumentVersionFactory.create() + + resp = valid_user_client.get( + reverse( + "reference_documents:preferential_quota_order_number_create", + kwargs={"pk": ref_doc_version.pk}, + ), + ) + assert resp.status_code == 403 + + def test_get_with_permissions(self, superuser_client): + ref_doc_version = factories.ReferenceDocumentVersionFactory.create() + + resp = superuser_client.get( + reverse( + "reference_documents:preferential_quota_order_number_create", + kwargs={"pk": ref_doc_version.pk}, + ), + ) + assert resp.status_code == 200 + + def test_post_with_permission_pass_not_sub_quota(self, superuser_client): + ref_doc_version = factories.ReferenceDocumentVersionFactory.create() + + post_data = { + "quota_order_number": "012345", + "start_date_0": 1, + "start_date_1": 1, + "start_date_2": 2022, + "end_date_0": 1, + "end_date_1": 1, + "end_date_2": 2023, + "coefficient": "", + "main_order_number_id": "", + "reference_document_version_id": ref_doc_version.pk, + } + + resp = superuser_client.post( + reverse( + "reference_documents:preferential_quota_order_number_create", + kwargs={"pk": ref_doc_version.pk}, + ), + post_data, + ) + + assert resp.status_code == 302 + + pref_quota_order_number = ( + ref_doc_version.preferential_quota_order_numbers.all().last() + ) + + # check the update was applied + assert pref_quota_order_number.valid_between.lower == date(2022, 1, 1) + assert pref_quota_order_number.valid_between.upper == date(2023, 1, 1) + assert pref_quota_order_number.coefficient is None + assert pref_quota_order_number.quota_order_number == "012345" + assert pref_quota_order_number.main_order_number_id is None + + def test_post_with_permission_pass_with_sub_quota(self, superuser_client): + pref_quota_order_number_main = ( + factories.PreferentialQuotaOrderNumberFactory.create() + ) + ref_doc_version = factories.ReferenceDocumentVersionFactory.create() + + post_data = { + "quota_order_number": "012345", + "start_date_0": 1, + "start_date_1": 1, + "start_date_2": 2022, + "end_date_0": 1, + "end_date_1": 1, + "end_date_2": 2023, + "coefficient": "1.2", + "main_order_number_id": pref_quota_order_number_main.pk, + "reference_document_version_id": ref_doc_version.pk, + } + + resp = superuser_client.post( + reverse( + "reference_documents:preferential_quota_order_number_create", + kwargs={"pk": ref_doc_version.pk}, + ), + post_data, + ) + + assert resp.status_code == 302 + + pref_quota_order_number = ( + ref_doc_version.preferential_quota_order_numbers.all().last() + ) + + # check the update was applied + assert pref_quota_order_number.valid_between.lower == date(2022, 1, 1) + assert pref_quota_order_number.valid_between.upper == date(2023, 1, 1) + assert pref_quota_order_number.coefficient == Decimal("1.2") + assert pref_quota_order_number.quota_order_number == "012345" + assert pref_quota_order_number.main_order_number_id == None + + +@pytest.mark.reference_documents +class TestPreferentialQuotaOrderNumberDeleteView: def test_get_without_permissions(self, valid_user_client): - pref_quota = factories.PreferentialQuotaFactory.create() + pref_quota_order_number = factories.PreferentialQuotaOrderNumberFactory.create() + ref_doc_version = pref_quota_order_number.reference_document_version - response = valid_user_client.get( + resp = valid_user_client.get( reverse( - "reference_documents:preferential_quotas_edit", - kwargs={"pk": pref_quota.pk}, + "reference_documents:preferential_quota_order_number_delete", + kwargs={ + "pk": pref_quota_order_number.pk, + "version_pk": ref_doc_version.pk, + }, ), ) - assert response.status_code == 200 + assert resp.status_code == 403 def test_get_with_permissions(self, superuser_client): - pref_quota = factories.PreferentialQuotaFactory.create() + pref_quota_order_number = factories.PreferentialQuotaOrderNumberFactory.create() + ref_doc_version = pref_quota_order_number.reference_document_version + + resp = superuser_client.get( + reverse( + "reference_documents:preferential_quota_order_number_delete", + kwargs={ + "pk": pref_quota_order_number.pk, + "version_pk": ref_doc_version.pk, + }, + ), + ) + + assert resp.status_code == 200 + + def test_post_with_permission(self, superuser_client): + pref_quota_order_number = factories.PreferentialQuotaOrderNumberFactory.create() + ref_doc_version = pref_quota_order_number.reference_document_version + pref_quota_order_number_id = pref_quota_order_number.id + resp = superuser_client.post( + reverse( + "reference_documents:preferential_quota_order_number_delete", + kwargs={ + "pk": pref_quota_order_number.pk, + "version_pk": ref_doc_version.pk, + }, + ), + {}, + ) + + assert resp.status_code == 302 + results = PreferentialQuotaOrderNumber.objects.all().filter( + id=pref_quota_order_number_id, + ) + assert len(results) == 0 - response = superuser_client.get( + def test_post_without_permission(self, valid_user_client): + pref_quota_order_number = factories.PreferentialQuotaOrderNumberFactory.create() + ref_doc_version = pref_quota_order_number.reference_document_version + pref_quota_order_number_id = pref_quota_order_number.id + resp = valid_user_client.post( reverse( - "reference_documents:preferential_quotas_edit", - kwargs={"pk": pref_quota.pk}, + "reference_documents:preferential_quota_order_number_delete", + kwargs={ + "pk": pref_quota_order_number.pk, + "version_pk": ref_doc_version.pk, + }, ), + {}, + ) + + assert resp.status_code == 403 + results = PreferentialQuotaOrderNumber.objects.all().filter( + id=pref_quota_order_number_id, + ) + assert len(results) == 1 + + def test_post_with_permission_with_sub_records(self, superuser_client): + pref_quota_order_number = ( + factories.PreferentialQuotaFactory.create().preferential_quota_order_number + ) + ref_doc_version = pref_quota_order_number.reference_document_version + pref_quota_order_number_id = pref_quota_order_number.id + + resp = superuser_client.post( + reverse( + "reference_documents:preferential_quota_order_number_delete", + kwargs={ + "pk": pref_quota_order_number.pk, + "version_pk": ref_doc_version.pk, + }, + ), + {}, + ) + + assert resp.status_code == 200 + results = PreferentialQuotaOrderNumber.objects.all().filter( + id=pref_quota_order_number_id, ) - assert response.status_code == 200 + assert len(results) == 1 diff --git a/reference_documents/tests/test_preferential_quotas_models.py b/reference_documents/tests/test_preferential_quotas_models.py index 448a31eee..63e6ae414 100644 --- a/reference_documents/tests/test_preferential_quotas_models.py +++ b/reference_documents/tests/test_preferential_quotas_models.py @@ -1,19 +1,19 @@ import pytest -from reference_documents.tests.factories import PreferentialQuotaFactory +from reference_documents.models import PreferentialQuota pytestmark = pytest.mark.django_db @pytest.mark.reference_documents class TestPreferentialQuota: - def test_create_with_defaults(self): - target = PreferentialQuotaFactory() + def test_init(self): + target = PreferentialQuota() - assert target.preferential_quota_order_number is not None - assert target.commodity_code is not None - assert target.quota_duty_rate is not None - assert target.volume is not None - assert target.valid_between is not None - assert target.measurement is not None - assert target.order is not None + assert target.preferential_quota_order_number is None + assert target.commodity_code == "" + assert target.quota_duty_rate == "" + assert target.volume == "" + assert target.valid_between is None + assert target.measurement == "" + assert target.order is None diff --git a/reference_documents/tests/test_preferential_quotas_views.py b/reference_documents/tests/test_preferential_quotas_views.py index 7ff386ce6..495f4dda7 100644 --- a/reference_documents/tests/test_preferential_quotas_views.py +++ b/reference_documents/tests/test_preferential_quotas_views.py @@ -8,6 +8,7 @@ pytestmark = pytest.mark.django_db +@pytest.mark.reference_documents class TestPreferentialQuotaEditView: def test_get_without_permissions(self, valid_user_client): pref_quota = factories.PreferentialQuotaFactory.create() @@ -18,7 +19,7 @@ def test_get_without_permissions(self, valid_user_client): kwargs={"pk": pref_quota.pk}, ), ) - assert response.status_code == 200 + assert response.status_code == 403 def test_get_with_permissions(self, superuser_client): pref_quota = factories.PreferentialQuotaFactory.create() diff --git a/reference_documents/tests/test_preferential_rates_forms.py b/reference_documents/tests/test_preferential_rates_forms.py index 227d50dab..750d86fe5 100644 --- a/reference_documents/tests/test_preferential_rates_forms.py +++ b/reference_documents/tests/test_preferential_rates_forms.py @@ -3,6 +3,9 @@ from reference_documents.forms.preferential_rate_forms import ( PreferentialRateCreateUpdateForm, ) +from reference_documents.forms.preferential_rate_forms import PreferentialRateDeleteForm +from reference_documents.models import PreferentialRate +from reference_documents.tests import factories pytestmark = pytest.mark.django_db @@ -42,5 +45,15 @@ def test_validation_no_comm_code(self): assert "end_date" not in form.errors.as_data().keys() +@pytest.mark.reference_documents class TestPreferentialRateDeleteForm: - pass + def test_init(self): + pref_rate = factories.PreferentialRateFactory() + + target = PreferentialRateDeleteForm( + instance=pref_rate, + ) + + assert target.instance == pref_rate + assert target.Meta.fields == [] + assert target.Meta.model == PreferentialRate diff --git a/reference_documents/tests/test_preferential_rates_models.py b/reference_documents/tests/test_preferential_rates_models.py index 70d493d9a..d178352b2 100644 --- a/reference_documents/tests/test_preferential_rates_models.py +++ b/reference_documents/tests/test_preferential_rates_models.py @@ -1,16 +1,16 @@ import pytest -from reference_documents.tests.factories import PreferentialRateFactory +from reference_documents.models import PreferentialRate pytestmark = pytest.mark.django_db @pytest.mark.reference_documents class TestPreferentialRate: - def test_create_with_defaults(self): - target = PreferentialRateFactory() - assert target.commodity_code is not None - assert target.duty_rate is not None - assert target.order is not None - assert target.reference_document_version is not None - assert target.valid_between is not None + def test_init(self): + target = PreferentialRate() + assert target.commodity_code == "" + assert target.duty_rate == "" + assert target.order is None + assert target.reference_document_version is None + assert target.valid_between is None diff --git a/reference_documents/tests/test_reference_document_versions_models.py b/reference_documents/tests/test_reference_document_versions_models.py index 6fa72fd9e..20ed54c5e 100644 --- a/reference_documents/tests/test_reference_document_versions_models.py +++ b/reference_documents/tests/test_reference_document_versions_models.py @@ -1,8 +1,5 @@ import pytest -from common.tests.factories import GeographicalAreaDescriptionFactory -from common.tests.factories import GeographicalAreaFactory -from geo_areas.models import GeographicalAreaDescription from reference_documents.tests import factories pytestmark = pytest.mark.django_db @@ -21,18 +18,11 @@ def test_create_with_defaults(self): assert target.reference_document is not None assert target.status is not None + def test_preferential_quotas(self): + target = factories.ReferenceDocumentVersionFactory.create() + # add a pref quota + factories.PreferentialQuotaFactory.create( + preferential_quota_order_number__reference_document_version=target, + ) -@pytest.mark.reference_documents -def test_get_area_name_by_area_id(): - ref_doc = factories.ReferenceDocumentFactory.create(area_id="BE") - geo_area = GeographicalAreaFactory.create(area_id="BE") - GeographicalAreaDescriptionFactory(described_geographicalarea=geo_area) - - ref_doc_area_name = ref_doc.get_area_name_by_area_id() - geo_area_description = ( - GeographicalAreaDescription.objects.latest_approved() - .filter(described_geographicalarea__area_id=geo_area.area_id) - .order_by("-validity_start") - .first() - ) - assert ref_doc_area_name == geo_area_description.description + assert len(target.preferential_quotas()) == 1 diff --git a/reference_documents/tests/test_reference_documents_models.py b/reference_documents/tests/test_reference_documents_models.py index c5c9a86fb..a3d434811 100644 --- a/reference_documents/tests/test_reference_documents_models.py +++ b/reference_documents/tests/test_reference_documents_models.py @@ -1,15 +1,30 @@ import pytest -from reference_documents.tests.factories import ReferenceDocumentFactory +from common.tests.factories import GeographicalAreaFactory +from reference_documents.models import ReferenceDocument pytestmark = pytest.mark.django_db @pytest.mark.reference_documents class TestReferenceDocumentVersion: - def test_create_with_defaults(self): - target = ReferenceDocumentFactory() + def test_init(self): + target = ReferenceDocument() - assert target.created_at is not None - assert target.title is not None - assert target.area_id is not None + assert target.created_at is None + assert target.title is "" + assert target.area_id is "" + + def test_get_area_name_by_area_id_no_match_to_database(self): + target = ReferenceDocument() + + assert target.get_area_name_by_area_id() == " (unknown description)" + + def test_get_area_name_by_area_id_match_to_database(self): + GeographicalAreaFactory.create( + area_id="TEST", + description__description="test description", + ) + target = ReferenceDocument(area_id="TEST") + + assert target.get_area_name_by_area_id() == "test description" diff --git a/reference_documents/views/reference_document_views.py b/reference_documents/views/reference_document_views.py index 44c419399..cd5a74074 100644 --- a/reference_documents/views/reference_document_views.py +++ b/reference_documents/views/reference_document_views.py @@ -19,18 +19,22 @@ from reference_documents.models import ReferenceDocument -class ReferenceDocumentList(PermissionRequiredMixin, ListView): - """UI endpoint for viewing and filtering workbaskets.""" +class ReferenceDocumentContext: + def __init__(self, object_list): + self.object_list = object_list - template_name = "reference_documents/index.jinja" - permission_required = "reference_documents.view_reference_document" - model = ReferenceDocument + def get_reference_document_context_headers(self): + return [ + {"text": "Latest Version"}, + {"text": "Country"}, + {"text": "Duties"}, + {"text": "Order Numbers"}, + {"text": "Actions"}, + ] - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) + def get_reference_document_context_rows(self): reference_documents = [] - - for reference in context["object_list"].order_by("area_id"): + for reference in self.object_list.order_by("area_id"): if reference.reference_document_versions.count() == 0: reference_documents.append( [ @@ -68,15 +72,25 @@ def get_context_data(self, **kwargs): }, ], ) + return reference_documents - context["reference_documents"] = reference_documents - context["reference_document_headers"] = [ - {"text": "Latest Version"}, - {"text": "Country"}, - {"text": "Duties"}, - {"text": "Order Numbers"}, - {"text": "Actions"}, - ] + def get_context(self): + return { + "reference_documents": self.get_reference_document_context_rows(), + "reference_document_headers": self.get_reference_document_context_headers(), + } + + +class ReferenceDocumentList(PermissionRequiredMixin, ListView): + """UI endpoint for viewing and filtering workbaskets.""" + + template_name = "reference_documents/index.jinja" + permission_required = "reference_documents.view_reference_document" + model = ReferenceDocument + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context.update(ReferenceDocumentContext(context["object_list"]).get_context()) return context @@ -120,9 +134,9 @@ def get_context_data(self, *args, **kwargs): "text": version.entry_into_force_date, }, { - "html": f'Version details
    ' - f'Edit
    ' - f'Delete
    ' + "html": f'Version details
    ' + f'Edit
    ' + f'Delete
    ' f'Alignment reports', }, ], From 7b4833ed850c76ea9695a7d6ca901ef7d807e7e7 Mon Sep 17 00:00:00 2001 From: Doug Mills Date: Wed, 27 Mar 2024 13:58:53 +0000 Subject: [PATCH 114/118] models 100% tested, forms 100% tested --- .gitignore | 1 + .../views/preferential_quota_order_number_views.py | 2 -- reference_documents/views/preferential_quota_views.py | 3 +-- reference_documents/views/preferential_rate_views.py | 3 +-- reference_documents/views/reference_document_version_views.py | 3 +-- reference_documents/views/reference_document_views.py | 3 +-- 6 files changed, 5 insertions(+), 10 deletions(-) diff --git a/.gitignore b/.gitignore index 76fce04d9..073eab4be 100644 --- a/.gitignore +++ b/.gitignore @@ -166,3 +166,4 @@ _dumped_cache.pkl # Database dumps *.sql +/.vscode/settings.json diff --git a/reference_documents/views/preferential_quota_order_number_views.py b/reference_documents/views/preferential_quota_order_number_views.py index d24f50bf7..f68d98caf 100644 --- a/reference_documents/views/preferential_quota_order_number_views.py +++ b/reference_documents/views/preferential_quota_order_number_views.py @@ -4,7 +4,6 @@ from django.views.generic import CreateView from django.views.generic import DeleteView from django.views.generic import UpdateView -from django.views.generic.edit import FormMixin from reference_documents.forms.preferential_quota_order_number_forms import ( PreferentialQuotaOrderNumberCreateUpdateForm, @@ -75,7 +74,6 @@ def get_success_url(self): class PreferentialQuotaOrderNumberDelete( PermissionRequiredMixin, - FormMixin, DeleteView, ): form_class = PreferentialQuotaOrderNumberDeleteForm diff --git a/reference_documents/views/preferential_quota_views.py b/reference_documents/views/preferential_quota_views.py index 0a9441b8e..9f79ab3fe 100644 --- a/reference_documents/views/preferential_quota_views.py +++ b/reference_documents/views/preferential_quota_views.py @@ -5,7 +5,6 @@ from django.views.generic import FormView from django.views.generic import UpdateView from django.views.generic.edit import DeleteView -from django.views.generic.edit import FormMixin from reference_documents.forms.preferential_quota_forms import ( PreferentialQuotaBulkCreateForm, @@ -150,7 +149,7 @@ def get_success_url(self): ) -class PreferentialQuotaDelete(PermissionRequiredMixin, FormMixin, DeleteView): +class PreferentialQuotaDelete(PermissionRequiredMixin, DeleteView): form_class = PreferentialQuotaDeleteForm template_name = "reference_documents/preferential_quotas/delete.jinja" permission_required = "reference_documents.edit_reference_document" diff --git a/reference_documents/views/preferential_rate_views.py b/reference_documents/views/preferential_rate_views.py index 05afbb1fb..cfb186266 100644 --- a/reference_documents/views/preferential_rate_views.py +++ b/reference_documents/views/preferential_rate_views.py @@ -3,7 +3,6 @@ from django.views.generic import CreateView from django.views.generic import DeleteView from django.views.generic import UpdateView -from django.views.generic.edit import FormMixin from reference_documents.forms.preferential_rate_forms import ( PreferentialRateCreateUpdateForm, @@ -49,7 +48,7 @@ def form_valid(self, form): return super(PreferentialRateCreate, self).form_valid(form) -class PreferentialRateDelete(PermissionRequiredMixin, FormMixin, DeleteView): +class PreferentialRateDelete(PermissionRequiredMixin, DeleteView): template_name = "reference_documents/preferential_rates/delete.jinja" permission_required = "reference_documents.delete_preferentialrate" model = PreferentialRate diff --git a/reference_documents/views/reference_document_version_views.py b/reference_documents/views/reference_document_version_views.py index b77c71304..07a8d14af 100644 --- a/reference_documents/views/reference_document_version_views.py +++ b/reference_documents/views/reference_document_version_views.py @@ -6,7 +6,6 @@ from django.views.generic import DetailView from django.views.generic import TemplateView from django.views.generic import UpdateView -from django.views.generic.edit import FormMixin from commodities.models import GoodsNomenclature from quotas.models import QuotaOrderNumber @@ -340,7 +339,7 @@ def get_success_url(self): ) -class ReferenceDocumentVersionDelete(PermissionRequiredMixin, FormMixin, DeleteView): +class ReferenceDocumentVersionDelete(PermissionRequiredMixin, DeleteView): form_class = ReferenceDocumentVersionDeleteForm model = ReferenceDocumentVersion permission_required = "reference_documents.delete_referencedocumentversion" diff --git a/reference_documents/views/reference_document_views.py b/reference_documents/views/reference_document_views.py index cd5a74074..b82587c27 100644 --- a/reference_documents/views/reference_document_views.py +++ b/reference_documents/views/reference_document_views.py @@ -7,7 +7,6 @@ from django.views.generic import ListView from django.views.generic import TemplateView from django.views.generic import UpdateView -from django.views.generic.edit import FormMixin from reference_documents import models from reference_documents.forms.reference_document_forms import ( @@ -172,7 +171,7 @@ def get_success_url(self): ) -class ReferenceDocumentDelete(PermissionRequiredMixin, FormMixin, DeleteView): +class ReferenceDocumentDelete(PermissionRequiredMixin, DeleteView): form_class = ReferenceDocumentDeleteForm model = ReferenceDocument permission_required = "reference_documents.delete_referencedocument" From 9a7373ded749f81571562576480f6284a1b2f723 Mon Sep 17 00:00:00 2001 From: Matthew McKenzie <97194636+mattjamc@users.noreply.github.com> Date: Thu, 28 Mar 2024 13:57:12 +0000 Subject: [PATCH 115/118] Ref doc and versions view tests --- .../reference_documents/confirm_delete.jinja | 6 +- .../tests/test_preferential_quotas_views.py | 7 +- .../test_reference_document_versions_views.py | 85 ++++++++++++++++++- .../tests/test_reference_documents_views.py | 64 +++++++++++++- .../views/reference_document_version_views.py | 1 - .../views/reference_document_views.py | 8 +- 6 files changed, 157 insertions(+), 14 deletions(-) diff --git a/reference_documents/jinja2/reference_documents/confirm_delete.jinja b/reference_documents/jinja2/reference_documents/confirm_delete.jinja index caa3ce4f3..00b3b5a2d 100644 --- a/reference_documents/jinja2/reference_documents/confirm_delete.jinja +++ b/reference_documents/jinja2/reference_documents/confirm_delete.jinja @@ -4,7 +4,9 @@ {% from "components/button/macro.njk" import govukButton %} {% from "components/breadcrumbs/macro.njk" import govukBreadcrumbs %} -{% set page_title = "Reference document " ~ deleted_pk ~ " deleted" %} +{% set area_id = request.session['deleted_version']['area_id'] %} + +{% set page_title = "Reference document " ~ area_id ~ " successfully deleted" %} {% block breadcrumb %} {{ govukBreadcrumbs({ @@ -18,7 +20,7 @@
    {{ govukPanel({ - "titleText": "Reference document " ~ deleted_pk ~ " has been deleted", + "titleText": "Reference document " ~ area_id ~ " has been deleted", "text": "This change has taken immediate effect", "classes": "govuk-!-margin-bottom-7" }) }} diff --git a/reference_documents/tests/test_preferential_quotas_views.py b/reference_documents/tests/test_preferential_quotas_views.py index 495f4dda7..5319ac250 100644 --- a/reference_documents/tests/test_preferential_quotas_views.py +++ b/reference_documents/tests/test_preferential_quotas_views.py @@ -79,8 +79,11 @@ def test_quota_bulk_create_creates_object_and_redirects(valid_user, client): } create_url = reverse( - "reference_documents:preferential_quotas_bulk_create", - kwargs={"pk": ref_doc_version.pk}, + "reference_documents:preferential_quotas_bulk_create_for_order", + kwargs={ + "pk": ref_doc_version.pk, + "order_pk": preferential_quota_order_number.pk, + }, ) response = client.get(create_url) assert response.status_code == 200 diff --git a/reference_documents/tests/test_reference_document_versions_views.py b/reference_documents/tests/test_reference_document_versions_views.py index 4c6d9ebd6..f68537e36 100644 --- a/reference_documents/tests/test_reference_document_versions_views.py +++ b/reference_documents/tests/test_reference_document_versions_views.py @@ -5,6 +5,9 @@ from django.contrib.auth.models import Permission from django.urls import reverse +from common.tests.factories import QuotaOrderNumberFactory +from common.tests.factories import SimpleGoodsNomenclatureFactory +from common.tests.factories import date_ranges from reference_documents.models import ReferenceDocument from reference_documents.models import ReferenceDocumentVersion from reference_documents.tests import factories @@ -27,6 +30,9 @@ def test_ref_doc_version_create_creates_object_and_redirects(valid_user, client) kwargs={"pk": ref_doc.pk}, ) + resp = client.get(create_url) + assert resp.status_code == 200 + form_data = { "reference_document": ref_doc.pk, "version": "2.0", @@ -101,9 +107,13 @@ def test_successfully_delete_ref_doc_version(valid_user, client): Permission.objects.get(codename="delete_referencedocumentversion"), ) client.force_login(valid_user) - ref_doc_version = factories.ReferenceDocumentVersionFactory.create() - ref_doc_pk = ref_doc_version.reference_document.pk - area_id = ref_doc_version.reference_document.area_id + ref_doc = factories.ReferenceDocumentFactory.create(area_id="XY") + ref_doc_version = factories.ReferenceDocumentVersionFactory.create( + reference_document=ref_doc, + version=3.0, + ) + ref_doc_pk = ref_doc.pk + area_id = ref_doc.area_id assert ReferenceDocumentVersion.objects.filter(pk=ref_doc_version.pk) delete_url = reverse( "reference_documents:version-delete", @@ -123,6 +133,11 @@ def test_successfully_delete_ref_doc_version(valid_user, client): kwargs={"deleted_pk": ref_doc_version.pk}, ) assert not ReferenceDocumentVersion.objects.filter(pk=ref_doc_version.pk) + resp = client.get(resp.url) + assert ( + f"Reference document {area_id} version {ref_doc_version.version} has been deleted" + in str(resp.content) + ) @pytest.mark.reference_documents @@ -187,3 +202,67 @@ def test_ref_doc_crud_without_permission(valid_user_client): assert resp.status_code == 403 resp = valid_user_client.post(delete_url) assert resp.status_code == 403 + + +@pytest.mark.reference_documents +def test_ref_doc_version_detail_view(superuser_client): + """Test that the reference document version detail view shows preferential + rate and tariff quota data.""" + ref_doc = factories.ReferenceDocumentFactory.create( + area_id="XY", + title="Reference document for XY", + ) + ref_doc_version = factories.ReferenceDocumentVersionFactory( + reference_document=ref_doc, + version=1.0, + ) + preferential_rate_batch = factories.PreferentialRateFactory.create_batch( + 10, + reference_document_version=ref_doc_version, + ) + first_preferential_rate = preferential_rate_batch[0] + order_number_batch = factories.PreferentialQuotaOrderNumberFactory.create_batch( + 5, + reference_document_version=ref_doc_version, + ) + first_quota_order_number = order_number_batch[0].quota_order_number + # Recreate the first quota and first preferential rate's commodity code in TAP + tap_quota = QuotaOrderNumberFactory.create(order_number=first_quota_order_number) + tap_commodity_code = SimpleGoodsNomenclatureFactory.create( + item_id=first_preferential_rate.commodity_code, + valid_between=date_ranges("big"), + suffix=80, + ) + core_data_tab = ( + reverse( + "reference_documents:version-details", + kwargs={"pk": ref_doc_version.pk}, + ) + + "#core-data" + ) + response = superuser_client.get(core_data_tab) + page = BeautifulSoup(response.content, "html.parser") + assert response.status_code == 200 + # Assert the first rate's commodity code which exists in TAP appears as a link + assert page.find("a", href=True, text=f"{first_preferential_rate.commodity_code}") + table_rows = page.select("tr") + # Assert there is a row for each preferential rate + assert len(table_rows) == 11 + tariff_quotas_tab = ( + reverse( + "reference_documents:version-details", + kwargs={"pk": ref_doc_version.pk}, + ) + + "#tariff_quotas" + ) + response = superuser_client.get(tariff_quotas_tab) + page = BeautifulSoup(response.content, "html.parser") + quota_link_header = page.select("h2 a")[0].get_text() + # Assert the first quota which exists in TAP appears as a URL + assert first_quota_order_number in quota_link_header + assert response.status_code == 200 + # Assert the remaining four quotas appear too + for order_number in order_number_batch[1:]: + assert f"Order Number {order_number.quota_order_number}" in str( + response.content, + ) diff --git a/reference_documents/tests/test_reference_documents_views.py b/reference_documents/tests/test_reference_documents_views.py index c9706aaa7..8bb50b131 100644 --- a/reference_documents/tests/test_reference_documents_views.py +++ b/reference_documents/tests/test_reference_documents_views.py @@ -84,7 +84,7 @@ def test_successfully_delete_ref_doc(valid_user, client): ) client.force_login(valid_user) - ref_doc = factories.ReferenceDocumentFactory.create() + ref_doc = factories.ReferenceDocumentFactory.create(area_id="XY") assert ReferenceDocument.objects.filter(pk=ref_doc.pk) delete_url = reverse( "reference_documents:delete", @@ -106,6 +106,9 @@ def test_successfully_delete_ref_doc(valid_user, client): ) assert not ReferenceDocument.objects.filter(pk=ref_doc.pk) + response = client.get(response.url) + assert f"Reference document XY has been deleted" in str(response.content) + @pytest.mark.reference_documents def test_delete_ref_doc_with_versions(valid_user, client): @@ -152,3 +155,62 @@ def test_ref_doc_crud_without_permission(valid_user_client): assert response.status_code == 403 response = valid_user_client.post(delete_url) assert response.status_code == 403 + + +@pytest.mark.reference_documents +def test_ref_doc_list_view(superuser_client): + """Test that the reference document list view renders correctly and shows + the correct version number.""" + area_ids_and_titles = { + "AD": "Andorra", + "AG": "Antigua and Barbuda", + "AL": "Albania", + "AT": "Austria", + "AU": "Australia", + } + for country in area_ids_and_titles.items(): + factories.ReferenceDocumentFactory.create(area_id=country[0], title=country[1]) + list_url = reverse("reference_documents:index") + response = superuser_client.get(list_url) + page = BeautifulSoup(response.content, "html.parser") + assert response.status_code == 200 + table_rows = page.select(".govuk-table tbody tr") + header = page.h1.get_text().strip() + assert "Reference documents" == header + assert len(table_rows) == 5 + # Test for ref docs with a version + ref_doc = ReferenceDocument.objects.first() + ref_doc_version = factories.ReferenceDocumentVersionFactory( + reference_document=ref_doc, + ) + response = superuser_client.get(list_url) + page = BeautifulSoup(response.content, "html.parser") + assert response.status_code == 200 + andorra_row = page.select("tr")[1] + latest_version_cell = andorra_row.select("td")[0] + assert str(ref_doc_version.version) in latest_version_cell + + +@pytest.mark.reference_documents +def test_ref_doc_detail_view(superuser_client): + """Test that the reference document detail view shows a row for each + reference document version.""" + ref_doc = factories.ReferenceDocumentFactory.create() + ref_doc_version_1 = factories.ReferenceDocumentVersionFactory( + reference_document=ref_doc, + version=1.0, + ) + ref_doc_version_2 = factories.ReferenceDocumentVersionFactory( + reference_document=ref_doc, + version=2.0, + ) + ref_doc_version_2 = factories.ReferenceDocumentVersionFactory( + reference_document=ref_doc, + version=3.0, + ) + details_url = reverse("reference_documents:details", kwargs={"pk": ref_doc.pk}) + response = superuser_client.get(details_url) + page = BeautifulSoup(response.content, "html.parser") + assert response.status_code == 200 + table_rows = page.select("tr") + assert len(table_rows) == 4 diff --git a/reference_documents/views/reference_document_version_views.py b/reference_documents/views/reference_document_version_views.py index 07a8d14af..d7c2fb307 100644 --- a/reference_documents/views/reference_document_version_views.py +++ b/reference_documents/views/reference_document_version_views.py @@ -345,7 +345,6 @@ class ReferenceDocumentVersionDelete(PermissionRequiredMixin, DeleteView): permission_required = "reference_documents.delete_referencedocumentversion" template_name = "reference_documents/reference_document_versions/delete.jinja" - # TODO: Update this to get rid of FormMixin with Django 4.2 as no need to overwrite the post anymore def get_success_url(self) -> str: return reverse( "reference_documents:version-confirm-delete", diff --git a/reference_documents/views/reference_document_views.py b/reference_documents/views/reference_document_views.py index b82587c27..d2659196b 100644 --- a/reference_documents/views/reference_document_views.py +++ b/reference_documents/views/reference_document_views.py @@ -81,8 +81,6 @@ def get_context(self): class ReferenceDocumentList(PermissionRequiredMixin, ListView): - """UI endpoint for viewing and filtering workbaskets.""" - template_name = "reference_documents/index.jinja" permission_required = "reference_documents.view_reference_document" model = ReferenceDocument @@ -113,8 +111,6 @@ def get_context_data(self, *args, **kwargs): ] reference_document_versions = [] - print(self.request) - for version in context["object"].reference_document_versions.order_by( "version", ): @@ -177,7 +173,6 @@ class ReferenceDocumentDelete(PermissionRequiredMixin, DeleteView): permission_required = "reference_documents.delete_referencedocument" template_name = "reference_documents/delete.jinja" - # TODO: Update this to get rid of FormMixin with Django 4.2 as no need to overwrite the post anymore def get_success_url(self) -> str: return reverse( "reference_documents:confirm-delete", @@ -193,6 +188,9 @@ def post(self, request, *args, **kwargs): self.object = self.get_object() form = self.get_form() if form.is_valid(): + self.request.session["deleted_version"] = { + "area_id": f"{self.object.area_id}", + } return self.form_valid(form) else: return self.form_invalid(form) From 83b046ab5039773cb1890bedf4451c83ef4c3eda Mon Sep 17 00:00:00 2001 From: Doug Mills Date: Thu, 28 Mar 2024 15:58:54 +0000 Subject: [PATCH 116/118] added more tests --- .../tests/test_preferential_quotas_views.py | 681 +++++++++++++----- .../tests/test_preferential_rates_views.py | 138 +++- .../tests/test_reference_documents_models.py | 4 +- reference_documents/urls.py | 2 +- .../views/preferential_quota_views.py | 32 +- 5 files changed, 646 insertions(+), 211 deletions(-) diff --git a/reference_documents/tests/test_preferential_quotas_views.py b/reference_documents/tests/test_preferential_quotas_views.py index 495f4dda7..edf1eed67 100644 --- a/reference_documents/tests/test_preferential_quotas_views.py +++ b/reference_documents/tests/test_preferential_quotas_views.py @@ -3,6 +3,7 @@ from django.contrib.auth.models import Permission from django.urls import reverse +from reference_documents.models import PreferentialQuota from reference_documents.tests import factories pytestmark = pytest.mark.django_db @@ -13,208 +14,524 @@ class TestPreferentialQuotaEditView: def test_get_without_permissions(self, valid_user_client): pref_quota = factories.PreferentialQuotaFactory.create() - response = valid_user_client.get( + resp = valid_user_client.get( reverse( "reference_documents:preferential_quotas_edit", kwargs={"pk": pref_quota.pk}, ), ) - assert response.status_code == 403 + assert resp.status_code == 403 def test_get_with_permissions(self, superuser_client): pref_quota = factories.PreferentialQuotaFactory.create() - response = superuser_client.get( + resp = superuser_client.get( reverse( "reference_documents:preferential_quotas_edit", kwargs={"pk": pref_quota.pk}, ), ) - assert response.status_code == 200 + assert resp.status_code == 200 + + def test_post_with_permissions(self, superuser_client): + pref_quota = factories.PreferentialQuotaFactory.create() + + post_data = { + "preferential_quota_order_number": pref_quota.preferential_quota_order_number.pk, + "commodity_code": pref_quota.commodity_code, + "quota_duty_rate": "33%", + "volume": 3000, + "measurement": "tonnes", + "start_date_0": 1, + "start_date_1": 1, + "start_date_2": 2022, + "end_date_0": 1, + "end_date_1": 1, + "end_date_2": 2023, + } + + resp = superuser_client.post( + reverse( + "reference_documents:preferential_quotas_edit", + kwargs={"pk": pref_quota.pk}, + ), + data=post_data, + ) + + assert resp.status_code == 302 + + pref_quota.refresh_from_db() + + assert pref_quota.volume == "3000" + assert pref_quota.measurement == "tonnes" + assert pref_quota.quota_duty_rate == "33%" + + def test_post_without_permissions(self, valid_user_client): + pref_quota = factories.PreferentialQuotaFactory.create() + + post_data = { + "preferential_quota_order_number": pref_quota.preferential_quota_order_number.pk, + "commodity_code": pref_quota.commodity_code, + "quota_duty_rate": "33%", + "volume": 3000, + "measurement": "tonnes", + "start_date_0": 1, + "start_date_1": 1, + "start_date_2": 2022, + "end_date_0": 1, + "end_date_1": 1, + "end_date_2": 2023, + } + + resp = valid_user_client.post( + reverse( + "reference_documents:preferential_quotas_edit", + kwargs={"pk": pref_quota.pk}, + ), + data=post_data, + ) + + assert resp.status_code == 403 @pytest.mark.reference_documents -def test_quota_bulk_create_creates_object_and_redirects(valid_user, client): - """Test that posting the bulk create from creates all preferential quotas - and redirects.""" - valid_user.user_permissions.add( - Permission.objects.get(codename="add_preferentialquota"), - ) - client.force_login(valid_user) - - ref_doc_version = factories.ReferenceDocumentVersionFactory.create() - preferential_quota_order_number = ( - factories.PreferentialQuotaOrderNumberFactory.create( - reference_document_version=ref_doc_version, - ) - ) - assert not ref_doc_version.preferential_quotas() - - data = { - "preferential_quota_order_number": preferential_quota_order_number.pk, - "commodity_codes": "1234567890\r\n2345678901", - "quota_duty_rate": "5%", - "measurement": "KG", - "start_date_0_0": "1", - "start_date_0_1": "1", - "start_date_0_2": "2023", - "end_date_0_0": "31", - "end_date_0_1": "12", - "end_date_0_2": "2023", - "volume_0": "500", - "start_date_1_0": "1", - "start_date_1_1": "1", - "start_date_1_2": "2024", - "end_date_1_0": "31", - "end_date_1_1": "12", - "end_date_1_2": "2024", - "volume_1": "400", - "start_date_2_0": "1", - "start_date_2_1": "1", - "start_date_2_2": "2025", - "end_date_2_0": "31", - "end_date_2_1": "12", - "end_date_2_2": "2025", - "volume_2": "300", - } - - create_url = reverse( - "reference_documents:preferential_quotas_bulk_create", - kwargs={"pk": ref_doc_version.pk}, - ) - response = client.get(create_url) - assert response.status_code == 200 - - response = client.post(create_url, data) - assert response.status_code == 302 - new_preferential_quotas = ref_doc_version.preferential_quotas() - assert len(new_preferential_quotas) == 6 - assert ( - response.url - == reverse( - "reference_documents:version-details", - args=[ref_doc_version.pk], - ) - + "#tariff-quotas" - ) +class TestPreferentialQuotaCreate: + def test_get_without_permissions(self, valid_user_client): + ref_doc_version = factories.ReferenceDocumentVersionFactory.create() + + resp = valid_user_client.get( + reverse( + "reference_documents:preferential_quotas_create", + kwargs={"version_pk": ref_doc_version.pk}, + ), + ) + + assert resp.status_code == 403 + + def test_get_without_permissions_with_order_number(self, valid_user_client): + pref_order_number = factories.PreferentialQuotaOrderNumberFactory.create() + + resp = valid_user_client.get( + reverse( + "reference_documents:preferential_quotas_create_for_order", + kwargs={ + "version_pk": pref_order_number.reference_document_version.pk, + "order_pk": pref_order_number.pk, + }, + ), + ) + + assert resp.status_code == 403 + + def test_get_with_permissions(self, superuser_client): + ref_doc_version = factories.ReferenceDocumentVersionFactory.create() + + resp = superuser_client.get( + reverse( + "reference_documents:preferential_quotas_create", + kwargs={"version_pk": ref_doc_version.pk}, + ), + ) + + assert resp.status_code == 200 + + def test_get_with_permissions_with_order_number(self, superuser_client): + pref_order_number = factories.PreferentialQuotaOrderNumberFactory.create() + + resp = superuser_client.get( + reverse( + "reference_documents:preferential_quotas_create_for_order", + kwargs={ + "version_pk": pref_order_number.reference_document_version.pk, + "order_pk": pref_order_number.pk, + }, + ), + ) + + assert resp.status_code == 200 + + def test_post_without_permissions(self, valid_user_client): + pref_quota_order_number = factories.PreferentialQuotaOrderNumberFactory.create() + + post_data = { + "preferential_quota_order_number": pref_quota_order_number.pk, + "commodity_code": "1231231230", + "quota_duty_rate": "33%", + "volume": 3000, + "measurement": "tonnes", + "start_date_0": 1, + "start_date_1": 1, + "start_date_2": 2022, + "end_date_0": 1, + "end_date_1": 1, + "end_date_2": 2023, + } + + resp = valid_user_client.post( + reverse( + "reference_documents:preferential_quotas_create", + kwargs={"version_pk": pref_quota_order_number.pk}, + ), + data=post_data, + ) + + assert resp.status_code == 403 + + def test_post_without_permissions_with_order_number(self, valid_user_client): + pref_order_number = factories.PreferentialQuotaOrderNumberFactory.create() + + post_data = { + "preferential_quota_order_number": pref_order_number.pk, + "commodity_code": "1231231230", + "quota_duty_rate": "33%", + "volume": 3000, + "measurement": "tonnes", + "start_date_0": 1, + "start_date_1": 1, + "start_date_2": 2022, + "end_date_0": 1, + "end_date_1": 1, + "end_date_2": 2023, + } + + resp = valid_user_client.post( + reverse( + "reference_documents:preferential_quotas_create_for_order", + kwargs={ + "version_pk": pref_order_number.reference_document_version.pk, + "order_pk": pref_order_number.pk, + }, + ), + data=post_data, + ) + + assert resp.status_code == 403 + + def test_post_with_permissions(self, superuser_client): + pref_quota_order_number = factories.PreferentialQuotaOrderNumberFactory.create() + + post_data = { + "preferential_quota_order_number": pref_quota_order_number.pk, + "commodity_code": "1231231230", + "quota_duty_rate": "33%", + "volume": 3000, + "measurement": "tonnes", + "start_date_0": 1, + "start_date_1": 1, + "start_date_2": 2022, + "end_date_0": 1, + "end_date_1": 1, + "end_date_2": 2023, + } + + resp = superuser_client.post( + reverse( + "reference_documents:preferential_quotas_create", + kwargs={ + "version_pk": pref_quota_order_number.reference_document_version.pk, + }, + ), + data=post_data, + ) + + assert resp.status_code == 302 + + def test_post_with_permissions_with_order_number(self, superuser_client): + pref_order_number = factories.PreferentialQuotaOrderNumberFactory.create() + + post_data = { + "preferential_quota_order_number": pref_order_number.pk, + "commodity_code": "1231231230", + "quota_duty_rate": "33%", + "volume": 3000, + "measurement": "tonnes", + "start_date_0": 1, + "start_date_1": 1, + "start_date_2": 2022, + "end_date_0": 1, + "end_date_1": 1, + "end_date_2": 2023, + } + + resp = superuser_client.post( + reverse( + "reference_documents:preferential_quotas_create_for_order", + kwargs={ + "version_pk": pref_order_number.reference_document_version.pk, + "order_pk": pref_order_number.pk, + }, + ), + data=post_data, + ) + + assert resp.status_code == 302 @pytest.mark.reference_documents -def test_quota_bulk_create_invalid(valid_user, client): - """Test that posting the bulk create form with invalid data fails and - reloads the form with errors.""" - valid_user.user_permissions.add( - Permission.objects.get(codename="add_preferentialquota"), - ) - client.force_login(valid_user) - - ref_doc_version = factories.ReferenceDocumentVersionFactory.create() - preferential_quota_order_number = ( - factories.PreferentialQuotaOrderNumberFactory.create( - reference_document_version=ref_doc_version, - ) - ) - assert not ref_doc_version.preferential_quotas() - - data = { - "preferential_quota_order_number": preferential_quota_order_number.pk, - "commodity_codes": "1234567890\r\n2345678901\r\n12345678910", - "quota_duty_rate": "", - "measurement": "", - "start_date_0_0": "1", - "start_date_0_1": "1", - "start_date_0_2": "2023", - "end_date_0_0": "31", - "end_date_0_1": "12", - "end_date_0_2": "2023", - "volume_0": "500", - "start_date_1_0": "1", - "start_date_1_1": "1", - "start_date_1_2": "2024", - "end_date_1_0": "31", - "end_date_1_1": "12", - "end_date_1_2": "2022", - "volume_1": "400", - "start_date_2_0": "", - "start_date_2_1": "1", - "start_date_2_2": "2025", - "end_date_2_0": "31", - "end_date_2_1": "12", - "end_date_2_2": "2025", - "volume_2": "300", - } - - create_url = reverse( - "reference_documents:preferential_quotas_bulk_create", - kwargs={"pk": ref_doc_version.pk}, - ) - - response = client.post(create_url, data) - assert response.status_code == 200 - soup = BeautifulSoup(response.content.decode(response.charset), "html.parser") - error_messages = soup.select("ul.govuk-list.govuk-error-summary__list a") - - assert "Duty rate is required" == error_messages[0].text - assert "Measurement is required" in error_messages[1].text - assert "Enter the day, month and year" in error_messages[2].text - assert ( - "Ensure all commodity codes are 10 digits and each on a new line" - in error_messages[3].text - ) - assert ( - "The end date must be the same as or after the start date" - in error_messages[4].text - ) - - new_preferential_quotas = ref_doc_version.preferential_quotas() - assert len(new_preferential_quotas) == 0 +class TestPreferentialQuotaBulkCreate: + def test_quota_bulk_create_creates_object_and_redirects(self, valid_user, client): + """Test that posting the bulk create from creates all preferential + quotas and redirects.""" + valid_user.user_permissions.add( + Permission.objects.get(codename="add_preferentialquota"), + ) + client.force_login(valid_user) + + ref_doc_version = factories.ReferenceDocumentVersionFactory.create() + preferential_quota_order_number = ( + factories.PreferentialQuotaOrderNumberFactory.create( + reference_document_version=ref_doc_version, + ) + ) + assert not ref_doc_version.preferential_quotas() + + data = { + "preferential_quota_order_number": preferential_quota_order_number.pk, + "commodity_codes": "1234567890\r\n2345678901", + "quota_duty_rate": "5%", + "measurement": "KG", + "start_date_0_0": "1", + "start_date_0_1": "1", + "start_date_0_2": "2023", + "end_date_0_0": "31", + "end_date_0_1": "12", + "end_date_0_2": "2023", + "volume_0": "500", + "start_date_1_0": "1", + "start_date_1_1": "1", + "start_date_1_2": "2024", + "end_date_1_0": "31", + "end_date_1_1": "12", + "end_date_1_2": "2024", + "volume_1": "400", + "start_date_2_0": "1", + "start_date_2_1": "1", + "start_date_2_2": "2025", + "end_date_2_0": "31", + "end_date_2_1": "12", + "end_date_2_2": "2025", + "volume_2": "300", + } + + create_url = reverse( + "reference_documents:preferential_quotas_bulk_create", + kwargs={"pk": ref_doc_version.pk}, + ) + resp = client.get(create_url) + assert resp.status_code == 200 + + resp = client.post(create_url, data) + assert resp.status_code == 302 + new_preferential_quotas = ref_doc_version.preferential_quotas() + assert len(new_preferential_quotas) == 6 + assert ( + resp.url + == reverse( + "reference_documents:version-details", + args=[ref_doc_version.pk], + ) + + "#tariff-quotas" + ) + + def test_quota_bulk_create_invalid(self, valid_user, client): + """Test that posting the bulk create form with invalid data fails and + reloads the form with errors.""" + valid_user.user_permissions.add( + Permission.objects.get(codename="add_preferentialquota"), + ) + client.force_login(valid_user) + + ref_doc_version = factories.ReferenceDocumentVersionFactory.create() + preferential_quota_order_number = ( + factories.PreferentialQuotaOrderNumberFactory.create( + reference_document_version=ref_doc_version, + ) + ) + assert not ref_doc_version.preferential_quotas() + + data = { + "preferential_quota_order_number": preferential_quota_order_number.pk, + "commodity_codes": "1234567890\r\n2345678901\r\n12345678910", + "quota_duty_rate": "", + "measurement": "", + "start_date_0_0": "1", + "start_date_0_1": "1", + "start_date_0_2": "2023", + "end_date_0_0": "31", + "end_date_0_1": "12", + "end_date_0_2": "2023", + "volume_0": "500", + "start_date_1_0": "1", + "start_date_1_1": "1", + "start_date_1_2": "2024", + "end_date_1_0": "31", + "end_date_1_1": "12", + "end_date_1_2": "2022", + "volume_1": "400", + "start_date_2_0": "", + "start_date_2_1": "1", + "start_date_2_2": "2025", + "end_date_2_0": "31", + "end_date_2_1": "12", + "end_date_2_2": "2025", + "volume_2": "300", + } + + create_url = reverse( + "reference_documents:preferential_quotas_bulk_create", + kwargs={"pk": ref_doc_version.pk}, + ) + + resp = client.post(create_url, data) + assert resp.status_code == 200 + soup = BeautifulSoup(resp.content.decode(resp.charset), "html.parser") + error_messages = soup.select("ul.govuk-list.govuk-error-summary__list a") + + assert "Duty rate is required" == error_messages[0].text + assert "Measurement is required" in error_messages[1].text + assert "Enter the day, month and year" in error_messages[2].text + assert ( + "Ensure all commodity codes are 10 digits and each on a new line" + in error_messages[3].text + ) + assert ( + "The end date must be the same as or after the start date" + in error_messages[4].text + ) + + new_preferential_quotas = ref_doc_version.preferential_quotas() + assert len(new_preferential_quotas) == 0 + + def test_quota_bulk_create_without_permission(self, valid_user_client): + """Test that posting the bulk create form without relevant user + permissions does not work.""" + ref_doc_version = factories.ReferenceDocumentVersionFactory.create() + preferential_quota_order_number = ( + factories.PreferentialQuotaOrderNumberFactory.create( + reference_document_version=ref_doc_version, + ) + ) + assert not ref_doc_version.preferential_quotas() + + data = { + "preferential_quota_order_number": preferential_quota_order_number.pk, + "commodity_codes": "1234567890\r\n2345678901", + "quota_duty_rate": "5%", + "measurement": "KG", + "start_date_0_0": "1", + "start_date_0_1": "1", + "start_date_0_2": "2023", + "end_date_0_0": "31", + "end_date_0_1": "12", + "end_date_0_2": "2023", + "volume_0": "500", + "start_date_1_0": "1", + "start_date_1_1": "1", + "start_date_1_2": "2024", + "end_date_1_0": "31", + "end_date_1_1": "12", + "end_date_1_2": "2024", + "volume_1": "400", + "start_date_2_0": "1", + "start_date_2_1": "1", + "start_date_2_2": "2025", + "end_date_2_0": "31", + "end_date_2_1": "12", + "end_date_2_2": "2025", + "volume_2": "300", + } + + create_url = reverse( + "reference_documents:preferential_quotas_bulk_create", + kwargs={"pk": ref_doc_version.pk}, + ) + + resp = valid_user_client.post(create_url, data) + assert resp.status_code == 403 + assert not ref_doc_version.preferential_quotas() @pytest.mark.reference_documents -def test_quota_bulk_create_without_permission(valid_user_client): - """Test that posting the bulk create form without relevant user permissions - does not work.""" - ref_doc_version = factories.ReferenceDocumentVersionFactory.create() - preferential_quota_order_number = ( - factories.PreferentialQuotaOrderNumberFactory.create( - reference_document_version=ref_doc_version, - ) - ) - assert not ref_doc_version.preferential_quotas() - - data = { - "preferential_quota_order_number": preferential_quota_order_number.pk, - "commodity_codes": "1234567890\r\n2345678901", - "quota_duty_rate": "5%", - "measurement": "KG", - "start_date_0_0": "1", - "start_date_0_1": "1", - "start_date_0_2": "2023", - "end_date_0_0": "31", - "end_date_0_1": "12", - "end_date_0_2": "2023", - "volume_0": "500", - "start_date_1_0": "1", - "start_date_1_1": "1", - "start_date_1_2": "2024", - "end_date_1_0": "31", - "end_date_1_1": "12", - "end_date_1_2": "2024", - "volume_1": "400", - "start_date_2_0": "1", - "start_date_2_1": "1", - "start_date_2_2": "2025", - "end_date_2_0": "31", - "end_date_2_1": "12", - "end_date_2_2": "2025", - "volume_2": "300", - } - - create_url = reverse( - "reference_documents:preferential_quotas_bulk_create", - kwargs={"pk": ref_doc_version.pk}, - ) - - response = valid_user_client.post(create_url, data) - assert response.status_code == 403 - assert not ref_doc_version.preferential_quotas() +class TestPreferentialQuotaDelete: + def test_get_without_permissions(self, valid_user_client): + pref_quota = factories.PreferentialQuotaFactory.create() + ref_doc_version = ( + pref_quota.preferential_quota_order_number.reference_document_version + ) + + resp = valid_user_client.get( + reverse( + "reference_documents:preferential_quotas_delete", + kwargs={ + "pk": pref_quota.pk, + "version_pk": ref_doc_version.pk, + }, + ), + ) + assert resp.status_code == 403 + + def test_get_with_permissions(self, superuser_client): + pref_quota = factories.PreferentialQuotaFactory.create() + ref_doc_version = ( + pref_quota.preferential_quota_order_number.reference_document_version + ) + + resp = superuser_client.get( + reverse( + "reference_documents:preferential_quotas_delete", + kwargs={ + "pk": pref_quota.pk, + "version_pk": ref_doc_version.pk, + }, + ), + ) + + assert resp.status_code == 200 + + def test_post_with_permission(self, superuser_client): + pref_quota = factories.PreferentialQuotaFactory.create() + pref_quota_id = pref_quota.pk + ref_doc_version = ( + pref_quota.preferential_quota_order_number.reference_document_version + ) + + resp = superuser_client.post( + reverse( + "reference_documents:preferential_quotas_delete", + kwargs={ + "pk": pref_quota.pk, + "version_pk": ref_doc_version.pk, + }, + ), + {}, + ) + + assert resp.status_code == 302 + results = PreferentialQuota.objects.all().filter( + id=pref_quota_id, + ) + assert len(results) == 0 + + def test_post_without_permission(self, valid_user_client): + pref_quota = factories.PreferentialQuotaFactory.create() + pref_quota_id = pref_quota.pk + ref_doc_version = ( + pref_quota.preferential_quota_order_number.reference_document_version + ) + + resp = valid_user_client.post( + reverse( + "reference_documents:preferential_quotas_delete", + kwargs={ + "pk": pref_quota.pk, + "version_pk": ref_doc_version.pk, + }, + ), + {}, + ) + + assert resp.status_code == 403 + results = PreferentialQuota.objects.all().filter( + id=pref_quota_id, + ) + assert len(results) == 1 diff --git a/reference_documents/tests/test_preferential_rates_views.py b/reference_documents/tests/test_preferential_rates_views.py index a3cad0b61..f58309080 100644 --- a/reference_documents/tests/test_preferential_rates_views.py +++ b/reference_documents/tests/test_preferential_rates_views.py @@ -1,11 +1,11 @@ import pytest -from django.contrib.auth.models import Permission from django.urls import reverse from reference_documents.forms.preferential_rate_forms import ( PreferentialRateCreateUpdateForm, ) from reference_documents.tests import factories +from reference_documents.views.preferential_rate_views import PreferentialRateCreate from reference_documents.views.preferential_rate_views import PreferentialRateEdit pytestmark = pytest.mark.django_db @@ -14,11 +14,10 @@ @pytest.mark.reference_documents class TestPreferentialRateEditView: @pytest.mark.parametrize( - "has_permissions, user_type, expected_http_status", + "user_type, expected_http_status", [ - (["change_preferentialrate"], "regular", 200), - ([], "regular", 403), - ([], "superuser", 200), + ("regular", 403), + ("superuser", 200), ], ) def test_get( @@ -26,7 +25,6 @@ def test_get( valid_user, superuser, client, - has_permissions, user_type, expected_http_status, ): @@ -35,22 +33,17 @@ def test_get( else: user = valid_user - for permission in has_permissions: - user.user_permissions.add( - Permission.objects.get(codename=permission), - ) - client.force_login(user) pref_rate = factories.PreferentialRateFactory.create() - response = client.get( + resp = client.get( reverse( "reference_documents:preferential_rates_edit", kwargs={"pk": pref_rate.pk}, ), ) - assert response.status_code == expected_http_status + assert resp.status_code == expected_http_status def test_success_url(self): pref_rate = factories.PreferentialRateFactory.create() @@ -103,6 +96,123 @@ def test_form_invalid(self): target.form_valid(form) +@pytest.mark.reference_documents +class TestPreferentialRateCreate: + @pytest.mark.parametrize( + "user_type, expected_http_status", + [ + ("regular", 403), + ("superuser", 200), + ], + ) + def test_get( + self, + valid_user, + superuser, + client, + user_type, + expected_http_status, + ): + if user_type == "superuser": + user = superuser + else: + user = valid_user + + client.force_login(user) + ref_doc_ver = factories.ReferenceDocumentVersionFactory.create() + + resp = client.get( + reverse( + "reference_documents:preferential_rates_create", + kwargs={"version_pk": ref_doc_ver.pk}, + ), + ) + + assert resp.status_code == expected_http_status + + def test_success_url(self): + pref_rate = factories.PreferentialRateFactory.create() + target = PreferentialRateCreate() + target.object = pref_rate + assert target.get_success_url() == reverse( + "reference_documents:version-details", + args=[target.object.reference_document_version.pk], + ) + + @pytest.mark.parametrize( + "user_type, expected_http_status", + [ + ("regular", 403), + ("superuser", 200), + ], + ) + def test_post( + self, + valid_user, + superuser, + client, + user_type, + expected_http_status, + ): + if user_type == "superuser": + user = superuser + else: + user = valid_user + + client.force_login(user) + ref_doc_ver = factories.ReferenceDocumentVersionFactory.create() + + post_data = { + "reference_document_version": ref_doc_ver.pk, + "start_date_0": 1, + "start_date_1": 1, + "start_date_2": 2024, + "commodity_code": "1231231230", + "duty_rate": "12.5%", + } + + resp = client.post( + reverse( + "reference_documents:preferential_rates_create", + kwargs={"version_pk": ref_doc_ver.pk}, + ), + data=post_data, + ) + + assert resp.status_code == expected_http_status + + @pytest.mark.reference_documents class TestPreferentialRateDeleteView: - pass + @pytest.mark.parametrize( + "http_method, user_type, expected_http_status", + [ + ("get", "regular", 403), + ("get", "superuser", 200), + ("post", "regular", 403), + ("post", "superuser", 302), + ], + ) + def test_get_without_permissions( + self, + valid_user_client, + superuser_client, + http_method, + user_type, + expected_http_status, + ): + pref_rate = factories.PreferentialRateFactory.create() + + client = superuser_client + if user_type == "regular": + client = valid_user_client + + resp = getattr(client, http_method)( + reverse( + "reference_documents:preferential_rates_delete", + kwargs={ + "pk": pref_rate.pk, + }, + ), + ) + assert resp.status_code == expected_http_status diff --git a/reference_documents/tests/test_reference_documents_models.py b/reference_documents/tests/test_reference_documents_models.py index a3d434811..8faad368f 100644 --- a/reference_documents/tests/test_reference_documents_models.py +++ b/reference_documents/tests/test_reference_documents_models.py @@ -12,8 +12,8 @@ def test_init(self): target = ReferenceDocument() assert target.created_at is None - assert target.title is "" - assert target.area_id is "" + assert target.title == "" + assert target.area_id == "" def test_get_area_name_by_area_id_no_match_to_database(self): target = ReferenceDocument() diff --git a/reference_documents/urls.py b/reference_documents/urls.py index 99c32397c..58fdeafac 100644 --- a/reference_documents/urls.py +++ b/reference_documents/urls.py @@ -209,7 +209,7 @@ name="preferential_rates_edit", ), path( - "reference_document_versions//create_preferential_rates/", + "reference_document_versions//create_preferential_rates/", PreferentialRateCreate.as_view(), name="preferential_rates_create", ), diff --git a/reference_documents/views/preferential_quota_views.py b/reference_documents/views/preferential_quota_views.py index 9f79ab3fe..270a3ba60 100644 --- a/reference_documents/views/preferential_quota_views.py +++ b/reference_documents/views/preferential_quota_views.py @@ -25,6 +25,14 @@ class PreferentialQuotaEdit(PermissionRequiredMixin, UpdateView): model = PreferentialQuota form_class = PreferentialQuotaCreateUpdateForm + def get_success_url(self): + reverse( + "reference_documents:version-details", + args=[ + self.get_object().preferential_quota_order_number.reference_document_version.pk, + ], + ) + def get_form_kwargs(self): kwargs = super(PreferentialQuotaEdit, self).get_form_kwargs() kwargs["reference_document_version"] = PreferentialQuota.objects.get( @@ -35,18 +43,18 @@ def get_form_kwargs(self): ).preferential_quota_order_number return kwargs - def post(self, request, *args, **kwargs): - quota = self.get_object() - quota.save() - return redirect( - reverse( - "reference_documents:version-details", - args=[ - quota.preferential_quota_order_number.reference_document_version.pk, - ], - ) - + "#tariff-quotas", - ) + # def post(self, request, *args, **kwargs): + # quota = self.get_object() + # quota.save() + # return redirect( + # reverse( + # "reference_documents:version-details", + # args=[ + # quota.preferential_quota_order_number.reference_document_version.pk, + # ], + # ) + # + "#tariff-quotas", + # ) class PreferentialQuotaCreate(PermissionRequiredMixin, CreateView): From 4e714ad963329b8a06945652e6ad9e790f0a8790 Mon Sep 17 00:00:00 2001 From: Doug Mills Date: Tue, 2 Apr 2024 14:31:42 +0100 Subject: [PATCH 117/118] added more tests --- .../tests/test_preferential_rates_views.py | 41 ++++++++++++++----- .../views/preferential_rate_views.py | 2 +- 2 files changed, 32 insertions(+), 11 deletions(-) diff --git a/reference_documents/tests/test_preferential_rates_views.py b/reference_documents/tests/test_preferential_rates_views.py index f58309080..f4fa20c00 100644 --- a/reference_documents/tests/test_preferential_rates_views.py +++ b/reference_documents/tests/test_preferential_rates_views.py @@ -143,7 +143,7 @@ def test_success_url(self): "user_type, expected_http_status", [ ("regular", 403), - ("superuser", 200), + ("superuser", 302), ], ) def test_post( @@ -185,27 +185,48 @@ def test_post( @pytest.mark.reference_documents class TestPreferentialRateDeleteView: @pytest.mark.parametrize( - "http_method, user_type, expected_http_status", + "http_method, expected_http_status", [ - ("get", "regular", 403), - ("get", "superuser", 200), - ("post", "regular", 403), - ("post", "superuser", 302), + ("get", 200), + ("post", 302), ], ) def test_get_without_permissions( self, - valid_user_client, superuser_client, http_method, - user_type, expected_http_status, ): pref_rate = factories.PreferentialRateFactory.create() client = superuser_client - if user_type == "regular": - client = valid_user_client + + resp = getattr(client, http_method)( + reverse( + "reference_documents:preferential_rates_delete", + kwargs={ + "pk": pref_rate.pk, + }, + ), + ) + assert resp.status_code == expected_http_status + + @pytest.mark.parametrize( + "http_method, expected_http_status", + [ + ("get", 403), + ("post", 403), + ], + ) + def test_regular_user_get_post( + self, + valid_user_client, + http_method, + expected_http_status, + ): + pref_rate = factories.PreferentialRateFactory.create() + + client = valid_user_client resp = getattr(client, http_method)( reverse( diff --git a/reference_documents/views/preferential_rate_views.py b/reference_documents/views/preferential_rate_views.py index cfb186266..568570e5d 100644 --- a/reference_documents/views/preferential_rate_views.py +++ b/reference_documents/views/preferential_rate_views.py @@ -40,7 +40,7 @@ def get_success_url(self): def form_valid(self, form): instance = form.instance reference_document_version = ReferenceDocumentVersion.objects.get( - pk=self.kwargs["pk"], + pk=self.kwargs["version_pk"], ) instance.order = len(reference_document_version.preferential_rates.all()) + 1 instance.reference_document_version = reference_document_version From 6a246f07ff96a6369771889120dcbcee33e11af6 Mon Sep 17 00:00:00 2001 From: Matthew McKenzie <97194636+mattjamc@users.noreply.github.com> Date: Thu, 4 Apr 2024 14:05:51 +0100 Subject: [PATCH 118/118] Quota order number check --- reference_documents/checks/base.py | 11 +++++++++++ .../checks/preferential_quota_order_numbers.py | 12 ++++++++++++ 2 files changed, 23 insertions(+) diff --git a/reference_documents/checks/base.py b/reference_documents/checks/base.py index 058bb427e..dda97f610 100644 --- a/reference_documents/checks/base.py +++ b/reference_documents/checks/base.py @@ -7,6 +7,7 @@ from common.models import Transaction from geo_areas.models import GeographicalArea from geo_areas.models import GeographicalAreaDescription +from quotas.models import QuotaOrderNumber from reference_documents.models import PreferentialQuota from reference_documents.models import PreferentialQuotaOrderNumber from reference_documents.models import PreferentialRate @@ -31,6 +32,16 @@ def __init__(self, preferential_quota_order_number: PreferentialQuotaOrderNumber super().__init__() self.preferential_quota_order_number = preferential_quota_order_number + def order_number(self): + try: + order_number = QuotaOrderNumber.objects.all().get( + order_number=self.preferential_quota_order_number.quota_order_number, + valid_between=self.preferential_quota_order_number.valid_between, + ) + return order_number + except QuotaOrderNumber.DoesNotExist: + return None + class BasePreferentialRateCheck(BaseCheck): def __init__(self, preferential_rate: PreferentialRate): diff --git a/reference_documents/checks/preferential_quota_order_numbers.py b/reference_documents/checks/preferential_quota_order_numbers.py index 8b1378917..26a4f9d0c 100644 --- a/reference_documents/checks/preferential_quota_order_numbers.py +++ b/reference_documents/checks/preferential_quota_order_numbers.py @@ -1 +1,13 @@ +from reference_documents.checks.base import BasePreferentialQuotaOrderNumberCheck +from reference_documents.models import AlignmentReportCheckStatus + +class OrderNumberExists(BasePreferentialQuotaOrderNumberCheck): + def run_check(self): + if not self.order_number(): + message = f"order number not found" + print("FAIL", message) + return AlignmentReportCheckStatus.FAIL, message + else: + print(f"PASS - order number {self.order_number()} found") + return AlignmentReportCheckStatus.PASS, ""