Hello
", is_active=True): + """Persist a banner configuration directly for setup.""" + banner = BannerConfiguration( + banner_type=banner_type, + content=content, + is_active=is_active, + ) + banner.save() + return banner + + +def test_get_active_banner_public_returns_active(client, session): + """Public active endpoint returns the active banner without auth.""" + _create_banner(content="Active banner
", is_active=True) + session.flush() + + response = client.get(ACTIVE_URL) + + assert response.status_code == HTTPStatus.OK + data = response.get_json() + assert data["content"] == "Active banner
" + assert data["is_active"] is True + assert data["banner_type"] == "Info" + + +def test_get_active_banner_public_empty_when_none(client, session): + """Public active endpoint returns an empty object when nothing is active.""" + _create_banner(content="Inactive
", is_active=False) + session.flush() + + response = client.get(ACTIVE_URL) + + assert response.status_code == HTTPStatus.OK + assert response.get_json() == {} + + +def test_create_banner_success(client, session, jwt): + """A full_access staff user can create a banner (201).""" + auth_guid = TestJwtClaims.staff_admin_role["preferred_username"] + factory_user_model(auth_guid=auth_guid) + session.flush() + + headers = factory_auth_header(jwt=jwt, claims=TestJwtClaims.staff_admin_role) + payload = { + "banner_type": "Warning", + "content": "Heads up
", + "is_active": True, + } + + response = client.post(BANNERS_URL, json=payload, headers=headers) + + assert response.status_code == HTTPStatus.CREATED + data = response.get_json() + assert data["content"] == "Heads up
" + assert data["banner_type"] == "Warning" + assert data["is_active"] is True + + +def test_create_banner_deactivates_previous(client, session, jwt): + """Creating an active banner deactivates any previously active banner.""" + previous = _create_banner(content="Old
", is_active=True) + session.flush() + + auth_guid = TestJwtClaims.staff_admin_role["preferred_username"] + factory_user_model(auth_guid=auth_guid) + session.flush() + + headers = factory_auth_header(jwt=jwt, claims=TestJwtClaims.staff_admin_role) + payload = { + "banner_type": "Success", + "content": "New
", + "is_active": True, + } + + response = client.post(BANNERS_URL, json=payload, headers=headers) + + assert response.status_code == HTTPStatus.CREATED + session.refresh(previous) + assert previous.is_active is False + + +def test_create_banner_invalid_type_returns_422(client, session, jwt): + """An invalid banner_type is rejected with 422.""" + auth_guid = TestJwtClaims.staff_admin_role["preferred_username"] + factory_user_model(auth_guid=auth_guid) + session.flush() + + headers = factory_auth_header(jwt=jwt, claims=TestJwtClaims.staff_admin_role) + payload = {"banner_type": "Purple", "content": "x
", "is_active": True} + + response = client.post(BANNERS_URL, json=payload, headers=headers) + + assert response.status_code == HTTPStatus.UNPROCESSABLE_ENTITY + + +def test_create_banner_unauthorized(client, session): + """Missing auth header returns 401.""" + payload = {"banner_type": "Info", "content": "x
", "is_active": True} + + response = client.post(BANNERS_URL, json=payload) + + assert response.status_code == HTTPStatus.UNAUTHORIZED + + +def test_create_banner_forbidden_for_proponent(client, session, jwt): + """A proponent (no full_access) cannot create a banner (403).""" + claims = copy.deepcopy(TestJwtClaims.proponent_role.value) + headers = factory_auth_header(jwt=jwt, claims=claims) + payload = {"banner_type": "Info", "content": "x
", "is_active": True} + + response = client.post(BANNERS_URL, json=payload, headers=headers) + + assert response.status_code in (HTTPStatus.FORBIDDEN, HTTPStatus.UNAUTHORIZED) + + +def test_list_banners_returns_only_active(client, session, jwt): + """The list endpoint returns only the active banner.""" + _create_banner(content="Inactive
", is_active=False) + _create_banner(content="Active
", is_active=True) + session.flush() + + auth_guid = TestJwtClaims.staff_admin_role["preferred_username"] + factory_user_model(auth_guid=auth_guid) + session.flush() + + headers = factory_auth_header(jwt=jwt, claims=TestJwtClaims.staff_admin_role) + response = client.get(BANNERS_URL, headers=headers) + + assert response.status_code == HTTPStatus.OK + data = response.get_json() + assert isinstance(data, list) + assert len(data) == 1 + assert data[0]["content"] == "Active
" + + +def test_update_banner_success(client, session, jwt): + """A full_access staff user can update a banner (200).""" + banner = _create_banner(content="Before
", is_active=False) + session.flush() + + auth_guid = TestJwtClaims.staff_admin_role["preferred_username"] + factory_user_model(auth_guid=auth_guid) + session.flush() + + headers = factory_auth_header(jwt=jwt, claims=TestJwtClaims.staff_admin_role) + payload = {"content": "After
", "is_active": True} + + response = client.put(f"{BANNERS_URL}/{banner.id}", json=payload, headers=headers) + + assert response.status_code == HTTPStatus.OK + data = response.get_json() + assert data["content"] == "After
" + assert data["is_active"] is True + + +def test_update_banner_not_found(client, session, jwt): + """Updating a non-existent banner returns 404.""" + auth_guid = TestJwtClaims.staff_admin_role["preferred_username"] + factory_user_model(auth_guid=auth_guid) + session.flush() + + headers = factory_auth_header(jwt=jwt, claims=TestJwtClaims.staff_admin_role) + response = client.put( + f"{BANNERS_URL}/999999", json={"content": "x
"}, headers=headers + ) + + assert response.status_code == HTTPStatus.NOT_FOUND diff --git a/submit-api/tests/unit/services/test_banner_configuration_service.py b/submit-api/tests/unit/services/test_banner_configuration_service.py new file mode 100644 index 000000000..0152dfb87 --- /dev/null +++ b/submit-api/tests/unit/services/test_banner_configuration_service.py @@ -0,0 +1,118 @@ +"""Unit tests for BannerConfigurationService single-active enforcement.""" +from unittest.mock import Mock, patch + +import pytest + +from submit_api.enums.banner import BannerType +from submit_api.exceptions import ResourceNotFoundError +from submit_api.services.banner_configuration_service import BannerConfigurationService + + +MODULE_PATH = "submit_api.services.banner_configuration_service" + + +@pytest.fixture(autouse=True) +def _mock_current_app(): + """Provide a logger so the service can log without an app context.""" + with patch(f"{MODULE_PATH}.current_app", new=Mock()) as mock_app: + yield mock_app + + +class TestBannerConfigurationCreate: + """Tests for BannerConfigurationService.create.""" + + @patch(f"{MODULE_PATH}.db") + @patch(f"{MODULE_PATH}.BannerConfiguration") + def test_create_active_deactivates_others(self, mock_model, mock_db): + """Creating an active banner deactivates all existing banners first.""" + created = Mock() + created.id = 5 + mock_model.create_banner.return_value = created + + data = { + "banner_type": BannerType.INFO, + "content": "Hello
", + "is_active": True, + } + result = BannerConfigurationService.create(data) + + mock_model.deactivate_all.assert_called_once_with() + mock_model.create_banner.assert_called_once() + mock_db.session.commit.assert_called_once() + assert result is created + + @patch(f"{MODULE_PATH}.db") + @patch(f"{MODULE_PATH}.BannerConfiguration") + def test_create_inactive_does_not_deactivate(self, mock_model, mock_db): + """Creating an inactive banner leaves other banners untouched.""" + mock_model.create_banner.return_value = Mock(id=6) + + data = { + "banner_type": BannerType.WARNING, + "content": "Later
", + "is_active": False, + } + BannerConfigurationService.create(data) + + mock_model.deactivate_all.assert_not_called() + mock_db.session.commit.assert_called_once() + + +class TestBannerConfigurationUpdate: + """Tests for BannerConfigurationService.update.""" + + @patch(f"{MODULE_PATH}.db") + @patch(f"{MODULE_PATH}.BannerConfiguration") + def test_update_activating_deactivates_others(self, mock_model, mock_db): + """Updating a banner to active deactivates every other banner.""" + banner = Mock() + banner.id = 3 + mock_model.find_by_id.return_value = banner + + result = BannerConfigurationService.update(3, {"is_active": True}) + + mock_model.deactivate_all.assert_called_once_with(exclude_id=3) + assert banner.is_active is True + mock_db.session.commit.assert_called_once() + assert result is banner + + @patch(f"{MODULE_PATH}.db") + @patch(f"{MODULE_PATH}.BannerConfiguration") + def test_update_content_and_type(self, mock_model, mock_db): + """Updating content and type mutates the banner without deactivating.""" + banner = Mock() + banner.id = 9 + mock_model.find_by_id.return_value = banner + + BannerConfigurationService.update( + 9, {"banner_type": BannerType.SUCCESS, "content": "New
"} + ) + + assert banner.banner_type == BannerType.SUCCESS + assert banner.content == "New
" + mock_model.deactivate_all.assert_not_called() + mock_db.session.commit.assert_called_once() + + @patch(f"{MODULE_PATH}.db") + @patch(f"{MODULE_PATH}.BannerConfiguration") + def test_update_missing_banner_raises_not_found(self, mock_model, mock_db): + """Updating a non-existent banner raises ResourceNotFoundError.""" + mock_model.find_by_id.return_value = None + + with pytest.raises(ResourceNotFoundError): + BannerConfigurationService.update(404, {"content": "x
"}) + + mock_db.session.commit.assert_not_called() + + +class TestBannerConfigurationGetActive: + """Tests for BannerConfigurationService.get_active.""" + + @patch(f"{MODULE_PATH}.BannerConfiguration") + def test_get_active_delegates_to_model(self, mock_model): + """get_active returns whatever the model reports as active.""" + active = Mock() + mock_model.get_active.return_value = active + + assert BannerConfigurationService.get_active() is active + mock_model.get_active.assert_called_once_with() diff --git a/submit-web/package-lock.json b/submit-web/package-lock.json index eee803b4f..87aa1d3f2 100644 --- a/submit-web/package-lock.json +++ b/submit-web/package-lock.json @@ -12,6 +12,11 @@ "@emotion/styled": "^11.11.5", "@epic/centre-analytics": "git+https://github.com/bcgov/EPIC.analytics.git#v1.0.0", "@hookform/resolvers": "^3.9.0", + "@lexical/html": "^0.23.1", + "@lexical/link": "^0.23.1", + "@lexical/list": "^0.23.1", + "@lexical/react": "^0.23.1", + "@lexical/utils": "^0.23.1", "@mui/icons-material": "^5.15.21", "@mui/material": "^5.15.20", "@mui/x-date-pickers": "^7.19.0", @@ -25,6 +30,7 @@ "epic.theme": "latest", "jwt-decode": "^4.0.0", "keycloak-js": "^25.0.1", + "lexical": "^0.23.1", "maplibre-gl": "^5.20.2", "oidc-client-ts": "^3.0.1", "react": "^18.2.0", @@ -1449,6 +1455,241 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@lexical/clipboard": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@lexical/clipboard/-/clipboard-0.23.1.tgz", + "integrity": "sha512-MT8IXl1rhTe8VcwnkhgFtWra6sRYNsl/I7nE9aw6QxwvPReKmRDmyBmEIeXwnKSGHRe19OJhu4/A9ciKPyVdMA==", + "dependencies": { + "@lexical/html": "0.23.1", + "@lexical/list": "0.23.1", + "@lexical/selection": "0.23.1", + "@lexical/utils": "0.23.1", + "lexical": "0.23.1" + } + }, + "node_modules/@lexical/code": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@lexical/code/-/code-0.23.1.tgz", + "integrity": "sha512-TOxaFAwoewrX3rHp4Po+u1LJT8oteP/6Kn2z6j9DaynBW62gIqTuSAFcMPysVx/Puq5hhJHPRD/be9RWDteDZw==", + "dependencies": { + "@lexical/utils": "0.23.1", + "lexical": "0.23.1", + "prismjs": "^1.27.0" + } + }, + "node_modules/@lexical/devtools-core": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@lexical/devtools-core/-/devtools-core-0.23.1.tgz", + "integrity": "sha512-QsgcrECy11ZHhWAfyNW/ougXFF1o0EuQnhFybgTdqQmw0rJ2ZgPLpPjD5lws3CE8mP8g5knBV4/cyxvv42fzzg==", + "dependencies": { + "@lexical/html": "0.23.1", + "@lexical/link": "0.23.1", + "@lexical/mark": "0.23.1", + "@lexical/table": "0.23.1", + "@lexical/utils": "0.23.1", + "lexical": "0.23.1" + }, + "peerDependencies": { + "react": ">=17.x", + "react-dom": ">=17.x" + } + }, + "node_modules/@lexical/dragon": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@lexical/dragon/-/dragon-0.23.1.tgz", + "integrity": "sha512-ZoY9VJDrTpO69sinRhIs3RlPAWviy4mwnC7lqtM77/pVK0Kaknv7z2iDqv+414PKQCgUhyoXp7PfYXu/3yb6LQ==", + "dependencies": { + "lexical": "0.23.1" + } + }, + "node_modules/@lexical/hashtag": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@lexical/hashtag/-/hashtag-0.23.1.tgz", + "integrity": "sha512-EkRCHV/IQwKlggy3VQDF9b4Krc9DKNZEjXe84CkEVrRpQSOwXi0qORzuaAipARyN632WKLSXOZJmNzkUNocJ6A==", + "dependencies": { + "@lexical/utils": "0.23.1", + "lexical": "0.23.1" + } + }, + "node_modules/@lexical/history": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@lexical/history/-/history-0.23.1.tgz", + "integrity": "sha512-5Vro4bIePw37MwffpvPm56WlwPdlY/u+fVkvXsxdhK9bqiFesmLZhBirokDPvJEMP35V59kzmN5mmWXSYfuRpg==", + "dependencies": { + "@lexical/utils": "0.23.1", + "lexical": "0.23.1" + } + }, + "node_modules/@lexical/html": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@lexical/html/-/html-0.23.1.tgz", + "integrity": "sha512-kNkDUaDe/Awypaw8JZn65BzT1gwNj2bNkaGFcmIkXUrTtiqlvgYvKvJeOKLkoAb/i2xq990ZAbHOsJrJm1jMbw==", + "dependencies": { + "@lexical/selection": "0.23.1", + "@lexical/utils": "0.23.1", + "lexical": "0.23.1" + } + }, + "node_modules/@lexical/link": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@lexical/link/-/link-0.23.1.tgz", + "integrity": "sha512-HRaOp7prtcbHjbgq8AjJ4O02jYb8pTeS8RrGcgIRhCOq3/EcsSb1dXMwuraqmh9oxbuFyEu/JE31EFksiOW6qA==", + "dependencies": { + "@lexical/utils": "0.23.1", + "lexical": "0.23.1" + } + }, + "node_modules/@lexical/list": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@lexical/list/-/list-0.23.1.tgz", + "integrity": "sha512-TI3WyWk3avv9uaJwaq8V+m9zxLRgnzXDYNS0rREafnW09rDpaFkpVmDuX+PZVR3NqPlwVt+slWVSBuyfguAFbA==", + "dependencies": { + "@lexical/utils": "0.23.1", + "lexical": "0.23.1" + } + }, + "node_modules/@lexical/mark": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@lexical/mark/-/mark-0.23.1.tgz", + "integrity": "sha512-E7cMOBVMrNGMw0LsyWKNFQZ5Io3bUIHCC3aCUdH24z1XWnuTmDFKMqNrphywPniO7pzSgVyGpkQBZIAIN76+YA==", + "dependencies": { + "@lexical/utils": "0.23.1", + "lexical": "0.23.1" + } + }, + "node_modules/@lexical/markdown": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@lexical/markdown/-/markdown-0.23.1.tgz", + "integrity": "sha512-TQx8oXenaiVYffBPxD85m4CydbDAuYOonATiABAFG6CHkA6vi898M1TCTgVDS6/iISjtjQpqHo0SW7YjLt14jw==", + "dependencies": { + "@lexical/code": "0.23.1", + "@lexical/link": "0.23.1", + "@lexical/list": "0.23.1", + "@lexical/rich-text": "0.23.1", + "@lexical/text": "0.23.1", + "@lexical/utils": "0.23.1", + "lexical": "0.23.1" + } + }, + "node_modules/@lexical/offset": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@lexical/offset/-/offset-0.23.1.tgz", + "integrity": "sha512-ylw5egME/lldacVXDoRsdGDXPuk9lGmYgcqx/aITGrSymav+RDjQoAapHbz1HQqGmm/m18+VLaWTdjtkbrIN6g==", + "dependencies": { + "lexical": "0.23.1" + } + }, + "node_modules/@lexical/overflow": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@lexical/overflow/-/overflow-0.23.1.tgz", + "integrity": "sha512-WubTqozpxOeyTm/tKIHXinsjuRcgPESacOvu93dS+sC7q3n+xeBIu5FL7lM6bbsk3zNtNJQ9sG0svZngmWRjCw==", + "dependencies": { + "lexical": "0.23.1" + } + }, + "node_modules/@lexical/plain-text": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@lexical/plain-text/-/plain-text-0.23.1.tgz", + "integrity": "sha512-tM4DJw+HyT9XV4BKGVECDnejcC//jsFggjFmJgwIMTCxJPiGXEEZLZTXmGqf8QdFZ6cH1I5bhreZPQUWu6dRvg==", + "dependencies": { + "@lexical/clipboard": "0.23.1", + "@lexical/selection": "0.23.1", + "@lexical/utils": "0.23.1", + "lexical": "0.23.1" + } + }, + "node_modules/@lexical/react": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@lexical/react/-/react-0.23.1.tgz", + "integrity": "sha512-g5CQMOiK+Djqp75UaSFUceHZEUQVIXBzWBuVR69pCiptCgNqN3CNAoIxy0hTTaVrLq6S0SCjUOduBDtioN0bLA==", + "dependencies": { + "@lexical/clipboard": "0.23.1", + "@lexical/code": "0.23.1", + "@lexical/devtools-core": "0.23.1", + "@lexical/dragon": "0.23.1", + "@lexical/hashtag": "0.23.1", + "@lexical/history": "0.23.1", + "@lexical/link": "0.23.1", + "@lexical/list": "0.23.1", + "@lexical/mark": "0.23.1", + "@lexical/markdown": "0.23.1", + "@lexical/overflow": "0.23.1", + "@lexical/plain-text": "0.23.1", + "@lexical/rich-text": "0.23.1", + "@lexical/selection": "0.23.1", + "@lexical/table": "0.23.1", + "@lexical/text": "0.23.1", + "@lexical/utils": "0.23.1", + "@lexical/yjs": "0.23.1", + "lexical": "0.23.1", + "react-error-boundary": "^3.1.4" + }, + "peerDependencies": { + "react": ">=17.x", + "react-dom": ">=17.x" + } + }, + "node_modules/@lexical/rich-text": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@lexical/rich-text/-/rich-text-0.23.1.tgz", + "integrity": "sha512-Y77HGxdF5aemjw/H44BXETD5KNeaNdwMRu9P7IrlK7cC1dvvimzL2D6ezbub5i7F1Ef5T0quOXjwK056vrqaKQ==", + "dependencies": { + "@lexical/clipboard": "0.23.1", + "@lexical/selection": "0.23.1", + "@lexical/utils": "0.23.1", + "lexical": "0.23.1" + } + }, + "node_modules/@lexical/selection": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@lexical/selection/-/selection-0.23.1.tgz", + "integrity": "sha512-xoehAURMZJZYf046GHUXiv8FSv5zTobhwDD2dML4fmNHPp9NxugkWHlNUinTK/b+jGgjSYVsqpEKPBmue4ZHdQ==", + "dependencies": { + "lexical": "0.23.1" + } + }, + "node_modules/@lexical/table": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@lexical/table/-/table-0.23.1.tgz", + "integrity": "sha512-Qs+iuwSVkV4OGTt+JdL9hvyl/QO3X9waH70L5Fxu9JmQk/jLl02tIGXbE38ocJkByfpyk4PrphoXt6l7CugJZA==", + "dependencies": { + "@lexical/clipboard": "0.23.1", + "@lexical/utils": "0.23.1", + "lexical": "0.23.1" + } + }, + "node_modules/@lexical/text": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@lexical/text/-/text-0.23.1.tgz", + "integrity": "sha512-aOuuAhmc+l2iSK99uP0x/Zg9LSQswQdNG3IxzGa0rTx844mWUHuEbAUaOqqlgDA1/zZ0WjObyhPfZJL775y63g==", + "dependencies": { + "lexical": "0.23.1" + } + }, + "node_modules/@lexical/utils": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@lexical/utils/-/utils-0.23.1.tgz", + "integrity": "sha512-yXEkF6fj32+mJblCoP0ZT/vA0S05FA0nRUkVrvGX6sbZ9y+cIzuIbBoHi4z1ytutcWHQrwCK4TsN9hPYBIlb2w==", + "dependencies": { + "@lexical/list": "0.23.1", + "@lexical/selection": "0.23.1", + "@lexical/table": "0.23.1", + "lexical": "0.23.1" + } + }, + "node_modules/@lexical/yjs": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@lexical/yjs/-/yjs-0.23.1.tgz", + "integrity": "sha512-ygodSxmC65srNicMIhqBRIXI2LHhmnHcR1EO9fLO7flZWGCR1HIoeGmwhHo9FLgJoc5LHanV+dE0z1onFo1qqQ==", + "dependencies": { + "@lexical/offset": "0.23.1", + "@lexical/selection": "0.23.1", + "lexical": "0.23.1" + }, + "peerDependencies": { + "yjs": ">=13.5.22" + } + }, "node_modules/@mapbox/jsonlint-lines-primitives": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/@mapbox/jsonlint-lines-primitives/-/jsonlint-lines-primitives-2.0.2.tgz", @@ -1927,6 +2168,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1940,6 +2182,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1953,6 +2196,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1966,6 +2210,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1979,6 +2224,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1992,6 +2238,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2005,6 +2252,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2018,6 +2266,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2031,6 +2280,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2044,6 +2294,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2057,6 +2308,7 @@ "cpu": [ "loong64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2070,6 +2322,7 @@ "cpu": [ "loong64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2083,6 +2336,7 @@ "cpu": [ "ppc64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2096,6 +2350,7 @@ "cpu": [ "ppc64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2109,6 +2364,7 @@ "cpu": [ "riscv64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2122,6 +2378,7 @@ "cpu": [ "riscv64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2135,6 +2392,7 @@ "cpu": [ "s390x" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2148,6 +2406,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2161,6 +2420,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2174,6 +2434,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2187,6 +2448,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2200,6 +2462,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2213,6 +2476,7 @@ "cpu": [ "ia32" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2226,6 +2490,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2239,6 +2504,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2991,7 +3257,7 @@ "version": "20.14.14", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.14.tgz", "integrity": "sha512-d64f00982fS9YoOgJkAMolK7MN8Iq3TDdVjchbYHdEmjth/DHowx82GnoA+tVUAN+7vxfYUgAzi+JXbKNd2SDQ==", - "devOptional": true, + "dev": true, "dependencies": { "undici-types": "~5.26.4" } @@ -5453,7 +5719,7 @@ "version": "4.3.9", "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.9.tgz", "integrity": "sha512-ObHy4YN7ycwZOUCLI1/6svfyAFu7vL8RhAvVu/bh/RZW9EPlOyDaQ9jDQWCtdqzaXUjgXZCW1migtHE7YI7UGQ==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/import-fresh": { @@ -5659,6 +5925,16 @@ "node": ">=0.10.0" } }, + "node_modules/isomorphic.js": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/isomorphic.js/-/isomorphic.js-0.2.5.tgz", + "integrity": "sha512-PIeMbHqMt4DnUP3MA/Flc0HElYjMXArsw1qwJZcm9sqR8mq3l8NYizFMty0pWwE/tzIGH3EKK5+jes5mAr85yw==", + "peer": true, + "funding": { + "type": "GitHub Sponsors ❤", + "url": "https://github.com/sponsors/dmonad" + } + }, "node_modules/istanbul-lib-coverage": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", @@ -6004,6 +6280,32 @@ "node": ">= 0.8.0" } }, + "node_modules/lexical": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/lexical/-/lexical-0.23.1.tgz", + "integrity": "sha512-iuS72HcAYUemsCRQCm4XZzkGhZb8a9KagW+ee2TFfkkf9f3ZpUYSrobMpjYVZRkgMOx7Zk5VCPMxm1nouJTfnQ==" + }, + "node_modules/lib0": { + "version": "0.2.117", + "resolved": "https://registry.npmjs.org/lib0/-/lib0-0.2.117.tgz", + "integrity": "sha512-DeXj9X5xDCjgKLU/7RR+/HQEVzuuEUiwldwOGsHK/sfAfELGWEyTcf0x+uOvCvK3O2zPmZePXWL85vtia6GyZw==", + "peer": true, + "dependencies": { + "isomorphic.js": "^0.2.4" + }, + "bin": { + "0ecdsa-generate-keypair": "bin/0ecdsa-generate-keypair.js", + "0gentesthtml": "bin/gentesthtml.js", + "0serve": "bin/0serve.js" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "type": "GitHub Sponsors ❤", + "url": "https://github.com/sponsors/dmonad" + } + }, "node_modules/lines-and-columns": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", @@ -7098,6 +7400,14 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/prismjs": { + "version": "1.30.0", + "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.30.0.tgz", + "integrity": "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==", + "engines": { + "node": ">=6" + } + }, "node_modules/process-on-spawn": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/process-on-spawn/-/process-on-spawn-1.0.0.tgz", @@ -7219,6 +7529,21 @@ "react": ">= 16.8 || 18.0.0" } }, + "node_modules/react-error-boundary": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/react-error-boundary/-/react-error-boundary-3.1.4.tgz", + "integrity": "sha512-uM9uPzZJTF6wRQORmSrvOIgt4lJ9MC1sNgEOj2XGsDTRE4kmpWxg7ENK9EWNKJRMAOY9z0MuF4yIfl6gp4sotA==", + "dependencies": { + "@babel/runtime": "^7.12.5" + }, + "engines": { + "node": ">=10", + "npm": ">=6" + }, + "peerDependencies": { + "react": ">=16.13.1" + } + }, "node_modules/react-hook-form": { "version": "7.52.2", "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.52.2.tgz", @@ -7569,7 +7894,7 @@ "version": "1.77.8", "resolved": "https://registry.npmjs.org/sass/-/sass-1.77.8.tgz", "integrity": "sha512-4UHg6prsrycW20fqLGPShtEvo/WyHRVRHwOP4DzkUrObWoWI05QBSfzU71TVB7PFaL104TwNaHpjlWXAZbQiNQ==", - "devOptional": true, + "dev": true, "dependencies": { "chokidar": ">=3.0.0 <4.0.0", "immutable": "^4.0.0", @@ -8146,7 +8471,7 @@ "version": "5.26.5", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", - "devOptional": true + "dev": true }, "node_modules/unicorn-magic": { "version": "0.3.0", @@ -8502,6 +8827,21 @@ } } }, + "node_modules/vite-tsconfig-paths/node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "optional": true, + "peer": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, "node_modules/vite/node_modules/@esbuild/aix-ppc64": { "version": "0.25.12", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", @@ -8509,6 +8849,7 @@ "cpu": [ "ppc64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -8525,6 +8866,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -8541,6 +8883,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -8557,6 +8900,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -8573,6 +8917,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -8589,6 +8934,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -8605,6 +8951,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -8621,6 +8968,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -8637,6 +8985,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -8653,6 +9002,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -8669,6 +9019,7 @@ "cpu": [ "ia32" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -8685,6 +9036,7 @@ "cpu": [ "loong64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -8701,6 +9053,7 @@ "cpu": [ "mips64el" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -8717,6 +9070,7 @@ "cpu": [ "ppc64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -8733,6 +9087,7 @@ "cpu": [ "riscv64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -8749,6 +9104,7 @@ "cpu": [ "s390x" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -8765,6 +9121,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -8781,6 +9138,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -8797,6 +9155,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -8813,6 +9172,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -8829,6 +9189,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -8845,6 +9206,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -8861,6 +9223,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -8877,6 +9240,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -8893,6 +9257,7 @@ "cpu": [ "ia32" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -8909,6 +9274,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -9367,6 +9733,23 @@ "node": ">=8" } }, + "node_modules/yjs": { + "version": "13.6.32", + "resolved": "https://registry.npmjs.org/yjs/-/yjs-13.6.32.tgz", + "integrity": "sha512-lfiJIIC4Xayt5ItynE407ehlE03pCjeOc4hkR4yxxvvNJ4kuiN25B0g+Qp8XagYz361LLL7DCzR5bvFJ81QKtQ==", + "peer": true, + "dependencies": { + "lib0": "^0.2.99" + }, + "engines": { + "node": ">=16.0.0", + "npm": ">=8.0.0" + }, + "funding": { + "type": "GitHub Sponsors ❤", + "url": "https://github.com/sponsors/dmonad" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/submit-web/package.json b/submit-web/package.json index e7287620e..003a60ba1 100644 --- a/submit-web/package.json +++ b/submit-web/package.json @@ -20,6 +20,11 @@ "@emotion/styled": "^11.11.5", "@epic/centre-analytics": "git+https://github.com/bcgov/EPIC.analytics.git#v1.0.0", "@hookform/resolvers": "^3.9.0", + "@lexical/html": "^0.23.1", + "@lexical/link": "^0.23.1", + "@lexical/list": "^0.23.1", + "@lexical/react": "^0.23.1", + "@lexical/utils": "^0.23.1", "@mui/icons-material": "^5.15.21", "@mui/material": "^5.15.20", "@mui/x-date-pickers": "^7.19.0", @@ -33,6 +38,7 @@ "epic.theme": "latest", "jwt-decode": "^4.0.0", "keycloak-js": "^25.0.1", + "lexical": "^0.23.1", "maplibre-gl": "^5.20.2", "oidc-client-ts": "^3.0.1", "react": "^18.2.0", diff --git a/submit-web/public/docs/Holder-User-Guide-EPIC.submit-V2.0.pdf b/submit-web/public/docs/Holder-User-Guide-EPIC.submit-V2.0.pdf new file mode 100644 index 0000000000000000000000000000000000000000..323dc1a3d29ac2be1ff5b592e50fe56af1e31f65 GIT binary patch literal 1221312 zcmeEtg;!Ml8>fnbipYydO9%qe-K8|r-5@bE3>`x%-O}CN-KB$ov~)9c3=Koq-r@cI z&hDN)d(Qp=JI8b8j&twR&*zEnK&c=q&cMRRh4GTI4tQa_WFccF0~=akyyWL+QgO2d zF^L#B8d!l%nG_66K@McBz}HGlG6puLA3-)WWK7DA_D)8QD)u0d5*X}A#=_10aD%*) zqm{W0a6b$CzfZ^-*jq~3n1BJQ$V-qh8So%TP>@L$t7w-_TCh{@P}-ag z(S5(%IM*rn68@beZKJK%CQ_~jRo@P=B~5U^(yp$qFh9)3U`AS8obz>CXDj)JzN(+< zW%R90^Kt<}#|7x1gFX2DS=TS|d;UXOaR+hs=hE=yyt-^t(*S4+wv#JO0!qHUs}7R( zV1mVZuA=%}o+Fb>E!TD6n>*xkFcHK0?WG3(B-(@118uIiQvmZW+gQ~kMR~py*mf zeF$!L81}%HP5P9^$-!a&YdEV3alp#fsG;YcmwlqQbb3-|vw59v6){Ge)?9Y|t79w6 z#>VSaXl~jp`)pxmT4l~iyI}y-5IdH6i5lw(>#_WSqvdvTb|asq)}JeJajl3oeLW?k zneys+0ZJ%k3s2cxlh9n`%wygTdhr*S3HwfI`1(16n`zo@-z01|^@@PJrSVvu)WI-z zJLCK@G(t4UWS^3f!e+n9qh5qSCt7KZecJW)HRda>v>l$$Mx4gmrUtC~T@!vJ#-6RE zG3S1SdFQvtGhV69F8+--!zZSboTK 2yKQ$h{eGdSlQFy%u*@z!=TFa>maX8 z+5oFnp`~$Glzy?cm$lrOqwH6chTqBjdV lQ#Yy z*{Cwinv(ks6+wC4$~h=u(XNZIPA=w7!GM!g3ZWRTJfg z=paALWSPFUif2;v#8mkrp0?M%bZ5;jU`+V;d;RP#pot&&?m!{3jT4xLD3FT8&r=sd z9 Kl|${dHkqLK8MKJXyn%r<5NSMB6EZc{|<8Ea%f=sX?P?d$_1+e zmpIfaj9Q54rC&I}^;C3p)YSC+YUqwS>ronx+dB(y$rp^>J-0%d4k>G(3!&IOl5>~I zv%E%w9`zQ`WSN=!Z;mps0T!4AyMFU|D=RCY^8?JJzC(qqjyLkx2zH||t?W#0@2z}2 zJI!Pt7zxFJ?$W!X|T1-ipI2#b!k zpDfia=%(?u+ndf1fZwx z*LH2M1?=tZ-FGajDB}=G9joK9HQUN0w0*#AzBMb;_2OzO3N>Su_CeqE0V)7%!9LxT zyYJ8L&v`s~{qa}7ZdF{J{lK6oCGx2OeQyt|5MWC#90nQodaoQ;kB{ZYaQo-sX1%rM z74~>O9q|X>XCoJ;-Fh#cyIc&&p*Wte_iygH-&{yzkVh%8SYdX=hIC6Y_ktw2xVQ)? z7pC&$lTxb8sfFBykAT3(bsrT4*h&VogrH7s&c}uV`YN^iY4b7KfI;CrKR)hpyUTQ2 z^jMa+L@!{nnl|aNAFeO-{A`3 C{`qPe%j%u5!a^*#+dcJ~TQvJ1p*5pKZZ)?cnQ^Uc|ABV3apaPJ( z^`!<*M0!e+k8|5f^ otp-v_UcGs0sc& zn< UpxCQeaJufL_-a`Y^ZY&9GV!<+s2Pmwi8Hi zLQi*|sb3cS;LL1MN!8Zzn~TQ8U Y>pYua%XgQ$3j$$oEIJRjI{dhqa3o)=aKkU0x zd_y?N49ksfTqGv|NgN#`x}dWAEs0AA%jxeXfFMQz+ih^ZA&uWb)cMTPY9^sIi6f{F z*xg kVks(Ciono}sKTGikMwh-(C&zM3F#3n?e? z(VaL1(g756h{Klg5lhp#M{aws-Mo>hgy~hBKE4H|!ouLyu)LmvxQmY=71tw?ZHGhf z;sTfX`O(o)2qbklcgzJL8Tk3y7OQW+Z4_x&7*&z63%c*U`M5PaB&zSTwpH26#ECKO zG)qX5W8ok(>H!AooyD^{zplQ?LEFP3*T**RLg#Q `%`K;4 z_w;FKGpU4xxsFUO%2|MT0hvHV4!yl>Lu`N0c-WR?zOHRS=x6)|qKkyj$S>n3rRY;Z zqZ@z kwMVDdVkRD1^8bg8D1M#K@kjjW{~@8 zIZb*_7$iap4QqMAaa_WUKj}+;kjWa;d@|c@8-~Z;!9jnAkZV80fYy-nr(Yq=f#s z?%p9Zq~u<^DINhoJ}H2`tIxbnjgqmmyq<*_`{Q;oV356MkZa9}KHlZATbZQ-TIa71 zl$un|+kzLL`x|flBPTD9k9vy_4DFMUI&hPGEMM{F8Uc%W!sXl*iO??Bb6a|^XQHaQ zl$LC7MUG$o5yD!u)6}0W62f^k-3Ygs1T=YN=Nz}_>=_))vr!d5ze$Vqq8F9j7y6)$ z@Ni$qHbOo#>nJFKA(fR4FZYMQgPGTK%YG=E{LEiB*S1a*tY2Iz*^XG%2C7uCOXx@m z=$qLZo9?bIKKprOQ2_*WT2=>`6t{-@7e~Gd6J@O06$uG9&PnpD90~Kq07nJhUo0zZ zra|^O<67ikRpzsov9YoHb9J *taJxkC~ZSpkebC?eGiusO>n=6^_PR;EsD1ZMD^y`_^}pz9i0}8f|$G zQ$C<>6Hw7D9Ef$wl|^_#{mIIkLyO;?#vs0EWwVcu$w)OdjPQLd;?FjJ{GOKziIkKS zFEFEJ_x(m-Ixz5!$6`ieav(Q+#;zbyDAB`>y5k2qlV9(%}NYs~6EjeRE;`kZgy zk4D!4y6eYD38}w>Q=A_p3hS(V64D8{OCab5+RIBaN`2$B@>wckvedB2`w*&5K>-Mr zzp_hQ`P}bBv3eT-R>0T;EOU+R%#XL-(Jkn+jMr{ED|zxn)S_wzYLAd=OmMrr{QN=V z@85tZTpY}Y9nL>TG@z}l%}lW0$ya8Z$x>M{*DcqLl)Wl}%R6^EI~lZDS#BAh=BDZc zYT)zdw$jo)5+-E^1_nhyeWqAI0EdMTrHG9?a3-_?;hL+lnaYugK_Zd-3&}hV3rwuq zWwR~bhdt^A!c3B=eSmuWXl7QhH&X>VPxV;v^%yTy74g2XovSS~#LgM#I{nwLUjRZ= z6~ WPDL+Y2nbyM8u{Q4SlJ~rGYf~gT04MF$Dqy~0M8eQn|3EE zj0==9zAgU#Y^&pDL}aAxSpL3?h<7wKzx~DDjBFbJ;X)IqhzLeQTH0MnNnAXuAZUwl zOYU;wb6eY3N=kP$wY;Jt{D;|`*k(V4sPo#`kT3})CFNPu?;J>^n5wEO0`6hl fT2y(dQKD6L1g58_Z}I^1iTVwDA1WHR2tGe= z^*x zGgfe816=^bb}+-ZAm7gg*e2Y%x(%D0U$Jc9sD>go&h#-?vM>)GQkLt5td-48zIf4} z347l=UWCDYuywsxwM n-w%$9DP^W7h{k>^wf{7kZtNDT~hJ0nUDWfx^AIZCnb90?v)~P4dTNz}6!(WZ_`G zr>zErwpZ4d6ZgRrm5rLJUS+~QR=pKuNne92#)0j06oj9YWbC*sv2Z~x)HZd7Ari$i zUbOH&+nT=&1NQZ|q2Gt)tzcCkw9qQk6G1Kozya9HErycr5>g4d?;(#@#jcKW8csI` zjK4dc?*zK-KH|5No2_zKydEDvJA12ZGBX3{6IParJJ9(Bt^`A7B3SXVQidqCUq-v# z98~10H$=bQQ5C7@!{f3sO?$RI$M2IT=y5;=bsR|LjR+6Fk9vp=mT=}-Mn*;emluec z9Dpsc+7)#OySWD9uR5!-e7iYZTHk8*grwC~8O-~=X{QJ>&T?x&?r{le6wp79Necd^ z^}n?ZSQ@JvRsn&AorDzF`=5bf4jL_{z*8k40wH&w+yNrJTI&fUF5rZPUW&hbd>@59 z8US7`fYkuqVAIx5e^o?Lqk&zVc5zwp#`$cz@ ge@e+c}Cz<&t*hd}XtZ^TmrH?Jk9 zW&r`FKu7#%3-_H#R7 E|zlYVAY39$nHk5EF_TKc`T*!{nMj|xKg(y zzrN&RBzS&LlKAgJSsd}KLOeI)3ZXrnZv4Lm1b~a(o7^xH&(0eG0k89n8*C!kf9*d6 z{_i62qHS61kmsy1jS;ioIgI<8{(TlyEt1<#00ur(X62;2Z_yyXK L8zVJ|7b2KZ z)(YM0gZyGUpr|SLsdkX)AL>@NeE&H$2bHkvEbuwO@$DOjarwzW(ckANII< pPk`+^Z&@_;5UD-pr7+4+AnqTPn`kFXDI_!gDqQe$3y+Ps#Pwu z|NHneC J7V`c+VZ$Mm;CZ?`v)d8-;egcueJq~)zyAmr zJ1*q#5JPEPhr5xcc}y{>*!(`Q!`Z8-t~MV?i+SQJ)6=zot}yoW_qiGlx YiFv=9H%n)e3R%4i%}EwJPgQmz zVLmcm`$UEcz^N;@x@#W$Osq$C)t%PepB9yvkr6!69LkqknOV5C@d~>y) I+eWCJ&%l!wA
GNmr zMW4?2dC1X>9Hywx`%gwU$0+A-=iS`qG{hbv_AwQ6XK7)mW*K=pXD+FP!}1eru;Iyw z3Zk1|_fYuD@MlaTUHa^$JooivHu2G#tv^0TBJ|IV`% b@`&-8=eOQ z;_jYMf_}prBdf}8PAKeOEw~GlTTe-4$OY;>F )d>{$1Mib~AmFjS7zFooc`{X7)DTz3;^E5E3m4LBy+R(Zm)lgr z* g~QP3qDs}{*hg)D77 Y~Gt5@*5yH8t1>fCbwXWEaO+h_x% zn4kRqb+y)R3kGc~FfqRPIB)dn`Xip2cNr_c<6GajV4z5C!kwsqg&R6iCH-z{@45tD zSKgHU9pmpjfVUk^D3{HAImyqNP BCZv@Nb2(2Ay1lo)ze}I{4?FsM=hBjjeEMyEX&*j*96Cga+nsB zde_-mXb`-^x53{2GIvr{fklWQSzZPh>zliQU4qjZp@OA6xu9-EyMcm3&CAO9V`V~q z|G3OznCLNm<<_m`50*k8Ie%6Ean6QOSA%5cL%KTeAAHh5GwNNZD{KlJUK- ?AZ{3Ju}4J< U!CYQcCwxz*vcXVKNE4Uc1zBOqIFvWjT}L;;``2bYpITl z?{C>Y>0OG{f*gj`s>?9>nQX0aN)PINCz=QjK2whuTxJm>cDM`%FI1vakqbrZwH6I` zT~6sR6-$X>7q7W4i~d?qh?+biyO_(=>Izn{&VsQao>o4)I*FI@W+@w(S8@KkiorZS z!D) 5u`h?hy7Re0VfLq5Mt4!(=vl=c(*&jgKE0jMR>W7)_{8XId<~6- zLARLVJ_xV~yhxjz!E6{C8UOOggi2SIT(RJQfPmB$pW9Y^;uCB3Yv&!6 tIyeE=xj(3m)$ zVd<>QQj>Zn0G$P@Z_~&QDr*Z4Y*NfAp7mXMzo}Vr-m=2Qu_je1Ws*rJGXhvZ?lL?> zl)NXhewo3nfM<+#tF7(FkFVAXYxWySg_LJ@YwZqXmiI|re*a*0=(Kism&6HPV=-|S zyMiC@7Yxw5K_O%D$)CA>|B)y-SHjZz%;54TgWS2fvtU>D2}3ay>j_m+t|;%--i?M< zyMJ`@2kz`&zQz?rCX5oh(TXT1Z|AibhvTv=F`Pzt`lbkKxz{1(z-s{+lP9rUUQCbu z`QTo5Y4>>G7<74D6f|<7KHc;w%SWPBa>d6PUOkX?je&nkauR>AxO$YFY=Wj?(hq@u zEro14If;|3JJ)(2t*lS+|AHJ#4Y7Tfdh<*wpaVIh2^u=77-#y8HFo;GRjC<)n@{nb z=^E?O%SwkhB>>p>P`%v!EGCX%bDZmh;E+e9iyf2R{alSKOFl8RtEMma4+HDgKRVe1 z4_Lz^dgbd@Lt)Bhdex$SoRuy6{`Gudcr|BE95Dny=b1`XR@6Ux77o0+4I~0*^Hymr zEgr^JwwCsw%C7sdSGL?{=}#`b)&~yxBI);kmHW8#>=KWHZ~E5L;I*R;(f%xD{#(1f zl2BJ6=l6G|Dv0^)n>yZB`gDRX;^nrE<1T9wiu}$diapAR4F(?&V6pDrPh4T2$lJZ) zw^NbBLeV!#aoRMS;Kh;2VNA+TmgwTtH21B7W unccbsqCWgPvzFRlf|6xFpDRq8h0+F+0?-Fv(}nU`Aq>j&Bg zo*~391Ze4za~l+LrCD(IP36tKz7XlPGCVObbYzL8JvrTE6O%z$edV)pp$7I`X)@D{ z-~Lk9j2io}G5;$tHgc{uFxh_0u(7onmkW4kvI; hHVu)?QtaX)+2* z+oBc*pF8g0$a2kZzMFCc&hm{!wvwd8qbxRE9kKF(-LYAdi+%dmOb1wtc2NrKGTHT6 zbFt57`QA{0S&wBo=h?%s#6*48qa{38@&e~kk(ZpBnw3mu-c19mE;YlS(ASA22R%^;8(nXqnsrpRbQSikM(Kwh%?+)R?5sW#4l{l8&fCJMx7~d6GMd iTS?w-9&-w6zkYpR3s`HY!9oJ^x zs=#cHrNx6DKb@V9pv7f}xRw4D$NK?tVfU61i)%^A6q_xAXG>Eu`%AkvsuKy}EUT>d z!Mklnx$fC_y~d0Th&ku(gX0x}^G5fU^*)Ap@RWS0%6sa#2Fw4wVBQf~a2q=58x7aj z#lDJivF}Rq?KE{Cdt 8Y$P5t)LDZ?>k-*r`jn#vgSXegJ2Ty zh-#5MY#HpmwqOb`nxJBFjy_~YBl~;4qebJz$)#@1tw%sT6}wA+RU 3iANkXHv&n2y^fa;{?m!`M9%5?ER|7xnyPS%z5|HBQeU%GA8J zBq>q{wYXC~KzP{cA1MO`E#+q=-p!y;Yak5&<+rhTED5^V-W6C(>2M_1I8vVvOjYYB zZBVJtaV1Rh>Wgn;(uwJEMQ*EA&v#1q>U?cYN=}w%9TcQnJFe1cW3m(Dy+7xL3-_FU zD*2bbV94vtWwr*uQWQ$Q2wK9<%T{|3okx$D^>EuIK);Ii8y7OQ5ZmU^>+u6Dd&cnU zCN+xUf^CH$O;O~uQHXrzKtK!MJi0XJqa=B$2qFey4BIovV;mrPtI1C{F`ALec|_V$ z&1-3~!%B%i?jU{9NTCuaCgT#WlEpJWS9FGV!&M!;xOfTsmZ=jx2~CYlTWn4bQwqxb zDD6Rd t5lfjQazc~m|#;eBv|r|u%1;#qR0h$i4?UEGslY96#n4$)K|8vTF~r$V*} z3X03r2?izUOKA+EGOI|<*^v$obXxtH5r7!@tLW{Q^vo=Mhw*`05v)jLp}@cIgfU(J z4yS&(H5?fB&za$w?QPiJ!c^Y7ckZ~IF?C+p@{U?b!D@O5@T0RG *a+BjcN=)DVXj$$cU6gsNU&y!l;9Wrp_B9R|6T?L6JiF=I zQFVMD;wyzI-_7Sd*kfWku0Pv=9l_&F9mjgh!Gpqh<+{=p25$Su&==xI-dh^9W9YzL z BvkkE>19jlRHhjpJ{0{Wj)#g&~lmr|3y>1ISA5qrzNn6f3$Goco+Bsn81@%#7 z%4UGekE}9!@j2JqlsUE FbElV zqN{HL3p@jk7F1H$PSMM-OMPeJH `*jhVnQhMR`Ptl9YctH5Arq zBW~CF{-&mH1yQERWusrNtB9Pk9gGN3%|GFime=|Le_WICIr@GfDhSS9=C?-;x=$~( zJ1kLnmfGHGTt}+WnJXGKTgPELv5<-NzM+!f@Ay?Vq~g;>N+-|N9X#`u+00Y<2Gy1r zpKVxL#SPEV7n2H}$z~Oc-{OeY=6#oKTa_Z0Iuq?f)Kd)*c4oD!Kl$y;d+f)B!eDy2 z{_U>t9k>28gI$H!fO6pDB8kccyMQ5RrG2}71Za?cR~%}o)HcpC$@je1wwVE_>-raF zl!A4$TaxY4<76B()?U{r;?ME&+;qge_HCUzi$66$Z?y4$t){u|cVT%;I=~*9#yj)& zoV|srE&kla+oTym8=`}sF=H}&Q#jC<+Jf2Z1AMG%)q`46v$EC_KkT@5k;cc8F4lp9 z?BIn?H=Bs+Y-l(Pn|`rp2t}>ms^*E@zC$N{U**_LbJu>2mf2mcTC`B^q%X-q+grrD z(WGIRqf7*`acpZ$i2qiVEm5xQ*~W{+5*?8q Gq?xvSMDubkj``=fx?2h4IJZ(gu+-o?y zaRY~>r3>}V)2;5=(r5j~r^Q30Q#S)L=igePle2>Dbw2kL3@Uy$g!@~LZjM@>SMV4Z zbjcX{U8!EWi7f=v>NqX>tbR}mS8-5sg(09?i(0a7OIXmYLFnzr?X}%Xw)C=%yc8{M z64jA@E kW{VM6EhW@}- KyA+*{XHEf+ss9K0}oOH-zCFKgpRF&{ny8u#&7w3+G8kMnO`s~5SUE=m(N z+Z?=7tB%>rg E&c4h(R@qpUB-*wY4P4?qty{(OHYfXJmISsUU;&{SQp}C zZHDLh><9}W+npebTuUrz{8aD@iz76nj*9v4>nY_vu8RYU&fzUKoD*wJY|?jO1^2xz z>w>K$*+GxuLl%P$hAKatHm>$>U*^N}(iBqDLj>sM(qv8lQ0gXquJmWf`me0}`9dt8 zCAnX8Ja=}E6Mdk^q}+FM);ol!b|Hfo!=Merf`LE9-7;+=z4Iu_K{A#XIRc|CO0Aba z#vbdB+wk#TxII%}wb+n^ovvGXarror=il9%BAU25-)}?B3IQ=@?o$?YE5;7 UzcNw!Z*>HCd47$SjGPPd+#9mll@8(cZ$lnj5^7y>TzvWnIVX$`nrnO!H<*4tFISAQ3+PD# z)jEDGL7Ii)6`#z^KvF)vRRq3S>5$;D*S^Tp!9v$ N8t zkz`cv;?+j(En*x)vuQ*mGW9Z@XS+FGkPY#9%Z0oUd@TOd4JKbr>PXKlM?$2_Hnqag zi)oNIH7VZN>?IK{i>HDQ#Gg`RjT|t+MFj^#yaET;mf`V~VJ2wU#%A%2of_W@*bJ|3 z)CtkISx_DYLa6okX|;&wpB7d$vmm~aT*$M~&A3zb@?8eSetwUlTrjOR1gf8U@Pcyv zFOSo0!|oVu&M26}6Sd9KZH3W+)VS&vad^c_l#K#|SPE(zJ4xlZ@oT7|EzaTmIOV^! zMIDW`*p$s$!f@$KxepIKM~9^tNr$!ch4vght5&5?R|>Ed+tRQYLccG;t_B`rmCgd0 z3)lyd;?(<_xSspbb%$^VJ~tKGVkUqr*na6#CB`@?*}!c!{$M3jeHulC;ocr%6c?HY zdNBUfmrsJoZu`^;LbUx%Iof}Km5bo@X>S+$aQ9_FoY2F-sKa9VAsd@&u;ZpTK!L{H zF;^gItjSIKl_jX)+&ofhL((1LzZ|LSO;0wex<|j1yuwtlrbHNpF07}9x<1#>0s{cG zt#Hg`tEGZuQn!8-mgvJ01L$7>Vtf^$dgtc7AI$yCC~_%}=Vr*~bkK(5)=}3+rJR}0 zg2@9*)FN!W%mya|Gvnp3&-ymDC3McGR(iHo-L(n!9btM;vpRw&Xuf>`opB7srg21n zH3wO;BDFgeUlW& od-GLbGpVwC@`y+*if#B=XaNgIucZ+j-dq-7>Q&zF|RI G_Lyw o1;QmE#ohBUh5w+R=7>xLhCH7t8~Yot!qTdF-?;xkHB7{ z;_vG5w<;L|C)g~--!yrxS a@mL6Xa|^+>3X|ZDdTi z! &9=7H9 t92VB!vi6G-47vSi1t z!?-x6fNl;l4&a(4;LsUT!jY2Tc-bKE+1txcen&`VdzoIrQE;^lysoZmPl~wFxf;HK z_-VqyoF1GQZJL0lis_f^ZTx7Zb-~lN@2@QmF{9@5<7^|FVwVn(bDcI($(>v%9rzN~ z!t!Bh?n)6mb*;d0jQ2>P2sP*p^E!eZxDNJ?#kf$Gg>;UaN(EYwEpl0g-~srIU6NW3 zGOoUG(AK$q=! AF-4598 zM9x;^4Uxc+b_wLZvpJOMiINbbiugTV&k0CH(A3<)hTO?lUB3mn0|o^7Ziv3WWWy7# zIN-bY7903d;#&lqX(+@u)uMm$f(uX~E9=8%R*5~9^Q@qFzSN{zl! V39GkTv`8 zq1y%A)JyNZvija~LsBNdofYzoM%5y@Z6Gs8)0bOeCoH8r$-JB_&8 z zbO-x(&jFDeDp3oTU>Wg6w5fZZ_C~zSxLvob7ONWY10Jc>9N`EKwl>;|;cqY=L!|#t z;152%N(b4xhCY~06851P(8G|oJ5A+auFEmdgi##pbIC&hp*%;vkdXhnizhpr$;=h` zw^FZDhasvuowO?@KO C9zI`H(*NCex8MM~HM~JWmL}hMN$=*2$;Lu$=1QE|r?4Hamkq= 8=QI8S)b_!o(iwP$N!BM8eM@jj%I?9sZzIOT*NSo=he znGLIXSNVUTFl-xZ@+OA?STRj=?TFzK%Td{!Zj)+R%J~8VmbOkgQZ|5d^G?-p`EIVP zZLLh+DvV@V#RfcIxG;xigQz5Y!8*RD_D?pOjB>ESpMW1S15~5{F2KC2sZDPU^w+H7 zJ&I~TP*}Wc% ?uL2YtERFb1s_skXv4fw z!eypgC8#$#{?aDoUV`p8@XH;~fmL=?CBliN%7-p~l9VfmrhK3BiPgX^8C~Ng7QbA| z^ k9kI?bvPWKsix+LV!B_9<1G_51=uZ0EW;2dM@JHZo*7JW$ z$7aC=)I$6eC{l=f3xzU^t}qDx_!8nhOJkFrQCZxOW1wh}lk(LT)KM#=S+2YS4VEEZ zz^jh@x>dWocR4i2^%$r?B#g=P11~$v^U?vgDDsF&i=7{9+njT3IK#7}%y|VWKAH9& z*I|RGovjB}1G-e8@tO+wW)IV?|EpU_>Rq)6<@j{;k$#+Ib-k2}{sd>kpkXsVQ(X}S zTNipu6n?Y)5b=C`a~F75VEYhU|7 grdE!8vPCY`Dj&jWg5P7EMgRmpmWpz4-5Km2`<~5SeUU4! zysq_=n5XQ;|3jhnX!%EDPC&lgvcfb>>coCvspZn~5Z>{vfHkb)* @0L8$cpj zCpf;8bjR{ZjW)Cs=l5kfH+3{;zVbnbeFl{%f9%moXpquGdG-Kr$R}Arr|x#;Q@j4B zSWp`=O&er`KLkGk@}U~y>IM9;45zEoraFNjZY5K-oa*~DWzT_VdnU{RQto?EJ++G7 zv{A8|s^CB>o=kPiVj}0I%l!-=FU_RM#(a@NWpWqL!7Onqux)7xrQ70_$rB@0_X{zt zw2d#=sA}G$Ur{lZ^#x$~SP4j<_E$%4@!JOcDk7dGmK(TZvH&3C!dD`)8k=nFxA_Qo zNCS>SI^)Kk<}9ABTuh}k#P)RfKls$yWr9XyekKShgIiTPIGsuK2nGuY*1D;dUNajF z;RfJ4^Hs~+>v=0_FZ n(*{!+DsWAQW-$&!+SpfXNb -*B%b#+=J>yw#h@#eXV`Oo S zZVd93`AERS5&b7ojy-K=W@m!%GX+<3@%1~yb1OVp`VD5O`24Yh^qXMvgSA6oQb7xk zlB?g`OmYGGx@}gcu$;W>%nQ9JAG-e#Vn`b!6yUZNdDJgKzGQ7dXc!W|)L@!}udlW? zUQ1x+m3O4Qr=)vVOW1IFXVEEWnVs^nX_=v(#Gsw6*`oBY; Zb{0dP1m=q zkl9qlCU#a9P6ZV_KIWL5xG}u0MB+9L@4}gU2X+CH(o!}Cnz~5AT?$W_sP^x-Y83bQ z?)IbsVp
>SSw|?71nT{s9&3?O}y;0=qe?+ z95qBJXx&>n=K`Y4PnGUewMKNH3d39tSmn&K w)T%WfUX0h)#O%c+M$?sCSaJZstw(WQ{^NQ28QZ7Q^ w0lqe= zs0i ^uP24|k8~?b&!Q?0!#luPdkb=T)-HG<)35sPPE_@DNSqx8z0(2de#dF^5%4 ztHdtdLle$G4Zz0=+|_y*P}~33hHEOk&wo(_`mO7o2#hhY?v{^To{kBu%`Ap*TRa57 z%z({tFpjcY733K%F?Ios-x+Guv8<^U2sy&aD}LM`R0VoJaPZm2?t0J1*@UQ2*KV7I z@txN;mJq=H3(&q9S=CFVtDwtS9KJ3u&ONULzej^CJ}w{+aaO#JIBSe-lag3y~W~ zVW_&tb_H3ku@xb9!{58YpMrFIaDJ0IjYd~<=RRrUd`67(nPp)UBzhrI`7j@l;B+Hh zpRMKBtZcZV>w6N3!i`j_-T 3q?Z*hf!zuZu(&Q)-6Cg zs<#iR)oM+|?lP2jdm4?z NH^GPDSy*@9 zwEGRl0?D-CzrMy&Cjft_viP|-uIvRl1+^2`DWc6@7wHmhPafq^mU-&^PK`&=PR;Vk zB1-wD0^^6Mm3B}V^$a?)B28FyJAO+ot?cUPiOSWDN)DpjGk+4?iODu<2@W8N$C?(n zbd^&=`#4B42^|t6x>(Q;NJ{HzcTF&weKP;mhxpv)bYg *9>OP9Pk%X?JNu<_P-D_N#O$a0^ zj004+W`9VV?5JscJ= nHTskytk^o*t~ zt{c_;YWKA(yl7Kwhcl@2Rhm7tYa3Bnpkb!l)%PR#=vh`2E%42vDgNxxlFFMaq?5q! znm~`nHfvYc9A{(>g>=sxPpRXt9!jPTVfH+s!|*CC+!#k bnC*+ zf$#;HmF9jZ@@vXUFKXYe_=R!a*oi)ue!AT}C!^Gt+>u&Mdd>v!$X_jPy4CMsVduLT ztk%;xY2fS>_>{7gWvQJ}N4lf8E&3mMHcwhJ>g%eaGZ_WoPdkHuZ)>U+s4x pjXx=ut84o^$Lc(&(n&&GAME3a2`3?|H4jc#_Om+ze^(Px718)U%zz3O@{0EL_s zwx_mJK3lUvp3U?c5Q~)j6;?t1j%#e92wz(wrA2qQN5{%v>URtrQqU~Z+fJ@3n&le} zY?om|q`NU7c{5+O>iA1nMC6moIGR*&p!%A#jQ04rG7(cRftE?LC8D;|Z5sFr^9Xzu zPrsKjlpZQMHFOV@f>MHT&+%|ET%7TvSz{?)T1m@%@NkZ+aIj0RuQ5;o QV3LwNlw3NmxK#n4BcSRa z=5w1uY9+UK@_s~|+g08oh}JGij?Yk1)Bn9=FLLQ^cGk;rD;2#o6~jwM-rYH%5-DXb z@|0WcOXijT`{%)>*RKpCy>6Td=Y7Dh6i7T{dP*bU|D?(0@Egf-4ewr6AmJ7$I2oC8 z=w|n8PBgiAKIG$ON0(OJYsnU~>YF)R8%|799yn0J#chUNCbmC`pk!(knWgrETpul5 zT}sK>$!)T{UJ%S%|4pDr7dQOUc|NNr=^=;Yq%lv@D_iVNeC<+*dEoWj3(9U2VVKI` zOH92VIAHq1B9J^8wM2#`V2+Yh5=@b5^G?;1YW>oO(tcp@#)s9;zF`PPJ5?n4_&lgB zc)<`})}9^TNeO@v+RvBR?sd}~)4|M+A#hL#yVBaH0OuGF3K*(!^{%yYALhyZGzzcd zD?#e#6)Ys3_k-YG>IXxEU4!j~;xUv+czVrE1p(ynZ?2fB8SswVAsH@gaa4eNB;}39 z;nfI ) zl=2dIcEUYadw}#Ng^Q4jyERkZVQ`e;YphSAoRip54ztC+o%k~+S2Qx<={qh8(!=94 zM7Qmy(#B54Nype`HvLmQSc7GMsrjawRLrh{=^iGBlFJM7X12tDR9VYv)JW;4+Hs01 zRPO3b|DF=AI`LjaNw)PBt#!?0p0V9q{gvzTiL5=51J4B)_UZ#{B0bE-L20qQ0fd5} z>AGts4uDQ(#1l$?@Av}8S`rA0IhN@kFD4>BiZ&S=NSdy;?$i`&pMprXz1(2$VAffH zz;vg6B@687P;4FaeD6}3`__U2Ba;%Qo;YsJh+o+Fyf0v0Xijth^c~k)CFPOEm2p|& zAH8Bm&_;m{m-r-^Ym&^4CLm2E*-!8G^^ H^=D+bfNNQLGc@bzm8jR}Sr+ghaAeebqhrE6U*vX~b z(U>OFNFOL(>Nyxh=x`m`_&>7o@&2q;;XcL=^E{yE*Jvk}gzZQc@ee -p3kZ{>J>xzaF }MwyG(