- ∑ns
+ ∑ns
x
an
diff --git a/contentcuration/contentcuration/tests/utils/test_markdown.py b/contentcuration/contentcuration/tests/utils/test_markdown.py
index c111146a85..0088d4a09e 100644
--- a/contentcuration/contentcuration/tests/utils/test_markdown.py
+++ b/contentcuration/contentcuration/tests/utils/test_markdown.py
@@ -120,8 +120,8 @@ def test_mixed_inline_and_block(self):
"\n"
"And this is block math:
\n"
'"
"Back to text with more inline: "
@@ -132,6 +132,62 @@ def test_mixed_inline_and_block(self):
self._assert_conversion(markdown_text, expected)
+ def test_block_sum_uses_munderover(self):
+ """Block-mode \\sum with sub+superscript must use , not """
+
+ markdown_text = "$$\\sum_{i=1}^{n} x_i$$"
+ expected = (
+ '"
+ )
+
+ self._assert_conversion(markdown_text, expected)
+
+ def test_inline_sum_uses_msubsup(self):
+ """Inline-mode \\sum with sub+superscript must use , not """
+
+ markdown_text = "The sum $$\\sum_{i=1}^{n} x_i$$ is finite."
+ expected = (
+ "The sum "
+ ''
+ " is finite.
\n"
+ )
+
+ self._assert_conversion(markdown_text, expected)
+
+ def test_block_prod_uses_munderover(self):
+ """Block-mode \\prod with sub+superscript must use """
+
+ markdown_text = "$$\\prod_{k=0}^{n} a_k$$"
+ expected = (
+ '"
+ )
+
+ self._assert_conversion(markdown_text, expected)
+
+ def test_block_int_unaffected(self):
+ """Block-mode \\int should still use (not affected by munderover fix)"""
+
+ markdown_text = "$$\\int_{a}^{b} f(x) dx$$"
+ expected = (
+ '"
+ )
+
+ self._assert_conversion(markdown_text, expected)
+
def test_no_math_content(self):
"""Test that regular markdown without math still works"""
diff --git a/contentcuration/contentcuration/tests/utils/test_publish.py b/contentcuration/contentcuration/tests/utils/test_publish.py
new file mode 100644
index 0000000000..81d1c23453
--- /dev/null
+++ b/contentcuration/contentcuration/tests/utils/test_publish.py
@@ -0,0 +1,178 @@
+import os
+import tempfile
+import uuid
+from unittest import mock
+
+from django.conf import settings
+
+from contentcuration.tests import testdata
+from contentcuration.tests.base import StudioTestCase
+from contentcuration.tests.utils.restricted_filesystemstorage import (
+ RestrictedFileSystemStorage,
+)
+from contentcuration.utils.publish import create_draft_channel_version
+from contentcuration.utils.publish import ensure_versioned_database_exists
+from contentcuration.utils.publish import increment_channel_version
+
+
+class EnsureVersionedDatabaseTestCase(StudioTestCase):
+ def setUp(self):
+ super().setUp()
+
+ self._temp_directory_ctx = tempfile.TemporaryDirectory()
+ self.test_db_root_dir = self._temp_directory_ctx.__enter__()
+
+ storage = RestrictedFileSystemStorage(location=self.test_db_root_dir)
+
+ self._storage_patch_ctx = mock.patch(
+ "contentcuration.utils.publish.storage",
+ new=storage,
+ )
+ self._storage_patch_ctx.__enter__()
+
+ os.makedirs(
+ os.path.join(self.test_db_root_dir, settings.DB_ROOT), exist_ok=True
+ )
+
+ self.channel = testdata.channel()
+ self.channel.version = 2
+ self.channel.save()
+
+ self.versioned_db_path = os.path.join(
+ self.test_db_root_dir,
+ settings.DB_ROOT,
+ f"{self.channel.id}-{self.channel.version}.sqlite3",
+ )
+ self.unversioned_db_path = os.path.join(
+ self.test_db_root_dir, settings.DB_ROOT, f"{self.channel.id}.sqlite3"
+ )
+
+ def tearDown(self):
+ self._temp_directory_ctx.__exit__(None, None, None)
+ self._storage_patch_ctx.__exit__(None, None, None)
+
+ super().tearDown()
+
+ def test_versioned_database_exists(self):
+ # In reality, the versioned database for the current version
+ # and the unversioned database would have the same content,
+ # but here we provide different content so that we can test
+ # that the versioned database is not overwritten.
+ versioned_db_content = "Versioned content"
+ unversioned_db_content = "Unversioned content"
+
+ with open(self.versioned_db_path, "w") as f:
+ f.write(versioned_db_content)
+ with open(self.unversioned_db_path, "w") as f:
+ f.write(unversioned_db_content)
+
+ ensure_versioned_database_exists(self.channel.id, self.channel.version)
+
+ with open(self.versioned_db_path) as f:
+ read_versioned_content = f.read()
+ self.assertEqual(read_versioned_content, versioned_db_content)
+
+ def test_versioned_database_does_not_exist(self):
+ unversioned_db_content = "Unversioned content"
+
+ with open(self.unversioned_db_path, "w") as f:
+ f.write(unversioned_db_content)
+
+ ensure_versioned_database_exists(self.channel.id, self.channel.version)
+
+ with open(self.versioned_db_path) as f:
+ read_versioned_content = f.read()
+ self.assertEqual(read_versioned_content, unversioned_db_content)
+
+ def test_not_published(self):
+ self.channel.version = 0
+ self.channel.save()
+ self.versioned_db_path = os.path.join(
+ self.test_db_root_dir,
+ settings.DB_ROOT,
+ f"{self.channel.id}-{self.channel.version}.sqlite3",
+ )
+
+ with self.assertRaises(ValueError):
+ ensure_versioned_database_exists(self.channel.id, self.channel.version)
+
+
+class IncrementChannelVersionTestCase(StudioTestCase):
+ """Test increment_channel_version function with ChannelVersion integration."""
+
+ def setUp(self):
+ super().setUp()
+ self.channel = testdata.channel()
+ self.channel.version = 0
+ self.channel.save()
+
+ def test_increment_published_version(self):
+ """Test incrementing version for published channel."""
+ initial_version = self.channel.version
+
+ increment_channel_version(self.channel)
+
+ self.channel.refresh_from_db()
+
+ self.assertEqual(self.channel.version, initial_version + 1)
+
+ self.assertIsNotNone(self.channel.version_info)
+ self.assertEqual(self.channel.version_info.version, self.channel.version)
+ self.assertEqual(self.channel.version_info.channel, self.channel)
+
+ self.assertIsNone(self.channel.version_info.secret_token)
+
+ def test_increment_draft_version(self):
+ """Test incrementing version for draft channel."""
+ initial_version = self.channel.version
+
+ channel_version = create_draft_channel_version(self.channel)
+
+ self.channel.refresh_from_db()
+
+ self.assertEqual(self.channel.version, initial_version)
+
+ self.assertIsNotNone(channel_version)
+ self.assertIsNone(channel_version.version)
+ self.assertEqual(channel_version.channel, self.channel)
+
+ if self.channel.version_info:
+ self.assertNotEqual(self.channel.version_info, channel_version)
+
+ self.assertIsNotNone(channel_version.secret_token)
+ self.assertFalse(channel_version.secret_token.is_primary)
+
+ def test_multiple_published_versions(self):
+ """Test creating multiple published versions."""
+ increment_channel_version(self.channel)
+ increment_channel_version(self.channel)
+ increment_channel_version(self.channel)
+
+ self.channel.refresh_from_db()
+
+ self.assertEqual(self.channel.version, 3)
+
+ self.assertEqual(self.channel.channel_versions.count(), 3)
+
+ self.assertIsNotNone(self.channel.version_info)
+ self.assertEqual(self.channel.version_info.version, 3)
+
+ def test_mixed_draft_and_published_versions(self):
+ """Test creating mix of draft and published versions."""
+ increment_channel_version(self.channel)
+ draft1 = create_draft_channel_version(self.channel)
+ draft2 = create_draft_channel_version(self.channel)
+
+ self.channel.refresh_from_db()
+
+ self.assertEqual(self.channel.version, 1)
+
+ self.assertEqual(self.channel.channel_versions.count(), 2)
+ self.assertEqual(uuid.UUID(str(draft1.id)), uuid.UUID(str(draft2.id)))
+
+ self.assertIsNotNone(draft1.secret_token)
+ self.assertIsNotNone(draft2.secret_token)
+
+ self.assertIsNotNone(self.channel.version_info)
+ self.assertEqual(self.channel.version_info.version, 1)
+ self.assertIsNone(self.channel.version_info.secret_token)
diff --git a/contentcuration/contentcuration/tests/utils/test_restricted_filesystemstorage.py b/contentcuration/contentcuration/tests/utils/test_restricted_filesystemstorage.py
new file mode 100644
index 0000000000..ca5ce7be71
--- /dev/null
+++ b/contentcuration/contentcuration/tests/utils/test_restricted_filesystemstorage.py
@@ -0,0 +1,44 @@
+import tempfile
+
+from django.core.files.base import ContentFile
+from django.test import TestCase
+
+from contentcuration.tests.utils.restricted_filesystemstorage import (
+ RestrictedFileSystemStorage,
+)
+
+
+class RestrictedFileSystemStorageTestCase(TestCase):
+ # Sanity-checks that the RestrictedFileSystemStorage wrapper used in tests
+ # works as expected, not actually testing application code
+
+ def setUp(self):
+ super().setUp()
+
+ self._temp_directory_ctx = tempfile.TemporaryDirectory()
+ self.temp_dir = self._temp_directory_ctx.__enter__()
+
+ self.storage = RestrictedFileSystemStorage(location=self.temp_dir)
+
+ def tearDown(self):
+ self._temp_directory_ctx.__exit__(None, None, None)
+ super().tearDown()
+
+ def test_opening_for_read_works(self):
+ test_content = "test content"
+
+ self.storage.save("filename", ContentFile(test_content))
+ with self.storage.open("filename", "r") as f:
+ content = f.read()
+ self.assertEqual(content, test_content)
+
+ def test_opening_for_write_does_not_work(self):
+ test_content = "test content"
+
+ with self.assertRaises(ValueError):
+ with self.storage.open("filename", "w") as f:
+ f.write(test_content)
+
+ def test_path_does_not_work(self):
+ with self.assertRaises(NotImplementedError):
+ self.storage.path("filename")
diff --git a/contentcuration/contentcuration/tests/viewsets/base.py b/contentcuration/contentcuration/tests/viewsets/base.py
index 617d23bb26..c75b7f9549 100644
--- a/contentcuration/contentcuration/tests/viewsets/base.py
+++ b/contentcuration/contentcuration/tests/viewsets/base.py
@@ -2,9 +2,8 @@
from django.urls import reverse
-from contentcuration.celery import app
from contentcuration.models import Change
-from contentcuration.tests.helpers import clear_tasks
+from contentcuration.tests.helpers import EagerTasksTestMixin
from contentcuration.viewsets.sync.constants import CHANNEL
from contentcuration.viewsets.sync.constants import SYNCED
from contentcuration.viewsets.sync.utils import _generate_event as base_generate_event
@@ -94,31 +93,15 @@ def generate_publish_channel_event(channel_id):
return event
-def generate_publish_next_event(channel_id):
- event = base_generate_publish_next_event(channel_id)
+def generate_publish_next_event(channel_id, use_staging_tree=False):
+ event = base_generate_publish_next_event(
+ channel_id, use_staging_tree=use_staging_tree
+ )
event["rev"] = random.randint(1, 10000000)
return event
-class SyncTestMixin(object):
- celery_task_always_eager = None
-
- @classmethod
- def setUpClass(cls):
- super(SyncTestMixin, cls).setUpClass()
- # update celery so tasks are always eager for this test, meaning they'll execute synchronously
- cls.celery_task_always_eager = app.conf.task_always_eager
- app.conf.update(task_always_eager=True)
-
- def setUp(self):
- super(SyncTestMixin, self).setUp()
- clear_tasks()
-
- @classmethod
- def tearDownClass(cls):
- super(SyncTestMixin, cls).tearDownClass()
- app.conf.update(task_always_eager=cls.celery_task_always_eager)
-
+class SyncTestMixin(EagerTasksTestMixin):
@property
def sync_url(self):
return reverse("sync")
diff --git a/contentcuration/contentcuration/tests/viewsets/test_channel.py b/contentcuration/contentcuration/tests/viewsets/test_channel.py
index 8309f47c8c..2c72cf2dcc 100644
--- a/contentcuration/contentcuration/tests/viewsets/test_channel.py
+++ b/contentcuration/contentcuration/tests/viewsets/test_channel.py
@@ -6,14 +6,24 @@
from django.urls import reverse
from kolibri_public.models import ContentNode as PublicContentNode
from le_utils.constants import content_kinds
+from le_utils.constants.labels import subjects
from mock import patch
from contentcuration import models
from contentcuration import models as cc
from contentcuration.constants import channel_history
+from contentcuration.constants import community_library_submission
+from contentcuration.models import AuditedSpecialPermissionsLicense
+from contentcuration.models import Change
+from contentcuration.models import Channel
+from contentcuration.models import ChannelVersion
+from contentcuration.models import CommunityLibrarySubmission
from contentcuration.models import ContentNode
+from contentcuration.models import Country
+from contentcuration.tasks import apply_channel_changes_task
from contentcuration.tests import testdata
from contentcuration.tests.base import StudioAPITestCase
+from contentcuration.tests.helpers import reverse_with_query
from contentcuration.tests.viewsets.base import generate_create_event
from contentcuration.tests.viewsets.base import generate_delete_event
from contentcuration.tests.viewsets.base import generate_deploy_channel_event
@@ -24,6 +34,9 @@
from contentcuration.tests.viewsets.base import SyncTestMixin
from contentcuration.viewsets.channel import _unpublished_changes_query
from contentcuration.viewsets.sync.constants import CHANNEL
+from contentcuration.viewsets.sync.utils import (
+ generate_added_to_community_library_event,
+)
class SyncTestCase(SyncTestMixin, StudioAPITestCase):
@@ -492,7 +505,7 @@ def test_publish_next(self):
self.assertEqual(response.status_code, 200)
modified_channel = models.Channel.objects.get(id=channel.id)
- self.assertEqual(modified_channel.staging_tree.published, True)
+ self.assertEqual(modified_channel.staging_tree.published, False)
def test_publish_next_with_incomplete_staging_tree(self):
channel = testdata.channel()
@@ -507,7 +520,9 @@ def test_publish_next_with_incomplete_staging_tree(self):
channel.save()
self.assertEqual(channel.staging_tree.published, False)
- response = self.sync_changes([generate_publish_next_event(channel.id)])
+ response = self.sync_changes(
+ [generate_publish_next_event(channel.id, use_staging_tree=True)]
+ )
self.assertEqual(response.status_code, 200)
self.assertTrue(
@@ -517,6 +532,115 @@ def test_publish_next_with_incomplete_staging_tree(self):
modified_channel = models.Channel.objects.get(id=channel.id)
self.assertEqual(modified_channel.staging_tree.published, False)
+ def test_sync_added_to_community_library_change(self):
+ # Syncing the change from the frontend should be disallowed
+ self.client.force_authenticate(self.admin_user)
+
+ channel = testdata.channel()
+ channel.version = 1
+ channel.public = True
+ channel.save()
+
+ added_to_community_library_change = generate_added_to_community_library_event(
+ key=channel.id,
+ channel_version=1,
+ )
+ response = self.sync_changes([added_to_community_library_change])
+
+ self.assertEqual(response.status_code, 200, response.content)
+ self.assertEqual(len(response.json()["allowed"]), 0, response.content)
+ self.assertEqual(len(response.json()["disallowed"]), 1, response.content)
+
+ @mock.patch("contentcuration.viewsets.channel.export_channel_to_kolibri_public")
+ def test_process_added_to_community_library_change(self, mock_export_func):
+ # Creating the change on the backend should be supported
+ self.client.force_authenticate(self.admin_user)
+
+ editor_user = testdata.user("channel@editor.com")
+
+ channel = testdata.channel()
+ channel.version = 2
+ channel.public = False
+ channel.editors.add(editor_user)
+ channel.save()
+
+ current_live_submission = CommunityLibrarySubmission.objects.create(
+ channel=channel,
+ channel_version=1,
+ author=editor_user,
+ status=community_library_submission.STATUS_LIVE,
+ )
+ new_submission = CommunityLibrarySubmission.objects.create(
+ channel=channel,
+ channel_version=2,
+ author=editor_user,
+ status=community_library_submission.STATUS_APPROVED,
+ )
+
+ categories = {
+ "category1": True,
+ "category2": True,
+ }
+
+ country1 = Country.objects.create(code="C1", name="Country 1")
+ country2 = Country.objects.create(code="C2", name="Country 2")
+ countries = [country1, country2]
+ country_codes = [country.code for country in countries]
+
+ added_to_community_library_change = generate_added_to_community_library_event(
+ key=channel.id,
+ channel_version=2,
+ categories=categories,
+ country_codes=country_codes,
+ )
+ Change.create_change(
+ added_to_community_library_change, created_by_id=self.admin_user.id
+ )
+
+ # This task will run immediately thanks to SyncTestMixin
+ apply_channel_changes_task.fetch_or_enqueue(
+ user=self.admin_user,
+ channel_id=channel.id,
+ )
+
+ # We cannot easily use the assert_called_once_with method here
+ # because we are not checking countries for strict equality,
+ # so we need to check the call arguments manually
+ mock_export_func.assert_called_once()
+
+ (call_args, call_kwargs) = mock_export_func.call_args
+ self.assertEqual(len(call_args), 0)
+ self.assertCountEqual(
+ call_kwargs.keys(),
+ [
+ "channel_id",
+ "channel_version",
+ "categories",
+ "countries",
+ "public",
+ ],
+ )
+ self.assertEqual(call_kwargs["channel_id"], channel.id)
+ self.assertEqual(call_kwargs["channel_version"], 2)
+ self.assertCountEqual(call_kwargs["categories"], categories.keys())
+
+ # The countries argument used when creating the mapper is in fact
+ # not a list, but a QuerySet, but it contains the same elements
+ self.assertCountEqual(call_kwargs["countries"], countries)
+ self.assertEqual(call_kwargs["public"], False)
+
+ # Check that the current submission became the live one
+ current_live_submission.refresh_from_db()
+ new_submission.refresh_from_db()
+ self.assertEqual(
+ current_live_submission.status,
+ community_library_submission.STATUS_APPROVED,
+ )
+ self.assertEqual(
+ new_submission.status,
+ community_library_submission.STATUS_LIVE,
+ )
+
class CRUDTestCase(StudioAPITestCase):
@property
@@ -554,6 +678,186 @@ def test_fetch_admin_channels_invalid_filter(self):
)
self.assertEqual(response.status_code, 200, response.content)
+ def test_admin_channel_detail__latest_community_library_submission__exists(self):
+ older_submission = testdata.community_library_submission()
+ older_submission.channel.version = 2
+ older_submission.channel_version = 1
+ older_submission.status = community_library_submission.STATUS_LIVE
+ older_submission.channel.save()
+ older_submission.save()
+
+ latest_submission = CommunityLibrarySubmission.objects.create(
+ channel=older_submission.channel,
+ channel_version=2,
+ author=older_submission.author,
+ )
+
+ self.client.force_authenticate(self.admin_user)
+ response = self.client.get(
+ reverse(
+ "admin-channels-detail", kwargs={"pk": older_submission.channel.id}
+ ),
+ format="json",
+ )
+ self.assertEqual(response.status_code, 200, response.content)
+ self.assertEqual(
+ response.data["latest_community_library_submission_id"],
+ latest_submission.id,
+ )
+ self.assertEqual(
+ response.data["latest_community_library_submission_status"],
+ community_library_submission.STATUS_PENDING,
+ )
+ self.assertTrue(response.data["has_any_live_community_library_submission"])
+
+ def test_admin_channel_detail__latest_community_library_submission__none_exist(
+ self,
+ ):
+ channel = testdata.channel()
+
+ self.client.force_authenticate(self.admin_user)
+ response = self.client.get(
+ reverse("admin-channels-detail", kwargs={"pk": channel.id}),
+ format="json",
+ )
+ self.assertEqual(response.status_code, 200, response.content)
+ self.assertIsNone(response.data["latest_community_library_submission_id"])
+ self.assertIsNone(response.data["latest_community_library_submission_status"])
+ self.assertFalse(response.data["has_any_live_community_library_submission"])
+
+ def test_admin_channel_filter__latest_community_library_submission_status__any(
+ self,
+ ):
+ self.client.force_authenticate(user=self.admin_user)
+
+ submission = testdata.community_library_submission()
+
+ response = self.client.get(
+ reverse_with_query(
+ "admin-channels-list",
+ query={
+ "id__in": submission.channel.id,
+ },
+ ),
+ format="json",
+ )
+ self.assertEqual(response.status_code, 200, response.content)
+ self.assertEqual(len(response.data), 1)
+
+ def test_admin_channel_filter__latest_community_library_submission_status__multiple(
+ self,
+ ):
+ self.client.force_authenticate(user=self.admin_user)
+
+ submission1 = testdata.community_library_submission()
+ submission1.status = community_library_submission.STATUS_LIVE
+ submission1.save()
+
+ submission2 = testdata.community_library_submission()
+ submission2.status = community_library_submission.STATUS_PENDING
+ submission2.save()
+
+ submission3 = testdata.community_library_submission()
+ submission3.status = community_library_submission.STATUS_APPROVED
+ submission3.save()
+
+ response = self.client.get(
+ reverse_with_query(
+ "admin-channels-list",
+ query=[
+ (
+ "latest_community_library_submission_status",
+ community_library_submission.STATUS_LIVE,
+ ),
+ (
+ "latest_community_library_submission_status",
+ community_library_submission.STATUS_PENDING,
+ ),
+ ],
+ ),
+ format="json",
+ )
+ self.assertEqual(response.status_code, 200, response.content)
+ self.assertCountEqual(
+ [ch["id"] for ch in response.data],
+ [
+ submission1.channel.id,
+ submission2.channel.id,
+ ],
+ )
+
+ def test_admin_channel_filter__community_library_live(self):
+ self.client.force_authenticate(user=self.admin_user)
+
+ submission1 = testdata.community_library_submission()
+ submission1.channel.version = 2
+ submission1.channel.save()
+ submission1.status = community_library_submission.STATUS_LIVE
+ submission1.channel_version = 1
+ submission1.save()
+
+ CommunityLibrarySubmission.objects.create(
+ channel=submission1.channel,
+ channel_version=2,
+ author=submission1.author,
+ status=community_library_submission.STATUS_PENDING,
+ )
+
+ other_channel_submission = testdata.community_library_submission()
+ other_channel_submission.status = community_library_submission.STATUS_PENDING
+ other_channel_submission.save()
+
+ response = self.client.get(
+ reverse_with_query(
+ "admin-channels-list",
+ query={"community_library_live": True},
+ ),
+ format="json",
+ )
+ self.assertEqual(response.status_code, 200, response.content)
+ self.assertCountEqual(
+ [ch["id"] for ch in response.data],
+ [submission1.channel.id],
+ )
+
+ def test_has_community_library_submission_endpoint(self):
+ """Test the on-demand has_community_library_submission endpoint"""
+ user = testdata.user()
+ channel_with_submission = testdata.channel()
+ channel_with_submission.editors.add(user)
+ channel_with_submission.version = 1
+ channel_with_submission.save()
+ submission = testdata.community_library_submission()
+ submission.channel = channel_with_submission
+ submission.author = user
+ submission.channel_version = channel_with_submission.version
+ submission.save()
+
+ channel_without_submission = testdata.channel()
+ channel_without_submission.editors.add(user)
+
+ self.client.force_authenticate(user=user)
+
+ response = self.client.get(
+ reverse(
+ "channel-has-community-library-submission",
+ kwargs={"pk": channel_with_submission.id},
+ ),
+ format="json",
+ )
+ self.assertEqual(response.status_code, 200, response.content)
+ self.assertTrue(response.data["has_community_library_submission"])
+
+ response = self.client.get(
+ reverse(
+ "channel-has-community-library-submission",
+ kwargs={"pk": channel_without_submission.id},
+ ),
+ format="json",
+ )
+ self.assertEqual(response.status_code, 200, response.content)
+ self.assertFalse(response.data["has_community_library_submission"])
+
def test_create_channel(self):
user = testdata.user()
self.client.force_authenticate(user=user)
@@ -627,6 +931,45 @@ def test_admin_restore_channel(self):
channel.history.filter(actor=user, action=channel_history.RECOVERY).count(),
)
+ def test_channel_detail_includes_draft_token_when_draft_version_exists(self):
+ """Test that the channel API response includes draft_token when a draft ChannelVersion exists."""
+ user = testdata.user()
+ channel = models.Channel.objects.create(
+ actor_id=user.id, **self.channel_metadata
+ )
+ channel.editors.add(user)
+
+ # Create a draft ChannelVersion (version=None) with a token
+ draft_version = ChannelVersion.objects.create(
+ channel=channel,
+ version=None,
+ )
+ token = draft_version.new_token()
+
+ self.client.force_authenticate(user=user)
+ response = self.client.get(
+ reverse("channel-detail", kwargs={"pk": channel.id}),
+ format="json",
+ )
+ self.assertEqual(response.status_code, 200, response.content)
+ self.assertEqual(response.data["draft_token"], token.token)
+
+ def test_channel_detail_draft_token_is_none_when_no_draft_version(self):
+ """Test that the channel API response has draft_token=None when no draft version exists."""
+ user = testdata.user()
+ channel = models.Channel.objects.create(
+ actor_id=user.id, **self.channel_metadata
+ )
+ channel.editors.add(user)
+
+ self.client.force_authenticate(user=user)
+ response = self.client.get(
+ reverse("channel-detail", kwargs={"pk": channel.id}),
+ format="json",
+ )
+ self.assertEqual(response.status_code, 200, response.content)
+ self.assertIsNone(response.data["draft_token"])
+
class UnpublishedChangesQueryTestCase(StudioAPITestCase):
def test_unpublished_changes_query_with_channel_object(self):
@@ -940,3 +1283,402 @@ def _perform_action(self, url_path, channel_id):
reverse(url_path, kwargs={"pk": channel_id}), format="json"
)
return response
+
+
+class GetPublishedDataTestCase(StudioAPITestCase):
+ def setUp(self):
+ super().setUp()
+
+ self.editor_user = testdata.user(email="editor@user.com")
+ self.admin_user = testdata.user(email="admin@user.com")
+ self.forbidden_user = testdata.user(email="forbidden@user.com")
+
+ self.channel = testdata.channel()
+ self.channel.editors.add(self.editor_user)
+ self.channel.editors.add(self.admin_user)
+
+ self.channel.published_data = {
+ "key1": "value1",
+ "key2": "value2",
+ }
+ self.channel.save()
+
+ def test_get_version_detail__is_editor(self):
+ """Test that editors can get version detail with populated versionInfo."""
+ from contentcuration.models import ChannelVersion
+
+ self.client.force_authenticate(user=self.editor_user)
+
+ self.channel.version = 1
+ self.channel.save()
+
+ channel_version, created = ChannelVersion.objects.get_or_create(
+ channel=self.channel,
+ version=1,
+ defaults={
+ "resource_count": 100,
+ "size": 1024000,
+ },
+ )
+ if not created:
+ channel_version.resource_count = 100
+ channel_version.size = 1024000
+ channel_version.save()
+
+ self.channel.version_info = channel_version
+ self.channel.save()
+
+ response = self.client.get(
+ reverse("channel-version-detail", kwargs={"pk": self.channel.id}),
+ format="json",
+ )
+ self.assertEqual(response.status_code, 200, response.content)
+ data = response.json()
+
+ self.assertEqual(data["version"], 1)
+ self.assertEqual(data["resource_count"], 100)
+ self.assertEqual(data["size"], 1024000)
+
+ def test_get_version_detail__is_admin(self):
+ """Test that admins can get version detail with populated versionInfo."""
+ from contentcuration.models import ChannelVersion
+
+ self.client.force_authenticate(user=self.admin_user)
+
+ self.channel.version = 2
+ self.channel.save()
+
+ channel_version, created = ChannelVersion.objects.get_or_create(
+ channel=self.channel,
+ version=2,
+ defaults={
+ "resource_count": 200,
+ "size": 2048000,
+ },
+ )
+ if not created:
+ channel_version.resource_count = 200
+ channel_version.size = 2048000
+ channel_version.save()
+
+ self.channel.version_info = channel_version
+ self.channel.save()
+
+ response = self.client.get(
+ reverse("channel-version-detail", kwargs={"pk": self.channel.id}),
+ format="json",
+ )
+ self.assertEqual(response.status_code, 200, response.content)
+ data = response.json()
+
+ # Assert against the most recent ChannelVersion
+ self.assertEqual(data["version"], 2)
+ self.assertEqual(data["resource_count"], 200)
+ self.assertEqual(data["size"], 2048000)
+
+ def test_get_version_detail__is_forbidden_user(self):
+ """Test that forbidden users cannot access version detail."""
+ self.client.force_authenticate(user=self.forbidden_user)
+
+ response = self.client.get(
+ reverse("channel-version-detail", kwargs={"pk": self.channel.id}),
+ format="json",
+ )
+ self.assertEqual(response.status_code, 404, response.content)
+
+
+class GetVersionDetailEndpointTestCase(StudioAPITestCase):
+ """Test get_version_detail API endpoint."""
+
+ def setUp(self):
+ super(GetVersionDetailEndpointTestCase, self).setUp()
+ self.user = testdata.user()
+ self.client.force_authenticate(user=self.user)
+
+ self.channel = testdata.channel()
+ self.channel.version = 3
+ self.channel.published = True
+ self.channel.editors.add(self.user)
+ self.channel.save()
+
+ self.channel_version, _ = ChannelVersion.objects.update_or_create(
+ channel=self.channel,
+ version=3,
+ defaults={
+ "version_notes": "Test version",
+ "size": 5000,
+ "resource_count": 25,
+ "kind_count": [
+ {"count": 10, "kind_id": "video"},
+ {"count": 15, "kind_id": "document"},
+ ],
+ "included_languages": ["en", "es"],
+ "included_licenses": [1, 2],
+ "included_categories": [
+ subjects.SUBJECTSLIST[0],
+ subjects.SUBJECTSLIST[1],
+ ],
+ },
+ )
+
+ self.channel.version_info = self.channel_version
+ self.channel.save()
+
+ def test_get_version_detail_success(self):
+ """Test successfully retrieving version detail."""
+ url = reverse("channel-version-detail", kwargs={"pk": self.channel.id})
+ response = self.client.get(url)
+
+ self.assertEqual(response.status_code, 200)
+
+ data = response.json()
+ self.assertEqual(data["version"], 3)
+ self.assertEqual(data["version_notes"], "Test version")
+ self.assertEqual(data["size"], 5000)
+ self.assertEqual(data["resource_count"], 25)
+
+ def test_get_version_detail_no_version_info(self):
+ """Test endpoint when channel has no version_info."""
+ channel2 = testdata.channel()
+ channel2.version = 1
+ channel2.editors.add(self.user)
+ channel2.save()
+
+ ChannelVersion.objects.filter(channel=channel2).delete()
+
+ Channel.objects.filter(pk=channel2.pk).update(version_info=None)
+
+ url = reverse("channel-version-detail", kwargs={"pk": channel2.id})
+ response = self.client.get(url)
+
+ self.assertEqual(response.status_code, 200)
+ self.assertEqual(response.json(), {})
+
+ def test_get_version_detail_returns_all_fields(self):
+ """Test that get_version_detail returns all expected fields."""
+ url = reverse("channel-version-detail", kwargs={"pk": self.channel.id})
+ response = self.client.get(url)
+
+ self.assertEqual(response.status_code, 200)
+ data = response.json()
+
+ expected_fields = [
+ "id",
+ "version",
+ "resource_count",
+ "kind_count",
+ "size",
+ "date_published",
+ "version_notes",
+ "included_languages",
+ "included_licenses",
+ "included_categories",
+ "non_distributable_licenses_included",
+ ]
+ for field in expected_fields:
+ self.assertIn(field, data, f"Field '{field}' should be in response")
+
+ def test_get_version_detail_excludes_special_permissions_included(self):
+ """Test that special_permissions_included is not in the response."""
+ special_license = AuditedSpecialPermissionsLicense.objects.create(
+ description="Test special permissions license"
+ )
+ self.channel_version.special_permissions_included.add(special_license)
+
+ url = reverse("channel-version-detail", kwargs={"pk": self.channel.id})
+ response = self.client.get(url)
+
+ self.assertEqual(response.status_code, 200)
+ data = response.json()
+
+ self.assertNotIn(
+ "special_permissions_included",
+ data,
+ "special_permissions_included should not be in the response. "
+ "Use /api/audited_special_permissions_license/?channel_version= instead.",
+ )
+
+ def test_get_version_detail_with_special_permissions_licenses(self):
+ """Test that get_version_detail works correctly even when special permissions licenses exist."""
+ license1 = AuditedSpecialPermissionsLicense.objects.create(
+ description="First special permissions license"
+ )
+ license2 = AuditedSpecialPermissionsLicense.objects.create(
+ description="Second special permissions license"
+ )
+
+ self.channel_version.special_permissions_included.add(license1, license2)
+
+ url = reverse("channel-version-detail", kwargs={"pk": self.channel.id})
+ response = self.client.get(url)
+
+ self.assertEqual(response.status_code, 200)
+ data = response.json()
+
+ self.assertNotIn("special_permissions_included", data)
+ self.assertEqual(data["version"], 3)
+ self.assertEqual(data["resource_count"], 25)
+
+
+class ChannelVersionViewsetTestCase(StudioAPITestCase):
+ """Test channel_version Viewset."""
+
+ def setUpChannelWithVersions(self, name, version_count):
+ channel = testdata.channel(name=name)
+ channel.version = version_count
+ channel.published = True
+ channel.editors.add(self.user)
+ channel.save()
+
+ for i in range(version_count):
+ ChannelVersion.objects.update_or_create(
+ channel=channel,
+ version=i + 1,
+ defaults={
+ "version_notes": f"Test version {i + 1}",
+ },
+ )
+ return channel
+
+ def setUp(self):
+ super().setUp()
+ self.user = testdata.user()
+ self.client.force_authenticate(user=self.user)
+
+ self.channel = self.setUpChannelWithVersions(
+ name="Test Channel with Versions", version_count=3
+ )
+
+ self.channel_2 = self.setUpChannelWithVersions(
+ name="Test Channel with Versions 2", version_count=5
+ )
+
+ def test_get_channel_version_without_channel_filter_should_fail(self):
+ """Test retrieving channel versions without channel filter."""
+ url = reverse("channelversion-list")
+ response = self.client.get(url)
+ self.assertEqual(response.status_code, 412)
+
+ def test_get_channel_version_with_invalid_channel_filter_should_fail(self):
+ """Test retrieving channel versions with invalid channel filter."""
+ url = reverse("channelversion-list") + "?channel=invalid-channel-id"
+ response = self.client.get(url)
+ self.assertEqual(response.status_code, 400)
+
+ def test_get_channel_versions_with_valid_channel_filter(self):
+ """Test retrieving channel versions with valid channel filter."""
+ url = reverse("channelversion-list") + f"?channel={self.channel.id}"
+ response = self.client.get(url)
+ self.assertEqual(response.status_code, 200)
+ data = response.json()
+ results = data["results"] if "results" in data else data
+ self.assertEqual(len(results), 3)
+ for version_data in results:
+ self.assertEqual(version_data["channel"], self.channel.id)
+
+ def test_get_specific_channel_version(self):
+ """Test retrieving a specific channel version."""
+ channel_version = ChannelVersion.objects.filter(channel=self.channel).first()
+ url = reverse("channelversion-detail", kwargs={"pk": channel_version.id})
+ response = self.client.get(url)
+ self.assertEqual(response.status_code, 200)
+ data = response.json()
+ self.assertEqual(data["id"], channel_version.id)
+ self.assertEqual(data["channel"], self.channel.id)
+ self.assertEqual(data["version"], channel_version.version)
+
+ def test_get_channel_versions_ordering(self):
+ """Test ordering of channel versions."""
+ url = (
+ reverse("channelversion-list")
+ + f"?channel={self.channel_2.id}&ordering=version"
+ )
+ response = self.client.get(url)
+ self.assertEqual(response.status_code, 200)
+ results = response.json()
+ versions = [version_data["version"] for version_data in results]
+ self.assertEqual(versions, sorted(versions))
+
+ url_desc = (
+ reverse("channelversion-list")
+ + f"?channel={self.channel_2.id}&ordering=-version"
+ )
+ response_desc = self.client.get(url_desc)
+ self.assertEqual(response_desc.status_code, 200)
+ results_desc = response_desc.json()
+ versions_desc = [version_data["version"] for version_data in results_desc]
+ self.assertEqual(versions_desc, sorted(versions_desc, reverse=True))
+
+ def test_get_channel_versions_filter_by_version(self):
+ """Test filtering channel versions by specific version number."""
+ target_version = 2
+ url = (
+ reverse("channelversion-list")
+ + f"?channel={self.channel.id}&version={target_version}"
+ )
+ response = self.client.get(url)
+ self.assertEqual(response.status_code, 200)
+ results = response.json()
+ self.assertEqual(len(results), 1)
+ self.assertEqual(results[0]["version"], target_version)
+
+ def test_get_channel_versions_filter_by_version_gte(self):
+ """Test filtering channel versions by specific version number."""
+ target_version = 2
+ url = (
+ reverse("channelversion-list")
+ + f"?channel={self.channel.id}&version__gte={target_version}"
+ )
+ response = self.client.get(url)
+ self.assertEqual(response.status_code, 200)
+ results = response.json()
+ self.assertEqual(len(results), 2)
+ for version_data in results:
+ self.assertGreaterEqual(version_data["version"], target_version)
+
+ def test_channel_version_cannot_be_created_via_api(self):
+ """Test that channel versions cannot be created via the API."""
+ url = reverse("channelversion-list")
+ payload = {
+ "channel": self.channel.id,
+ "version": 4,
+ "version_notes": "New version via API",
+ }
+ response = self.client.post(url, payload, format="json")
+ self.assertEqual(response.status_code, 405)
+
+ def test_channel_version_cannot_be_updated_via_api(self):
+ """Test that channel versions cannot be updated via the API."""
+ channel_version = ChannelVersion.objects.filter(channel=self.channel).first()
+ url = reverse("channelversion-detail", kwargs={"pk": channel_version.id})
+ payload = {
+ "version_notes": "Updated version notes via API",
+ }
+ response = self.client.patch(url, payload, format="json")
+ self.assertEqual(response.status_code, 405)
+
+ def test_channel_version_cannot_be_deleted_via_api(self):
+ """Test that channel versions cannot be deleted via the API."""
+ channel_version = ChannelVersion.objects.filter(channel=self.channel).first()
+ url = reverse("channelversion-detail", kwargs={"pk": channel_version.id})
+ response = self.client.delete(url)
+ self.assertEqual(response.status_code, 405)
+
+ def test_channel_viewer_can_access_channel_versions(self):
+ """Test that a user with channel viewer permissions can access channel versions."""
+ viewer_user = testdata.user(email="vieweruser@example.com")
+ self.channel.viewers.add(viewer_user)
+ self.client.force_authenticate(user=viewer_user)
+ url = reverse("channelversion-list") + f"?channel={self.channel.id}"
+ response = self.client.get(url)
+ results = response.json()
+ self.assertEqual(len(results), 3)
+
+ def test_non_channel_viewer_cannot_access_channel_versions(self):
+ """Test that a user without channel viewer permissions cannot access channel versions."""
+ other_user = testdata.user(email="otheruser@example.com")
+ self.client.force_authenticate(user=other_user)
+ url = reverse("channelversion-list") + f"?channel={self.channel.id}"
+ response = self.client.get(url)
+ results = response.json()
+ self.assertEqual(len(results), 0)
diff --git a/contentcuration/contentcuration/tests/viewsets/test_community_library_submission.py b/contentcuration/contentcuration/tests/viewsets/test_community_library_submission.py
new file mode 100644
index 0000000000..5dfe4ae0be
--- /dev/null
+++ b/contentcuration/contentcuration/tests/viewsets/test_community_library_submission.py
@@ -0,0 +1,1336 @@
+import datetime
+from unittest import mock
+
+import pytz
+from django.urls import reverse
+
+from contentcuration.constants import (
+ community_library_submission as community_library_submission_constants,
+)
+from contentcuration.models import AuditedSpecialPermissionsLicense
+from contentcuration.models import Change
+from contentcuration.models import Channel
+from contentcuration.models import ChannelVersion
+from contentcuration.models import CommunityLibrarySubmission
+from contentcuration.models import User
+from contentcuration.tests import testdata
+from contentcuration.tests.base import StudioAPITestCase
+from contentcuration.tests.helpers import reverse_with_query
+from contentcuration.viewsets.sync.constants import ADDED_TO_COMMUNITY_LIBRARY
+
+
+class CRUDTestCase(StudioAPITestCase):
+ @property
+ def new_submission_metadata(self):
+ return {
+ "channel": self.channel_without_submission.id,
+ "countries": [self.country1.code],
+ }
+
+ @property
+ def updated_submission_metadata(self):
+ return {
+ "countries": [
+ "C2",
+ ],
+ "channel": self.channel_with_submission1.id,
+ }
+
+ def setUp(self):
+ super().setUp()
+ self.author_user = testdata.user(email="author@user.com")
+ self.editor_user = testdata.user(email="editor@user.com")
+ self.forbidden_user = testdata.user(email="forbidden@user.com")
+ self.admin_user = testdata.user(email="admin@user.com")
+ self.admin_user.is_admin = True
+ self.admin_user.save()
+
+ self.country1 = testdata.country(name="Country 1", code="C1")
+ self.country2 = testdata.country(name="Country 2", code="C2")
+
+ self.channel_with_submission1 = testdata.channel()
+ self.channel_with_submission1.public = False
+ self.channel_with_submission1.version = 1
+ self.channel_with_submission1.editors.add(self.author_user)
+ self.channel_with_submission1.editors.add(self.editor_user)
+ self.channel_with_submission1.save()
+
+ self.channel_with_submission2 = testdata.channel()
+ self.channel_with_submission2.public = False
+ self.channel_with_submission2.version = 1
+ self.channel_with_submission2.editors.add(self.author_user)
+ self.channel_with_submission2.editors.add(self.editor_user)
+ self.channel_with_submission2.save()
+
+ self.channel_without_submission = testdata.channel()
+ self.channel_without_submission.public = False
+ self.channel_without_submission.version = 1
+ self.channel_without_submission.editors.add(self.author_user)
+ self.channel_without_submission.editors.add(self.editor_user)
+ self.channel_without_submission.save()
+
+ self.unpublished_channel = testdata.channel()
+ self.unpublished_channel.public = False
+ self.unpublished_channel.version = 0
+ self.unpublished_channel.editors.add(self.author_user)
+ self.unpublished_channel.editors.add(self.editor_user)
+ self.unpublished_channel.save()
+
+ self.public_channel = testdata.channel()
+ self.public_channel.public = True
+ self.public_channel.version = 1
+ self.public_channel.editors.add(self.author_user)
+ self.public_channel.editors.add(self.editor_user)
+ self.public_channel.save()
+
+ self.existing_submission1 = testdata.community_library_submission()
+ self.existing_submission1.channel = self.channel_with_submission1
+ self.existing_submission1.author = self.author_user
+ self.existing_submission1.save()
+ self.existing_submission1.countries.add(self.country1)
+ self.existing_submission1.save()
+
+ self.existing_submission2 = testdata.community_library_submission()
+ self.existing_submission2.channel = self.channel_with_submission2
+ self.existing_submission2.author = self.author_user
+ self.existing_submission2.save()
+ self.existing_submission2.countries.add(self.country1)
+ self.existing_submission2.save()
+
+ def tearDown(self):
+ super().tearDown()
+
+ def test_create_submission__is_editor(self):
+ self.client.force_authenticate(user=self.editor_user)
+ submission = self.new_submission_metadata
+ response = self.client.post(
+ reverse("community-library-submission-list"),
+ submission,
+ format="json",
+ )
+ self.assertEqual(response.status_code, 201, response.content)
+
+ def test_create_submission__is_forbidden(self):
+ self.client.force_authenticate(user=self.forbidden_user)
+ submission = self.new_submission_metadata
+ response = self.client.post(
+ reverse("community-library-submission-list"),
+ submission,
+ format="json",
+ )
+ self.assertEqual(response.status_code, 400, response.content)
+
+ def test_create_submission__unpublished_channel(self):
+ self.client.force_authenticate(user=self.editor_user)
+ submission = self.new_submission_metadata
+ submission["channel"] = self.unpublished_channel.id
+
+ response = self.client.post(
+ reverse("community-library-submission-list"),
+ submission,
+ format="json",
+ )
+ self.assertEqual(response.status_code, 400, response.content)
+
+ def test_create_submission__public_channel(self):
+ self.client.force_authenticate(user=self.editor_user)
+ submission = self.new_submission_metadata
+ submission["channel"] = self.public_channel.id
+
+ response = self.client.post(
+ reverse("community-library-submission-list"),
+ submission,
+ format="json",
+ )
+ self.assertEqual(response.status_code, 400, response.content)
+
+ def test_create_submission__explicit_channel_version(self):
+ self.client.force_authenticate(user=self.editor_user)
+ submission_metadata = self.new_submission_metadata
+ submission_metadata["channel_version"] = 2
+ response = self.client.post(
+ reverse("community-library-submission-list"),
+ submission_metadata,
+ format="json",
+ )
+ self.assertEqual(response.status_code, 201, response.content)
+
+ created_submission_id = response.data["id"]
+ created_submission = CommunityLibrarySubmission.objects.get(
+ id=created_submission_id
+ )
+
+ # The explicitly set channel version should be ignored by the serializer
+ self.assertEqual(created_submission.channel_version, 1)
+
+ def test_create_submission__publishing_channel(self):
+ self.client.force_authenticate(user=self.editor_user)
+
+ submission_metadata = self.new_submission_metadata
+
+ # Mark the channel referenced by the metadata as publishing
+ from contentcuration.models import Channel
+ from contentcuration.models import ContentNode
+
+ channel = Channel.objects.get(id=submission_metadata["channel"])
+ # Ensure main_tree exists; mark its publishing flag
+ main_tree = channel.main_tree or ContentNode.objects.get(
+ id=channel.main_tree_id
+ )
+ main_tree.publishing = True
+ main_tree.save()
+
+ response = self.client.post(
+ reverse("community-library-submission-list"),
+ submission_metadata,
+ format="json",
+ )
+ self.assertEqual(response.status_code, 400, response.content)
+
+ def test_list_submissions__is_editor(self):
+ self.client.force_authenticate(user=self.editor_user)
+ response = self.client.get(
+ reverse(
+ "community-library-submission-list",
+ )
+ )
+ self.assertEqual(response.status_code, 200, response.content)
+
+ results = response.data
+ self.assertEqual(len(results), 2)
+
+ def test_list_submissions__is_admin(self):
+ self.client.force_authenticate(user=self.admin_user)
+ response = self.client.get(
+ reverse(
+ "community-library-submission-list",
+ )
+ )
+ self.assertEqual(response.status_code, 200, response.content)
+
+ results = response.data
+ self.assertEqual(len(results), 2)
+
+ def test_list_submissions__is_forbidden(self):
+ self.client.force_authenticate(user=self.forbidden_user)
+ response = self.client.get(
+ reverse(
+ "community-library-submission-list",
+ )
+ )
+ self.assertEqual(response.status_code, 200, response.content)
+
+ results = response.data
+ self.assertEqual(len(results), 0)
+
+ def test_list_submissions__filter_by_channel(self):
+ self.client.force_authenticate(user=self.editor_user)
+ response = self.client.get(
+ reverse_with_query(
+ "community-library-submission-list",
+ query={"channel": self.channel_with_submission1.id},
+ )
+ )
+ self.assertEqual(response.status_code, 200, response.content)
+
+ results = response.data
+ self.assertEqual(len(results), 1)
+ self.assertEqual(results[0]["channel_id"], self.channel_with_submission1.id)
+
+ def test_list_submissions__pagination(self):
+ self.client.force_authenticate(user=self.author_user)
+ response = self.client.get(
+ reverse_with_query(
+ "community-library-submission-list",
+ query={"max_results": 1},
+ )
+ )
+ self.assertEqual(response.status_code, 200, response.content)
+
+ results = response.data["results"]
+ more = response.data["more"]
+
+ self.assertEqual(len(results), 1)
+ self.assertIsNotNone(more)
+
+ response = self.client.get(
+ reverse_with_query(
+ "community-library-submission-list",
+ query=more,
+ )
+ )
+ self.assertEqual(response.status_code, 200, response.content)
+
+ results = response.data["results"]
+ more = response.data["more"]
+
+ self.assertEqual(len(results), 1)
+ self.assertIsNone(more)
+
+ def test_get_single_submission__is_editor(self):
+ self.client.force_authenticate(user=self.editor_user)
+ response = self.client.get(
+ reverse(
+ "community-library-submission-detail",
+ args=[self.existing_submission1.id],
+ ),
+ format="json",
+ )
+ self.assertEqual(response.status_code, 200, response.content)
+
+ result = response.data
+ self.assertEqual(result["id"], self.existing_submission1.id)
+
+ def test_get_single_submission__is_admin(self):
+ self.client.force_authenticate(user=self.admin_user)
+ response = self.client.get(
+ reverse(
+ "community-library-submission-detail",
+ args=[self.existing_submission1.id],
+ ),
+ format="json",
+ )
+ self.assertEqual(response.status_code, 200, response.content)
+
+ result = response.data
+ self.assertEqual(result["id"], self.existing_submission1.id)
+
+ def test_get_single_submission__is_forbidden(self):
+ self.client.force_authenticate(user=self.forbidden_user)
+ response = self.client.get(
+ reverse(
+ "community-library-submission-detail",
+ args=[self.existing_submission1.id],
+ ),
+ format="json",
+ )
+ self.assertEqual(response.status_code, 404, response.content)
+
+ def test_get_single_submission__author_name(self):
+ self.client.force_authenticate(user=self.author_user)
+ response = self.client.get(
+ reverse(
+ "community-library-submission-detail",
+ args=[self.existing_submission1.id],
+ ),
+ format="json",
+ )
+ self.assertEqual(response.status_code, 200, response.content)
+
+ result = response.data
+ self.assertEqual(
+ result["author_name"],
+ f"{self.author_user.first_name} {self.author_user.last_name}",
+ )
+
+ def test_update_submission__is_author(self):
+ self.client.force_authenticate(user=self.author_user)
+ response = self.client.put(
+ reverse(
+ "community-library-submission-detail",
+ args=[self.existing_submission1.id],
+ ),
+ self.updated_submission_metadata,
+ format="json",
+ )
+ self.assertEqual(response.status_code, 200, response.content)
+
+ updated_submission = CommunityLibrarySubmission.objects.get(
+ id=self.existing_submission1.id
+ )
+ self.assertEqual(updated_submission.countries.count(), 1)
+ self.assertEqual(updated_submission.countries.first().code, "C2")
+
+ def test_update_submission__is_editor(self):
+ self.client.force_authenticate(user=self.editor_user)
+ response = self.client.put(
+ reverse(
+ "community-library-submission-detail",
+ args=[self.existing_submission1.id],
+ ),
+ self.updated_submission_metadata,
+ format="json",
+ )
+ self.assertEqual(response.status_code, 404, response.content)
+
+ def test_update_submission__is_admin__change_countries(self):
+ self.client.force_authenticate(user=self.admin_user)
+ response = self.client.put(
+ reverse(
+ "community-library-submission-detail",
+ args=[self.existing_submission1.id],
+ ),
+ self.updated_submission_metadata,
+ format="json",
+ )
+ self.assertEqual(response.status_code, 200, response.content)
+
+ updated_submission = CommunityLibrarySubmission.objects.get(
+ id=self.existing_submission1.id
+ )
+ self.assertEqual(updated_submission.countries.count(), 1)
+ self.assertEqual(updated_submission.countries.first().code, "C2")
+
+ def test_update_submission__is_admin__keep_countries(self):
+ self.client.force_authenticate(user=self.admin_user)
+
+ updated_submission_metadata = self.updated_submission_metadata.copy()
+ updated_submission_metadata.pop("countries")
+
+ response = self.client.put(
+ reverse(
+ "community-library-submission-detail",
+ args=[self.existing_submission1.id],
+ ),
+ updated_submission_metadata,
+ format="json",
+ )
+ self.assertEqual(response.status_code, 200, response.content)
+
+ updated_submission = CommunityLibrarySubmission.objects.get(
+ id=self.existing_submission1.id
+ )
+ self.assertEqual(updated_submission.countries.count(), 1)
+ self.assertEqual(updated_submission.countries.first().code, "C1")
+
+ def test_update_submission__change_channel(self):
+ self.client.force_authenticate(user=self.admin_user)
+ submission_metadata = self.updated_submission_metadata
+ submission_metadata["channel"] = self.channel_without_submission.id
+ response = self.client.put(
+ reverse(
+ "community-library-submission-detail",
+ args=[self.existing_submission1.id],
+ ),
+ submission_metadata,
+ format="json",
+ )
+ self.assertEqual(response.status_code, 400, response.content)
+
+ def test_partial_update_submission__missing_channel(self):
+ self.client.force_authenticate(user=self.admin_user)
+ submission_metadata = self.updated_submission_metadata
+ del submission_metadata["channel"]
+ response = self.client.patch(
+ reverse(
+ "community-library-submission-detail",
+ args=[self.existing_submission1.id],
+ ),
+ submission_metadata,
+ format="json",
+ )
+ self.assertEqual(response.status_code, 200, response.content)
+ updated_submission = CommunityLibrarySubmission.objects.get(
+ id=self.existing_submission1.id
+ )
+ self.assertEqual(updated_submission.channel, self.channel_with_submission1)
+
+ def test_delete_submission__is_author(self):
+ self.client.force_authenticate(user=self.author_user)
+ response = self.client.delete(
+ reverse(
+ "community-library-submission-detail",
+ args=[self.existing_submission1.id],
+ ),
+ format="json",
+ )
+ self.assertEqual(response.status_code, 204, response.content)
+ self.assertFalse(
+ CommunityLibrarySubmission.objects.filter(
+ id=self.existing_submission1.id
+ ).exists()
+ )
+
+ def test_delete_submission__is_editor(self):
+ self.client.force_authenticate(user=self.editor_user)
+ response = self.client.delete(
+ reverse(
+ "community-library-submission-detail",
+ args=[self.existing_submission1.id],
+ ),
+ format="json",
+ )
+ self.assertEqual(response.status_code, 404, response.content)
+ self.assertTrue(
+ CommunityLibrarySubmission.objects.filter(
+ id=self.existing_submission1.id
+ ).exists()
+ )
+
+ def test_delete_submission__is_admin(self):
+ self.client.force_authenticate(user=self.admin_user)
+ response = self.client.delete(
+ reverse(
+ "community-library-submission-detail",
+ args=[self.existing_submission1.id],
+ ),
+ format="json",
+ )
+ self.assertEqual(response.status_code, 204, response.content)
+ self.assertFalse(
+ CommunityLibrarySubmission.objects.filter(
+ id=self.existing_submission1.id
+ ).exists()
+ )
+
+ def test_delete_submission__is_forbidden(self):
+ self.client.force_authenticate(user=self.forbidden_user)
+ response = self.client.delete(
+ reverse(
+ "community-library-submission-detail",
+ args=[self.existing_submission1.id],
+ ),
+ format="json",
+ )
+ self.assertEqual(response.status_code, 404, response.content)
+ self.assertTrue(
+ CommunityLibrarySubmission.objects.filter(
+ id=self.existing_submission1.id
+ ).exists()
+ )
+
+
+class AdminViewSetTestCase(StudioAPITestCase):
+ def setUp(self):
+ super().setUp()
+
+ self.resolved_time = datetime.datetime(2023, 10, 1, tzinfo=pytz.utc)
+
+ # Mock django.utils.timezone.now for auto_now field updates
+ self.django_timezone_patcher = mock.patch(
+ "django.utils.timezone.now",
+ return_value=self.resolved_time,
+ )
+ self.django_timezone_patcher.start()
+
+ self.submission = testdata.community_library_submission()
+ self.submission.channel.version = 3
+ self.submission.channel.save()
+ self.submission.channel_version = 2
+ self.submission.save()
+
+ # Create the ChannelVersion for this submission (needed when approving)
+ self.channel_version = ChannelVersion.objects.create(
+ channel=self.submission.channel,
+ version=self.submission.channel_version,
+ )
+
+ self.editor_user = self.submission.channel.editors.first()
+
+ self.superseded_submission = CommunityLibrarySubmission.objects.create(
+ channel=self.submission.channel,
+ author=self.editor_user,
+ status=community_library_submission_constants.STATUS_PENDING,
+ date_created=datetime.datetime(2023, 1, 1, tzinfo=pytz.utc),
+ channel_version=1,
+ )
+ self.not_superseded_submission = CommunityLibrarySubmission.objects.create(
+ channel=self.submission.channel,
+ author=self.editor_user,
+ status=community_library_submission_constants.STATUS_PENDING,
+ date_created=datetime.datetime(2024, 1, 1, tzinfo=pytz.utc),
+ channel_version=3,
+ )
+ self.submission_for_other_channel = testdata.community_library_submission()
+ self.submission_for_other_channel.channel_version = 1
+ self.submission_for_other_channel.save()
+
+ self.feedback_notes = "Feedback"
+ self.internal_notes = "Internal notes"
+
+ self.resolve_approve_metadata = {
+ "status": community_library_submission_constants.STATUS_APPROVED,
+ "feedback_notes": self.feedback_notes,
+ "internal_notes": self.internal_notes,
+ }
+ self.resolve_reject_metadata = {
+ "status": community_library_submission_constants.STATUS_REJECTED,
+ "resolution_reason": community_library_submission_constants.REASON_INVALID_METADATA,
+ "feedback_notes": self.feedback_notes,
+ "internal_notes": self.internal_notes,
+ }
+
+ def tearDown(self):
+ self.django_timezone_patcher.stop()
+ super().tearDown()
+
+ def _manually_reject_submission(self):
+ self.submission.status = community_library_submission_constants.STATUS_REJECTED
+ self.submission.resolved_by = self.admin_user
+ self.submission.resolution_reason = (
+ community_library_submission_constants.REASON_INVALID_METADATA
+ )
+ self.submission.feedback_notes = self.feedback_notes
+ self.submission.internal_notes = self.internal_notes
+ self.submission.save()
+
+ def _refresh_submissions_from_db(self):
+ self.submission.refresh_from_db()
+ self.superseded_submission.refresh_from_db()
+ self.not_superseded_submission.refresh_from_db()
+ self.submission_for_other_channel.refresh_from_db()
+
+ def test_list_submissions__admin(self):
+ self.client.force_authenticate(user=self.admin_user)
+
+ self._manually_reject_submission()
+
+ response = self.client.get(
+ reverse("admin-community-library-submission-list"),
+ )
+ self.assertEqual(response.status_code, 200, response.content)
+
+ results = response.data
+ self.assertEqual(len(results), 4)
+ rejected_results = [
+ result
+ for result in results
+ if result["status"]
+ == community_library_submission_constants.STATUS_REJECTED
+ ]
+ self.assertEqual(len(rejected_results), 1)
+ result = rejected_results[0]
+
+ self.assertEqual(result["resolved_by_id"], self.admin_user.id)
+ self.assertEqual(
+ result["resolved_by_name"],
+ f"{self.admin_user.first_name} {self.admin_user.last_name}",
+ )
+ self.assertEqual(result["internal_notes"], self.internal_notes)
+
+ def test_list_submissions__editor(self):
+ self.client.force_authenticate(user=self.editor_user)
+
+ self._manually_reject_submission()
+
+ response = self.client.get(
+ reverse("admin-community-library-submission-list"),
+ )
+ self.assertEqual(response.status_code, 403, response.content)
+
+ def test_submission_detail__admin(self):
+ self.client.force_authenticate(user=self.admin_user)
+
+ self._manually_reject_submission()
+
+ response = self.client.get(
+ reverse(
+ "admin-community-library-submission-detail",
+ args=[self.submission.id],
+ ),
+ )
+ self.assertEqual(response.status_code, 200, response.content)
+
+ result = response.data
+ self.assertEqual(result["id"], self.submission.id)
+ self.assertEqual(result["resolved_by_id"], self.admin_user.id)
+ self.assertEqual(
+ result["resolved_by_name"],
+ f"{self.admin_user.first_name} {self.admin_user.last_name}",
+ )
+ self.assertEqual(result["internal_notes"], self.internal_notes)
+
+ def test_submission_detail__editor(self):
+ self.client.force_authenticate(user=self.editor_user)
+
+ self._manually_reject_submission()
+
+ response = self.client.get(
+ reverse(
+ "admin-community-library-submission-detail",
+ args=[self.submission.id],
+ ),
+ )
+ self.assertEqual(response.status_code, 403, response.content)
+
+ def test_update_submission(self):
+ self.client.force_authenticate(user=self.admin_user)
+
+ response = self.client.put(
+ reverse(
+ "admin-community-library-submission-detail",
+ args=[self.submission.id],
+ ),
+ {},
+ format="json",
+ )
+ self.assertEqual(response.status_code, 405, response.content)
+
+ def test_partial_update_submission(self):
+ self.client.force_authenticate(user=self.admin_user)
+
+ response = self.client.patch(
+ reverse(
+ "admin-community-library-submission-detail",
+ args=[self.submission.id],
+ ),
+ {},
+ format="json",
+ )
+ self.assertEqual(response.status_code, 405, response.content)
+
+ def test_destroy_submission(self):
+ self.client.force_authenticate(user=self.admin_user)
+
+ response = self.client.delete(
+ reverse(
+ "admin-community-library-submission-detail",
+ args=[self.submission.id],
+ ),
+ format="json",
+ )
+ self.assertEqual(response.status_code, 405, response.content)
+
+ def test_resolve_submission__editor(self):
+ self.client.force_authenticate(user=self.editor_user)
+ response = self.client.post(
+ reverse(
+ "admin-community-library-submission-resolve",
+ args=[self.submission.id],
+ ),
+ self.resolve_approve_metadata,
+ format="json",
+ )
+ self.assertEqual(response.status_code, 403, response.content)
+
+ @mock.patch(
+ "contentcuration.viewsets.community_library_submission.apply_channel_changes_task"
+ )
+ def test_resolve_submission__accept_correct(self, apply_task_mock):
+ self.client.force_authenticate(user=self.admin_user)
+ response = self.client.post(
+ reverse(
+ "admin-community-library-submission-resolve",
+ args=[self.submission.id],
+ ),
+ self.resolve_approve_metadata,
+ format="json",
+ )
+ self.assertEqual(response.status_code, 200, response.content)
+
+ resolved_submission = CommunityLibrarySubmission.objects.get(
+ id=self.submission.id
+ )
+ self.assertEqual(
+ resolved_submission.status,
+ community_library_submission_constants.STATUS_APPROVED,
+ )
+ self.assertEqual(resolved_submission.feedback_notes, self.feedback_notes)
+ self.assertEqual(resolved_submission.internal_notes, self.internal_notes)
+ self.assertEqual(resolved_submission.resolved_by, self.admin_user)
+ self.assertEqual(resolved_submission.date_updated, self.resolved_time)
+
+ self.assertTrue(
+ Change.objects.filter(
+ channel=self.submission.channel,
+ change_type=ADDED_TO_COMMUNITY_LIBRARY,
+ ).exists()
+ )
+ apply_task_mock.fetch_or_enqueue.assert_called_once_with(
+ self.admin_user,
+ channel_id=self.submission.channel.id,
+ )
+
+ @mock.patch(
+ "contentcuration.viewsets.community_library_submission.apply_channel_changes_task"
+ )
+ def test_resolve_submission__reject_correct(self, apply_task_mock):
+ self.client.force_authenticate(user=self.admin_user)
+ response = self.client.post(
+ reverse(
+ "admin-community-library-submission-resolve",
+ args=[self.submission.id],
+ ),
+ self.resolve_reject_metadata,
+ format="json",
+ )
+ self.assertEqual(response.status_code, 200, response.content)
+
+ resolved_submission = CommunityLibrarySubmission.objects.get(
+ id=self.submission.id
+ )
+ self.assertEqual(
+ resolved_submission.status,
+ community_library_submission_constants.STATUS_REJECTED,
+ )
+ self.assertEqual(
+ resolved_submission.resolution_reason,
+ community_library_submission_constants.REASON_INVALID_METADATA,
+ )
+ self.assertEqual(resolved_submission.feedback_notes, self.feedback_notes)
+ self.assertEqual(resolved_submission.internal_notes, self.internal_notes)
+ self.assertEqual(resolved_submission.resolved_by, self.admin_user)
+ self.assertEqual(resolved_submission.date_updated, self.resolved_time)
+
+ self.assertFalse(
+ Change.objects.filter(
+ channel=self.submission.channel,
+ change_type=ADDED_TO_COMMUNITY_LIBRARY,
+ ).exists()
+ )
+ apply_task_mock.fetch_or_enqueue.assert_not_called()
+
+ def test_resolve_submission__reject_missing_resolution_reason(self):
+ self.client.force_authenticate(user=self.admin_user)
+ metadata = self.resolve_reject_metadata.copy()
+ del metadata["resolution_reason"]
+ response = self.client.post(
+ reverse(
+ "admin-community-library-submission-resolve",
+ args=[self.submission.id],
+ ),
+ metadata,
+ format="json",
+ )
+ self.assertEqual(response.status_code, 400, response.content)
+
+ def test_resolve_submission__reject_missing_feedback_notes(self):
+ self.client.force_authenticate(user=self.admin_user)
+ metadata = self.resolve_reject_metadata.copy()
+ del metadata["feedback_notes"]
+ response = self.client.post(
+ reverse(
+ "admin-community-library-submission-resolve",
+ args=[self.submission.id],
+ ),
+ metadata,
+ format="json",
+ )
+ self.assertEqual(response.status_code, 400, response.content)
+
+ def test_resolve_submission__invalid_status(self):
+ self.client.force_authenticate(user=self.admin_user)
+ metadata = self.resolve_approve_metadata.copy()
+ metadata["status"] = (community_library_submission_constants.STATUS_PENDING,)
+ response = self.client.post(
+ reverse(
+ "admin-community-library-submission-resolve",
+ args=[self.submission.id],
+ ),
+ metadata,
+ format="json",
+ )
+ self.assertEqual(response.status_code, 400, response.content)
+
+ def test_resolve_submission__not_pending(self):
+ self.client.force_authenticate(user=self.admin_user)
+ self.submission.status = community_library_submission_constants.STATUS_APPROVED
+ self.submission.save()
+
+ response = self.client.post(
+ reverse(
+ "admin-community-library-submission-resolve",
+ args=[self.submission.id],
+ ),
+ self.resolve_approve_metadata,
+ format="json",
+ )
+ self.assertEqual(response.status_code, 400, response.content)
+
+ @mock.patch(
+ "contentcuration.viewsets.community_library_submission.apply_channel_changes_task"
+ )
+ def test_resolve_submission__overrite_categories(self, apply_task_mock):
+ self.client.force_authenticate(user=self.admin_user)
+ categories = ["Category 1"]
+ self.resolve_approve_metadata["categories"] = categories
+
+ response = self.client.post(
+ reverse(
+ "admin-community-library-submission-resolve",
+ args=[self.submission.id],
+ ),
+ self.resolve_approve_metadata,
+ format="json",
+ )
+ self.assertEqual(response.status_code, 200, response.content)
+
+ resolved_submission = CommunityLibrarySubmission.objects.get(
+ id=self.submission.id
+ )
+ self.assertListEqual(resolved_submission.categories, categories)
+
+ @mock.patch(
+ "contentcuration.viewsets.community_library_submission.apply_channel_changes_task"
+ )
+ def test_resolve_submission__accept_mark_superseded(self, apply_task_mock):
+ self.client.force_authenticate(user=self.admin_user)
+
+ response = self.client.post(
+ reverse(
+ "admin-community-library-submission-resolve",
+ args=[self.submission.id],
+ ),
+ self.resolve_approve_metadata,
+ format="json",
+ )
+ self.assertEqual(response.status_code, 200, response.content)
+
+ self._refresh_submissions_from_db()
+
+ self.assertEqual(
+ self.superseded_submission.status,
+ community_library_submission_constants.STATUS_SUPERSEDED,
+ )
+ self.assertEqual(
+ self.not_superseded_submission.status,
+ community_library_submission_constants.STATUS_PENDING,
+ )
+ self.assertEqual(
+ self.submission_for_other_channel.status,
+ community_library_submission_constants.STATUS_PENDING,
+ )
+
+ def test_resolve_submission__reject_do_not_mark_superseded(self):
+ self.client.force_authenticate(user=self.admin_user)
+
+ response = self.client.post(
+ reverse(
+ "admin-community-library-submission-resolve",
+ args=[self.submission.id],
+ ),
+ self.resolve_reject_metadata,
+ format="json",
+ )
+ self.assertEqual(response.status_code, 200, response.content)
+
+ self._refresh_submissions_from_db()
+
+ self.assertEqual(
+ self.superseded_submission.status,
+ community_library_submission_constants.STATUS_PENDING,
+ )
+ self.assertEqual(
+ self.not_superseded_submission.status,
+ community_library_submission_constants.STATUS_PENDING,
+ )
+ self.assertEqual(
+ self.submission_for_other_channel.status,
+ community_library_submission_constants.STATUS_PENDING,
+ )
+
+ @mock.patch(
+ "contentcuration.viewsets.community_library_submission.apply_channel_changes_task"
+ )
+ def test_resolve_submission__approve_marks_special_permissions_distributable(
+ self, apply_task_mock
+ ):
+ """Test that approving a submission marks special permissions as distributable."""
+ self.client.force_authenticate(user=self.admin_user)
+
+ # Create an audited special permissions license
+ special_license = AuditedSpecialPermissionsLicense.objects.create(
+ description="Community library special permissions"
+ )
+ self.assertFalse(special_license.distributable)
+
+ # Add the special permission to the channel version
+ self.channel_version.special_permissions_included.add(special_license)
+
+ # Approve the submission
+ response = self.client.post(
+ reverse(
+ "admin-community-library-submission-resolve",
+ args=[self.submission.id],
+ ),
+ self.resolve_approve_metadata,
+ format="json",
+ )
+ self.assertEqual(response.status_code, 200, response.content)
+
+ # Verify the special permission is marked as distributable
+ special_license.refresh_from_db()
+ self.assertTrue(special_license.distributable)
+ self.assertTrue(
+ self.channel_version.special_permissions_included.filter(
+ id=special_license.id
+ ).exists()
+ )
+
+
+class FilteringAndSearchTestCase(StudioAPITestCase):
+ """
+ Test cases for the new filtering and search functionality added for the notifications page.
+ Tests date_updated filters, status__in filter, and channel name search.
+ """
+
+ def setUp(self):
+ super().setUp()
+ self.editor_user = testdata.user(email="editor@user.com")
+ self.admin_user = testdata.user(email="admin@user.com")
+ self.admin_user.is_admin = True
+ self.admin_user.first_name = "Admin"
+ self.admin_user.last_name = "User"
+ self.admin_user.save()
+
+ self.math_channel = testdata.channel(name="Math Basics")
+ self.math_channel.public = False
+ self.math_channel.version = 1
+ self.math_channel.editors.add(self.editor_user)
+ self.math_channel.save()
+
+ self.science_channel = testdata.channel(name="Science Fundamentals")
+ self.science_channel.public = False
+ self.science_channel.version = 1
+ self.science_channel.editors.add(self.editor_user)
+ self.science_channel.save()
+
+ self.history_channel = testdata.channel(name="World History")
+ self.history_channel.public = False
+ self.history_channel.version = 1
+ self.history_channel.editors.add(self.editor_user)
+ self.history_channel.save()
+
+ self.math_advanced_channel = testdata.channel(name="Advanced Math")
+ self.math_advanced_channel.public = False
+ self.math_advanced_channel.version = 1
+ self.math_advanced_channel.editors.add(self.editor_user)
+ self.math_advanced_channel.save()
+
+ with mock.patch(
+ "django.utils.timezone.now",
+ return_value=datetime.datetime(2023, 1, 1, tzinfo=pytz.utc),
+ ):
+ self.old_pending_submission = CommunityLibrarySubmission.objects.create(
+ channel=self.math_channel,
+ author=self.editor_user,
+ channel_version=1,
+ status=community_library_submission_constants.STATUS_PENDING,
+ date_created=datetime.datetime(2023, 1, 1, tzinfo=pytz.utc),
+ )
+
+ with mock.patch(
+ "django.utils.timezone.now",
+ return_value=datetime.datetime(2024, 6, 15, tzinfo=pytz.utc),
+ ):
+ self.recent_approved_submission = CommunityLibrarySubmission.objects.create(
+ channel=self.science_channel,
+ author=self.editor_user,
+ channel_version=1,
+ status=community_library_submission_constants.STATUS_APPROVED,
+ date_created=datetime.datetime(2024, 6, 1, tzinfo=pytz.utc),
+ resolved_by=self.admin_user,
+ )
+
+ with mock.patch(
+ "django.utils.timezone.now",
+ return_value=datetime.datetime(2024, 7, 10, tzinfo=pytz.utc),
+ ):
+ self.recent_rejected_submission = CommunityLibrarySubmission.objects.create(
+ channel=self.history_channel,
+ author=self.editor_user,
+ channel_version=1,
+ status=community_library_submission_constants.STATUS_REJECTED,
+ date_created=datetime.datetime(2024, 7, 1, tzinfo=pytz.utc),
+ resolved_by=self.admin_user,
+ )
+
+ with mock.patch(
+ "django.utils.timezone.now",
+ return_value=datetime.datetime(2024, 12, 1, tzinfo=pytz.utc),
+ ):
+ self.very_recent_pending_submission = (
+ CommunityLibrarySubmission.objects.create(
+ channel=self.math_advanced_channel,
+ author=self.editor_user,
+ channel_version=1,
+ status=community_library_submission_constants.STATUS_PENDING,
+ date_created=datetime.datetime(2024, 12, 1, tzinfo=pytz.utc),
+ )
+ )
+
+ def tearDown(self):
+ super().tearDown()
+
+ def test_filter_by_date_updated_gte(self):
+ """Test filtering submissions updated on or after a specific date"""
+ self.client.force_authenticate(user=self.editor_user)
+ response = self.client.get(
+ reverse_with_query(
+ "community-library-submission-list",
+ query={"date_updated__gte": "2024-06-01"},
+ )
+ )
+ self.assertEqual(response.status_code, 200, response.content)
+
+ results = response.data
+ self.assertEqual(len(results), 3)
+ submission_ids = [r["id"] for r in results]
+ self.assertNotIn(self.old_pending_submission.id, submission_ids)
+
+ def test_filter_by_date_updated_lte(self):
+ """Test filtering submissions updated on or before a specific date"""
+ self.client.force_authenticate(user=self.editor_user)
+ response = self.client.get(
+ reverse_with_query(
+ "community-library-submission-list",
+ query={"date_updated__lte": "2024-06-30"},
+ )
+ )
+ self.assertEqual(response.status_code, 200, response.content)
+
+ results = response.data
+ self.assertEqual(len(results), 2)
+ submission_ids = [r["id"] for r in results]
+ self.assertIn(self.old_pending_submission.id, submission_ids)
+ self.assertIn(self.recent_approved_submission.id, submission_ids)
+
+ def test_filter_by_date_updated_range(self):
+ """Test filtering submissions within a date range"""
+ self.client.force_authenticate(user=self.editor_user)
+ response = self.client.get(
+ reverse_with_query(
+ "community-library-submission-list",
+ query={
+ "date_updated__gte": "2024-06-01",
+ "date_updated__lte": "2024-07-31",
+ },
+ )
+ )
+ self.assertEqual(response.status_code, 200, response.content)
+
+ results = response.data
+ self.assertEqual(len(results), 2)
+ submission_ids = [r["id"] for r in results]
+ self.assertIn(self.recent_approved_submission.id, submission_ids)
+ self.assertIn(self.recent_rejected_submission.id, submission_ids)
+
+ def test_filter_by_status_in_single(self):
+ """Test filtering by a single status"""
+ self.client.force_authenticate(user=self.editor_user)
+ response = self.client.get(
+ reverse_with_query(
+ "community-library-submission-list",
+ query={
+ "status__in": community_library_submission_constants.STATUS_APPROVED
+ },
+ )
+ )
+ self.assertEqual(response.status_code, 200, response.content)
+
+ results = response.data
+ self.assertEqual(len(results), 1)
+ self.assertEqual(results[0]["id"], self.recent_approved_submission.id)
+ self.assertEqual(
+ results[0]["status"],
+ community_library_submission_constants.STATUS_APPROVED,
+ )
+
+ def test_filter_by_status_in_multiple(self):
+ """Test filtering by multiple statuses"""
+ self.client.force_authenticate(user=self.editor_user)
+ url = reverse("community-library-submission-list")
+ response = self.client.get(
+ f"{url}?status__in={community_library_submission_constants.STATUS_APPROVED},{community_library_submission_constants.STATUS_REJECTED}"
+ )
+ self.assertEqual(response.status_code, 200, response.content)
+
+ results = response.data
+ self.assertEqual(len(results), 2)
+ statuses = [r["status"] for r in results]
+ self.assertIn(community_library_submission_constants.STATUS_APPROVED, statuses)
+ self.assertIn(community_library_submission_constants.STATUS_REJECTED, statuses)
+ self.assertNotIn(
+ community_library_submission_constants.STATUS_PENDING, statuses
+ )
+
+ def test_search_by_channel_name(self):
+ """Test searching submissions by channel name"""
+ self.client.force_authenticate(user=self.editor_user)
+ response = self.client.get(
+ reverse_with_query(
+ "community-library-submission-list",
+ query={"search": "Math"},
+ )
+ )
+ self.assertEqual(response.status_code, 200, response.content)
+
+ results = response.data
+ self.assertEqual(len(results), 2)
+ channel_ids = [r["channel_id"] for r in results]
+ self.assertIn(self.math_channel.id, channel_ids)
+ self.assertIn(self.math_advanced_channel.id, channel_ids)
+
+ def test_search_by_channel_name_partial(self):
+ """Test searching with partial channel name"""
+ self.client.force_authenticate(user=self.editor_user)
+ response = self.client.get(
+ reverse_with_query(
+ "community-library-submission-list",
+ query={"search": "Science"},
+ )
+ )
+ self.assertEqual(response.status_code, 200, response.content)
+
+ results = response.data
+ self.assertEqual(len(results), 1)
+ self.assertEqual(results[0]["channel_id"], self.science_channel.id)
+
+ def test_combined_filters(self):
+ """Test combining multiple filters together"""
+ self.client.force_authenticate(user=self.editor_user)
+ response = self.client.get(
+ reverse_with_query(
+ "community-library-submission-list",
+ query={
+ "date_updated__gte": "2024-01-01",
+ "status__in": community_library_submission_constants.STATUS_PENDING,
+ "search": "Math",
+ },
+ )
+ )
+ self.assertEqual(response.status_code, 200, response.content)
+
+ results = response.data
+ self.assertEqual(len(results), 1)
+ self.assertEqual(results[0]["id"], self.very_recent_pending_submission.id)
+ self.assertEqual(
+ results[0]["status"],
+ community_library_submission_constants.STATUS_PENDING,
+ )
+ self.assertEqual(results[0]["channel_id"], self.math_advanced_channel.id)
+
+ def test_resolved_by_name_visible_to_editor(self):
+ """Test that editors can now see who resolved their submissions"""
+ self.client.force_authenticate(user=self.editor_user)
+ response = self.client.get(
+ reverse(
+ "community-library-submission-detail",
+ args=[self.recent_approved_submission.id],
+ ),
+ format="json",
+ )
+ self.assertEqual(response.status_code, 200, response.content)
+
+ result = response.data
+ self.assertIn("resolved_by_id", result)
+ self.assertIn("resolved_by_name", result)
+ self.assertEqual(result["resolved_by_id"], self.admin_user.id)
+ self.assertEqual(
+ result["resolved_by_name"],
+ f"{self.admin_user.first_name} {self.admin_user.last_name}",
+ )
+
+ def test_resolved_by_name_null_for_unresolved(self):
+ """Test that resolved_by_name is None for unresolved submissions"""
+ self.client.force_authenticate(user=self.editor_user)
+ response = self.client.get(
+ reverse(
+ "community-library-submission-detail",
+ args=[self.old_pending_submission.id],
+ ),
+ format="json",
+ )
+ self.assertEqual(response.status_code, 200, response.content)
+
+ result = response.data
+ self.assertIsNone(result.get("resolved_by_id"))
+ self.assertIsNone(result.get("resolved_by_name"))
+
+ def test_pagination_ordering_by_date_updated(self):
+ """Test that results are ordered by -date_updated (most recent first)"""
+ self.client.force_authenticate(user=self.editor_user)
+ response = self.client.get(
+ reverse("community-library-submission-list"),
+ format="json",
+ )
+ self.assertEqual(response.status_code, 200, response.content)
+
+ results = response.data
+ self.assertEqual(len(results), 4)
+ self.assertEqual(results[0]["id"], self.very_recent_pending_submission.id)
+ self.assertEqual(results[3]["id"], self.old_pending_submission.id)
+
+ def test_admin_can_use_filters(self):
+ """Test that admin users can also use the new filters"""
+ self.client.force_authenticate(user=self.admin_user)
+ response = self.client.get(
+ reverse_with_query(
+ "admin-community-library-submission-list",
+ query={
+ "date_updated__gte": "2024-06-01",
+ "status__in": community_library_submission_constants.STATUS_APPROVED,
+ },
+ )
+ )
+ self.assertEqual(response.status_code, 200, response.content)
+
+ results = response.data
+ self.assertEqual(len(results), 1)
+ self.assertEqual(results[0]["id"], self.recent_approved_submission.id)
+
+
+class CommunityLibrarySubmissionChannelVersionTestCase(StudioAPITestCase):
+ """Test CommunityLibrarySubmission creates ChannelVersion and tokens."""
+
+ def setUp(self):
+ self.user = User.objects.create(email="test@test.com", is_admin=True)
+ self.channel = Channel.objects.create(
+ name="Test Channel",
+ version=10,
+ actor_id=self.user.id,
+ )
+ self.channel.editors.add(self.user)
+
+ def test_submission_creates_channel_version(self):
+ """Test that creating a submission creates a ChannelVersion."""
+ initial_count = ChannelVersion.objects.filter(channel=self.channel).count()
+
+ submission = CommunityLibrarySubmission.objects.create(
+ channel=self.channel,
+ channel_version=5,
+ author=self.user,
+ description="Test submission",
+ )
+
+ self.assertIsNotNone(submission)
+ self.assertEqual(
+ ChannelVersion.objects.filter(channel=self.channel).count(),
+ initial_count + 1,
+ )
+
+ channel_version = ChannelVersion.objects.get(channel=self.channel, version=5)
+ self.assertIsNotNone(channel_version)
+
+ def test_submission_creates_token(self):
+ """Test that creating a submission creates a token for the ChannelVersion."""
+ submission = CommunityLibrarySubmission.objects.create(
+ channel=self.channel,
+ channel_version=5,
+ author=self.user,
+ description="Test submission",
+ )
+
+ self.assertIsNotNone(submission)
+ channel_version = ChannelVersion.objects.get(channel=self.channel, version=5)
+
+ self.assertIsNotNone(channel_version.secret_token)
+ self.assertFalse(channel_version.secret_token.is_primary)
+
+ def test_submissions_different_versions(self):
+ """Test that submissions for different versions create different tokens."""
+ self.channel.version = 6
+ self.channel.save()
+
+ submission1 = CommunityLibrarySubmission.objects.create(
+ channel=self.channel,
+ channel_version=5,
+ author=self.user,
+ description="Version 5 submission",
+ )
+
+ submission2 = CommunityLibrarySubmission.objects.create(
+ channel=self.channel,
+ channel_version=6,
+ author=self.user,
+ description="Version 6 submission",
+ )
+
+ self.assertIsNotNone(submission1)
+ self.assertIsNotNone(submission2)
+ v5 = ChannelVersion.objects.get(channel=self.channel, version=5)
+ v6 = ChannelVersion.objects.get(channel=self.channel, version=6)
+
+ self.assertIsNotNone(v5.secret_token)
+ self.assertIsNotNone(v6.secret_token)
+ self.assertNotEqual(v5.secret_token, v6.secret_token)
diff --git a/contentcuration/contentcuration/tests/viewsets/test_user.py b/contentcuration/contentcuration/tests/viewsets/test_user.py
index 5e8554f35a..914404bf24 100644
--- a/contentcuration/contentcuration/tests/viewsets/test_user.py
+++ b/contentcuration/contentcuration/tests/viewsets/test_user.py
@@ -173,3 +173,65 @@ def test_fetch_users_no_permissions(self):
)
self.assertEqual(response.status_code, 200, response.content)
self.assertEqual(response.json(), [])
+
+
+class MarkReadNotificationsTimestampTestCase(StudioAPITestCase):
+ def setUp(self):
+ super(MarkReadNotificationsTimestampTestCase, self).setUp()
+ self.user = testdata.user()
+
+ def test_mark_read_notifications_timestamp_success(self):
+ self.client.force_authenticate(user=self.user)
+ response = self.client.post(
+ reverse("user-mark-notifications-read"),
+ data={"timestamp": "2023-12-16T10:00:00Z"},
+ format="json",
+ )
+ self.assertEqual(response.status_code, 204, response.content)
+
+ def test_mark_read_notifications_timestamp_invalid_format(self):
+ self.client.force_authenticate(user=self.user)
+ response = self.client.post(
+ reverse("user-mark-notifications-read"),
+ data={"timestamp": "invalid-timestamp"},
+ format="json",
+ )
+ self.assertEqual(response.status_code, 400, response.content)
+
+ def test_mark_read_notifications_timestamp_missing_timestamp(self):
+ self.client.force_authenticate(user=self.user)
+ response = self.client.post(
+ reverse("user-mark-notifications-read"),
+ data={},
+ format="json",
+ )
+ self.assertEqual(response.status_code, 400, response.content)
+
+ def test_mark_read_notifications_timestamp_unauthenticated(self):
+ response = self.client.post(
+ reverse("user-mark-notifications-read"),
+ data={"timestamp": "2023-12-16T10:00:00Z"},
+ format="json",
+ )
+ self.assertEqual(response.status_code, 403, response.content)
+
+ def test_mark_read_notifications_timestamp_updates_field(self):
+ timestamp = "2023-12-16T10:00:00Z"
+ self.client.force_authenticate(user=self.user)
+
+ response = self.client.post(
+ reverse("user-mark-notifications-read"),
+ data={"timestamp": timestamp},
+ format="json",
+ )
+
+ self.assertEqual(response.status_code, 204, response.content)
+
+ # Refresh user from database and check the timestamp was updated
+ self.user.refresh_from_db()
+ # Check that the last_read_notification_date field was updated
+ self.assertIsNotNone(self.user.last_read_notification_date)
+ self.assertEqual(
+ self.user.last_read_notification_date.isoformat(),
+ timestamp.replace("Z", "+00:00"),
+ )
diff --git a/contentcuration/contentcuration/urls.py b/contentcuration/contentcuration/urls.py
index 720bbc3d55..9b942b79ae 100644
--- a/contentcuration/contentcuration/urls.py
+++ b/contentcuration/contentcuration/urls.py
@@ -34,12 +34,22 @@
import contentcuration.views.zip as zip_views
from contentcuration.views import pwa
from contentcuration.viewsets.assessmentitem import AssessmentItemViewSet
+from contentcuration.viewsets.audited_special_permissions_license import (
+ AuditedSpecialPermissionsLicenseViewSet,
+)
from contentcuration.viewsets.bookmark import BookmarkViewSet
from contentcuration.viewsets.channel import AdminChannelViewSet
from contentcuration.viewsets.channel import CatalogViewSet
+from contentcuration.viewsets.channel import ChannelVersionViewSet
from contentcuration.viewsets.channel import ChannelViewSet
from contentcuration.viewsets.channelset import ChannelSetViewSet
from contentcuration.viewsets.clipboard import ClipboardViewSet
+from contentcuration.viewsets.community_library_submission import (
+ AdminCommunityLibrarySubmissionViewSet,
+)
+from contentcuration.viewsets.community_library_submission import (
+ CommunityLibrarySubmissionViewSet,
+)
from contentcuration.viewsets.contentnode import ContentNodeViewSet
from contentcuration.viewsets.feedback import FlagFeedbackEventViewSet
from contentcuration.viewsets.feedback import RecommendationsEventViewSet
@@ -87,6 +97,26 @@ def get_redirect_url(self, *args, **kwargs):
RecommendationsInteractionEventViewSet,
basename="recommendations-interaction-events",
)
+router.register(
+ r"communitylibrary_submission",
+ CommunityLibrarySubmissionViewSet,
+ basename="community-library-submission",
+)
+router.register(
+ r"admin_communitylibrary_submission",
+ AdminCommunityLibrarySubmissionViewSet,
+ basename="admin-community-library-submission",
+)
+router.register(
+ r"audited-special-permissions-license",
+ AuditedSpecialPermissionsLicenseViewSet,
+ basename="audited-special-permissions-license",
+)
+router.register(
+ r"channelversion",
+ ChannelVersionViewSet,
+ basename="channelversion",
+)
urlpatterns = [
re_path(r"^api/", include(router.urls)),
diff --git a/contentcuration/contentcuration/utils/publish.py b/contentcuration/contentcuration/utils/publish.py
index 3e28f2d0e0..1b8f08131a 100644
--- a/contentcuration/contentcuration/utils/publish.py
+++ b/contentcuration/contentcuration/utils/publish.py
@@ -35,6 +35,7 @@
from le_utils.constants import exercises
from le_utils.constants import file_formats
from le_utils.constants import format_presets
+from le_utils.constants import licenses
from le_utils.constants import roles
from search.models import ChannelFullTextSearch
from search.models import ContentNodeFullTextSearch
@@ -144,20 +145,24 @@ def create_content_database(
user_id,
force_exercises,
progress_tracker=None,
+ is_draft_version=False,
use_staging_tree=False,
):
"""
:type progress_tracker: contentcuration.utils.celery.ProgressTracker|None
"""
+ if not is_draft_version and use_staging_tree:
+ raise ValueError("Staging tree is only supported for draft versions")
+
if not channel.language:
raise ChannelIncompleteError("Channel must have a language set to be published")
- if not use_staging_tree and not force:
+ if not is_draft_version and not force:
raise_if_nodes_are_all_unchanged(channel)
fh, tempdb = tempfile.mkstemp(suffix=".sqlite3")
with using_content_database(tempdb):
- if not use_staging_tree and not channel.main_tree.publishing:
+ if not is_draft_version and not channel.main_tree.publishing:
channel.mark_publishing(user_id)
call_command(
@@ -183,11 +188,11 @@ def create_content_database(
progress_tracker.track(90)
map_prerequisites(base_tree)
# Need to save as version being published, not current version
- version = "next" if use_staging_tree else channel.version + 1
+ version = "next" if is_draft_version else channel.version + 1
save_export_database(
channel.pk,
version,
- use_staging_tree,
+ is_draft_version,
)
if channel.public:
mapper = ChannelMapper(kolibri_channel)
@@ -207,10 +212,24 @@ def create_kolibri_license_object(ccnode):
def increment_channel_version(channel):
+
channel.version += 1
channel.save()
+def create_draft_channel_version(channel):
+
+ channel_version, created = ccmodels.ChannelVersion.objects.get_or_create(
+ channel=channel,
+ version=None,
+ )
+
+ if created:
+ channel_version.new_token()
+
+ return channel_version
+
+
def assign_license_to_contentcuration_nodes(channel, license):
channel.main_tree.get_family().update(license_id=license.pk)
@@ -846,17 +865,19 @@ def mark_all_nodes_as_published(tree):
logging.info("Marked all nodes as published.")
-def save_export_database(channel_id, version, use_staging_tree=False):
+def get_content_db_path(channel_id, version=None):
+ if version is not None:
+ return os.path.join(settings.DB_ROOT, f"{channel_id}-{version}.sqlite3")
+ return os.path.join(settings.DB_ROOT, f"{channel_id}.sqlite3")
+
+
+def save_export_database(channel_id, version, is_draft_version=False):
logging.debug("Saving export database")
current_export_db_location = get_active_content_database()
- target_paths = [
- os.path.join(settings.DB_ROOT, "{}-{}.sqlite3".format(channel_id, version))
- ]
- # Only create non-version path if not using the staging tree
- if not use_staging_tree:
- target_paths.append(
- os.path.join(settings.DB_ROOT, "{id}.sqlite3".format(id=channel_id))
- )
+ target_paths = [get_content_db_path(channel_id, version)]
+ # Only create non-version path if not is_draft_version
+ if not is_draft_version:
+ target_paths.append(get_content_db_path(channel_id))
for target_export_db_location in target_paths:
with open(current_export_db_location, "rb") as currentf:
@@ -896,13 +917,34 @@ def fill_published_fields(channel, version_notes):
node_languages = published_nodes.exclude(language=None).values_list(
"language", flat=True
)
- file_languages = published_nodes.values_list("files__language", flat=True)
+ file_languages = published_nodes.exclude(files__language=None).values_list(
+ "files__language", flat=True
+ )
language_list = list(set(chain(node_languages, file_languages)))
for lang in language_list:
if lang:
channel.included_languages.add(lang)
+ included_licenses = published_nodes.exclude(license=None).values_list(
+ "license", flat=True
+ )
+ license_list = sorted(set(included_licenses))
+
+ included_categories_dicts = published_nodes.exclude(categories=None).values_list(
+ "categories", flat=True
+ )
+ category_list = sorted(
+ set(
+ chain.from_iterable(
+ (
+ node_categories_dict.keys()
+ for node_categories_dict in included_categories_dicts
+ )
+ )
+ )
+ )
+
# TODO: Eventually, consolidate above operations to just use this field for storing historical data
channel.published_data.update(
{
@@ -915,9 +957,83 @@ def fill_published_fields(channel, version_notes):
),
"version_notes": version_notes,
"included_languages": language_list,
+ "included_licenses": license_list,
+ "included_categories": category_list,
}
}
)
+
+ # Calculate non-distributable licenses (All Rights Reserved)
+ all_rights_reserved_id = (
+ ccmodels.License.objects.filter(license_name=licenses.ALL_RIGHTS_RESERVED)
+ .values_list("id", flat=True)
+ .first()
+ )
+
+ non_distributable_licenses_included = (
+ [all_rights_reserved_id]
+ if all_rights_reserved_id and all_rights_reserved_id in license_list
+ else []
+ )
+
+ # records for each unique description so reviewers can approve/reject them individually.
+ # This allows centralized tracking of custom licenses across all channels.
+ special_permissions_id = (
+ ccmodels.License.objects.filter(license_name=licenses.SPECIAL_PERMISSIONS)
+ .values_list("id", flat=True)
+ .first()
+ )
+
+ special_perms_descriptions = None
+ if special_permissions_id and special_permissions_id in license_list:
+ special_perms_descriptions = list(
+ published_nodes.filter(license_id=special_permissions_id)
+ .exclude(license_description__isnull=True)
+ .exclude(license_description="")
+ .values_list("license_description", flat=True)
+ .distinct()
+ )
+
+ if special_perms_descriptions:
+ new_licenses = [
+ ccmodels.AuditedSpecialPermissionsLicense(
+ description=description, distributable=False
+ )
+ for description in special_perms_descriptions
+ ]
+
+ ccmodels.AuditedSpecialPermissionsLicense.objects.bulk_create(
+ new_licenses, ignore_conflicts=True
+ )
+
+ if channel.version_info:
+ channel.version_info.resource_count = channel.total_resource_count
+ channel.version_info.kind_count = kind_counts
+ channel.version_info.size = int(channel.published_size)
+ channel.version_info.date_published = channel.last_published
+ channel.version_info.version_notes = version_notes
+ channel.version_info.included_languages = language_list
+ channel.version_info.included_licenses = license_list
+ channel.version_info.included_categories = category_list
+ channel.version_info.non_distributable_licenses_included = (
+ non_distributable_licenses_included
+ )
+ channel.version_info.save()
+
+ if special_perms_descriptions:
+ channel.version_info.special_permissions_included.set(
+ ccmodels.AuditedSpecialPermissionsLicense.objects.filter(
+ description__in=special_perms_descriptions
+ )
+ )
+ else:
+ channel.version_info.special_permissions_included.clear()
+
+ if channel.public:
+ ccmodels.AuditedSpecialPermissionsLicense.mark_channel_version_as_distributable(
+ channel.version_info.id
+ )
+
channel.save()
@@ -995,6 +1111,7 @@ def publish_channel( # noqa: C901
send_email=False,
progress_tracker=None,
language=settings.LANGUAGE_CODE,
+ is_draft_version=False,
use_staging_tree=False,
):
"""
@@ -1015,23 +1132,32 @@ def publish_channel( # noqa: C901
user_id,
force_exercises,
progress_tracker=progress_tracker,
+ is_draft_version=is_draft_version,
use_staging_tree=use_staging_tree,
)
add_tokens_to_channel(channel)
- if not use_staging_tree:
+ if is_draft_version:
+ create_draft_channel_version(channel)
+ else:
increment_channel_version(channel)
+ if not is_draft_version:
+ ccmodels.ChannelVersion.objects.filter(
+ channel=channel, version=None
+ ).delete()
+ draft_db_path = get_content_db_path(channel_id, "next")
+ if storage.exists(draft_db_path):
+ storage.delete(draft_db_path)
+
sync_contentnode_and_channel_tsvectors(channel_id=channel.id)
mark_all_nodes_as_published(base_tree)
fill_published_fields(channel, version_notes)
-
- # Attributes not getting set for some reason, so just save it here
- base_tree.publishing = False
- base_tree.changed = False
- base_tree.published = True
- base_tree.save()
+ base_tree.publishing = False
+ base_tree.changed = False
+ base_tree.published = True
+ base_tree.save()
# Delete public channel cache.
- if not use_staging_tree and channel.public:
+ if not is_draft_version and channel.public:
delete_public_channel_cache_keys()
if send_email:
@@ -1053,8 +1179,9 @@ def publish_channel( # noqa: C901
finally:
if kolibri_temp_db and os.path.exists(kolibri_temp_db):
os.remove(kolibri_temp_db)
- base_tree.publishing = False
- base_tree.save()
+ if not is_draft_version:
+ base_tree.publishing = False
+ base_tree.save()
elapsed = time.time() - start
@@ -1066,3 +1193,36 @@ def publish_channel( # noqa: C901
except SlowPublishError as e:
report_exception(e)
return channel
+
+
+def ensure_versioned_database_exists(channel_id, channel_version):
+ """
+ Ensures that the versioned database exists, and if not, copies the unversioned database to the versioned path.
+ This happens if the channel was published back when versioned databases were not used.
+ """
+ if channel_version == 0:
+ raise ValueError("An unpublished channel cannot have a versioned database.")
+
+ unversioned_db_storage_path = get_content_db_path(channel_id)
+ versioned_db_storage_path = get_content_db_path(channel_id, channel_version)
+
+ if not storage.exists(versioned_db_storage_path):
+ if not storage.exists(unversioned_db_storage_path):
+ # This should never happen, a published channel should always have an unversioned database
+ raise FileNotFoundError(
+ f"Neither unversioned nor versioned database found for channel {channel_id}."
+ )
+
+ # NOTE: This should not result in a race condition in the case that a newer
+ # version of the channel is published before the task running this function
+ # is executed. In that case, the publishing logic would have already created
+ # the versioned database. The only case where this could be problematic is
+ # if this happens between the check above this comment and the commands below
+ # it. However, this is EXTREMELY unlikely, and could probably only be solved
+ # by introducing a locking mechanism for the database storage objects.
+ with storage.open(unversioned_db_storage_path, "rb") as unversioned_db_file:
+ storage.save(versioned_db_storage_path, unversioned_db_file)
+
+ logging.info(
+ f"Versioned database for channel {channel_id} did not exist, copied the unversioned database to {versioned_db_storage_path}."
+ )
diff --git a/contentcuration/contentcuration/views/admin.py b/contentcuration/contentcuration/views/admin.py
index 8e674c3ac4..0dc4ece51b 100644
--- a/contentcuration/contentcuration/views/admin.py
+++ b/contentcuration/contentcuration/views/admin.py
@@ -25,7 +25,10 @@ def send_custom_email(request):
data = json.loads(request.body)
try:
sendcustomemails_task.enqueue(
- request.user, data["subject"], data["message"], data["query"]
+ request.user,
+ subject=data["subject"],
+ message=data["message"],
+ query=data["query"],
)
except KeyError:
raise ObjectDoesNotExist("Missing attribute from data: {}".format(data))
diff --git a/contentcuration/contentcuration/views/base.py b/contentcuration/contentcuration/views/base.py
index 082d96376d..01af89cc96 100644
--- a/contentcuration/contentcuration/views/base.py
+++ b/contentcuration/contentcuration/views/base.py
@@ -76,6 +76,8 @@
"clipboard_tree_id",
"policies",
"feature_flags",
+ "newest_notification_date",
+ "last_read_notification_date",
)
diff --git a/contentcuration/contentcuration/views/json_dump.py b/contentcuration/contentcuration/views/json_dump.py
index 0678420ecd..81b52e1209 100644
--- a/contentcuration/contentcuration/views/json_dump.py
+++ b/contentcuration/contentcuration/views/json_dump.py
@@ -1,5 +1,6 @@
import json
+from django.core.serializers.json import DjangoJSONEncoder
from rest_framework.renderers import JSONRenderer
"""
@@ -15,7 +16,9 @@ def _json_dumps(value):
"""
json.dumps parameters for dumping unicode into JS
"""
- return json.dumps(value, separators=(",", ":"), ensure_ascii=False)
+ return json.dumps(
+ value, separators=(",", ":"), ensure_ascii=False, cls=DjangoJSONEncoder
+ )
def json_for_parse_from_data(data):
diff --git a/contentcuration/contentcuration/viewsets/audited_special_permissions_license.py b/contentcuration/contentcuration/viewsets/audited_special_permissions_license.py
new file mode 100644
index 0000000000..df4a766971
--- /dev/null
+++ b/contentcuration/contentcuration/viewsets/audited_special_permissions_license.py
@@ -0,0 +1,53 @@
+from django_filters.rest_framework import BooleanFilter
+from django_filters.rest_framework import DjangoFilterBackend
+from django_filters.rest_framework import FilterSet
+from django_filters.rest_framework import UUIDFilter
+from rest_framework.permissions import IsAuthenticated
+
+from contentcuration.models import AuditedSpecialPermissionsLicense
+from contentcuration.viewsets.base import ReadOnlyValuesViewset
+from contentcuration.viewsets.common import UUIDInFilter
+
+
+class AuditedSpecialPermissionsLicenseFilter(FilterSet):
+ """
+ Filter for AuditedSpecialPermissionsLicense viewset.
+ Supports filtering by IDs, distributable status, and channelVersion.
+ """
+
+ by_ids = UUIDInFilter(field_name="id")
+ distributable = BooleanFilter()
+ channel_version = UUIDFilter(
+ field_name="channel_versions__id",
+ help_text="Filter by ChannelVersion ID",
+ )
+
+ class Meta:
+ model = None
+ fields = ("by_ids", "distributable", "channel_version")
+
+ def __init__(self, *args, **kwargs):
+
+ self.Meta.model = AuditedSpecialPermissionsLicense
+ super().__init__(*args, **kwargs)
+
+
+class AuditedSpecialPermissionsLicenseViewSet(ReadOnlyValuesViewset):
+ """
+ Read-only viewset for AuditedSpecialPermissionsLicense.
+ Allows filtering by IDs, distributable status, and channelVersion.
+ """
+
+ permission_classes = [IsAuthenticated]
+ filter_backends = [DjangoFilterBackend]
+ filterset_class = AuditedSpecialPermissionsLicenseFilter
+
+ values = (
+ "id",
+ "description",
+ "distributable",
+ )
+
+ def get_queryset(self):
+
+ return AuditedSpecialPermissionsLicense.objects.all()
diff --git a/contentcuration/contentcuration/viewsets/channel.py b/contentcuration/contentcuration/viewsets/channel.py
index c2462836d1..46d6d063e8 100644
--- a/contentcuration/contentcuration/viewsets/channel.py
+++ b/contentcuration/contentcuration/viewsets/channel.py
@@ -8,6 +8,7 @@
from django.conf import settings
from django.db import IntegrityError
from django.db.models import Exists
+from django.db.models import FilteredRelation
from django.db.models import OuterRef
from django.db.models import Q
from django.db.models import Subquery
@@ -21,6 +22,9 @@
from django_cte import With
from django_filters.rest_framework import BooleanFilter
from django_filters.rest_framework import CharFilter
+from kolibri_public.utils.export_channel_to_kolibri_public import (
+ export_channel_to_kolibri_public,
+)
from le_utils.constants import content_kinds
from le_utils.constants import roles
from rest_framework import serializers
@@ -39,16 +43,23 @@
from search.utils import get_fts_search_query
import contentcuration.models as models
+from contentcuration.constants import (
+ community_library_submission as community_library_submission_constants,
+)
from contentcuration.decorators import cache_no_user_data
from contentcuration.models import Change
from contentcuration.models import Channel
+from contentcuration.models import ChannelVersion
+from contentcuration.models import CommunityLibrarySubmission
from contentcuration.models import ContentNode
+from contentcuration.models import Country
from contentcuration.models import File
from contentcuration.models import generate_storage_url
from contentcuration.models import SecretToken
from contentcuration.models import User
from contentcuration.utils.garbage_collect import get_deleted_chefs_root
from contentcuration.utils.pagination import CachedListPagination
+from contentcuration.utils.pagination import ValuesViewsetCursorPagination
from contentcuration.utils.pagination import ValuesViewsetPageNumberPagination
from contentcuration.utils.publish import ChannelIncompleteError
from contentcuration.utils.publish import publish_channel
@@ -86,6 +97,12 @@ class CatalogListPagination(CachedListPagination):
max_page_size = 1000
+class ChannelVersionListPagination(ValuesViewsetCursorPagination):
+ ordering = "-version"
+ page_size_query_param = "max_results"
+ max_page_size = 100
+
+
primary_token_subquery = Subquery(
SecretToken.objects.filter(channels=OuterRef("id"), is_primary=True)
.values("token")
@@ -437,7 +454,12 @@ class ChannelViewSet(ValuesViewset):
ordering = "-modified"
field_map = channel_field_map
- values = base_channel_values + ("edit", "view", "unpublished_changes")
+ values = base_channel_values + (
+ "edit",
+ "view",
+ "unpublished_changes",
+ "draft_token",
+ )
def create(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
@@ -493,6 +515,14 @@ def get_queryset(self):
view=Exists(user_queryset.filter(view_only_channels=OuterRef("id"))),
)
+ def _annotate_draft_token(self, queryset):
+ draft_token_subquery = Subquery(
+ ChannelVersion.objects.filter(channel=OuterRef("id"), version=None).values(
+ "secret_token__token"
+ )[:1]
+ )
+ return queryset.annotate(draft_token=draft_token_subquery)
+
def annotate_queryset(self, queryset):
queryset = queryset.annotate(primary_token=primary_token_subquery)
channel_main_tree_nodes = ContentNode.objects.filter(
@@ -514,6 +544,8 @@ def annotate_queryset(self, queryset):
unpublished_changes=Exists(_unpublished_changes_query(OuterRef("id")))
)
+ queryset = self._annotate_draft_token(queryset)
+
return queryset
def publish_from_changes(self, changes):
@@ -564,7 +596,9 @@ def publish(self, pk, version_notes="", language=None):
{
"published": True,
"publishing": False,
+ "version": channel.version,
"primary_token": channel.get_human_token().token,
+ "draft_token": None,
"last_published": channel.last_published,
"unpublished_changes": _unpublished_changes_query(
channel
@@ -606,28 +640,27 @@ def publish(self, pk, version_notes="", language=None):
raise
def publish_next_from_changes(self, changes):
+
errors = []
for publish in changes:
try:
- self.publish_next(publish["key"])
+ self.publish_next(
+ publish["key"],
+ use_staging_tree=publish.get("use_staging_tree", False),
+ )
except Exception as e:
log_sync_exception(e, user=self.request.user, change=publish)
publish["errors"] = [str(e)]
errors.append(publish)
return errors
- def publish_next(self, pk):
+ def publish_next(self, pk, use_staging_tree=False):
logging.debug("Entering the publish staging channel endpoint")
channel = self.get_edit_queryset().get(pk=pk)
if channel.deleted:
raise ValidationError("Cannot publish a deleted channel")
- elif channel.staging_tree.publishing:
- raise ValidationError("Channel staging tree is already publishing")
-
- channel.staging_tree.publishing = True
- channel.staging_tree.save()
with create_change_tracker(
pk, CHANNEL, channel.id, self.request.user, "export-channel-staging-tree"
@@ -637,15 +670,19 @@ def publish_next(self, pk):
self.request.user.pk,
channel.id,
progress_tracker=progress_tracker,
- use_staging_tree=True,
+ is_draft_version=True,
+ use_staging_tree=use_staging_tree,
)
+ draft_token = channel.get_draft_token()
Change.create_changes(
[
generate_update_event(
channel.id,
CHANNEL,
{
- "primary_token": channel.get_human_token().token,
+ "draft_token": draft_token.token
+ if draft_token
+ else None,
},
channel_id=channel.id,
),
@@ -653,12 +690,8 @@ def publish_next(self, pk):
applied=True,
)
except ChannelIncompleteError:
- channel.staging_tree.publishing = False
- channel.staging_tree.save()
raise ValidationError("Channel is not ready to be published")
except Exception:
- channel.staging_tree.publishing = False
- channel.staging_tree.save()
raise
def sync_from_changes(self, changes):
@@ -768,6 +801,65 @@ def deploy(self, user, pk):
created_by_id=user.id,
)
+ def add_to_community_library_from_changes(self, changes):
+ errors = []
+ for change in changes:
+ try:
+ self.add_to_community_library(
+ channel_id=change["key"],
+ channel_version=change["channel_version"],
+ categories=change["categories"],
+ country_codes=change["country_codes"],
+ )
+ except Exception as e:
+ log_sync_exception(e, user=self.request.user, change=change)
+ change["errors"] = [str(e)]
+ errors.append(change)
+ return errors
+
+ def add_to_community_library(
+ self, channel_id, channel_version, categories, country_codes
+ ):
+ # Note: The `categories` field should contain a _dict_, with the category IDs as keys
+ # and `True` as a value. This is to match the representation
+ # of sets in the changes architecture.
+
+ # The change to add a channel to the community library can only
+ # be created server-side, so in theory we should not be getting
+ # malformed requests here. However, just to be safe, we still
+ # do basic checks.
+
+ channel = self.get_edit_queryset().get(pk=channel_id)
+ countries = Country.objects.filter(code__in=country_codes)
+
+ if channel.public:
+ raise ValidationError(
+ "Public channels cannot be added to the community library"
+ )
+ if channel_version <= 0 or channel_version > channel.version:
+ raise ValidationError("Invalid channel version")
+
+ # Because of changes architecture, the categories are passed as a dict
+ # with the category IDs as keys and `True` as a value. At this point,
+ # we are no longer working with changes, so we switch to the more
+ # convenient representation as a list.
+ categories_list = [key for key, val in categories.items() if val]
+
+ export_channel_to_kolibri_public(
+ channel_id=channel_id,
+ channel_version=channel_version,
+ public=False, # Community library
+ categories=categories_list,
+ countries=countries,
+ )
+
+ new_live_submission = CommunityLibrarySubmission.objects.get(
+ channel_id=channel_id,
+ channel_version=channel_version,
+ status=community_library_submission_constants.STATUS_APPROVED,
+ )
+ new_live_submission.mark_live()
+
@action(
detail=True,
methods=["get"],
@@ -814,6 +906,62 @@ def get_languages_in_channel(
langs_in_content = self._get_channel_content_languages(pk, main_tree_id)
return JsonResponse({"languages": langs_in_content})
+ @action(
+ detail=True,
+ methods=["get"],
+ url_path="version_detail",
+ url_name="version-detail",
+ )
+ def get_version_detail(self, request, pk=None) -> Response:
+ """
+ Get the version detail for a channel.
+
+ :param request: The request object
+ :param pk: The ID of the channel
+ :return: Response with the version detail of the channel
+ :rtype: Response
+ """
+ channel = self.get_edit_object()
+
+ if not channel.version_info:
+ return Response({})
+
+ version_data = (
+ ChannelVersion.objects.filter(id=channel.version_info.id)
+ .values(
+ "id",
+ "version",
+ "resource_count",
+ "kind_count",
+ "size",
+ "date_published",
+ "version_notes",
+ "included_languages",
+ "included_licenses",
+ "included_categories",
+ "non_distributable_licenses_included",
+ )
+ .first()
+ )
+
+ if not version_data:
+ return Response({})
+
+ return Response(version_data)
+
+ @action(
+ detail=True,
+ methods=["get"],
+ url_path="has_community_library_submission",
+ url_name="has-community-library-submission",
+ )
+ def has_community_library_submission(self, request, pk=None) -> Response:
+ channel = self.get_object()
+ has_submission = CommunityLibrarySubmission.objects.filter(
+ channel_id=channel.id
+ ).exists()
+ return Response({"has_community_library_submission": has_submission})
+
def _channel_exists(self, channel_id) -> bool:
"""
Check if a channel exists.
@@ -902,6 +1050,38 @@ def _get_channel_content_languages(
return unique_lang_ids
+class ChannelVersionFilter(RequiredFilterSet):
+ class Meta:
+ model = ChannelVersion
+ fields = {
+ "channel": ["exact"],
+ "version": ["exact", "gte", "lte"],
+ }
+
+
+class ChannelVersionViewSet(ReadOnlyValuesViewset):
+ queryset = ChannelVersion.objects.all()
+ permission_classes = [IsAuthenticated]
+ pagination_class = ChannelVersionListPagination
+ filterset_class = ChannelVersionFilter
+ ordering_fields = ["version"]
+ ordering = "-version"
+
+ values = (
+ "id",
+ "channel",
+ "version",
+ "size",
+ "resource_count",
+ "kind_count",
+ "date_published",
+ "version_notes",
+ "included_languages",
+ "included_licenses",
+ "included_categories",
+ )
+
+
@method_decorator(
cache_page(
settings.PUBLIC_CHANNELS_CACHE_DURATION, key_prefix="public_catalog_list"
@@ -965,6 +1145,17 @@ def annotate_queryset(self, queryset):
class AdminChannelFilter(BaseChannelFilter):
+
+ latest_community_library_submission_status = CharFilter(
+ method="filter_latest_community_library_submission_status"
+ )
+ community_library_live = BooleanFilter(
+ method="filter_community_library_live",
+ )
+ has_community_library_submission = BooleanFilter(
+ method="filter_has_community_library_submission",
+ )
+
def filter_keywords(self, queryset, name, value):
keywords = value.split(" ")
editors_first_name = reduce(
@@ -982,6 +1173,20 @@ def filter_keywords(self, queryset, name, value):
| editors_email
)
+ def filter_latest_community_library_submission_status(self, queryset, name, value):
+ values = self.request.query_params.getlist(name)
+ if values:
+ return queryset.filter(
+ latest_community_library_submission__status__in=values
+ )
+ return queryset
+
+ def filter_community_library_live(self, queryset, name, value):
+ return queryset.filter(has_any_live_community_library_submission=value)
+
+ def filter_has_community_library_submission(self, queryset, name, value):
+ return queryset.filter(latest_community_library_submission__isnull=(not value))
+
class AdminChannelSerializer(ChannelSerializer):
"""
@@ -1002,6 +1207,15 @@ class Meta:
list_serializer_class = BulkListSerializer
nested_writes = True
+ def validate_public(self, value):
+ if value and hasattr(self, "instance") and self.instance:
+ if self.instance.is_community_channel():
+ raise ValidationError(
+ "This channel has been added to the Community Library and cannot be marked public.",
+ code="public_community_conflict",
+ )
+ return value
+
class AdminChannelViewSet(ChannelViewSet, RESTUpdateModelMixin, RESTDestroyModelMixin):
pagination_class = CatalogListPagination
@@ -1013,6 +1227,8 @@ class AdminChannelViewSet(ChannelViewSet, RESTUpdateModelMixin, RESTDestroyModel
"created": "main_tree__created",
"source_url": format_source_url,
"demo_server_url": format_demo_server_url,
+ "latest_community_library_submission_id": "latest_community_library_submission__id",
+ "latest_community_library_submission_status": "latest_community_library_submission__status",
}
values = (
@@ -1032,6 +1248,9 @@ class AdminChannelViewSet(ChannelViewSet, RESTUpdateModelMixin, RESTDestroyModel
"source_url",
"demo_server_url",
"primary_token",
+ "latest_community_library_submission__id",
+ "latest_community_library_submission__status",
+ "has_any_live_community_library_submission",
)
def perform_destroy(self, instance):
@@ -1061,11 +1280,28 @@ def get_queryset(self):
channel_main_tree_nodes = ContentNode.objects.filter(
tree_id=OuterRef("main_tree__tree_id")
)
+ latest_community_library_submission_id = Subquery(
+ CommunityLibrarySubmission.objects.filter(channel_id=OuterRef("id"))
+ .order_by("-date_created")
+ .values("id")[:1]
+ )
queryset = Channel.objects.all().annotate(
modified=Subquery(
channel_main_tree_nodes.values("modified").order_by("-modified")[:1]
),
primary_token=primary_token_subquery,
+ latest_community_library_submission=FilteredRelation(
+ "community_library_submissions",
+ condition=Q(
+ community_library_submissions__id=latest_community_library_submission_id,
+ ),
+ ),
+ has_any_live_community_library_submission=Exists(
+ CommunityLibrarySubmission.objects.filter(
+ channel_id=OuterRef("id"),
+ status=community_library_submission_constants.STATUS_LIVE,
+ )
+ ),
)
return queryset
diff --git a/contentcuration/contentcuration/viewsets/community_library_submission.py b/contentcuration/contentcuration/viewsets/community_library_submission.py
new file mode 100644
index 0000000000..22c549cc8f
--- /dev/null
+++ b/contentcuration/contentcuration/viewsets/community_library_submission.py
@@ -0,0 +1,355 @@
+from django.db.models import OuterRef
+from django.db.models import Subquery
+from django_filters import BaseInFilter
+from django_filters import ChoiceFilter
+from django_filters.rest_framework import DateTimeFilter
+from django_filters.rest_framework import DjangoFilterBackend
+from django_filters.rest_framework import FilterSet
+from rest_framework.decorators import action
+from rest_framework.filters import SearchFilter
+from rest_framework.permissions import IsAuthenticated
+from rest_framework.relations import PrimaryKeyRelatedField
+from rest_framework.response import Response
+from rest_framework.serializers import ValidationError
+
+from contentcuration.constants import (
+ community_library_submission as community_library_submission_constants,
+)
+from contentcuration.models import AuditedSpecialPermissionsLicense
+from contentcuration.models import Change
+from contentcuration.models import Channel
+from contentcuration.models import ChannelVersion
+from contentcuration.models import CommunityLibrarySubmission
+from contentcuration.models import Country
+from contentcuration.tasks import apply_channel_changes_task
+from contentcuration.utils.pagination import ValuesViewsetCursorPagination
+from contentcuration.viewsets.base import BulkListSerializer
+from contentcuration.viewsets.base import BulkModelSerializer
+from contentcuration.viewsets.base import ReadOnlyValuesViewset
+from contentcuration.viewsets.base import RESTCreateModelMixin
+from contentcuration.viewsets.base import RESTDestroyModelMixin
+from contentcuration.viewsets.base import RESTUpdateModelMixin
+from contentcuration.viewsets.common import UserFilteredPrimaryKeyRelatedField
+from contentcuration.viewsets.sync.utils import (
+ generate_added_to_community_library_event,
+)
+from contentcuration.viewsets.user import IsAdminUser
+
+
+class ChoiceInFilter(BaseInFilter, ChoiceFilter):
+ """
+ Allows passing multiple statuses in a single query param like status="STATUS_A,STATUS_B"
+ """
+
+ pass
+
+
+class CommunityLibrarySubmissionFilterSet(FilterSet):
+ """
+ FilterSet for CommunityLibrarySubmission to support notifications page filtering.
+ """
+
+ date_updated__lte = DateTimeFilter(field_name="date_updated", lookup_expr="lte")
+ date_updated__gte = DateTimeFilter(field_name="date_updated", lookup_expr="gte")
+ status__in = ChoiceInFilter(
+ field_name="status",
+ choices=community_library_submission_constants.status_choices,
+ )
+
+ class Meta:
+ model = CommunityLibrarySubmission
+ fields = ["channel", "date_updated__lte", "date_updated__gte", "status__in"]
+
+
+class CommunityLibrarySubmissionSerializer(BulkModelSerializer):
+ countries = PrimaryKeyRelatedField(
+ many=True,
+ queryset=Country.objects.all(),
+ required=False,
+ )
+ channel = UserFilteredPrimaryKeyRelatedField(
+ queryset=Channel.objects.all(),
+ edit=False,
+ )
+
+ class Meta:
+ model = CommunityLibrarySubmission
+ fields = [
+ "id",
+ "description",
+ "channel",
+ "countries",
+ "categories",
+ ]
+ list_serializer_class = BulkListSerializer
+
+ def create(self, validated_data):
+ channel = validated_data["channel"]
+ user = self.context["request"].user
+
+ # Prevent creating submissions while a publish is in progress
+ if getattr(getattr(channel, "main_tree", None), "publishing", False):
+ raise ValidationError(
+ "Cannot create a community library submission while the channel is being published."
+ )
+
+ if channel.version == 0:
+ # The channel is not published
+ raise ValidationError(
+ "Cannot create a community library submission for an "
+ "unpublished channel."
+ )
+
+ if channel.public:
+ raise ValidationError(
+ "Cannot create a community library submission for a public channel."
+ )
+
+ if not channel.editors.filter(id=user.id).exists():
+ raise ValidationError(
+ "Only editors can create a community library "
+ "submission for this channel."
+ )
+
+ validated_data["channel_version"] = channel.version
+ validated_data["author"] = self.context["request"].user
+
+ countries = validated_data.pop("countries", [])
+ instance = super().create(validated_data)
+
+ instance.countries.set(countries)
+
+ instance.save()
+ return instance
+
+ def update(self, instance, validated_data):
+ if (
+ "channel" in validated_data
+ and instance.channel.id != validated_data["channel"].id
+ ):
+ raise ValidationError(
+ "Cannot change the channel corresponding to "
+ "a community library submission."
+ )
+
+ if "countries" in validated_data:
+ countries = validated_data.pop("countries")
+ instance.countries.set(countries)
+
+ return super().update(instance, validated_data)
+
+
+class CommunityLibrarySubmissionResolveSerializer(CommunityLibrarySubmissionSerializer):
+ class Meta(CommunityLibrarySubmissionSerializer.Meta):
+ fields = CommunityLibrarySubmissionSerializer.Meta.fields + [
+ "status",
+ "resolution_reason",
+ "feedback_notes",
+ "internal_notes",
+ ]
+
+ def create(self, validated_data):
+ raise ValidationError(
+ "Cannot create a community library submission with this serializer. "
+ "Use the standard CommunityLibrarySubmissionSerializer instead."
+ )
+
+ def update(self, instance, validated_data):
+ if instance.status != community_library_submission_constants.STATUS_PENDING:
+ raise ValidationError(
+ "Cannot resolve a community library submission that is not pending."
+ )
+
+ if (
+ "status" in validated_data
+ and validated_data["status"]
+ == community_library_submission_constants.STATUS_APPROVED
+ and instance.channel.public
+ ):
+ raise ValidationError(
+ "Cannot approve a community library submission for a channel that has been marked public."
+ )
+
+ if "status" not in validated_data or validated_data["status"] not in [
+ community_library_submission_constants.STATUS_APPROVED,
+ community_library_submission_constants.STATUS_REJECTED,
+ ]:
+ raise ValidationError(
+ "Status must be either APPROVED or REJECTED when resolving a submission."
+ )
+
+ if (
+ "status" not in validated_data
+ or validated_data["status"]
+ == community_library_submission_constants.STATUS_REJECTED
+ ):
+ if not validated_data.get("resolution_reason", "").strip():
+ raise ValidationError(
+ "Resolution reason must be provided when rejecting a submission."
+ )
+ if not validated_data.get("feedback_notes", "").strip():
+ raise ValidationError(
+ "Feedback notes must be provided when rejecting a submission."
+ )
+
+ return super().update(instance, validated_data)
+
+
+class CommunityLibrarySubmissionPagination(ValuesViewsetCursorPagination):
+ ordering = "-date_updated"
+ page_size_query_param = "max_results"
+ max_page_size = 100
+
+
+def get_author_name(item):
+ return "{} {}".format(item["author__first_name"], item["author__last_name"])
+
+
+def get_resolved_by_name(item):
+ if item.get("resolved_by__first_name") or item.get("resolved_by__last_name"):
+ return "{} {}".format(
+ item.get("resolved_by__first_name", ""),
+ item.get("resolved_by__last_name", ""),
+ ).strip()
+ return None
+
+
+class CommunityLibrarySubmissionViewSetMixin:
+ """
+ Mixin with logic shared between the CommunityLibrarySubmissionViewSet and
+ AdminCommunityLibrarySubmissionViewSet.
+ """
+
+ values = (
+ "id",
+ "description",
+ "channel_id",
+ "channel__name",
+ "channel_version",
+ "author_id",
+ "author__first_name",
+ "author__last_name",
+ "categories",
+ "date_created",
+ "status",
+ "resolution_reason",
+ "feedback_notes",
+ "date_updated",
+ "resolved_by_id",
+ "resolved_by__first_name",
+ "resolved_by__last_name",
+ )
+ field_map = {
+ "author_name": get_author_name,
+ "resolved_by_name": get_resolved_by_name,
+ "channel_name": lambda item: item.get("channel__name"),
+ }
+ queryset = CommunityLibrarySubmission.objects.all().order_by("-date_updated")
+ filter_backends = [DjangoFilterBackend, SearchFilter]
+ filterset_class = CommunityLibrarySubmissionFilterSet
+ search_fields = ["channel__name"]
+ pagination_class = CommunityLibrarySubmissionPagination
+
+ def consolidate(self, items, queryset):
+ countries = {}
+ for (submission_id, country_code,) in Country.objects.filter(
+ community_library_submissions__in=queryset
+ ).values_list("community_library_submissions", "code"):
+ if submission_id not in countries:
+ countries[submission_id] = []
+ countries[submission_id].append(country_code)
+
+ for item in items:
+ item["countries"] = countries.get(item["id"], [])
+
+ return items
+
+
+class CommunityLibrarySubmissionViewSet(
+ CommunityLibrarySubmissionViewSetMixin,
+ RESTCreateModelMixin,
+ RESTUpdateModelMixin,
+ RESTDestroyModelMixin,
+ ReadOnlyValuesViewset,
+):
+ permission_classes = [IsAuthenticated]
+ serializer_class = CommunityLibrarySubmissionSerializer
+
+
+class AdminCommunityLibrarySubmissionViewSet(
+ CommunityLibrarySubmissionViewSetMixin,
+ ReadOnlyValuesViewset,
+):
+ permission_classes = [IsAdminUser]
+
+ values = CommunityLibrarySubmissionViewSetMixin.values + (
+ "internal_notes",
+ "version_token",
+ )
+ field_map = CommunityLibrarySubmissionViewSetMixin.field_map.copy()
+
+ def annotate_queryset(self, queryset):
+ queryset = super().annotate_queryset(queryset)
+ return queryset.annotate(
+ version_token=Subquery(
+ ChannelVersion.objects.filter(
+ channel_id=OuterRef("channel_id"),
+ version=OuterRef("channel_version"),
+ ).values("secret_token__token")[:1]
+ )
+ )
+
+ def _mark_previous_pending_submissions_as_superseded(self, submission):
+ CommunityLibrarySubmission.objects.filter(
+ status=community_library_submission_constants.STATUS_PENDING,
+ channel=submission.channel,
+ channel_version__lt=submission.channel_version,
+ ).update(status=community_library_submission_constants.STATUS_SUPERSEDED)
+
+ def _add_to_community_library(self, submission):
+ country_codes = sorted(country.code for country in submission.countries.all())
+
+ Change.create_change(
+ generate_added_to_community_library_event(
+ key=submission.channel.id,
+ channel_version=submission.channel_version,
+ categories=submission.categories,
+ country_codes=country_codes,
+ ),
+ created_by_id=submission.resolved_by_id,
+ )
+ apply_channel_changes_task.fetch_or_enqueue(
+ submission.resolved_by,
+ channel_id=submission.channel.id,
+ )
+
+ @action(
+ methods=["post"],
+ detail=True,
+ serializer_class=CommunityLibrarySubmissionResolveSerializer,
+ url_name="resolve",
+ url_path="resolve",
+ )
+ def resolve(self, request, pk=None):
+ instance = self.get_edit_object()
+ serializer = self.get_serializer(instance, data=request.data, partial=True)
+ serializer.is_valid(raise_exception=True)
+
+ submission = serializer.save(
+ resolved_by=request.user,
+ )
+
+ submission.notify_update_to_channel_editors()
+
+ if submission.status == community_library_submission_constants.STATUS_APPROVED:
+ self._mark_previous_pending_submissions_as_superseded(submission)
+ self._add_to_community_library(submission)
+ published_version = ChannelVersion.objects.get(
+ channel=submission.channel,
+ version=submission.channel_version,
+ )
+ AuditedSpecialPermissionsLicense.mark_channel_version_as_distributable(
+ published_version.id
+ )
+
+ return Response(self.serialize_object())
diff --git a/contentcuration/contentcuration/viewsets/sync/base.py b/contentcuration/contentcuration/viewsets/sync/base.py
index 1d4be37c6b..2379e5b0c2 100644
--- a/contentcuration/contentcuration/viewsets/sync/base.py
+++ b/contentcuration/contentcuration/viewsets/sync/base.py
@@ -12,6 +12,7 @@
from contentcuration.viewsets.contentnode import PrerequisitesUpdateHandler
from contentcuration.viewsets.file import FileViewSet
from contentcuration.viewsets.invitation import InvitationViewSet
+from contentcuration.viewsets.sync.constants import ADDED_TO_COMMUNITY_LIBRARY
from contentcuration.viewsets.sync.constants import ASSESSMENTITEM
from contentcuration.viewsets.sync.constants import BOOKMARK
from contentcuration.viewsets.sync.constants import CHANNEL
@@ -98,6 +99,7 @@ def get_change_type(obj):
DEPLOYED: "deploy_from_changes",
UPDATED_DESCENDANTS: "update_descendants_from_changes",
PUBLISHED_NEXT: "publish_next_from_changes",
+ ADDED_TO_COMMUNITY_LIBRARY: "add_to_community_library_from_changes",
}
diff --git a/contentcuration/contentcuration/viewsets/sync/constants.py b/contentcuration/contentcuration/viewsets/sync/constants.py
index 54091c9203..35d5ee5536 100644
--- a/contentcuration/contentcuration/viewsets/sync/constants.py
+++ b/contentcuration/contentcuration/viewsets/sync/constants.py
@@ -9,6 +9,7 @@
DEPLOYED = 8
UPDATED_DESCENDANTS = 9
PUBLISHED_NEXT = 10
+ADDED_TO_COMMUNITY_LIBRARY = 11
ALL_CHANGES = set(
@@ -23,6 +24,13 @@
DEPLOYED,
UPDATED_DESCENDANTS,
PUBLISHED_NEXT,
+ ADDED_TO_COMMUNITY_LIBRARY,
+ ]
+)
+
+SERVER_ONLY_CHANGES = set(
+ [
+ ADDED_TO_COMMUNITY_LIBRARY,
]
)
@@ -40,6 +48,7 @@
VIEWER_M2M = "viewer_m2m"
SAVEDSEARCH = "savedsearch"
CLIPBOARD = "clipboard"
+SESSION = "session"
ALL_TABLES = set(
@@ -54,6 +63,10 @@
FILE,
INVITATION,
USER,
+ # SESSION is not a real backend model/viewset, but the frontend
+ # does have a SESSION table, so adding this here enables us to
+ # sync changes to this SESSION table
+ SESSION,
SAVEDSEARCH,
EDITOR_M2M,
VIEWER_M2M,
diff --git a/contentcuration/contentcuration/viewsets/sync/endpoint.py b/contentcuration/contentcuration/viewsets/sync/endpoint.py
index 6825833823..fcc5bed4fb 100644
--- a/contentcuration/contentcuration/viewsets/sync/endpoint.py
+++ b/contentcuration/contentcuration/viewsets/sync/endpoint.py
@@ -20,6 +20,7 @@
from contentcuration.tasks import apply_user_changes_task
from contentcuration.viewsets.sync.constants import CHANNEL
from contentcuration.viewsets.sync.constants import CREATED
+from contentcuration.viewsets.sync.constants import SERVER_ONLY_CHANGES
CHANGE_RETURN_LIMIT = 200
@@ -72,7 +73,11 @@ def handle_changes(self, request):
# Changes that cannot be made
disallowed_changes = []
for c in changes:
- if c.get("channel_id") is None and c.get("user_id") == request.user.id:
+ if c.get("type") in SERVER_ONLY_CHANGES:
+ disallowed_changes.append(c)
+ elif (
+ c.get("channel_id") is None and c.get("user_id") == request.user.id
+ ):
user_only_changes.append(c)
elif c.get("channel_id") in allowed_ids:
channel_changes.append(c)
diff --git a/contentcuration/contentcuration/viewsets/sync/utils.py b/contentcuration/contentcuration/viewsets/sync/utils.py
index a0073ce731..8f5ce98a05 100644
--- a/contentcuration/contentcuration/viewsets/sync/utils.py
+++ b/contentcuration/contentcuration/viewsets/sync/utils.py
@@ -3,6 +3,7 @@
from django.conf import settings
from contentcuration.utils.sentry import report_exception
+from contentcuration.viewsets.sync.constants import ADDED_TO_COMMUNITY_LIBRARY
from contentcuration.viewsets.sync.constants import ALL_TABLES
from contentcuration.viewsets.sync.constants import CHANNEL
from contentcuration.viewsets.sync.constants import CONTENTNODE
@@ -97,10 +98,28 @@ def generate_update_descendants_event(key, mods, channel_id=None, user_id=None):
return event
-def generate_publish_next_event(key, version_notes="", language=None):
+def generate_publish_next_event(key, use_staging_tree=False):
event = _generate_event(key, CHANNEL, PUBLISHED_NEXT, key, None)
- event["version_notes"] = version_notes
- event["language"] = language
+ event["use_staging_tree"] = use_staging_tree
+ return event
+
+
+def generate_added_to_community_library_event(
+ key,
+ channel_version,
+ categories=None,
+ country_codes=None,
+):
+ event = _generate_event(
+ key,
+ CHANNEL,
+ ADDED_TO_COMMUNITY_LIBRARY,
+ channel_id=key,
+ user_id=None,
+ )
+ event["channel_version"] = channel_version
+ event["categories"] = categories or dict()
+ event["country_codes"] = country_codes or list()
return event
diff --git a/contentcuration/contentcuration/viewsets/user.py b/contentcuration/contentcuration/viewsets/user.py
index c779d1be47..bfacd08f2f 100644
--- a/contentcuration/contentcuration/viewsets/user.py
+++ b/contentcuration/contentcuration/viewsets/user.py
@@ -17,6 +17,7 @@
from django_filters.rest_framework import BooleanFilter
from django_filters.rest_framework import CharFilter
from django_filters.rest_framework import FilterSet
+from rest_framework import serializers
from rest_framework.decorators import action
from rest_framework.exceptions import ValidationError
from rest_framework.permissions import BasePermission
@@ -130,6 +131,13 @@ class Meta:
fields = ("ids",)
+class MarkNotificationsReadSerializer(serializers.Serializer):
+ timestamp = serializers.DateTimeField(
+ required=True,
+ help_text="Timestamp of the last read notification.",
+ )
+
+
class UserSerializer(BulkModelSerializer):
class Meta:
model = User
@@ -170,6 +178,24 @@ def get_storage_used(self, request):
def refresh_storage_used(self, request):
return Response(request.user.set_space_used())
+ @action(
+ detail=False,
+ methods=["post"],
+ serializer_class=MarkNotificationsReadSerializer,
+ )
+ def mark_notifications_read(self, request):
+ """
+ Allows a user to mark the timestamp of their last read notification.
+ """
+ user = request.user
+
+ serializer = self.get_serializer(data=request.data)
+ serializer.is_valid(raise_exception=True)
+
+ timestamp = serializer.validated_data["timestamp"]
+ user.mark_notifications_read(timestamp)
+ return Response(status=HTTP_204_NO_CONTENT)
+
def annotate_queryset(self, queryset):
queryset = queryset.annotate(
editable_channels__ids=NotNullArrayAgg("editable_channels__id"),
diff --git a/contentcuration/kolibri_public/management/commands/export_channels_to_kolibri_public.py b/contentcuration/kolibri_public/management/commands/export_channels_to_kolibri_public.py
index 3a0aee8fcd..07080fb849 100644
--- a/contentcuration/kolibri_public/management/commands/export_channels_to_kolibri_public.py
+++ b/contentcuration/kolibri_public/management/commands/export_channels_to_kolibri_public.py
@@ -1,23 +1,14 @@
import logging
import os
-import shutil
-import tempfile
from datetime import datetime
from datetime import timedelta
-from django.conf import settings
-from django.core.files.storage import default_storage as storage
-from django.core.management import call_command
from django.core.management.base import BaseCommand
from django.db.models import F
from django.db.models import Q
from django.utils import timezone
-from kolibri_content.apps import KolibriContentConfig
-from kolibri_content.models import ChannelMetadata as ExportedChannelMetadata
-from kolibri_content.router import get_active_content_database
-from kolibri_content.router import using_content_database
from kolibri_public.models import ChannelMetadata
-from kolibri_public.utils.mapper import ChannelMapper
+from kolibri_public.utils import export_channel_to_kolibri_public
from contentcuration.models import Channel
from contentcuration.models import User
@@ -55,7 +46,7 @@ def handle(self, *args, **options):
count = 0
for channel_id in ids_to_export:
try:
- self._export_channel(channel_id)
+ export_channel_to_kolibri_public(channel_id)
count += 1
except FileNotFoundError:
logger.warning(
@@ -71,31 +62,6 @@ def handle(self, *args, **options):
)
logger.info("Successfully put {} channels into kolibri_public".format(count))
- def _export_channel(self, channel_id):
- logger.info("Putting channel {} into kolibri_public".format(channel_id))
- db_location = os.path.join(
- settings.DB_ROOT, "{id}.sqlite3".format(id=channel_id)
- )
- with storage.open(db_location) as storage_file:
- with tempfile.NamedTemporaryFile(suffix=".sqlite3") as db_file:
- shutil.copyfileobj(storage_file, db_file)
- db_file.seek(0)
- with using_content_database(db_file.name):
- # Run migration to handle old content databases published prior to current fields being added.
- call_command(
- "migrate",
- app_label=KolibriContentConfig.label,
- database=get_active_content_database(),
- )
- channel = ExportedChannelMetadata.objects.get(id=channel_id)
- logger.info(
- "Found channel {} for id: {} mapping now".format(
- channel.name, channel_id
- )
- )
- mapper = ChannelMapper(channel)
- mapper.run()
-
def _republish_problem_channels(self):
twenty_19 = datetime(year=2019, month=1, day=1)
five_minutes = timedelta(minutes=5)
diff --git a/contentcuration/kolibri_public/migrations/0007_new_channel_metadata.py b/contentcuration/kolibri_public/migrations/0007_new_channel_metadata.py
new file mode 100644
index 0000000000..1e2144d6b3
--- /dev/null
+++ b/contentcuration/kolibri_public/migrations/0007_new_channel_metadata.py
@@ -0,0 +1,26 @@
+# Generated by Django 3.2.24 on 2025-07-22 10:38
+from django.db import migrations
+from django.db import models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ("contentcuration", "0156_communitylibrarysubmission_admin_fields"),
+ ("kolibri_public", "0006_auto_20250417_1516"),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name="channelmetadata",
+ name="categories",
+ field=models.JSONField(blank=True, null=True),
+ ),
+ migrations.AddField(
+ model_name="channelmetadata",
+ name="countries",
+ field=models.ManyToManyField(
+ related_name="public_channels", to="contentcuration.Country"
+ ),
+ ),
+ ]
diff --git a/contentcuration/kolibri_public/migrations/0008_channelmetadata_categories_bitmask_0.py b/contentcuration/kolibri_public/migrations/0008_channelmetadata_categories_bitmask_0.py
new file mode 100644
index 0000000000..ac60bbdd37
--- /dev/null
+++ b/contentcuration/kolibri_public/migrations/0008_channelmetadata_categories_bitmask_0.py
@@ -0,0 +1,18 @@
+# Generated by Django 3.2.24 on 2025-08-08 17:32
+from django.db import migrations
+from django.db import models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ("kolibri_public", "0007_new_channel_metadata"),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name="channelmetadata",
+ name="categories_bitmask_0",
+ field=models.BigIntegerField(blank=True, default=0, null=True),
+ ),
+ ]
diff --git a/contentcuration/kolibri_public/models.py b/contentcuration/kolibri_public/models.py
index c0056d9cf9..154ea5cb5f 100644
--- a/contentcuration/kolibri_public/models.py
+++ b/contentcuration/kolibri_public/models.py
@@ -1,12 +1,15 @@
from django.db import models
-from django.db.models import F
from kolibri_content import base_models
from kolibri_content.fields import JSONField
-from kolibri_public.search import bitmask_fieldnames
-from kolibri_public.search import metadata_bitmasks
+from kolibri_public.search import channelmetadata_bitmask_fieldnames
+from kolibri_public.search import channelmetadata_metadata_bitmasks
+from kolibri_public.search import contentnode_bitmask_fieldnames
+from kolibri_public.search import contentnode_metadata_bitmasks
+from kolibri_public.search import has_all_labels
from mptt.managers import TreeManager
from mptt.querysets import TreeQuerySet
+from contentcuration.models import Country
from contentcuration.models import Language
@@ -16,31 +19,7 @@ class ContentTag(base_models.ContentTag):
class ContentNodeQueryset(TreeQuerySet):
def has_all_labels(self, field_name, labels):
- bitmasks = metadata_bitmasks[field_name]
- bits = {}
- for label in labels:
- if label in bitmasks:
- bitmask_fieldname = bitmasks[label]["bitmask_field_name"]
- if bitmask_fieldname not in bits:
- bits[bitmask_fieldname] = 0
- bits[bitmask_fieldname] += bitmasks[label]["bits"]
-
- filters = {}
- annotations = {}
- for bitmask_fieldname, bits in bits.items():
- annotation_fieldname = "{}_{}".format(bitmask_fieldname, "masked")
- # To get the correct result, i.e. an AND that all the labels are present,
- # we need to check that the aggregated value is euqal to the bits.
- # If we wanted an OR (which would check for any being present),
- # we would have to use GREATER THAN 0 here.
- filters[annotation_fieldname] = bits
- # This ensures that the annotated value is the result of the AND operation
- # so if all the values are present, the result will be the same as the bits
- # but if any are missing, it will not be equal to the bits, but will only be
- # 0 if none of the bits are present.
- annotations[annotation_fieldname] = F(bitmask_fieldname).bitand(bits)
-
- return self.annotate(**annotations).filter(**filters)
+ return has_all_labels(self, contentnode_metadata_bitmasks, field_name, labels)
class ContentNodeManager(
@@ -78,7 +57,7 @@ class ContentNode(base_models.ContentNode):
objects = ContentNodeManager()
-for field_name in bitmask_fieldnames:
+for field_name in contentnode_bitmask_fieldnames:
field = models.BigIntegerField(default=0, null=True, blank=True)
field.contribute_to_class(ContentNode, field_name)
@@ -95,7 +74,20 @@ class AssessmentMetaData(base_models.AssessmentMetaData):
pass
+class ChannelMetadataQueryset(models.QuerySet):
+ def has_all_labels(self, field_name, labels):
+ return has_all_labels(
+ self, channelmetadata_metadata_bitmasks, field_name, labels
+ )
+
+
+class ChannelMetadataManager(models.Manager.from_queryset(ChannelMetadataQueryset)):
+ pass
+
+
class ChannelMetadata(base_models.ChannelMetadata):
+ # Note: The `categories` field should contain a _list_, NOT a _dict_.
+
# precalculated fields during annotation/migration
published_size = models.BigIntegerField(default=0, null=True, blank=True)
total_resource_count = models.IntegerField(default=0, null=True, blank=True)
@@ -104,6 +96,15 @@ class ChannelMetadata(base_models.ChannelMetadata):
)
order = models.PositiveIntegerField(default=0, null=True, blank=True)
public = models.BooleanField()
+ categories = models.JSONField(null=True, blank=True)
+ countries = models.ManyToManyField(Country, related_name="public_channels")
+
+ objects = ChannelMetadataManager()
+
+
+for field_name in channelmetadata_bitmask_fieldnames:
+ field = models.BigIntegerField(default=0, null=True, blank=True)
+ field.contribute_to_class(ChannelMetadata, field_name)
class MPTTTreeIDManager(models.Model):
diff --git a/contentcuration/kolibri_public/search.py b/contentcuration/kolibri_public/search.py
index fad6590b4f..35d2224340 100644
--- a/contentcuration/kolibri_public/search.py
+++ b/contentcuration/kolibri_public/search.py
@@ -26,37 +26,54 @@
from le_utils.constants.labels.subjects import SUBJECTSLIST
-metadata_lookup = {
+contentnode_metadata_lookup = {
"learning_activities": LEARNINGACTIVITIESLIST,
"categories": SUBJECTSLIST,
"grade_levels": LEVELSLIST,
"accessibility_labels": ACCESSIBILITYCATEGORIESLIST,
"learner_needs": NEEDSLIST,
}
+contentnode_metadata_bitmasks = {}
+contentnode_bitmask_fieldnames = {}
-
-metadata_bitmasks = {}
-
-bitmask_fieldnames = {}
-
-
-for key, labels in metadata_lookup.items():
- bitmask_lookup = {}
- i = 0
- while labels[i : i + 64]:
- bitmask_field_name = "{}_bitmask_{}".format(key, i)
- bitmask_fieldnames[bitmask_field_name] = []
- for j, label in enumerate(labels):
- info = {
- "bitmask_field_name": bitmask_field_name,
- "field_name": key,
- "bits": 2 ** j,
- "label": label,
- }
- bitmask_lookup[label] = info
- bitmask_fieldnames[bitmask_field_name].append(info)
- i += 64
- metadata_bitmasks[key] = bitmask_lookup
+channelmetadata_metadata_lookup = {
+ "categories": SUBJECTSLIST,
+}
+channelmetadata_metadata_bitmasks = {}
+channelmetadata_bitmask_fieldnames = {}
+
+
+def _populate_bitmask_data(metadata_lookup, metadata_bitmasks, bitmask_fieldnames):
+
+ for key, labels in metadata_lookup.items():
+ bitmask_lookup = {}
+ i = 0
+ while (chunk := labels[i : i + 64]) :
+ bitmask_field_name = "{}_bitmask_{}".format(key, i)
+ bitmask_fieldnames[bitmask_field_name] = []
+ for j, label in enumerate(chunk):
+ info = {
+ "bitmask_field_name": bitmask_field_name,
+ "field_name": key,
+ "bits": 2 ** j,
+ "label": label,
+ }
+ bitmask_lookup[label] = info
+ bitmask_fieldnames[bitmask_field_name].append(info)
+ i += 64
+ metadata_bitmasks[key] = bitmask_lookup
+
+
+_populate_bitmask_data(
+ contentnode_metadata_lookup,
+ contentnode_metadata_bitmasks,
+ contentnode_bitmask_fieldnames,
+)
+_populate_bitmask_data(
+ channelmetadata_metadata_lookup,
+ channelmetadata_metadata_bitmasks,
+ channelmetadata_bitmask_fieldnames,
+)
def _get_available_languages(base_queryset):
@@ -87,7 +104,7 @@ def _get_available_channels(base_queryset):
# Remove the SQLite Bitwise OR definition as not needed.
-def get_available_metadata_labels(base_queryset):
+def get_contentnode_available_metadata_labels(base_queryset):
# Updated to use the kolibri_public ChannelMetadata model
from kolibri_public.models import ChannelMetadata
@@ -101,12 +118,12 @@ def get_available_metadata_labels(base_queryset):
if cache_key not in cache:
base_queryset = base_queryset.order_by()
aggregates = {}
- for field in bitmask_fieldnames:
+ for field in contentnode_bitmask_fieldnames:
field_agg = field + "_agg"
aggregates[field_agg] = BitOr(field)
output = {}
agg = base_queryset.aggregate(**aggregates)
- for field, values in bitmask_fieldnames.items():
+ for field, values in contentnode_bitmask_fieldnames.items():
bit_value = agg[field + "_agg"]
for value in values:
if value["field_name"] not in output:
@@ -123,10 +140,12 @@ def get_all_contentnode_label_metadata():
# Updated to use the kolibri_public ContentNode model
from kolibri_public.models import ContentNode
- return get_available_metadata_labels(ContentNode.objects.filter(available=True))
+ return get_contentnode_available_metadata_labels(
+ ContentNode.objects.filter(available=True)
+ )
-def annotate_label_bitmasks(queryset):
+def annotate_label_bitmasks(queryset, bitmask_fieldnames):
update_statements = {}
for bitmask_fieldname, label_info in bitmask_fieldnames.items():
update_statements[bitmask_fieldname] = sum(
@@ -142,3 +161,39 @@ def annotate_label_bitmasks(queryset):
for info in label_info
)
queryset.update(**update_statements)
+
+
+def annotate_contentnode_label_bitmasks(queryset):
+ return annotate_label_bitmasks(queryset, contentnode_bitmask_fieldnames)
+
+
+def annotate_channelmetadata_label_bitmasks(queryset):
+ return annotate_label_bitmasks(queryset, channelmetadata_bitmask_fieldnames)
+
+
+def has_all_labels(queryset, metadata_bitmasks, field_name, labels):
+ bitmasks = metadata_bitmasks[field_name]
+ bits = {}
+ for label in labels:
+ if label in bitmasks:
+ bitmask_fieldname = bitmasks[label]["bitmask_field_name"]
+ if bitmask_fieldname not in bits:
+ bits[bitmask_fieldname] = 0
+ bits[bitmask_fieldname] += bitmasks[label]["bits"]
+
+ filters = {}
+ annotations = {}
+ for bitmask_fieldname, bits in bits.items():
+ annotation_fieldname = "{}_{}".format(bitmask_fieldname, "masked")
+ # To get the correct result, i.e. an AND that all the labels are present,
+ # we need to check that the aggregated value is euqal to the bits.
+ # If we wanted an OR (which would check for any being present),
+ # we would have to use GREATER THAN 0 here.
+ filters[annotation_fieldname] = bits
+ # This ensures that the annotated value is the result of the AND operation
+ # so if all the values are present, the result will be the same as the bits
+ # but if any are missing, it will not be equal to the bits, but will only be
+ # 0 if none of the bits are present.
+ annotations[annotation_fieldname] = F(bitmask_fieldname).bitand(bits)
+
+ return queryset.annotate(**annotations).filter(**filters)
diff --git a/contentcuration/kolibri_public/tests/test_channelmetadata_viewset.py b/contentcuration/kolibri_public/tests/test_channelmetadata_viewset.py
new file mode 100644
index 0000000000..616752d8e6
--- /dev/null
+++ b/contentcuration/kolibri_public/tests/test_channelmetadata_viewset.py
@@ -0,0 +1,161 @@
+from uuid import UUID
+
+from kolibri_public.models import ChannelMetadata
+from kolibri_public.tests.utils.mixer import KolibriPublicMixer
+from le_utils.constants.labels.subjects import SUBJECTSLIST
+
+from contentcuration.models import Country
+from contentcuration.tests import testdata
+from contentcuration.tests.base import StudioAPITestCase
+from contentcuration.tests.helpers import reverse_with_query
+
+
+class ChannelMetadataViewSetTestCase(StudioAPITestCase):
+ def test_annotate_countries(self):
+ mixer = KolibriPublicMixer()
+
+ country1 = mixer.blend(Country, code="C1")
+ country2 = mixer.blend(Country, code="C2")
+ country3 = mixer.blend(Country, code="C3")
+
+ channel = mixer.blend(
+ ChannelMetadata, countries=[country1, country2, country3], public=False
+ )
+
+ user = testdata.user("any@user.com")
+ self.client.force_authenticate(user)
+
+ response = self.client.get(
+ reverse_with_query(
+ "publicchannel-detail",
+ args=[channel.id],
+ query={"public": "false"},
+ ),
+ )
+
+ self.assertEqual(response.status_code, 200, response.content)
+ self.assertCountEqual(response.data["countries"], ["C1", "C2", "C3"])
+
+
+class ChannelMetadataFilterTestCase(StudioAPITestCase):
+ def setUp(self):
+ super().setUp()
+
+ mixer = KolibriPublicMixer()
+
+ self.categories = [
+ SUBJECTSLIST[0],
+ SUBJECTSLIST[1],
+ SUBJECTSLIST[2],
+ SUBJECTSLIST[3],
+ ]
+ self.category_bitmasks = [
+ 1,
+ 2,
+ 4,
+ 8,
+ ]
+
+ mixer.blend(Country, code="C1")
+ mixer.blend(Country, code="C2")
+ mixer.blend(Country, code="C3")
+
+ self.user = testdata.user("any@user.com")
+
+ # Manually set the bitmasks for testing
+ self.metadata1 = mixer.blend(
+ ChannelMetadata,
+ categories_bitmask_0=(
+ self.category_bitmasks[0]
+ | self.category_bitmasks[1]
+ | self.category_bitmasks[3]
+ ),
+ countries=["C1", "C3"],
+ public=False,
+ )
+ self.metadata2 = mixer.blend(
+ ChannelMetadata,
+ categories_bitmask_0=(
+ self.category_bitmasks[0]
+ | self.category_bitmasks[2]
+ | self.category_bitmasks[3]
+ ),
+ countries=["C1", "C2", "C3"],
+ public=False,
+ )
+ self.metadata3 = mixer.blend(
+ ChannelMetadata,
+ categories_bitmask_0=(
+ self.category_bitmasks[0]
+ | self.category_bitmasks[1]
+ | self.category_bitmasks[2]
+ ),
+ countries=["C3"],
+ public=False,
+ )
+
+ def test_filter_by_categories_bitmask__provided(self):
+ self.client.force_authenticate(self.user)
+
+ filter_query = {
+ "categories": f"{self.categories[0]},{self.categories[1]}",
+ "public": "false",
+ }
+
+ response = self.client.get(
+ reverse_with_query(
+ "publicchannel-list",
+ query=filter_query,
+ ),
+ )
+ self.assertEqual(response.status_code, 200, response.content)
+ self.assertCountEqual(
+ [UUID(item["id"]) for item in response.data],
+ [UUID(self.metadata1.id), UUID(self.metadata3.id)],
+ )
+
+ def test_filter_by_categories_bitmask__not_provided(self):
+ self.client.force_authenticate(self.user)
+
+ response = self.client.get(
+ reverse_with_query(
+ "publicchannel-list",
+ query={"public": "false"},
+ ),
+ )
+ self.assertEqual(response.status_code, 200, response.content)
+ self.assertCountEqual(
+ [UUID(item["id"]) for item in response.data],
+ [UUID(self.metadata1.id), UUID(self.metadata2.id), UUID(self.metadata3.id)],
+ )
+
+ def test_filter_by_countries(self):
+ self.client.force_authenticate(self.user)
+
+ filter_query = {"countries": "C1,C2", "public": "false"}
+
+ response = self.client.get(
+ reverse_with_query(
+ "publicchannel-list",
+ query=filter_query,
+ ),
+ )
+ self.assertEqual(response.status_code, 200, response.content)
+ self.assertCountEqual(
+ [UUID(item["id"]) for item in response.data],
+ [UUID(self.metadata1.id), UUID(self.metadata2.id)],
+ )
+
+ response1 = next(
+ filter(
+ lambda item: UUID(item["id"]) == UUID(self.metadata1.id), response.data
+ )
+ )
+ response2 = next(
+ filter(
+ lambda item: UUID(item["id"]) == UUID(self.metadata2.id), response.data
+ )
+ )
+
+ self.assertCountEqual(response1["countries"], ["C1", "C3"])
+ self.assertCountEqual(response2["countries"], ["C1", "C2", "C3"])
diff --git a/contentcuration/kolibri_public/tests/test_content_app.py b/contentcuration/kolibri_public/tests/test_content_app.py
index 30d14df272..cd82c03ded 100644
--- a/contentcuration/kolibri_public/tests/test_content_app.py
+++ b/contentcuration/kolibri_public/tests/test_content_app.py
@@ -500,6 +500,114 @@ def test_channelmetadata_has_exercises_filter(self):
with_filter_response.data[0]["name"], self.channel_data["name"]
)
+ def test_channelmetadata_public_filter_default_true(self):
+ community_channel = models.ContentNode.objects.create(
+ pk=uuid.uuid4().hex,
+ channel_id=uuid.uuid4().hex,
+ content_id=uuid.uuid4().hex,
+ kind="topic",
+ title="community channel",
+ )
+ models.ChannelMetadata.objects.create(
+ id=uuid.uuid4().hex,
+ name="community channel metadata",
+ root=community_channel,
+ # community channel
+ public=False,
+ )
+
+ # By default, only public channels should be returned
+ response = self.client.get(reverse("publicchannel-list"))
+ self.assertEqual(len(response.data), 1)
+ self.assertEqual(response.data[0]["name"], self.channel_data["name"])
+ self.assertTrue(response.data[0]["public"])
+
+ def test_channelmetadata_public_filter_explicit_true(self):
+ community_channel = models.ContentNode.objects.create(
+ pk=uuid.uuid4().hex,
+ channel_id=uuid.uuid4().hex,
+ content_id=uuid.uuid4().hex,
+ kind="topic",
+ title="community channel 2",
+ )
+ models.ChannelMetadata.objects.create(
+ id=uuid.uuid4().hex,
+ name="community channel metadata 2",
+ root=community_channel,
+ public=False,
+ )
+
+ # Explicitly request only public channels
+ response = self.client.get(reverse("publicchannel-list"), {"public": True})
+ self.assertEqual(len(response.data), 1)
+ self.assertEqual(response.data[0]["name"], self.channel_data["name"])
+ self.assertTrue(response.data[0]["public"])
+
+ def test_channelmetadata_public_filter_explicit_false(self):
+ community_channel = models.ContentNode.objects.create(
+ pk=uuid.uuid4().hex,
+ channel_id=uuid.uuid4().hex,
+ content_id=uuid.uuid4().hex,
+ kind="topic",
+ title="community channel 3",
+ )
+ community_metadata = models.ChannelMetadata.objects.create(
+ id=uuid.uuid4().hex,
+ name="community channel metadata 3",
+ root=community_channel,
+ public=False,
+ )
+
+ # Explicitly request only community channels
+ response = self.client.get(reverse("publicchannel-list"), {"public": False})
+ self.assertEqual(len(response.data), 1)
+ self.assertEqual(response.data[0]["name"], community_metadata.name)
+ self.assertFalse(response.data[0]["public"])
+
+ def test_channelmetadata_public_filter_mixed_channels(self):
+ community_channel1 = models.ContentNode.objects.create(
+ pk=uuid.uuid4().hex,
+ channel_id=uuid.uuid4().hex,
+ content_id=uuid.uuid4().hex,
+ kind="topic",
+ title="community channel 4",
+ )
+ models.ChannelMetadata.objects.create(
+ id=uuid.uuid4().hex,
+ name="community channel metadata 4",
+ root=community_channel1,
+ public=False,
+ )
+
+ community_channel2 = models.ContentNode.objects.create(
+ pk=uuid.uuid4().hex,
+ channel_id=uuid.uuid4().hex,
+ content_id=uuid.uuid4().hex,
+ kind="topic",
+ title="community channel 5",
+ )
+ models.ChannelMetadata.objects.create(
+ id=uuid.uuid4().hex,
+ name="community channel metadata 5",
+ root=community_channel2,
+ public=False,
+ )
+
+ # Test public filter
+ public_response = self.client.get(
+ reverse("publicchannel-list"), {"public": True}
+ )
+ self.assertEqual(len(public_response.data), 1)
+ self.assertTrue(public_response.data[0]["public"])
+
+ # Test community filter
+ community_response = self.client.get(
+ reverse("publicchannel-list"), {"public": False}
+ )
+ self.assertEqual(len(community_response.data), 2)
+ for channel in community_response.data:
+ self.assertFalse(channel["public"])
+
def test_filtering_coach_content_anon(self):
response = self.client.get(
reverse("publiccontentnode-list"),
diff --git a/contentcuration/kolibri_public/tests/test_export_channels_to_kolibri_public.py b/contentcuration/kolibri_public/tests/test_export_channels_to_kolibri_public.py
new file mode 100644
index 0000000000..cb7fb54487
--- /dev/null
+++ b/contentcuration/kolibri_public/tests/test_export_channels_to_kolibri_public.py
@@ -0,0 +1,130 @@
+import os.path
+import shutil
+import tempfile
+import uuid
+from unittest import mock
+
+from django.conf import settings
+from django.core.management import call_command
+from django.test import TestCase
+from kolibri_content.apps import KolibriContentConfig
+from kolibri_content.models import ChannelMetadata as ExportedChannelMetadata
+from kolibri_content.router import get_active_content_database
+from kolibri_content.router import using_content_database
+from kolibri_public.tests.utils.mixer import KolibriPublicMixer
+from kolibri_public.utils.export_channel_to_kolibri_public import (
+ export_channel_to_kolibri_public,
+)
+
+from contentcuration.models import Country
+from contentcuration.tests.utils.restricted_filesystemstorage import (
+ RestrictedFileSystemStorage,
+)
+
+
+class ExportTestCase(TestCase):
+ def setUp(self):
+ super().setUp()
+
+ self._temp_directory_ctx = tempfile.TemporaryDirectory()
+ test_db_root_dir = self._temp_directory_ctx.__enter__()
+
+ self.storage = RestrictedFileSystemStorage(location=test_db_root_dir)
+
+ self._storage_patch_ctx = mock.patch(
+ "kolibri_public.utils.export_channel_to_kolibri_public.storage",
+ new=self.storage,
+ )
+ self._storage_patch_ctx.__enter__()
+
+ os.makedirs(os.path.join(test_db_root_dir, settings.DB_ROOT), exist_ok=True)
+
+ self.channel_id = uuid.UUID(int=42).hex
+ self.channel_version = 1
+
+ self.versioned_db_path = os.path.join(
+ test_db_root_dir,
+ settings.DB_ROOT,
+ f"{self.channel_id}-{self.channel_version}.sqlite3",
+ )
+ open(self.versioned_db_path, "w").close()
+
+ with using_content_database(self.versioned_db_path):
+ call_command(
+ "migrate",
+ app_label=KolibriContentConfig.label,
+ database=get_active_content_database(),
+ )
+
+ mixer = KolibriPublicMixer()
+ self.exported_channel_metadata = mixer.blend(
+ ExportedChannelMetadata,
+ id=self.channel_id,
+ version=self.channel_version,
+ )
+
+ self.unversioned_db_path = os.path.join(
+ test_db_root_dir, settings.DB_ROOT, f"{self.channel_id}.sqlite3"
+ )
+ shutil.copyfile(self.versioned_db_path, self.unversioned_db_path)
+
+ def tearDown(self):
+ self._temp_directory_ctx.__exit__(None, None, None)
+ self._storage_patch_ctx.__exit__(None, None, None)
+
+ super().tearDown()
+
+ @mock.patch("kolibri_public.utils.export_channel_to_kolibri_public.ChannelMapper")
+ def test_export_channel_to_kolibri_public__existing_version(
+ self, mock_channel_mapper
+ ):
+ categories = ["Category1", "Category2"]
+ country1 = Country.objects.create(code="C1", name="Country 1")
+ country2 = Country.objects.create(code="C2", name="Country 2")
+ countries = [country1, country2]
+
+ export_channel_to_kolibri_public(
+ channel_id=self.channel_id,
+ channel_version=1,
+ public=True,
+ categories=categories,
+ countries=countries,
+ )
+
+ mock_channel_mapper.assert_called_once_with(
+ channel=self.exported_channel_metadata,
+ public=True,
+ categories=categories,
+ countries=countries,
+ )
+ mock_channel_mapper.return_value.run.assert_called_once_with()
+
+ @mock.patch("kolibri_public.utils.export_channel_to_kolibri_public.ChannelMapper")
+ def test_export_channel_to_kolibri_public__without_version(
+ self, mock_channel_mapper
+ ):
+ export_channel_to_kolibri_public(
+ channel_id=self.channel_id,
+ )
+
+ mock_channel_mapper.assert_called_once_with(
+ channel=self.exported_channel_metadata,
+ public=True,
+ categories=None,
+ countries=None,
+ )
+ mock_channel_mapper.return_value.run.assert_called_once_with()
+
+ def test_export_channel_to_kolibri_public__bad_channel(self):
+ with self.assertRaises(FileNotFoundError):
+ export_channel_to_kolibri_public(
+ channel_id="dummy_id",
+ channel_version=1,
+ )
+
+ def test_export_channel_to_kolibri_public__bad_version(self):
+ with self.assertRaises(FileNotFoundError):
+ export_channel_to_kolibri_public(
+ channel_id=self.channel_id,
+ channel_version=2,
+ )
diff --git a/contentcuration/kolibri_public/tests/test_mapper.py b/contentcuration/kolibri_public/tests/test_mapper.py
index d938233e63..92225a2b8a 100644
--- a/contentcuration/kolibri_public/tests/test_mapper.py
+++ b/contentcuration/kolibri_public/tests/test_mapper.py
@@ -1,6 +1,9 @@
+import datetime
import os
import tempfile
+from unittest import mock
+import pytz
from django.core.management import call_command
from django.test import TestCase
from kolibri_content import models as kolibri_content_models
@@ -12,8 +15,10 @@
from kolibri_public.tests.base import OKAY_TAG
from kolibri_public.utils.mapper import ChannelMapper
from le_utils.constants import content_kinds
+from le_utils.constants.labels.subjects import SUBJECTSLIST
from contentcuration.models import Channel
+from contentcuration.models import Country
from contentcuration.tests.testdata import user
@@ -28,16 +33,24 @@ def overrides(self):
kolibri_public_models.LocalFile: {
"available": True,
},
+ kolibri_public_models.ChannelMetadata: {
+ "last_updated": self.dummy_date,
+ },
}
- @classmethod
- def setUpClass(cls):
- super(ChannelMapperTest, cls).setUpClass()
+ def setUp(self):
+ super().setUp()
call_command("loadconstants")
- _, cls.tempdb = tempfile.mkstemp(suffix=".sqlite3")
+ _, self.tempdb = tempfile.mkstemp(suffix=".sqlite3")
admin_user = user()
- with using_content_database(cls.tempdb):
+ self.dummy_date = datetime.datetime(2020, 1, 1, 0, 0, 0, tzinfo=pytz.utc)
+ self._date_patcher = mock.patch(
+ "kolibri_public.utils.annotation.timezone.now", return_value=self.dummy_date
+ )
+ self._date_patcher.start()
+
+ with using_content_database(self.tempdb):
call_command(
"migrate",
"content",
@@ -52,23 +65,20 @@ def setUpClass(cls):
},
)
builder.insert_into_default_db()
- cls.source_root = kolibri_content_models.ContentNode.objects.get(
+ self.source_root = kolibri_content_models.ContentNode.objects.get(
id=builder.root_node["id"]
)
- cls.channel = kolibri_content_models.ChannelMetadata.objects.get(
+ self.channel = kolibri_content_models.ChannelMetadata.objects.get(
id=builder.channel["id"]
)
contentcuration_channel = Channel.objects.create(
actor_id=admin_user.id,
- id=cls.channel.id,
- name=cls.channel.name,
+ id=self.channel.id,
+ name=self.channel.name,
public=True,
)
contentcuration_channel.main_tree.published = True
contentcuration_channel.main_tree.save()
- cls.mapper = ChannelMapper(cls.channel)
- cls.mapper.run()
- cls.mapped_root = cls.mapper.mapped_root
def _assert_model(self, source, mapped, Model):
for field in Model._meta.fields:
@@ -79,7 +89,11 @@ def _assert_model(self, source, mapped, Model):
self.overrides[Model][column], getattr(mapped, column)
)
else:
- self.assertEqual(getattr(source, column), getattr(mapped, column))
+ self.assertEqual(
+ getattr(source, column),
+ getattr(mapped, column),
+ f"Mismatch in model {Model.__name__}, column {column}",
+ )
def _assert_node(self, source, mapped):
"""
@@ -133,6 +147,10 @@ def _recurse_and_assert(self, sources, mappeds, recursion_depth=0):
def test_map(self):
with using_content_database(self.tempdb):
+ self.mapper = ChannelMapper(self.channel)
+ self.mapper.run()
+ self.mapped_root = self.mapper.mapped_root
+
self._recurse_and_assert([self.source_root], [self.mapped_root])
self._assert_model(
self.channel,
@@ -142,6 +160,10 @@ def test_map(self):
def test_map_replace(self):
with using_content_database(self.tempdb):
+ self.mapper = ChannelMapper(self.channel)
+ self.mapper.run()
+ self.mapped_root = self.mapper.mapped_root
+
mapper = ChannelMapper(self.channel)
mapper.run()
self._recurse_and_assert([self.source_root], [mapper.mapped_root])
@@ -151,10 +173,127 @@ def test_map_replace(self):
kolibri_public_models.ChannelMetadata,
)
- @classmethod
- def tearDownClass(cls):
+ def test_categories__none_provided(self):
+ with using_content_database(self.tempdb):
+ kolibri_content_models.ContentNode.objects.filter(
+ channel_id=self.channel.id,
+ ).update(categories=None)
+
+ mapper = ChannelMapper(self.channel)
+ mapper.run()
+
+ self.assertEqual(mapper.mapped_channel.categories, [])
+
+ def test_categories__only_provided(self):
+ with using_content_database(self.tempdb):
+ kolibri_content_models.ContentNode.objects.filter(
+ channel_id=self.channel.id,
+ ).update(categories=None)
+
+ categories = ["Category1", "Category2"]
+ mapper = ChannelMapper(self.channel, categories=categories)
+ mapper.run()
+
+ self.assertEqual(mapper.mapped_channel.categories, categories)
+
+ def test_categories__only_on_content_nodes(self):
+ with using_content_database(self.tempdb):
+ source_contentnodes_queryset = (
+ kolibri_content_models.ContentNode.objects.filter(
+ channel_id=self.channel.id,
+ )
+ )
+ contentnode1 = source_contentnodes_queryset[0]
+ contentnode2 = source_contentnodes_queryset[1]
+
+ source_contentnodes_queryset.update(categories=None)
+ contentnode1.categories = "Category1,Category2"
+ contentnode2.categories = "Category3"
+ contentnode1.save()
+ contentnode2.save()
+
+ mapper = ChannelMapper(self.channel)
+ mapper.run()
+
+ self.assertEqual(
+ mapper.mapped_channel.categories,
+ ["Category1", "Category2", "Category3"],
+ )
+
+ def test_categories__both_provided_and_on_content_nodes(self):
+ with using_content_database(self.tempdb):
+ source_contentnodes_queryset = (
+ kolibri_content_models.ContentNode.objects.filter(
+ channel_id=self.channel.id,
+ )
+ )
+ contentnode1 = source_contentnodes_queryset[0]
+ contentnode2 = source_contentnodes_queryset[1]
+
+ source_contentnodes_queryset.update(categories=None)
+ contentnode1.categories = "Category1,Category2"
+ contentnode2.categories = "Category3"
+ contentnode1.save()
+ contentnode2.save()
+
+ categories = ["Category3", "Category4"]
+ mapper = ChannelMapper(self.channel, categories=categories)
+ mapper.run()
+
+ self.assertEqual(
+ mapper.mapped_channel.categories,
+ ["Category1", "Category2", "Category3", "Category4"],
+ )
+
+ def test_countries__none_provided(self):
+ with using_content_database(self.tempdb):
+ mapper = ChannelMapper(self.channel)
+ mapper.run()
+
+ self.assertEqual(mapper.mapped_channel.countries.count(), 0)
+
+ def test_countries__provided(self):
+ with using_content_database(self.tempdb):
+ country1 = Country.objects.create(code="C1", name="Country 1")
+ country2 = Country.objects.create(code="C2", name="Country 2")
+
+ countries = [country1, country2]
+ mapper = ChannelMapper(self.channel, countries=countries)
+ mapper.run()
+
+ self.assertCountEqual(mapper.mapped_channel.countries.all(), countries)
+
+ def test_categories_bitmask_annotation(self):
+ with using_content_database(self.tempdb):
+ categories = [
+ SUBJECTSLIST[0], # 1
+ SUBJECTSLIST[2], # 4
+ SUBJECTSLIST[4], # 16
+ ]
+
+ # Delete all categories in the tree, so that only explicitly provided categories
+ # are used
+ kolibri_content_models.ContentNode.objects.filter(
+ channel_id=self.channel.id,
+ ).update(categories=None)
+
+ mapper = ChannelMapper(
+ channel=self.channel,
+ public=False,
+ categories=categories,
+ )
+ mapper.run()
+
+ self.assertTrue(
+ hasattr(mapper.mapped_channel, "categories_bitmask_0"),
+ "ChannelMetadata should have categories_bitmask_0 field",
+ )
+ self.assertEqual(mapper.mapped_channel.categories_bitmask_0, 1 | 4 | 16)
+
+ def tearDown(self):
# Clean up datbase connection after the test
- cleanup_content_database_connection(cls.tempdb)
- super(ChannelMapperTest, cls).tearDownClass()
- if os.path.exists(cls.tempdb):
- os.remove(cls.tempdb)
+ self._date_patcher.stop()
+ cleanup_content_database_connection(self.tempdb)
+ super().tearDown()
+ if os.path.exists(self.tempdb):
+ os.remove(self.tempdb)
diff --git a/contentcuration/kolibri_public/tests/test_public_models_mutual_exclusivity.py b/contentcuration/kolibri_public/tests/test_public_models_mutual_exclusivity.py
new file mode 100644
index 0000000000..5e10a4026f
--- /dev/null
+++ b/contentcuration/kolibri_public/tests/test_public_models_mutual_exclusivity.py
@@ -0,0 +1,255 @@
+from django.contrib.auth import get_user_model
+from django.core.exceptions import ValidationError
+from django.urls import reverse
+from rest_framework import status
+
+from contentcuration.constants import community_library_submission as constants
+from contentcuration.models import Channel
+from contentcuration.models import CommunityLibrarySubmission
+from contentcuration.tests import testdata
+from contentcuration.tests.base import StudioAPITestCase
+from contentcuration.tests.base import StudioTestCase
+from contentcuration.viewsets.channel import AdminChannelSerializer
+
+
+User = get_user_model()
+
+
+class ChannelMutualExclusivityTestCase(StudioTestCase):
+ def setUp(self):
+ self.user = testdata.user()
+ self.channel = Channel.objects.create(
+ actor_id=self.user.id,
+ name="Test Channel",
+ description="Test Description",
+ version=0,
+ )
+ self.channel.version = 1
+ self.channel.save()
+ self.channel.editors.add(self.user)
+
+ def test_public_channel_cannot_be_submitted_to_community_library(self):
+ """Test that a public channel cannot be submitted to community library."""
+ self.channel.public = True
+ self.channel.save()
+
+ with self.assertRaises(ValidationError) as context:
+ CommunityLibrarySubmission.objects.create(
+ channel=self.channel,
+ channel_version=1,
+ author=self.user,
+ description="Test submission",
+ )
+
+ self.assertIn(
+ "Cannot create a community library submission for a public channel",
+ str(context.exception),
+ )
+
+ def test_community_channel_cannot_be_marked_public(self):
+ """Test that a community channel cannot be marked public."""
+ CommunityLibrarySubmission.objects.create(
+ channel=self.channel,
+ channel_version=1,
+ author=self.user,
+ description="Test submission",
+ status=constants.STATUS_APPROVED,
+ )
+
+ self.channel.public = True
+ with self.assertRaises(ValidationError) as context:
+ self.channel.clean()
+
+ self.assertIn(
+ "This channel has been added to the Community Library and cannot be marked public",
+ str(context.exception),
+ )
+
+ def test_is_community_channel_method(self):
+ """Test the is_community_channel method."""
+ self.assertFalse(self.channel.is_community_channel())
+
+ CommunityLibrarySubmission.objects.create(
+ channel=self.channel,
+ channel_version=1,
+ author=self.user,
+ description="Test submission",
+ status=constants.STATUS_APPROVED,
+ )
+ self.assertTrue(self.channel.is_community_channel())
+
+ self.channel.version = 2
+ self.channel.save()
+ CommunityLibrarySubmission.objects.create(
+ channel=self.channel,
+ channel_version=2,
+ author=self.user,
+ description="Test submission 2",
+ status=constants.STATUS_LIVE,
+ )
+ self.assertTrue(self.channel.is_community_channel())
+
+ self.channel.version = 3
+ self.channel.save()
+ CommunityLibrarySubmission.objects.create(
+ channel=self.channel,
+ channel_version=3,
+ author=self.user,
+ description="Test submission 3",
+ status=constants.STATUS_PENDING,
+ )
+ self.assertTrue(self.channel.is_community_channel())
+
+ def test_non_community_channel_can_be_marked_public(self):
+ """Test that a non-community channel can be marked public."""
+ self.channel.public = True
+ self.channel.clean()
+ self.channel.save()
+ self.assertTrue(self.channel.public)
+
+ def test_non_public_channel_can_be_submitted_to_community_library(self):
+ """Test that a non-public channel can be submitted to community library."""
+ submission = CommunityLibrarySubmission.objects.create(
+ channel=self.channel,
+ channel_version=1,
+ author=self.user,
+ description="Test submission",
+ )
+ self.assertEqual(submission.channel, self.channel)
+
+
+class AdminChannelSerializerMutualExclusivityTestCase(StudioTestCase):
+ """Test mutual exclusivity rules for AdminChannelSerializer."""
+
+ def setUp(self):
+ self.user = testdata.user()
+ self.channel = Channel.objects.create(
+ actor_id=self.user.id,
+ name="Test Channel",
+ description="Test Description",
+ version=0,
+ )
+
+ self.channel.version = 1
+ self.channel.save()
+ self.channel.editors.add(self.user)
+
+ def test_serializer_validates_community_channel_cannot_be_public(self):
+ """Test that serializer prevents marking community channel as public."""
+ CommunityLibrarySubmission.objects.create(
+ channel=self.channel,
+ channel_version=1,
+ author=self.user,
+ description="Test submission",
+ status=constants.STATUS_APPROVED,
+ )
+
+ serializer = AdminChannelSerializer(
+ instance=self.channel, data={"public": True}, partial=True
+ )
+
+ self.assertFalse(serializer.is_valid())
+ self.assertIn(
+ "This channel has been added to the Community Library and cannot be marked public",
+ str(serializer.errors),
+ )
+
+ def test_serializer_allows_non_community_channel_to_be_public(self):
+ """Test that serializer allows non-community channel to be public."""
+ serializer = AdminChannelSerializer(
+ instance=self.channel, data={"public": True}, partial=True
+ )
+
+ self.assertTrue(serializer.is_valid())
+ serializer.save()
+ self.channel.refresh_from_db()
+ self.assertTrue(self.channel.public)
+
+ def test_serializer_allows_community_channel_to_remain_non_public(self):
+ """Test that serializer allows community channel to remain non-public."""
+ CommunityLibrarySubmission.objects.create(
+ channel=self.channel,
+ channel_version=1,
+ author=self.user,
+ description="Test submission",
+ status=constants.STATUS_APPROVED,
+ )
+
+ serializer = AdminChannelSerializer(
+ instance=self.channel,
+ data={"source_url": "https://example.com"},
+ partial=True,
+ )
+
+ self.assertTrue(serializer.is_valid())
+ serializer.save()
+ self.channel.refresh_from_db()
+ self.assertEqual(self.channel.source_url, "https://example.com")
+ self.assertFalse(self.channel.public)
+
+
+class CommunityLibrarySubmissionMutualExclusivityAPITestCase(StudioAPITestCase):
+ """Test mutual exclusivity rules via API endpoints."""
+
+ def setUp(self):
+ super().setUp()
+
+ self.user = testdata.user()
+ self.channel = testdata.channel()
+ self.channel.public = False
+ self.channel.version = 1
+ self.channel.editors.add(self.user)
+ self.channel.save()
+
+ def tearDown(self):
+ super().tearDown()
+
+ def test_api_prevents_public_channel_submission_to_community_library(self):
+ """Test that API prevents submitting public channel to community library."""
+ self.channel.public = True
+ self.channel.save()
+
+ self.client.force_authenticate(user=self.user)
+
+ url = reverse("community-library-submission-list")
+ data = {
+ "channel": self.channel.id,
+ "description": "Test submission",
+ "countries": [],
+ }
+
+ response = self.client.post(url, data, format="json")
+ self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
+ self.assertIn(
+ "Cannot create a community library submission for a public channel",
+ str(response.data),
+ )
+
+ def test_api_prevents_approving_submission_for_public_channel(self):
+ """Test that API prevents approving submission for channel that became public."""
+ submission = CommunityLibrarySubmission.objects.create(
+ channel=self.channel,
+ channel_version=self.channel.version,
+ description="Test submission",
+ status=constants.STATUS_PENDING,
+ author=self.user,
+ )
+
+ self.channel.public = True
+ self.channel.save()
+
+ self.client.force_authenticate(user=self.admin_user)
+
+ url = reverse(
+ "admin-community-library-submission-resolve", kwargs={"pk": submission.id}
+ )
+ data = {
+ "status": constants.STATUS_APPROVED,
+ }
+
+ response = self.client.post(url, data, format="json")
+ self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
+ self.assertIn(
+ "Cannot approve a community library submission for a channel that has been marked public",
+ str(response.data),
+ )
diff --git a/contentcuration/kolibri_public/tests/utils/mixer.py b/contentcuration/kolibri_public/tests/utils/mixer.py
new file mode 100644
index 0000000000..6e39d09304
--- /dev/null
+++ b/contentcuration/kolibri_public/tests/utils/mixer.py
@@ -0,0 +1,33 @@
+from django.db import models as django_models
+from kolibri_content.fields import DateTimeTzField
+from kolibri_content.fields import UUIDField
+from kolibri_content.router import get_active_content_database
+from mixer.backend.django import GenFactory
+from mixer.backend.django import Mixer
+
+
+class KolibriPublicMixer(Mixer):
+ """Slightly modified Mixer that works correctly with the active
+ content database and with UUIDField.
+ """
+
+ def __init__(self, *args, **kwargs):
+ mixer_factory = GenFactory()
+ mixer_factory.generators[UUIDField] = mixer_factory.generators[
+ django_models.UUIDField
+ ]
+ mixer_factory.generators[DateTimeTzField] = mixer_factory.generators[
+ django_models.DateTimeField
+ ]
+
+ return super().__init__(*args, factory=mixer_factory, **kwargs)
+
+ def postprocess(self, target):
+ if self.params.get("commit"):
+ # Not sure why the `force_insert` is needed, but using the
+ # mixer causes "Save with update_fields did not affect any rows" error
+ # if this is not specified
+ used_db = get_active_content_database(return_none_if_not_set=True)
+ target.save(using=used_db, force_insert=True)
+
+ return target
diff --git a/contentcuration/kolibri_public/utils/annotation.py b/contentcuration/kolibri_public/utils/annotation.py
index 7295c97e39..0d2f5f4d34 100644
--- a/contentcuration/kolibri_public/utils/annotation.py
+++ b/contentcuration/kolibri_public/utils/annotation.py
@@ -1,33 +1,49 @@
"""
-Functions in here are the subset of annotation functions from Kolibri related to channel metadata.
+Functions in here are a modified subset of annotation functions from Kolibri related to channel metadata.
https://github.com/learningequality/kolibri/blob/caec91dd2da5617adfb50332fb698068248e8e47/kolibri/core/content/utils/annotation.py#L731
"""
-import datetime
+from itertools import chain
from django.db.models import Q
from django.db.models import Sum
+from django.utils import timezone
from kolibri_public.models import ChannelMetadata
from kolibri_public.models import ContentNode
from kolibri_public.models import LocalFile
+from kolibri_public.search import annotate_channelmetadata_label_bitmasks
from le_utils.constants import content_kinds
from contentcuration.models import Channel
-def set_channel_metadata_fields(channel_id, public=None):
+def set_channel_metadata_fields(
+ channel_id,
+ public=None,
+ categories=None,
+ countries=None,
+):
+ # Note: The `categories` argument should be a _list_, NOT a _dict_.
+
# Remove unneeded db_lock
- channel = ChannelMetadata.objects.get(id=channel_id)
+ channel_queryset = ChannelMetadata.objects.filter(id=channel_id)
+ channel = channel_queryset.get()
+
calculate_published_size(channel)
calculate_total_resource_count(channel)
calculate_included_languages(channel)
+ calculate_included_categories(channel, categories)
calculate_next_order(channel, public=public)
# Add this to ensure we keep this up to date.
- channel.last_updated = datetime.datetime.now()
+ channel.last_updated = timezone.now()
if public is not None:
channel.public = public
+ if countries is not None:
+ channel.countries.set(countries)
channel.save()
+ annotate_channelmetadata_label_bitmasks(channel_queryset)
+
def files_for_nodes(nodes):
return LocalFile.objects.filter(files__contentnode__in=nodes)
@@ -67,6 +83,28 @@ def calculate_included_languages(channel):
channel.included_languages.add(*list(languages))
+def calculate_included_categories(channel, categories):
+ content_nodes = ContentNode.objects.filter(
+ channel_id=channel.id, available=True
+ ).exclude(categories=None)
+
+ categories_comma_separated_lists = content_nodes.values_list(
+ "categories", flat=True
+ )
+ contentnode_categories = set(
+ chain.from_iterable(
+ (
+ categories_comma_separated_list.split(",")
+ for categories_comma_separated_list in categories_comma_separated_lists
+ )
+ )
+ )
+
+ all_categories = sorted(set(categories or []).union(contentnode_categories))
+ channel.categories = all_categories
+ channel.save()
+
+
def calculate_next_order(channel, public=False):
# This has been edited from the source Kolibri, in order
# to make the order match given by the public channel API on Studio.
diff --git a/contentcuration/kolibri_public/utils/export_channel_to_kolibri_public.py b/contentcuration/kolibri_public/utils/export_channel_to_kolibri_public.py
new file mode 100644
index 0000000000..e81a8badf1
--- /dev/null
+++ b/contentcuration/kolibri_public/utils/export_channel_to_kolibri_public.py
@@ -0,0 +1,91 @@
+import logging
+import os
+import shutil
+import tempfile
+
+from django.conf import settings
+from django.core.files.storage import default_storage as storage
+from django.core.management import call_command
+from kolibri_content.apps import KolibriContentConfig
+from kolibri_content.models import ChannelMetadata as ExportedChannelMetadata
+from kolibri_content.router import get_active_content_database
+from kolibri_content.router import using_content_database
+from kolibri_public.utils.mapper import ChannelMapper
+
+
+logger = logging.getLogger(__file__)
+
+
+class using_temp_migrated_content_database:
+ """
+ A wrapper context manager for read-only access to a content database
+ that might not have all current migrations applied. Works by copying
+ the database to a temporary file, applying migrations to this temporary
+ database and then using this temporary database.
+ """
+
+ def __init__(self, database_storage_path):
+ self.database_path = database_storage_path
+ self._inner_mgr = None
+
+ def __enter__(self):
+ self._named_temporary_file_mgr = tempfile.NamedTemporaryFile(suffix=".sqlite3")
+ self.temp_database_file = self._named_temporary_file_mgr.__enter__()
+
+ with storage.open(self.database_path, "rb") as db_file:
+ shutil.copyfileobj(db_file, self.temp_database_file)
+ self.temp_database_file.seek(0)
+
+ with using_content_database(self.temp_database_file.name):
+ # Run migration to handle old content databases published prior to current fields being added.
+ call_command(
+ "migrate",
+ app_label=KolibriContentConfig.label,
+ database=get_active_content_database(),
+ )
+
+ self._inner_mgr = using_content_database(self.temp_database_file.name)
+ self._inner_mgr.__enter__()
+
+ def __exit__(self, exc_type, exc_val, exc_tb):
+ self._inner_mgr.__exit__(exc_type, exc_val, exc_tb)
+ self._named_temporary_file_mgr.__exit__(exc_type, exc_val, exc_tb)
+
+
+def export_channel_to_kolibri_public(
+ channel_id,
+ channel_version=None,
+ public=True,
+ categories=None,
+ countries=None,
+):
+ # Note: The `categories` argument should be a _list_, NOT a _dict_.
+ logger.info("Putting channel {} into kolibri_public".format(channel_id))
+
+ versioned_db_filename = "{id}-{version}.sqlite3".format(
+ id=channel_id, version=channel_version
+ )
+ unversioned_db_filename = "{id}.sqlite3".format(id=channel_id)
+
+ versioned_db_storage_path = os.path.join(settings.DB_ROOT, versioned_db_filename)
+ unversioned_db_storage_path = os.path.join(
+ settings.DB_ROOT, unversioned_db_filename
+ )
+
+ if channel_version is None:
+ db_storage_path = unversioned_db_storage_path
+ else:
+ db_storage_path = versioned_db_storage_path
+
+ with using_temp_migrated_content_database(db_storage_path):
+ channel = ExportedChannelMetadata.objects.get(id=channel_id)
+ logger.info(
+ "Found channel {} for id: {} mapping now".format(channel.name, channel_id)
+ )
+ mapper = ChannelMapper(
+ channel=channel,
+ public=public,
+ categories=categories,
+ countries=countries,
+ )
+ mapper.run()
diff --git a/contentcuration/kolibri_public/utils/mapper.py b/contentcuration/kolibri_public/utils/mapper.py
index 01dc1f0726..0b17527b48 100644
--- a/contentcuration/kolibri_public/utils/mapper.py
+++ b/contentcuration/kolibri_public/utils/mapper.py
@@ -3,7 +3,7 @@
from kolibri_content import models as kolibri_content_models
from kolibri_content.base_models import MAX_TAG_LENGTH
from kolibri_public import models as kolibri_public_models
-from kolibri_public.search import annotate_label_bitmasks
+from kolibri_public.search import annotate_contentnode_label_bitmasks
from kolibri_public.utils.annotation import set_channel_metadata_fields
from le_utils.constants import content_kinds
@@ -19,9 +19,23 @@ class ChannelMapper(object):
Foreign Keyed from the root ContentNode.
"""
- def __init__(self, channel, public=True):
+ def __init__(
+ self,
+ channel,
+ public=True,
+ categories=None,
+ countries=None,
+ ):
+ # Note: The argument `channel` is an instance of `kolibri_content.models.ChannelMetadata,`
+ # which belongs to a specific channel version to be exported. Therefore, we do not
+ # need to explicitly pass the channel version as an argument here.
+
+ # Note: The `categories` argument should be a _list_, NOT a _dict_.
+
self.channel = channel
self.public = public
+ self.categories = categories
+ self.countries = countries
@property
def overrides(self):
@@ -54,10 +68,24 @@ def run(self):
)
self.mapped_channel.public = self.public
self.mapped_channel.save_base(raw=True)
- annotate_label_bitmasks(self.mapped_root.get_descendants(include_self=True))
+
+ annotate_contentnode_label_bitmasks(
+ self.mapped_root.get_descendants(include_self=True)
+ )
# Rather than set the ancestors fields after mapping, like it is done in Kolibri
# here we set it during mapping as we are already recursing through the tree.
- set_channel_metadata_fields(self.mapped_channel.id, public=self.public)
+
+ set_channel_metadata_fields(
+ self.mapped_channel.id,
+ public=self.public,
+ categories=self.categories,
+ countries=self.countries,
+ )
+
+ # Refreshing this is needed, because otherwise the fields set in the
+ # set_channel_metadata_fields function will not be reflected in the
+ # self.mapped_channel object.
+ self.mapped_channel.refresh_from_db()
def _map_model(self, source, Model):
properties = {}
diff --git a/contentcuration/kolibri_public/views.py b/contentcuration/kolibri_public/views.py
index 00817d4e64..867a677798 100644
--- a/contentcuration/kolibri_public/views.py
+++ b/contentcuration/kolibri_public/views.py
@@ -33,7 +33,7 @@
from django_filters.rest_framework import NumberFilter
from django_filters.rest_framework import UUIDFilter
from kolibri_public import models
-from kolibri_public.search import get_available_metadata_labels
+from kolibri_public.search import get_contentnode_available_metadata_labels
from kolibri_public.stopwords import stopwords_set
from le_utils.constants import content_kinds
from rest_framework import status
@@ -42,6 +42,7 @@
from contentcuration.middleware.locale import locale_exempt
from contentcuration.middleware.session import session_exempt
+from contentcuration.models import Country
from contentcuration.models import generate_storage_url
from contentcuration.utils.pagination import ValuesViewsetCursorPagination
from contentcuration.viewsets.base import BaseValuesViewset
@@ -82,13 +83,43 @@ def wrapper_func(*args, **kwargs):
MODALITIES = set(["QUIZ"])
+def bitmask_contains_and(queryset, name, value):
+ """
+ A filtering method that filters instances matching all provided
+ comma-separated values using bitmask fields on the model.
+ """
+ return queryset.has_all_labels(name, value.split(","))
+
+
+class UUIDInFilter(BaseInFilter, UUIDFilter):
+ pass
+
+
+class CharInFilter(BaseInFilter, CharFilter):
+ pass
+
+
class ChannelMetadataFilter(FilterSet):
+ def __init__(self, data=None, *args, **kwargs):
+ # if filterset is bound, use initial values as defaults
+ if data is not None:
+ data = data.copy()
+ for name, f in self.base_filters.items():
+ initial = f.extra.get("initial")
+ # filter param is either missing or empty, use initial as default
+ if not data.get(name) and initial:
+ data[name] = initial
+ super().__init__(data, *args, **kwargs)
+
available = BooleanFilter(method="filter_available", label="Available")
has_exercise = BooleanFilter(method="filter_has_exercise", label="Has exercises")
+ categories = CharFilter(method=bitmask_contains_and, label="Categories")
+ countries = CharInFilter(field_name="countries", label="Countries")
+ public = BooleanFilter(field_name="public", label="Public", initial=True)
class Meta:
model = models.ChannelMetadata
- fields = ("available", "has_exercise")
+ fields = ("available", "has_exercise", "categories", "countries", "public")
def filter_has_exercise(self, queryset, name, value):
queryset = queryset.annotate(
@@ -133,6 +164,7 @@ class ChannelMetadataViewSet(ReadOnlyValuesViewset):
"public",
"total_resource_count",
"published_size",
+ "categories",
)
field_map = {
@@ -148,6 +180,10 @@ def get_queryset(self):
return models.ChannelMetadata.objects.all()
def consolidate(self, items, queryset):
+ # Only keep a single item for every channel ID, to get rid of possible
+ # duplicates caused by filtering
+ items = list(OrderedDict((item["id"], item) for item in items).values())
+
included_languages = {}
for (
channel_id,
@@ -163,15 +199,19 @@ def consolidate(self, items, queryset):
for item in items:
item["included_languages"] = included_languages.get(item["id"], [])
item["last_published"] = item["last_updated"]
- return items
-
-class UUIDInFilter(BaseInFilter, UUIDFilter):
- pass
+ countries = {}
+ for (channel_id, country_code) in Country.objects.filter(
+ public_channels__in=queryset
+ ).values_list("public_channels", "code"):
+ if channel_id not in countries:
+ countries[channel_id] = []
+ countries[channel_id].append(country_code)
+ for item in items:
+ item["countries"] = countries.get(item["id"], [])
-class CharInFilter(BaseInFilter, CharFilter):
- pass
+ return items
contentnode_filter_fields = [
@@ -228,12 +268,12 @@ class ContentNodeFilter(FilterSet):
parent__isnull = BooleanFilter(field_name="parent", lookup_expr="isnull")
include_coach_content = BooleanFilter(method="filter_include_coach_content")
contains_quiz = CharFilter(method="filter_contains_quiz")
- grade_levels = CharFilter(method="bitmask_contains_and")
- resource_types = CharFilter(method="bitmask_contains_and")
- learning_activities = CharFilter(method="bitmask_contains_and")
- accessibility_labels = CharFilter(method="bitmask_contains_and")
- categories = CharFilter(method="bitmask_contains_and")
- learner_needs = CharFilter(method="bitmask_contains_and")
+ grade_levels = CharFilter(method=bitmask_contains_and)
+ resource_types = CharFilter(method=bitmask_contains_and)
+ learning_activities = CharFilter(method=bitmask_contains_and)
+ accessibility_labels = CharFilter(method=bitmask_contains_and)
+ categories = CharFilter(method=bitmask_contains_and)
+ learner_needs = CharFilter(method=bitmask_contains_and)
keywords = CharFilter(method="filter_keywords")
channels = UUIDInFilter(field_name="channel_id")
languages = CharInFilter(field_name="lang_id")
@@ -344,9 +384,6 @@ def filter_keywords(self, queryset, name, value):
return queryset.filter(query)
- def bitmask_contains_and(self, queryset, name, value):
- return queryset.has_all_labels(name, value.split(","))
-
def map_file(file):
file["checksum"] = file.pop("local_file__id")
@@ -533,7 +570,10 @@ def get_paginated_response(self, data):
[
("more", self.get_more()),
("results", data),
- ("labels", get_available_metadata_labels(self.queryset)),
+ (
+ "labels",
+ get_contentnode_available_metadata_labels(self.queryset),
+ ),
]
)
)
diff --git a/docs/dependencies.md b/docs/dependencies.md
index afa1e0f205..2c3ecbf385 100644
--- a/docs/dependencies.md
+++ b/docs/dependencies.md
@@ -1,11 +1,11 @@
# Adding or updating dependencies
-We use `pip-tools` to ensure all our dependencies use the same versions on all deployments.
+We use `uv` to manage Python dependencies and generate requirements files.
To add a dependency, add it to either `requirements.in` or `requirements-dev.in`, then
-run `pip-compile requirements[-dev].in` to generate the .txt file. Please make sure that
+run `uv pip compile requirements[-dev].in` to generate the .txt file. Please make sure that
both the `.in` and `.txt` file changes are part of the commit when updating dependencies.
-To update a dependency, use `pip-compile --upgrade-package [package-name] requirements[-dev].in`
+To update a dependency, use `uv pip compile --upgrade-package [package-name] requirements[-dev].in`
-For more details, please see the [pip-tools docs on Github](https://github.com/jazzband/pip-tools).
+For more details, please see the [uv documentation](https://docs.astral.sh/uv/).
diff --git a/docs/host_services_setup.md b/docs/host_services_setup.md
index 74aa267cbf..0034b140ea 100644
--- a/docs/host_services_setup.md
+++ b/docs/host_services_setup.md
@@ -3,7 +3,7 @@
This guide will walk through setting up Kolibri Studio for local development, where you'll run Studio's Python apps and all of Studio's services on your host machine, without the need for docker.
## Prerequisites
-For detailed instructions on installing and configuring Volta, pyenv, and pyenv-virtualenv, please see the [Prerequisites](./local_dev_host.md#prerequisites) section in our Local Development with host guide.
+For detailed instructions on installing and configuring Volta and uv, please see the [Prerequisites](./local_dev_host.md#prerequisites) section in our Local Development with host guide.
## Install system dependencies and services
Studio requires some background services to be running:
diff --git a/docs/local_dev_docker.md b/docs/local_dev_docker.md
index 3d89e3a38b..e30a5ab5ea 100644
--- a/docs/local_dev_docker.md
+++ b/docs/local_dev_docker.md
@@ -5,10 +5,10 @@ The following guide utilizes docker and docker-compose to run select services re
**Note:** If you are developing on Windows, it is recommended to use WSL (Windows Subsystem for Linux). Please follow the [WSL setup guide](./local_dev_wsl.md) for detailed instructions.
## Prerequisites
-For detailed instructions on installing and configuring Volta, pyenv, and pyenv-virtualenv, please see the [Prerequisites](./local_dev_host.md#prerequisites) section in our Local Development with host guide.
+For detailed instructions on installing and configuring Volta and uv, please see the [Prerequisites](./local_dev_host.md#prerequisites) section in our Local Development with host guide.
## Build your python virtual environment
-For complete instructions on installing Python 3.10.13, creating and activating the virtual environment, and installing Studio’s Python dependencies, please refer to the [Build Your Python Virtual Environment](./local_dev_host.md#build-your-python-virtual-environment) section in our Local Development with host guide.
+For complete instructions on creating and activating the virtual environment, and installing Studio’s Python dependencies, please refer to the [Build Your Python Virtual Environment](./local_dev_host.md#build-your-python-virtual-environment) section in our Local Development with host guide.
### A note about dependencies on Apple Silicon M1+
diff --git a/docs/local_dev_host.md b/docs/local_dev_host.md
index 548aa05d91..94b571aebe 100644
--- a/docs/local_dev_host.md
+++ b/docs/local_dev_host.md
@@ -6,7 +6,7 @@ This guide will walk through setting up Kolibri Studio for local development, wh
## Prerequisites
- [volta](https://docs.volta.sh/guide/getting-started)
-- [pyenv](https://kolibri-dev.readthedocs.io/en/develop/howtos/installing_pyenv.html) and [pyenv-virtualenv](https://github.com/pyenv/pyenv-virtualenv#installation)
+- [uv](https://docs.astral.sh/uv/) - Python package installer and virtual environment manager
## Install system dependencies and services
Studio requires some background services to be running:
@@ -75,29 +75,25 @@ exit # leave the postgres account
```
## Build your python virtual environment
-To determine what version of Python studio needs, you can check the `runtime.txt` file:
-```bash
-$ cat runtime.txt
-# This is the required version of Python to run Studio currently.
-# This is determined by the default Python 3 version that is installed
-# inside Ubuntu Bionic, which is used to build images for Studio.
-# We encode it here so that it can be picked up by Github's dependabot
-# to manage automated package upgrades.
-python-3.10.13
-```
-So to install python 3.10.13 through `pyenv` and set up a virtual environment:
+
+Studio uses [uv](https://docs.astral.sh/uv/) for Python dependency management and virtual environments. To set up your development environment:
+
```bash
-pyenv install 3.10.13
-pyenv virtualenv 3.10.13 studio-py3.10
-pyenv activate studio-py3.10
+# Create a virtual environment with uv (this will use the system Python or uv-managed Python)
+uv venv --seed
+
+# Activate the virtual environment
+source .venv/bin/activate
```
+
Now you may install Studio's Python dependencies:
```bash
-pip install -r requirements.txt -r requirements-dev.txt
+uv pip sync requirements.txt requirements-dev.txt
```
+
To deactivate the virtual environment, when you're finished developing on Studio for the time being:
```bash
-pyenv deactivate
+deactivate
```
### A note about `psycopg2`
diff --git a/docs/local_dev_wsl.md b/docs/local_dev_wsl.md
index d94f308553..f5405cb01a 100644
--- a/docs/local_dev_wsl.md
+++ b/docs/local_dev_wsl.md
@@ -53,7 +53,7 @@ git clone https://github.com/$USERNAME/studio.git
Replace `$USERNAME` with your GitHub username.
## Install Prerequisites
-For detailed instructions on installing and configuring Volta, pyenv, and pyenv-virtualenv, please see the [Prerequisites](./local_dev_host.md#prerequisites) section in our Local Development with host guide.
+For detailed instructions on installing and configuring Volta and uv, please see the [Prerequisites](./local_dev_host.md#prerequisites) section in our Local Development with host guide.
## Install System Dependencies and Services
@@ -171,7 +171,7 @@ Now that you have your project open in VS Code, you can run the same commands yo
2. **Activate the Python Virtual Environment**:
```sh
- pyenv activate studio-py3.10
+ source .venv/bin/activate
```
3. **Run the Services**:
diff --git a/docs/markdown_editor_viewer.md b/docs/markdown_editor_viewer.md
deleted file mode 100644
index 0f8c6d58dc..0000000000
--- a/docs/markdown_editor_viewer.md
+++ /dev/null
@@ -1,98 +0,0 @@
-
-# Markdown Editor/Viewer
-
-We use TOAST UI (TUI) Editor v2 as a basis for our markdown [editor](../contentcuration/contentcuration/frontend/shared/views/MarkdownEditor/MarkdownEditor/MarkdownEditor.vue) and [viewer](../contentcuration/contentcuration/frontend/shared/views/MarkdownEditor/MarkdownViewer/MarkdownViewer.vue).
-
-- [Documentation](https://ui.toast.com/tui-editor/)
-- [GitHub](https://github.com/nhn/tui.editor)
-- [API documentation](https://nhn.github.io/tui.editor/latest/)
-
-
-## WYSIWYG/Markdown
-
-TUI Editor provides both WYSIWYG and markdown mode. Currently, we use WYSIWYG mode only. Implementation-wise that means that although we need to define conversions both ways (we store markdown on our servers), there is currently no emphasis on making markdown editor work completely because it's not visible to users at all. Some additional steps might be needed (e.g. initialization of MathQuill fields).
-
-However, one of the reasons to choose this library was its potential to scale (maybe to allow users familiar with markdown syntax to use it in the future).
-
-**Development tip**: As mentioned, our custom logic might not work entirely in markdown mode. However, when developing conversions, it might be helpful to set `hideModeSwitch` option to `false` when initializing the editor so you can see whether your basic conversion logic is working both ways properly.
-
-
-## Viewer
-
-Besides the editor, TUI also provides the [viewer](https://github.com/nhn/tui.editor/blob/master/apps/editor/docs/viewer.md) that is a leaner version of the editor. Currently, we use it for assessment item's questions, answers, and hints preview.
-
-
-## Under the hood
-
-TUI editor uses the following 3rd party libraries and exposes their API for public use. We use some of them for our custom plugins:
-
-**WYSIWYG mode** is built around [Squire](https://github.com/neilj/Squire). Sometimes TUI's editor API is sufficient though there are some cases when we need to access [Squire's API](https://github.com/neilj/Squire#api) directly. That can be done via [`getSquire()`](https://nhn.github.io/tui.editor/latest/ToastUIEditor#getSquire) method:
-
-```javascript
- import Editor from '@toast-ui/editor';
-
- const editor = new Editor()
- const squire = editor.getSquire()
-```
-
-**Markdown mode** is built on top of [CodeMirror](https://codemirror.net/). We currently don't need to do that though it's API can be similarly accessed via [`getCodeMirror()`](https://nhn.github.io/tui.editor/latest/ToastUIEditor#getCodeMirror) if needed in the future.
-
-**Conversions from markdown to HTML** are processed by TUI editor's own markdown parser [ToastMark](https://github.com/nhn/tui.editor/tree/master/libs/toastmark). It can be extended by using [Custom HTML Renderer](https://github.com/nhn/tui.editor/blob/master/apps/editor/docs/custom-html-renderer.md).
-
-**Conversions from HTML to markdown** are processed by [to-mark](https://github.com/nhn/tui.editor/tree/master/libs/to-mark), another TUI editor's own library. Default conversion can be overwritten by defining a custom convertor and passing it into `customConvertor` parameter of [initialization`options`](https://nhn.github.io/tui.editor/latest/ToastUIEditor) . Unfortunately, it seems that there is no way to extend the default convertor using TUI Editor's public API at this point. That would be ideal for our use-case when we don't need to define the whole conversion logic but rather process some additional conversions (e.g. formulas). However, it can be extended using [this hackish solution](https://github.com/nhn/tui.editor/issues/615#issuecomment-527802641) which is how we currently extend conversions in this direction until there will be public API available.
-
-
-## Custom plugins
-
-Our custom plugins are located in [plugins directory](../contentcuration/contentcuration/frontend/shared/views/MarkdownEditor/plugins).
-
-### [Formulas editor plugin](../contentcuration/contentcuration/frontend/shared/views/MarkdownEditor/plugins/formulas)
-
-#### Adding new formulas
-
-1. A new formula's LaTeX representation is inserted as a new HTML element using `getSquire().insertHTML()`. It is also assigned a special class to denote that it is a new math field so that later we know which fields should be initialized as MathQuill static math fields.
-
-2. HTML is converted to markdown (conversions logic will be described later)
-
-3. All new math fields are initialized as MathQuill static math fields
-
-#### Editing existing formulas
-
-The steps are the same as when adding a new formula, except that instead of inserting a new formula element with `getSquire().insertHTML()`, an active element being edited is replaced by a new HTML using `parentNode.replaceChild()`.
-
-#### MathQuill
-
-We use [MathQuill's static math fields](http://docs.mathquill.com/en/latest/Getting_Started/#static-math-rendering) to render formulas in a list of all formulas in the formulas menu and in the editor. [Editable math fields](http://docs.mathquill.com/en/latest/Getting_Started/#editable-math-fields) are used in the edit part of the formulas menu.
-
-##### Customizations
-
-There is one customization in MathQuill code related to formulas logic: When initializing MathQuill fields (MathQuill replaces an element with its own HTML during that process), we add `data-formula` attribute to the root math element. Its value is the original formula's LaTeX representation. This attribute is used as a basis for conversion from HTML to markdown.
-
-**Important**
-All MathQuill customizations are saved in [this commit](https://github.com/learningequality/studio/commit/9c85577761a75d1c3c216496f4e3373e57623699). There's a need to be careful to reflect them if we upgrade MathQuill one day (or create MathQuill fork for the sake of clarity if there's a need to upgrade more often or add more customizations).
-
-#### [HTML to Markdown conversion](../contentcuration/contentcuration/frontend/shared/views/MarkdownEditor/plugins/formulas/formula-html-to-md.js)
-
-All elements in the input HTML containing `data-formula` attribute are replaced by a value saved in that attribute and wrapped in double `$`.
-
-#### [Markdown to HTML conversion](../contentcuration/contentcuration/frontend/shared/views/MarkdownEditor/plugins/formulas/formula-md-to-html.js)
-
-All markdown substrings wrapped in double `$` are converted to `span` element and assigned `math-field` class. We use this class to know which elements should be initialized with MathQuill.
-
-This conversion is needed for rendering content in WYSIWYG after the first load (and eventually in the future if we allow users to switch to markdown mode on the fly) because we store markdown on our servers.
-
-
-### [Image upload plugin](../contentcuration/contentcuration/frontend/shared/views/MarkdownEditor/plugins/image-upload)
-
-#### Adding/editing images
-
-New images can be added in two ways: An image can be dropped into the editor's
-content area, or it can be uploaded via the images menu.
-
-If a new image is dropped into the content area, then the images menu opens
-with the image and takes over the file upload process.
-
-After an image has been successfully uploaded using the images menu, a dummy
-image element is inserted into the editor's content area, and a Vue image
-field component is mounted on it. This component is responsible for resizing,
-editing, and removing an image.
diff --git a/docs/rich_text_editor.md b/docs/rich_text_editor.md
index a924925da9..c08bba23c6 100644
--- a/docs/rich_text_editor.md
+++ b/docs/rich_text_editor.md
@@ -1,277 +1,60 @@
-# Rich Text Editor Documentation
+## Rich Text Editor Documentation
-This Component replaces Studio’s text editor with a future-ready implementation, deliberately scoped to immediate needs:
-- Swap Toast UI while preserving Markdown storage
-- Support for core formatting
-- Support for more advanced formats (code and math blocks, image uploading)
+Studio has a Rich text Editor that is currently being used in the exercise editor: (questions / answers / hints)
-It uses TipTap which is a headless, framework-agnostic rich-text editor built on top of ProseMirror.
-[https://tiptap.dev/docs](https://tiptap.dev/docs)
+We use [TipTap](https://tiptap.dev/) that is a headless framework based on ProseMirror. It was built to replace a past long-lived Toast UI (TUI) based editor; that influenced some implementation decisions to maintain backward compatibility.
----
-
-Currently the editor is accessible through hidden routes:
-- `http://localhost:8080/en/channels//#/editor-dev` which requires a valid channel id first
-- `http://localhost:8080/#editor-dev`
-
-## Current Folder Structure
-
-```
-TipTapEditor/
-├── assets/
-│ └── # icons
-└── TipTapEditor/
-| ├── components/
-| │ ├── toolbar/
-| │ ├── ToolbarButton.vue
-| │ ├── FormatDropdown.vue
-| │ ├── PasteDropdown.vue
-| │ └── ToolbarDivider.vue
-| │ ├── EditorToolbar.vue
-| | └── EditorContentWrapper.vue
-| ├── composables/
-| │ ├── useDropdowns.js
-| │ ├── useEditor.js
-| │ └── useToolbarActions.js
-| ├── extensions/
-| │ ├── SmallTextExtension.js
-| └── TipTapEditor.vue # Main container
-└── TipTapEditorStrings.js
-```
-## Current Key Features
-
-- **Rich Text Formatting**: Bold, italic, underline, strikethrough
-- **Typography**: Multiple heading levels (H1, H2, H3), normal text, and small text
-- **Lists**: Bullet lists and numbered lists
-- **Advanced Clipboard**: Copy with formatting, paste with/without formatting
-- **History**: Undo/redo functionality
-- **Accessibility**: Full keyboard navigation and ARIA support
-- **RTL Support**: Right-to-left text direction support
-- **Internationalization**: Built-in string management system
-- **Custom Extensions**: Extensible architecture for additional features
-
-## Core Components
-
-### 1. TipTapEditor.vue
-
-**Main container component that orchestrates the entire editor**
-
-- **Purpose**: Acts as the root component that provides editor context to all child components
-- **Key Features**:
- - Uses composition API with `useEditor` composable
- - Provides editor instance and ready state to child components via Vue's provide/inject
- - Contains global typography styles for the editor content
-- **Dependencies**:
- - `EditorToolbar.vue`
- - `EditorContentWrapper.vue`
- - `useEditor` composable
-
-```vue
-// Example usage
-
-```
-
-### 2. EditorToolbar.vue
-
-**Main toolbar component containing all editing controls**
-
-- **Purpose**: Renders the complete toolbar with all formatting options
-- **Structure**: Organized into logical groups with dividers:
- - History actions (undo/redo)
- - Format dropdown
- - Text formatting (bold, italic, underline, strikethrough)
- - Copy/paste controls
- - List formatting
- - Script formatting (subscript, superscript)
- - Insert tools (image, link, math, code)
-- **Accessibility**: Full ARIA support with role groups and labels
-- **Dependencies**: Uses `useToolbarActions` composable for all action handlers
-
-### 3. EditorContentWrapper.vue
-
-**Wrapper for the actual editor content area**
-
-- **Purpose**: Provides the editable content area with proper styling
-- **Features**:
- - Proper padding and spacing
- - Typography styles for all content types
- - RTL text direction support
-- **Styling**: Deep selectors for ProseMirror content styling
-
-## Toolbar Components
-
-### 4. ToolbarButton.vue
-
-**Reusable button component for toolbar actions**
-
-- **Props**:
- - `title`: Button tooltip and accessibility label
- - `icon`: Path to button icon
- - `rtlIcon`: Optional RTL-specific icon
- - `isActive`: Boolean indicating if button is in active state
- - `isAvailable`: Boolean controlling button availability
- - `shouldFlipInRtl`: Boolean for RTL icon flipping
-- **Features**:
- - Automatic RTL icon switching
- - Disabled state handling
- - Keyboard navigation (Enter/Space)
- - Focus management with outline styles
- - Active state styling
-
-### 5. FormatDropdown.vue
-
-**Dropdown for text format selection (Normal, Small, H1, H2, H3)**
-
-- **Features**:
- - Dynamic format detection and display
- - Live preview of formats in dropdown
- - Full keyboard navigation (arrows, Enter, Escape, Home, End)
- - Focus management
- - ARIA menu implementation
-- **Format Options**:
- - Small text (12px)
- - Normal paragraph (16px)
- - Header 3 (18px)
- - Header 2 (24px)
- - Header 1 (32px)
-
-### 6. PasteDropdown.vue
-
-**Split button for paste operations**
-
-- **Structure**:
- - Main paste button (standard paste with formatting)
- - Dropdown arrow for paste options
-- **Options**:
- - Paste (with formatting)
- - Paste without formatting (plain text)
-- **Features**:
- - Clipboard API integration
- - HTML and plain text handling
- - Keyboard navigation
- - Split button interaction pattern
-
-### 7. ToolbarDivider.vue
+Currently editor code lives in: https://github.com/learningequality/studio/tree/unstable/contentcuration/contentcuration/frontend/shared/views/TipTapEditor
-**Visual separator between toolbar groups**
+Another point that had an impact on our architectural decisions is that there are future plans to extract the editor to be part of Kolibri-Design-System to be used in Kolibri too. That meant we had to keep the editor as decoupled from the rest of the codebase as much as possible.
-- Simple component providing consistent spacing and visual separation
-- Helps organize toolbar into logical sections
-
-## Composables (Business Logic)
-
-### 8. useEditor.js
-
-**Core editor initialization and lifecycle management**
-
-- **Purpose**: Creates and manages the TipTap editor instance
-- **Extensions Used**:
- - StarterKit (basic functionality)
- - Underline extension
- - Custom Small text extension
-- **Lifecycle**: Handles editor creation on mount and cleanup on unmount
-- **Returns**: Editor instance and ready state
-
-```javascript
-const { editor, isReady } = useEditor()
-```
-
-### 9. useToolbarActions.js
-
-**All toolbar action handlers and state management**
-
-- **Individual Handlers**: Each formatting action (bold, italic, etc.)
-- **Action Groups**: Organized arrays of related actions
-- **Features**:
- - Copy with HTML and plain text formats
- - Intelligent paste handling
- - Active state detection for buttons
- - Button availability checking (undo/redo)
- - Format application logic
-- **Internationalization**: Uses translator function for button labels
-
-### 10. useDropdowns.js
-
-**Dropdown state management and interaction logic**
-
-- **State Management**:
- - Current format detection
- - Dropdown open/close states
- - Format options configuration
-- **Format Detection**: Real-time monitoring of cursor position to update selected format
-- **Event Handling**: Click outside detection for dropdown closing
-- **Editor Integration**: Listens to editor transactions for format updates
-
-## Extensions
-
-### 11. SmallTextExtension.js
-
-**Custom TipTap extension for small text formatting**
-
-- **Type**: Block node extension
-- **Features**:
- - Creates `` HTML elements
- - Block-level content with inline children
- - Custom CSS class (`small-text`)
- - Keyboard shortcut (Mod+Shift+S)
-- **Commands**:
- - `setSmall()`: Convert current block to small text
- - `toggleSmall()`: Toggle between small text and paragraph
- - `unsetSmall()`: Convert back to paragraph
-- **Priority**: High priority (1000) for proper node precedence
-
->[!NOTE]
-> Why a Node instead of a Mark?
-> - Semantic structure: ` `is semantically a block-level structure in our context, representing a complete unit of small text rather than just formatted text spans.
-> - Content integrity: As a Node, we can ensure the entire content maintains consistent styling and behavior.
-> - Block-level control: Using a Node allows us to treat small text as a distinct content block that can be manipulated as a whole unit in the editor.
-> - DOM structure: We want a proper `` element in the output HTML rather than just applying a class or style to spans of text.
-
-
-### 12. TipTapEditorStrings.js
-
-**Centralized string management for internationalization**
-
-- **Structure**:
- - Namespace-based organization
- - Message key-value pairs
- - Translator factory function
-- **Coverage**: All user-facing strings including:
- - Button labels and tooltips
- - Format options
- - Accessibility labels
- - Dropdown options
-- **Usage**: Provides `getTranslator()` function for components
-
->[!NOTE]
->Uses Lazy Loading pattern to be able to use it without the need for it to be inside a vue lie cycle hook.
-## Styling Architecture
-
-asheres to [the suggested figma design](https://www.figma.com/design/uw8lx88ZKZU8X7kN9SdLeo/Rich-text-editor---GSOC-2025?node-id=377-633&t=0XAXleYjjGY2Fxzc-0)
-### Component Styles
-
-- Scoped styles for component isolation
-- Deep selectors for ProseMirror content
-- CSS custom properties for theming
-- Focus management with outline styles
+## Useful Links
+- Original figma design [link](https://www.figma.com/design/uw8lx88ZKZU8X7kN9SdLeo/Rich-text-editor---GSOC-2025?node-id=377-422&p=f&t=HIkJ8pF9xudcOnLd-0)
+- Original Tracking issue for creating the editor [link](https://github.com/learningequality/studio/issues/5049)
+- Tiptap basic editor [docs](https://tiptap.dev/docs/editor/getting-started/overview)
+---
+## Custom extensions
+For non-text elements, we create [custom extensions optionally with their node views](https://tiptap.dev/docs/editor/extensions/custom-extensions/node-views/vue).
+We currently have custom extensions for:
+- images
+- formulas
+- syntax highlighted code blocks
+- links
+- `` text nodes
+
+### How to add a custom plugin?
+This is a very high level guide, you'll still need to check the docs but make sure you check all the boxes in this list:
+1. Create a new file in
+ `TipTapEditor/extensions/`
+2. Define your node or mark using TipTap’s `Node.create()` or `Mark.create()`.
+3. Add the new extension to the editor’s extension list in `TipTapEditor/composables/useEditor.js`.
+4. If your node needs Markdown support, update the custom serializer in `TipTapEditor/utils/MarkdownSerializer.js` and don't forget to update the tests accordingly!
+---
+## Content Conversion Flow
+The old content API saved markdown in the database, the following data conversion flow maintains backward compatibility by implementing dual conversion between the strcutured JSON format TipTap uses and markdown.
-### Accessibility Features
+We support the conversion for:
+- Standard Markdown elements previously handled by the ToastUI editor and its Showdown converter.
+- A specific, legacy format for custom nodes, particularly for Images `()` and Math Formulas `$$latex$$`
-- High contrast focus indicators
-- ARIA roles and properties
-- Keyboard navigation support
-- Screen reader friendly labels
+The formats for the custom nodes are adapted from the old editor's standard syntax conversion.
+We have our own custom markdown serializer for that too! The following graph illustrates the whole flow.
+
-## Technical Specifications
+---
+## Mobile View
+As per the figma design, the mobile view is different from the desktop design to a point where it can't just be fixed with just CSS tweaks or media queries. We did some thinking&research and decided to take a Conditional Toolbar Layout approach where We've created different components for different screen sizes.
-### Dependencies
+That means, if you add a new button in the desktop's toolbar, you'll have to add it to the Mobile's toolbar component too, and make sure you keep the functionality extracted in a reusable way so you only repeat the template logic and not the whole javascript!
-- **TipTap**: Core editor framework
-- **TipTap Extensions**: StarterKit, Underline
-- **Clipboard API**: Modern clipboard operations
+As per the Figma design, the **mobile view** differs significantly from the desktop layout — more than what simple CSS tweaks or media queries can handle.
-### Browser Support
+We decided to take a **Conditional Toolbar Layout** approach:
+- Different toolbar components are used for desktop and mobile.
+- The logic (commands, editor state, etc.) is shared and reusable.
+- Only the **template structure** differs.
-- Modern browsers with Clipboard API support
-- RTL text direction support
-- Keyboard navigation compatibility
+>[!TIP]
+>That means:
+> If you add a new button to the desktop toolbar, you’ll also need to add it to the mobile toolbar component.
+> Keep the functionality extracted and reusable, so you only duplicate the **template**, not the **JavaScript logic**.
diff --git a/integration_testing/features/manage-resources/bulk-editing/bul-edit-multiple-resources.feature b/integration_testing/features/manage-resources/bulk-editing/bulk-edit-multiple-resources.feature
similarity index 100%
rename from integration_testing/features/manage-resources/bulk-editing/bul-edit-multiple-resources.feature
rename to integration_testing/features/manage-resources/bulk-editing/bulk-edit-multiple-resources.feature
diff --git a/integration_testing/features/manage-resources/create-exercise.feature b/integration_testing/features/manage-resources/create-exercise.feature
index 739839a59d..2122861d17 100755
--- a/integration_testing/features/manage-resources/create-exercise.feature
+++ b/integration_testing/features/manage-resources/create-exercise.feature
@@ -4,78 +4,121 @@ Feature: Create an exercise
Given I am signed in to Studio
And I am at the channel editor page
- Scenario: Create an exercise with questions of type *Single choice*
+ Scenario: Create a new exercise (Completion: When goal is met - Goal: 100% correct)
When I click the *Add* button
And I select the *New exercise* option
Then I see the *Details* tab of the *New exercise* modal
- When I fill in the required fields (*Title*, *Completion* and *Goal*)
+ When I fill in all of the required fields
+ And I set the completion criteria to *When goal is met - Goal: 100% correct)* #repeat with the rest of the available goal types
And I click the *Questions* tab
- Then I see the *New question* button
+ Then I see the text: *Exercise has no questions*
+ And I see a *New question* button
When I click the *New question* button
Then I see the question editor
- And I see that *Single choice* is the default *Response type*
- When I fill in the question text in the question editor field
- And I add an image
- And I provide answers
- And I select one answer as the correct answer
- And I provide a hint
- Then I've completed the creation of the question of type *Single choice* #repeat the same steps to add as many questions as necessary
- When I click the *Finish* button
- Then I am returned to the main topic tree view
- And I can see the newly created exercise
+ When I add one or several questions of the desired type
+ And I add answers and hints as necessary
+ And I click the *Finish* button
+ Then I am back at the channel editor page
+ And I can see the newly added exercise
+ And I see a small green dot indicating that the exercise is unpublished
- Scenario: Create an exercise with questions of type *Multiple choice*
+ Scenario: Create a new exercise - practice quiz
When I click the *Add* button
And I select the *New exercise* option
Then I see the *Details* tab of the *New exercise* modal
- When I fill in the required fields (*Title*, *Completion* and *Goal*)
+ When I fill in all of the required fields
+ And I set the completion criteria to *Practice quiz*
And I click the *Questions* tab
- Then I see the *New question* button
+ Then I see the text: *Exercise has no questions*
+ And I see a *New question* button
When I click the *New question* button
Then I see the question editor
- When I select the *Multiple choice* option from the *Response type* drop-down
- And I fill in the question text in the question editor field
- And I add an image
- And I provide answers
- And I select at least one answer as the correct answer
- And I provide a hint
- Then I've completed the creation of the question of type *Multiple choice* #repeat the same steps to add as many questions as necessary
- When I click the *Finish* button
- Then I am returned to the main topic tree view
- And I can see the newly created exercise
+ When I add one or several questions of the desired type
+ And I add answers and hints as necessary
+ And I click the *Finish* button
+ Then I am back at the channel editor page
+ And I can see the newly added exercise
+ And I see a small green dot indicating that the exercise is unpublished
- Scenario: Create an exercise with questions of type *Numeric input*
+ Scenario: Create a new exercise - survey
When I click the *Add* button
And I select the *New exercise* option
Then I see the *Details* tab of the *New exercise* modal
- When I fill in the required fields (*Title*, *Completion* and *Goal*)
+ When I fill in all of the required fields
+ And I set the completion criteria to *Survey*
And I click the *Questions* tab
- Then I see the *New question* button
+ Then I see the text: *Exercise has no questions*
+ And I see a *New question* button
When I click the *New question* button
Then I see the question editor
+ When I add one or several questions of the desired type
+ And I add answers and hints as necessary
+ And I click the *Finish* button
+ Then I am back at the channel editor page
+ And I can see the newly added exercise
+ And I see a small green dot indicating that the exercise is unpublished
+
+ Scenario: Existing and newly created exercises behave consistently
+ When I open a published channel containing previously created exercises with various completion types and questions
+ Then I can see all exercises displayed in the channel editor page
+ And exercises which were previously marked as *Incomplete* are still marked as *Incomplete*
+ And exercises without an *Incomplete* indicator are still displayed without it
+ And existing and newly created exercises look and function the same
+
+ Scenario: Create an exercise with questions of type *Single choice*
+ Given I am at the *Questions* tab
+ When I click the *New question* button
+ Then I see the question editor
+ And I see that *Single choice* is the default *Response type*
+ When I fill in the question text in the question editor field using all of the available editor options such as adding and resizing an image, editing the text or inserting formulas
+ And I provide answers
+ And I select one answer as the correct answer
+ And I provide a hint
+ And I click the *Finish* button
+ Then I am back at the channel editor page
+ And I can see the newly added exercise
+ And I see a small green dot indicating that the exercise is unpublished
+
+ Scenario: Create an exercise with questions of type *Multiple choice*
+ Given I am at the question editor
+ When I select the *Multiple choice* option from the *Response type* drop-down
+ And I fill in the question text in the question editor field using all of the available editor options such as adding and resizing an image, editing the text or inserting formulas
+ And I provide answers
+ And I select one answer as the correct answer
+ And I provide a hint
+ And I click the *Finish* button
+ Then I am back at the channel editor page
+ And I can see the newly added exercise
+ And I see a small green dot indicating that the exercise is unpublished
+
+ Scenario: Create an exercise with questions of type *Numeric input*
+ Given I am at the question editor
When I select the *Numeric input* option from the *Response type* drop-down
- And I fill in the question text in the question editor field
+ And I fill in the question text in the question editor field using all of the available editor options such as adding and resizing an image, editing the text or inserting formulas
And I provide answers
+ And I select one answer as the correct answer
And I provide a hint
- Then I've completed the creation of the question of type *Numeric input* #repeat the same steps to add as many questions as necessary
- When I click the *Finish* button
- Then I am returned to the main topic tree view
- And I can see the newly created exercise
+ And I click the *Finish* button
+ Then I am back at the channel editor page
+ And I can see the newly added exercise
+ And I see a small green dot indicating that the exercise is unpublished
Scenario: Create an exercise with questions of type *True/False*
- When I click the *Add* button
- And I select the *New exercise* option
- Then I see the *Details* tab of the *New exercise* modal
- When I fill in the required fields (*Title*, *Completion* and *Goal*)
- And I click the *Questions* tab
- Then I see the *New question* button
- When I click the *New question* button
- Then I see the question editor
+ Given I am at the question editor
When I select the *True/False* option from the *Response type* drop-down
- And I fill in the question text in the question editor field
- And I select either *True* or *False* as the correct answer
+ And I fill in the question text in the question editor field using all of the available editor options such as adding and resizing an image, editing the text or inserting formulas
+ And I select one answer as the correct answer
And I provide a hint
- Then I've completed the creation of the question of type *True/False* #repeat the same steps to add as many questions as necessary
- When I click the *Finish* button
- Then I am returned to the main topic tree view
- And I can see the newly created exercise
+ And I click the *Finish* button
+ Then I am back at the channel editor page
+ And I can see the newly added exercise
+ And I see a small green dot indicating that the exercise is unpublished
+
+ Scenario: Create an exercise with questions of type *Free response*
+ Given I am at the question editor for an exercise of type Survey
+ When I select the *Free response* option from the *Response type* drop-down
+ And I fill in the question text in the question editor field using all of the available editor options such as adding and resizing an image, editing the text or inserting formulas
+ And I click the *Finish* button
+ Then I am back at the channel editor page
+ And I can see the newly added exercise
+ And I see a small green dot indicating that the exercise is unpublished
diff --git a/integration_testing/features/manage-resources/restore-resources-from-trash.feature b/integration_testing/features/manage-resources/restore-resources-from-trash.feature
index ce3b55061a..05b4dbbd33 100755
--- a/integration_testing/features/manage-resources/restore-resources-from-trash.feature
+++ b/integration_testing/features/manage-resources/restore-resources-from-trash.feature
@@ -2,7 +2,7 @@ Feature: Restore resources from trash
Background:
Given I am signed in to Studio
- And I am at the channel editing page
+ And I am at the channel editor page
And I have already removed some resources
And I have opened the trash page
diff --git a/integration_testing/features/manage-channels/sync-channel.feature b/integration_testing/features/manage-resources/sync-resources.feature
similarity index 78%
rename from integration_testing/features/manage-channels/sync-channel.feature
rename to integration_testing/features/manage-resources/sync-resources.feature
index 3023eba6fc..3874a234dc 100755
--- a/integration_testing/features/manage-channels/sync-channel.feature
+++ b/integration_testing/features/manage-resources/sync-resources.feature
@@ -1,5 +1,5 @@
Feature: Sync resources
- Studio users need to be able to sync and update the resources in their channels that have been imported from other channels, but have been modified since the original import.
+ Studio users need to be able to sync and update the resources in their channels which have been imported from other channels, but have been modified since the original import.
Background:
Given I am signed in to Studio
@@ -10,12 +10,14 @@ Feature: Sync resources
Given there is a new version of the resource file in
When I click the *···* button in the top-right corner
And I select the *Sync resources* option
- Then I see *Sync resources* modal window
- And I see options to sync files, resource details, titles and description, assessment details
+ Then I see the *Sync resources* modal window
+ And I see the *Select what you would like to sync* section with options to sync files, resource details, titles and description, assessment details
When I select the Files checkbox
And I click the *Continue* button
Then I see the *Confirm sync* modal
- When I click *Sync*
+ And I see the following info: You are about to sync and update the following: Files
+ And I see a warning that this will overwrite any changes I've made to copied or imported resources
+ When I click the *Sync* button
Then the modal closes
When after a period of time I refresh the page
And inspect the updated resource(s)
@@ -24,7 +26,7 @@ Feature: Sync resources
Scenario: Sync resource details
Given there is a new version of the resource file in
And I am at the *Sync resources* modal window
- When I select the *Tags* checkbox
+ When I select the *Resource details* checkbox
And click the *Continue* button
Then I see the *Confirm sync* modal
When I click *Sync*
@@ -60,11 +62,11 @@ Feature: Sync resources
Scenario: Edited resource metadata is reverted after syncing
Given I have edited some of the resource's metadata (title, description or tags) after importing from
And I am at the *Sync resources* modal window
- When I select the *Resource details* or *Titles and descriptions* checkbox
+ When I select the *Resource details* and *Titles and descriptions* checkboxes
And click the *Continue* button
Then I see the *Confirm sync* modal
When I click *Sync*
Then the modal closes
When after a period of time I refresh the page
And inspect the updated resource
- Then I see that my previous edits of the title, description or tags for the resource have been reverted to reflect those on the
+ Then I see that my previous edits of the title, description or tags for the resource have been reverted to reflect those in
diff --git a/integration_testing/features/manage-resources/upload-files.feature b/integration_testing/features/manage-resources/upload-files.feature
index af25b4059a..e487001dcb 100755
--- a/integration_testing/features/manage-resources/upload-files.feature
+++ b/integration_testing/features/manage-resources/upload-files.feature
@@ -15,7 +15,7 @@ Feature: Upload files
Then I see a file explorer window
When I select a supported file for upload
And I click the *Open* button
- Then I see the *Edit files* modal
+ Then I see the *Edit files* modal #if I am uploading resources to a folder with metadata then I will first see the *Apply details from the folder *
When I fill in all the required fields
And I click the *Finish* button
Then I am returned to the main topic tree view
@@ -27,7 +27,7 @@ Feature: Upload files
Then I see a file explorer window
When I select several supported files for upload
And I click the *Open* button
- Then I see the *Edit files* modal
+ Then I see the *Edit files* modal #if I am uploading resources to a folder with metadata then I will first see the *Apply details from the folder *
When I fill in all the required fields for each file
And I click the *Finish* button
Then I am returned to the main topic tree view
@@ -39,7 +39,7 @@ Feature: Upload files
Then I see a file explorer window
When I select several supported files for upload
And I click the *Open* button
- Then I see the *Edit files* modal
+ Then I see the *Edit files* modal #if I am uploading resources to a folder with metadata then I will first see the *Apply details from the folder *
When I fill in all the required fields for each file
And I click the *Finish* button
Then I am returned to the main topic tree view
@@ -48,7 +48,7 @@ Feature: Upload files
Scenario: Upload more files by drag and drop
Given I am at the *Edit files* modal after having imported some files
When I drag and drop files
- Then I see the *Edit files* modal
+ Then I see the *Edit files* modal with the newly uploaded files #if I am uploading resources to a folder with metadata then I will first see the *Apply details from the folder *
When I fill in all the required fields for each file
And I click the *Finish* button
Then I am returned to the main topic tree view
@@ -63,4 +63,4 @@ Feature: Upload files
Then I am back at the *Upload files* modal
When I close the modal
Then I am returned to the main topic tree view
- And I can see the uploaded files are not present
+ And I can see the uploaded files are not present
diff --git a/integration_testing/features/studio-critical-workflows.feature b/integration_testing/features/studio-critical-workflows.feature
index f321872b7f..dd00bf8c7d 100644
--- a/integration_testing/features/studio-critical-workflows.feature
+++ b/integration_testing/features/studio-critical-workflows.feature
@@ -4,6 +4,7 @@ Feature: Kolibri Studio critical workflows
Background:
Given Kolibri Studio is accessible at https://studio.learningequality.org/ or any of the test environments
And I am at Kolibri Studio's sign-in page
+ And I already have several testing accounts with different channels and resources
Scenario: Create an account and sign in with the created account
When I click the *Create an account* button
@@ -37,7 +38,7 @@ Feature: Kolibri Studio critical workflows
Given I am not signed in to Studio
And I am at Kolibri Studio's sign-in page
When I click the *Explore without an account* link
- Then I see the *Content Library* page with the available public channels
+ Then I see the *Content library* page with the available public channels
And I can filter the search results by keyword, language, license, format, resources for coach, available captions and subtitles
And I can view or download the channel summary
@@ -58,14 +59,16 @@ Feature: Kolibri Studio critical workflows
Then the interface language changes to the selected language
Scenario: Open and close the sidebar menu
+ Given I am signed-in to Kolibri Studio
When I click the hamburger menu button in the upper left screen corner
Then I see the sidebar menu
- And I can see the following options: *Channels*, *Settings*, *Change language*, *Help and support*, *Sign out*, the LE logo, *© 2025 Learning Equality", *Give feedback*
+ And I can see the following options: *Channels*, *Settings*, *Change language*, *Help and support*, *Sign out*, the LE logo, *© 2026 Learning Equality*, *Give feedback*
And I can click any of the options inside
When I click the *X* button, or anywhere on the browser screen
Then I don't see the sidebar menu anymore
Scenario: Open and close the user menu
+ Given I am signed-in to Kolibri Studio
When I click the user menu button in the upper right screen corner
Then I see the user menu
And I can see the following options: *Settings*, *Change language*, *Help and support*, *Sign out*
@@ -75,7 +78,7 @@ Feature: Kolibri Studio critical workflows
Scenario: Create a new channel
Given I am signed in to Studio
- And I am at *My Channels* tab
+ And I am at *My channels* tab
When I click the *New channel* button
Then I see the *New channel* page
When I upload an image file as a channel thumbnail (optional)
@@ -84,7 +87,7 @@ Feature: Kolibri Studio critical workflows
And I enter channel description (optional)
And I fill in the default copyright fields (optional)
And I click the *Create* button
- Then I am at the channel editor view
+ Then I am at the channel editor page
And I see the title of the channel to the left
And I see a disabled *Publish* button
And I see *Click "ADD" to start building your channel Create, upload, or import resources from other channels*
@@ -92,7 +95,7 @@ Feature: Kolibri Studio critical workflows
Scenario: Edit channel details
Given I am signed in to Studio
- And I am at the channel editor view
+ And I am at the channel editor page
When I click the pencil icon next to the channel name
Then I see a modal window with the channel details
And I see the details for the channel - channel name, language, channel description etc.
@@ -132,13 +135,34 @@ Feature: Kolibri Studio critical workflows
Then I am back at the channel editor page
And I can see the uploaded files
- Scenario: Create a new exercise
+ Scenario: Create a new exercise (Completion: When goal is met - Goal: 100% correct)
+ Given I am signed in to Studio
+ And I am at the channel editor page
+ When I click the *Add* button
+ And I select the *New exercise* option
+ Then I see the *Details* tab of the *New exercise* modal
+ When I fill in all of the required fields
+ And I set the completion criteria to *When goal is met - Goal: 100% correct*
+ And I click the *Questions* tab
+ Then I see the text: *Exercise has no questions*
+ And I see a *New question* button
+ When I click the *New question* button
+ Then I see the question editor
+ When I add one or several questions of the desired type
+ And I add answers and hints as necessary
+ And I click the *Finish* button
+ Then I am back at the channel editor page
+ And I can see the newly added exercise
+ And I see a small green dot indicating that the exercise is unpublished
+
+ Scenario: Create a new exercise - practice quiz
Given I am signed in to Studio
And I am at the channel editor page
When I click the *Add* button
And I select the *New exercise* option
Then I see the *Details* tab of the *New exercise* modal
When I fill in all of the required fields
+ And I set the completion criteria to *Practice quiz*
And I click the *Questions* tab
Then I see the text: *Exercise has no questions*
And I see a *New question* button
@@ -149,6 +173,35 @@ Feature: Kolibri Studio critical workflows
And I click the *Finish* button
Then I am back at the channel editor page
And I can see the newly added exercise
+ And I see a small green dot indicating that the exercise is unpublished
+
+ Scenario: Create a new exercise - survey
+ Given I am signed in to Studio
+ And I am at the channel editor page
+ When I click the *Add* button
+ And I select the *New exercise* option
+ Then I see the *Details* tab of the *New exercise* modal
+ When I fill in all of the required fields
+ And I set the completion criteria to *Survey*
+ And I click the *Questions* tab
+ Then I see the text: *Exercise has no questions*
+ And I see a *New question* button
+ When I click the *New question* button
+ Then I see the question editor
+ When I add one or several questions of the desired type
+ And I add answers and hints as necessary
+ And I click the *Finish* button
+ Then I am back at the channel editor page
+ And I can see the newly added exercise
+ And I see a small green dot indicating that the exercise is unpublished
+
+ Scenario: Existing and newly created exercises behave consistently
+ Given I am signed in to Studio
+ When I open a published channel containing previously created exercises with various completion types and questions
+ Then I can see all exercises displayed in the channel editor page
+ And exercises which were previously marked as *Incomplete* are still marked as *Incomplete*
+ And exercises without an *Incomplete* indicator are still displayed without it
+ And existing and newly created exercises look and function the same
Scenario: Import content from another channel
Given I am signed in to Studio
@@ -167,6 +220,8 @@ Feature: Kolibri Studio critical workflows
And I click the *Import* button
Then I am back at the channel editor page
And I see the *Copying* indicator for each folder or resource which is being copied
+ When after a period of time I refresh the page
+ Then I can see the imported folders and resources in the channel editor page
Scenario: Publish a channel
Given I am signed in to Studio
@@ -179,10 +234,28 @@ Feature: Kolibri Studio critical workflows
When I fill in the required fields
And I click *Publish*
Then I see the *Publishing channel* progress indicator at the top right
- When the the channel has been published successfully
+ When the channel has been published successfully
Then I see the *Published N seconds ago* text
And I receive a confirmation email that the channel has been published successfully
+ Scenario: Publish a channel with incomplete resources
+ Given I am signed in to Studio
+ And I am at the channel editor page
+ And I have write access to the channel
+ And the channel contains unpublished resources or edits plus one or several incomplete resources
+ When I click the *Publish* button at the top right corner
+ Then the *Publish modal* appears
+ And I see a warning that incomplete resources will not be published and made available for download in Kolibri.
+ When I click *Continue*
+ Then I see the *Describe what's new in this channel version* field and the *Language* drop-down
+ When I fill in the required fields
+ And I click *Publish*
+ Then I see the *Publishing channel* progress indicator at the top right
+ When the channel has been published successfully
+ Then I see the *Published N seconds ago* text
+ And I see a yellow exclamation icon with the following text *N resources are incomplete and cannot be published*
+ And I receive a confirmation email that the channel has been published successfully
+
Scenario: Invite collaborators with *Can edit* permissions
Given I am signed in to Studio
And I am at the channel editor page
@@ -192,14 +265,39 @@ Feature: Kolibri Studio critical workflows
When I type the email of the person I want to invite
And I don't change the preselected *Can edit* option in the drop-down
And I click the *Send invitation* button
- Then the collaborator will be notified on their *My Channels* page, where they can accept or reject the pending invitation
- And the collaborator will receive an email allowing them to accept/reject the pending invitation
+ Then the collaborator will be notified on their *My channels* page, where they can accept or decline the pending invitation
+ And the collaborator will receive an email allowing them to accept/decline the pending invitation
+ When I sign in to Studio as the collaborator
+ Then I can accept the pending invitation
+ When I make some changes to the channel or resources
+ And I click the *Publish* button
+ Then I can publish the channel
+ And any changes made to the channel are visible by the other collaborators
+
+ Scenario: Invite collaborators with *Can view* permissions
+ Given I am signed in to Studio
+ And I am at the channel editor page
+ When I click the *...* (options) button in the topbar
+ And select the *Share channel* option
+ Then I am at the *Sharing* tab for the channel
+ When I type the email of the person I want to invite
+ And I select the *Can view* option from the drop-down
+ And I click the *Send invitation* button
+ Then the collaborator will be notified on their *View-only* page, where they can accept or decline the pending invitation
+ And the collaborator will receive an email allowing them to accept/decline the pending invitation
+ When I sign in to Studio as the collaborator
+ And I accept the pending invitation
+ And I open the channel editor page
+ Then I can see the channel marked as *View-only*
+ And I can see the available folders and resources
+ And I can't add or edit folders and resources
+ And I can't publish the channel
Scenario: Sync resources
Given I am signed in to Studio
And I am at the channel editor page for
And there is a resource in the that has been imported from
- And there is new version of the resource file in the
+ And there is a new version of the resource in
When I click the *···* button in the top-right corner
And I select the *Sync resources* option
Then I see *Sync resources* modal window
@@ -250,6 +348,17 @@ Feature: Kolibri Studio critical workflows
And I see the *Undo* button
And I no longer see the resource
+ Scenario: Removed folder and resources can be restored
+ Given I am signed in to Studio
+ And I am at the channel editor page
+ And there are available folders and resources of different types
+ When I check the checkbox of a folder or a resource
+ And I click the *Remove* button
+ Then I can see the *Sent to trash* snackbar message
+ And I see the *Undo* button
+ When I click the *Undo* button
+ Then I can still see the folder or resource in the channel editor page
+
Scenario: Copy multiple resources
Given I am signed in to Studio
And I am at the channel editor page
@@ -289,13 +398,13 @@ Feature: Kolibri Studio critical workflows
Scenario: Delete a channel
Given I am signed in to Studio
And I have permissions to edit
- And I am on *My Channels* tab
+ And I am on *My channels* tab
When I click the *Options* button of a channel #the three dots to the right
Then I see a *Delete channel* option
When I click the *Delete channel* option
And I click the *Delete channel* button
Then I see a message that the channel is deleted
- And the deleted channel is no longer displayed at *My Channels* page
+ And the deleted channel is no longer displayed at *My channels* page
Feature: View and edit account information
Given I am signed in to Studio
@@ -317,6 +426,7 @@ Feature: Kolibri Studio critical workflows
And I can see the *Request more space* section
Scenario: Submit more space request
+ Given I am signed-in to Kolibri Studio
When I fill in all the space request text fields
And I click the *Send request* submit button
Then I see all the text fields clear
@@ -338,7 +448,7 @@ Feature: Kolibri Studio critical workflows
And I see the newly created collection
And I see the number of channels in that collection
- Scenario: Explore the content library
+ Scenario: Explore the Content library
Given I am signed in to Studio
When I go to the *Content library*
Then I see a page with the available public channels
@@ -346,3 +456,11 @@ Feature: Kolibri Studio critical workflows
And I can view or download the channel summary
When I click on a channel card
Then I can see all of the channel's resources in *View-only* format
+
+ Scenario: Sign out and confirm that pages visible only to a signed-in user are no longer accessible
+ Given I am signed in to Studio
+ When I click the user profile icon
+ And I click *Sign out*
+ Then I am at Kolibri Studio's sign-in page
+ When I click the browser's *Back* button
+ Then I can't access any of the pages I've visited as a signed-in user
diff --git a/jest_config/jest.conf.js b/jest_config/jest.conf.js
index 20987fa663..b1292e4b99 100644
--- a/jest_config/jest.conf.js
+++ b/jest_config/jest.conf.js
@@ -19,6 +19,7 @@ module.exports = {
'shared/urls': path.resolve(__dirname, './globalMocks/urls.js'),
'^dexie$': require.resolve('dexie'),
'^mathlive$': 'identity-obj-proxy',
+ '^@tiptap/extension-code-block-lowlight$': '/node_modules/@tiptap/extension-code-block-lowlight/dist/index.js',
},
testEnvironment: 'jsdom',
testMatch: ['**/?(*.)+(spec|test).[jt]s?(x)'],
@@ -30,7 +31,7 @@ module.exports = {
'.*\\.(vue)$': '/node_modules/vue-jest',
},
transformIgnorePatterns: [
- '/node_modules/.pnpm/(?!(vuetify|epubjs|kolibri-design-system|kolibri-constants|axios|lowlight|@tiptap|tiptap|prosemirror-.*|unified|unist-.*|hast-.*|bail|trough|vfile.*|remark-.*|rehype-.*|mdast-.*|devlop))',
+ '/node_modules/.pnpm/(?!(vuetify|epubjs|kolibri-design-system|kolibri-constants|axios|lowlight|@tiptap|tiptap|prosemirror-.*|unified|unist-.*|hast-.*|bail|trough|vfile.*|remark-.*|rehype-.*|mdast-.*|devlop|marked))',
],
snapshotSerializers: ['/node_modules/jest-serializer-vue'],
setupFilesAfterEnv: ['/jest_config/setup.js'],
diff --git a/jest_config/setup.js b/jest_config/setup.js
index 15307f4570..82ed9b7733 100644
--- a/jest_config/setup.js
+++ b/jest_config/setup.js
@@ -14,9 +14,7 @@ import "core-js/stable/structured-clone";
import 'fake-indexeddb/auto';
// Polyfill webstreams
import {ReadableStream, WritableStream, TransformStream, CountQueuingStrategy} from 'web-streams-polyfill';
-import jquery from 'jquery';
-window.jQuery = window.$ = jquery;
window.ReadableStream = global.ReadableStream = ReadableStream;
window.WritableStream = global.WritableStream = WritableStream;
window.TransformStream = global.TransformStream = TransformStream;
@@ -98,3 +96,8 @@ Object.defineProperty(window, 'scrollTo', { value: () => {}, writable: true });
resetJestGlobal();
setupSchema();
+
+// Use of setImmediate by fake-indexeddb makes tests fail with inactive or premature transaction
+// commit errors. This has something to do with microtasks, but since our code works correctly
+// in the browser, this seems specific to node.js and how fake-indexeddb works.
+global.setImmediate = global.setImmediate || ((fn, ...args) => global.setTimeout(fn, 0, ...args));
diff --git a/k8s/images/app/Dockerfile b/k8s/images/app/Dockerfile
deleted file mode 120000
index 4750df1fc3..0000000000
--- a/k8s/images/app/Dockerfile
+++ /dev/null
@@ -1 +0,0 @@
-docker/Dockerfile.prod
\ No newline at end of file
diff --git a/k8s/images/nginx/Dockerfile b/k8s/images/nginx/Dockerfile
deleted file mode 120000
index a4867c19b0..0000000000
--- a/k8s/images/nginx/Dockerfile
+++ /dev/null
@@ -1 +0,0 @@
-docker/Dockerfile.nginx.prod
\ No newline at end of file
diff --git a/package.json b/package.json
index 5a08477887..1ceeff265f 100644
--- a/package.json
+++ b/package.json
@@ -53,21 +53,19 @@
},
"homepage": "https://github.com/learningequality/studio#readme",
"dependencies": {
- "@sentry/vue": "^9.40.0",
- "@tiptap/core": "^2.14.0",
- "@tiptap/extension-code-block-lowlight": "^2.23.0",
- "@tiptap/extension-link": "^2.23.1",
- "@tiptap/extension-subscript": "^2.14.0",
- "@tiptap/extension-superscript": "^2.26.1",
- "@tiptap/extension-underline": "^2.14.0",
- "@tiptap/starter-kit": "^2.13.0",
- "@tiptap/vue-2": "^2.13.0",
- "@toast-ui/editor": "^2.3.1",
+ "@sentry/vue": "^10.6.0",
+ "@tiptap/core": "^3.13.0",
+ "@tiptap/extension-code-block-lowlight": "^3.13.0",
+ "@tiptap/extension-link": "^3.13.0",
+ "@tiptap/extension-subscript": "^3.13.0",
+ "@tiptap/extension-superscript": "^3.13.0",
+ "@tiptap/extension-text-align": "^3.18.0",
+ "@tiptap/starter-kit": "^3.13.0",
+ "@tiptap/vue-2": "^3.13.0",
"ajv": "^8.12.0",
- "axios": "^1.11.0",
- "broadcast-channel": "^5.1.0",
- "codemirror": "5.58.2",
- "core-js": "^3.44.0",
+ "axios": "^1.13.5",
+ "broadcast-channel": "^7.1.0",
+ "core-js": "^3.47.0",
"crc-32": "^1.2.2",
"dexie": "^3.2.6",
"dexie-observable": "3.0.0-beta.11",
@@ -77,23 +75,22 @@
"html2canvas": "^1.0.0-rc.5",
"i18n-iso-countries": "^7.11.3",
"intl": "1.2.5",
- "jquery": "^2.2.4",
"jspdf": "https://github.com/parallax/jsPDF.git#b7a1d8239c596292ce86dafa77f05987bcfa2e6e",
"jszip": "^3.10.1",
"kolibri-constants": "^0.2.12",
- "kolibri-design-system": "5.3.0",
- "lodash": "^4.17.21",
+ "kolibri-design-system": "https://github.com/MisRob/kolibri-design-system#ec3efbe080efdda3fabf8d05fe85a1d8d9fdffc9",
+ "lodash": "^4.17.23",
"lowlight": "^3.3.0",
+ "marked": "^16.1.1",
"material-icons": "0.3.1",
- "mathlive": "^0.105.3",
+ "mathlive": "^0.108.2",
"mutex-js": "^1.1.5",
"node-vibrant": "^4.0.3",
"pako": "^2.1.0",
"papaparse": "^5.4.1",
"pdfjs-dist": "^2.16.105",
- "qs": "^6.11.2",
+ "qs": "^6.14.2",
"regenerator-runtime": "^0.14.1",
- "showdown": "^2.1.0",
"spark-md5": "^3.0.0",
"store2": "^2.14.4",
"string-strip-html": "8.3.0",
@@ -105,17 +102,17 @@
"vue-router": "3.6.5",
"vuetify": "^1.5.24",
"vuex": "^3.0.1",
- "workbox-core": "^7.3.0",
+ "workbox-core": "^7.4.0",
"workbox-precaching": "^7.3.0",
"workbox-routing": "^7.3.0",
"workbox-strategies": "^7.3.0",
"workbox-window": "^7.3.0"
},
"devDependencies": {
- "@babel/core": "^7.28.0",
- "@babel/plugin-syntax-import-assertions": "^7.27.1",
- "@babel/plugin-transform-runtime": "^7.28.0",
- "@babel/preset-env": "^7.28.0",
+ "@babel/core": "^7.29.0",
+ "@babel/plugin-syntax-import-assertions": "^7.28.6",
+ "@babel/plugin-transform-runtime": "^7.29.0",
+ "@babel/preset-env": "^7.29.0",
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/user-event": "^14.6.1",
"@testing-library/vue": "^5",
@@ -124,18 +121,18 @@
"autoprefixer": "^10.4.19",
"babel-core": "7.0.0-bridge.0",
"babel-jest": "^29.7.0",
- "babel-loader": "^9.2.1",
+ "babel-loader": "^10.0.0",
"circular-dependency-plugin": "^5.2.0",
"css-loader": "7.1.2",
"eslint": "^8.57.0",
"eslint-config-prettier": "^10.1.8",
"eslint-import-resolver-webpack": "0.13.10",
"eslint-plugin-import": "^2.31.0",
- "eslint-plugin-jest": "^28.11.0",
+ "eslint-plugin-jest": "^29.1.0",
"eslint-plugin-jest-dom": "^5.5.0",
"eslint-plugin-kolibri": "^0.18.0",
"eslint-plugin-vue": "^9.32.0",
- "fake-indexeddb": "^5.0.2",
+ "fake-indexeddb": "^6.2.5",
"file-loader": "^6.2.0",
"flush-promises": "^1.0.2",
"identity-obj-proxy": "^3.0.0",
@@ -146,7 +143,7 @@
"jest-serializer-vue": "^3.1.0",
"kolibri-format": "1.0.1",
"kolibri-tools": "0.18.2",
- "mini-css-extract-plugin": "^2.8.1",
+ "mini-css-extract-plugin": "^2.9.4",
"node-sass": "9.0.0",
"npm-run-all": "^4.1.3",
"postcss-html": "^1.8.0",
@@ -165,18 +162,18 @@
"stylelint-config-standard": "34.0.0",
"stylelint-csstree-validator": "3.0.0",
"stylelint-scss": "5.3.2",
- "stylus": "^0.63.0",
- "stylus-loader": "^8.1.0",
+ "stylus": "^0.64.0",
+ "stylus-loader": "^8.1.2",
"vue-jest": "^3.0.7",
"vue-loader": "15.11.1",
"vue-style-loader": "^4.1.3",
"vue-template-compiler": "2.7.16",
"web-streams-polyfill": "^4.0.0",
- "webpack": "^5.97.1",
+ "webpack": "^5.104.1",
"webpack-cli": "^6.0.1",
"webpack-dev-server": "^5.2.2",
"webpack-merge": "^6.0.1",
- "workbox-webpack-plugin": "^7.3.0"
+ "workbox-webpack-plugin": "^7.4.0"
},
"false": {},
"engines": {
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 7131ed1f80..40d24fbd77 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -9,50 +9,44 @@ importers:
.:
dependencies:
'@sentry/vue':
- specifier: ^9.40.0
- version: 9.40.0(vue@2.7.16)
+ specifier: ^10.6.0
+ version: 10.6.0(vue@2.7.16)
'@tiptap/core':
- specifier: ^2.14.0
- version: 2.23.1(@tiptap/pm@2.23.1)
+ specifier: ^3.13.0
+ version: 3.13.0(@tiptap/pm@3.13.0)
'@tiptap/extension-code-block-lowlight':
- specifier: ^2.23.0
- version: 2.23.1(@tiptap/core@2.23.1(@tiptap/pm@2.23.1))(@tiptap/extension-code-block@2.23.1(@tiptap/core@2.23.1(@tiptap/pm@2.23.1))(@tiptap/pm@2.23.1))(@tiptap/pm@2.23.1)(highlight.js@11.11.1)(lowlight@3.3.0)
+ specifier: ^3.13.0
+ version: 3.13.0(@tiptap/core@3.13.0(@tiptap/pm@3.13.0))(@tiptap/extension-code-block@3.13.0(@tiptap/core@3.13.0(@tiptap/pm@3.13.0))(@tiptap/pm@3.13.0))(@tiptap/pm@3.13.0)(highlight.js@11.11.1)(lowlight@3.3.0)
'@tiptap/extension-link':
- specifier: ^2.23.1
- version: 2.23.1(@tiptap/core@2.23.1(@tiptap/pm@2.23.1))(@tiptap/pm@2.23.1)
+ specifier: ^3.13.0
+ version: 3.13.0(@tiptap/core@3.13.0(@tiptap/pm@3.13.0))(@tiptap/pm@3.13.0)
'@tiptap/extension-subscript':
- specifier: ^2.14.0
- version: 2.23.1(@tiptap/core@2.23.1(@tiptap/pm@2.23.1))
+ specifier: ^3.13.0
+ version: 3.13.0(@tiptap/core@3.13.0(@tiptap/pm@3.13.0))(@tiptap/pm@3.13.0)
'@tiptap/extension-superscript':
- specifier: ^2.26.1
- version: 2.26.1(@tiptap/core@2.23.1(@tiptap/pm@2.23.1))
- '@tiptap/extension-underline':
- specifier: ^2.14.0
- version: 2.23.1(@tiptap/core@2.23.1(@tiptap/pm@2.23.1))
+ specifier: ^3.13.0
+ version: 3.13.0(@tiptap/core@3.13.0(@tiptap/pm@3.13.0))(@tiptap/pm@3.13.0)
+ '@tiptap/extension-text-align':
+ specifier: ^3.18.0
+ version: 3.18.0(@tiptap/core@3.13.0(@tiptap/pm@3.13.0))
'@tiptap/starter-kit':
- specifier: ^2.13.0
- version: 2.23.1
+ specifier: ^3.13.0
+ version: 3.13.0
'@tiptap/vue-2':
- specifier: ^2.13.0
- version: 2.23.1(@tiptap/core@2.23.1(@tiptap/pm@2.23.1))(@tiptap/pm@2.23.1)(vue@2.7.16)
- '@toast-ui/editor':
- specifier: ^2.3.1
- version: 2.5.4
+ specifier: ^3.13.0
+ version: 3.13.0(@floating-ui/dom@1.7.4)(@tiptap/core@3.13.0(@tiptap/pm@3.13.0))(@tiptap/pm@3.13.0)(vue@2.7.16)
ajv:
specifier: ^8.12.0
version: 8.17.1
axios:
- specifier: ^1.11.0
- version: 1.11.0
+ specifier: ^1.13.5
+ version: 1.13.5
broadcast-channel:
- specifier: ^5.1.0
- version: 5.5.1
- codemirror:
- specifier: 5.58.2
- version: 5.58.2
+ specifier: ^7.1.0
+ version: 7.1.0
core-js:
- specifier: ^3.44.0
- version: 3.44.0
+ specifier: ^3.47.0
+ version: 3.47.0
crc-32:
specifier: ^1.2.2
version: 1.2.2
@@ -80,9 +74,6 @@ importers:
intl:
specifier: 1.2.5
version: 1.2.5
- jquery:
- specifier: ^2.2.4
- version: 2.2.4
jspdf:
specifier: https://github.com/parallax/jsPDF.git#b7a1d8239c596292ce86dafa77f05987bcfa2e6e
version: jspdf-yworks@https://codeload.github.com/parallax/jsPDF/tar.gz/b7a1d8239c596292ce86dafa77f05987bcfa2e6e
@@ -93,20 +84,23 @@ importers:
specifier: ^0.2.12
version: 0.2.12
kolibri-design-system:
- specifier: 5.3.0
- version: 5.3.0
+ specifier: https://github.com/MisRob/kolibri-design-system#ec3efbe080efdda3fabf8d05fe85a1d8d9fdffc9
+ version: https://codeload.github.com/MisRob/kolibri-design-system/tar.gz/ec3efbe080efdda3fabf8d05fe85a1d8d9fdffc9
lodash:
- specifier: ^4.17.21
- version: 4.17.21
+ specifier: ^4.17.23
+ version: 4.17.23
lowlight:
specifier: ^3.3.0
version: 3.3.0
+ marked:
+ specifier: ^16.1.1
+ version: 16.1.1
material-icons:
specifier: 0.3.1
version: 0.3.1
mathlive:
- specifier: ^0.105.3
- version: 0.105.3
+ specifier: ^0.108.2
+ version: 0.108.2
mutex-js:
specifier: ^1.1.5
version: 1.1.5
@@ -123,14 +117,11 @@ importers:
specifier: ^2.16.105
version: 2.16.105
qs:
- specifier: ^6.11.2
- version: 6.14.0
+ specifier: ^6.14.2
+ version: 6.14.2
regenerator-runtime:
specifier: ^0.14.1
version: 0.14.1
- showdown:
- specifier: ^2.1.0
- version: 2.1.0
spark-md5:
specifier: ^3.0.0
version: 3.0.2
@@ -165,8 +156,8 @@ importers:
specifier: ^3.0.1
version: 3.6.2(vue@2.7.16)
workbox-core:
- specifier: ^7.3.0
- version: 7.3.0
+ specifier: ^7.4.0
+ version: 7.4.0
workbox-precaching:
specifier: ^7.3.0
version: 7.3.0
@@ -181,17 +172,17 @@ importers:
version: 7.3.0
devDependencies:
'@babel/core':
- specifier: ^7.28.0
- version: 7.28.0
+ specifier: ^7.29.0
+ version: 7.29.0
'@babel/plugin-syntax-import-assertions':
- specifier: ^7.27.1
- version: 7.27.1(@babel/core@7.28.0)
+ specifier: ^7.28.6
+ version: 7.28.6(@babel/core@7.29.0)
'@babel/plugin-transform-runtime':
- specifier: ^7.28.0
- version: 7.28.0(@babel/core@7.28.0)
+ specifier: ^7.29.0
+ version: 7.29.0(@babel/core@7.29.0)
'@babel/preset-env':
- specifier: ^7.28.0
- version: 7.28.0(@babel/core@7.28.0)
+ specifier: ^7.29.0
+ version: 7.29.0(@babel/core@7.29.0)
'@testing-library/jest-dom':
specifier: ^6.6.3
version: 6.6.3
@@ -212,19 +203,19 @@ importers:
version: 10.4.21(postcss@8.5.6)
babel-core:
specifier: 7.0.0-bridge.0
- version: 7.0.0-bridge.0(@babel/core@7.28.0)
+ version: 7.0.0-bridge.0(@babel/core@7.29.0)
babel-jest:
specifier: ^29.7.0
- version: 29.7.0(@babel/core@7.28.0)
+ version: 29.7.0(@babel/core@7.29.0)
babel-loader:
- specifier: ^9.2.1
- version: 9.2.1(@babel/core@7.28.0)(webpack@5.99.9)
+ specifier: ^10.0.0
+ version: 10.0.0(@babel/core@7.29.0)(webpack@5.104.1)
circular-dependency-plugin:
specifier: ^5.2.0
- version: 5.2.2(webpack@5.99.9)
+ version: 5.2.2(webpack@5.104.1)
css-loader:
specifier: 7.1.2
- version: 7.1.2(webpack@5.99.9)
+ version: 7.1.2(webpack@5.104.1)
eslint:
specifier: ^8.57.0
version: 8.57.1
@@ -233,13 +224,13 @@ importers:
version: 10.1.8(eslint@8.57.1)
eslint-import-resolver-webpack:
specifier: 0.13.10
- version: 0.13.10(eslint-plugin-import@2.32.0)(webpack@5.99.9)
+ version: 0.13.10(eslint-plugin-import@2.32.0)(webpack@5.104.1)
eslint-plugin-import:
specifier: ^2.31.0
version: 2.32.0(eslint-import-resolver-webpack@0.13.10)(eslint@8.57.1)
eslint-plugin-jest:
- specifier: ^28.11.0
- version: 28.14.0(eslint@8.57.1)(jest@29.7.0(@types/node@24.1.0))(typescript@5.8.3)
+ specifier: ^29.1.0
+ version: 29.1.0(eslint@8.57.1)(jest@29.7.0(@types/node@24.1.0))(typescript@5.8.3)
eslint-plugin-jest-dom:
specifier: ^5.5.0
version: 5.5.0(@testing-library/dom@9.3.4)(eslint@8.57.1)
@@ -250,11 +241,11 @@ importers:
specifier: ^9.32.0
version: 9.33.0(eslint@8.57.1)
fake-indexeddb:
- specifier: ^5.0.2
- version: 5.0.2
+ specifier: ^6.2.5
+ version: 6.2.5
file-loader:
specifier: ^6.2.0
- version: 6.2.0(webpack@5.99.9)
+ version: 6.2.0(webpack@5.104.1)
flush-promises:
specifier: ^1.0.2
version: 1.0.2
@@ -281,10 +272,10 @@ importers:
version: 1.0.1(@testing-library/dom@9.3.4)(eslint-import-resolver-webpack@0.13.10)(jest@29.7.0(@types/node@24.1.0))(postcss@8.5.6)(typescript@5.8.3)
kolibri-tools:
specifier: 0.18.2
- version: 0.18.2(@testing-library/dom@9.3.4)(@types/node@24.1.0)(ejs@3.1.10)(eslint-import-resolver-webpack@0.13.10)(file-loader@6.2.0(webpack@5.99.9))(postcss@8.5.6)(typescript@5.8.3)(vue@2.7.16)
+ version: 0.18.2(@testing-library/dom@9.3.4)(@types/node@24.1.0)(ejs@3.1.10)(eslint-import-resolver-webpack@0.13.10)(file-loader@6.2.0(webpack@5.104.1))(postcss@8.5.6)(typescript@5.8.3)(vue@2.7.16)
mini-css-extract-plugin:
- specifier: ^2.8.1
- version: 2.9.2(webpack@5.99.9)
+ specifier: ^2.9.4
+ version: 2.9.4(webpack@5.104.1)
node-sass:
specifier: 9.0.0
version: 9.0.0
@@ -299,7 +290,7 @@ importers:
version: 6.0.0(postcss@8.5.6)
postcss-loader:
specifier: ^8.1.1
- version: 8.1.1(postcss@8.5.6)(typescript@5.8.3)(webpack@5.99.9)
+ version: 8.1.1(postcss@8.5.6)(typescript@5.8.3)(webpack@5.104.1)
postcss-sass:
specifier: ^0.5.0
version: 0.5.0
@@ -308,10 +299,10 @@ importers:
version: 4.0.9(postcss@8.5.6)
sass-loader:
specifier: 16.0.5
- version: 16.0.5(node-sass@9.0.0)(webpack@5.99.9)
+ version: 16.0.5(node-sass@9.0.0)(webpack@5.104.1)
style-loader:
specifier: ^4.0.0
- version: 4.0.0(webpack@5.99.9)
+ version: 4.0.0(webpack@5.104.1)
stylelint:
specifier: ^15.11.0
version: 15.11.0(typescript@5.8.3)
@@ -340,17 +331,17 @@ importers:
specifier: 5.3.2
version: 5.3.2(stylelint@15.11.0(typescript@5.8.3))
stylus:
- specifier: ^0.63.0
- version: 0.63.0
+ specifier: ^0.64.0
+ version: 0.64.0
stylus-loader:
- specifier: ^8.1.0
- version: 8.1.1(stylus@0.63.0)(webpack@5.99.9)
+ specifier: ^8.1.2
+ version: 8.1.2(stylus@0.64.0)(webpack@5.104.1)
vue-jest:
specifier: ^3.0.7
- version: 3.0.7(babel-core@7.0.0-bridge.0(@babel/core@7.28.0))(vue-template-compiler@2.7.16)(vue@2.7.16)
+ version: 3.0.7(babel-core@7.0.0-bridge.0(@babel/core@7.29.0))(vue-template-compiler@2.7.16)(vue@2.7.16)
vue-loader:
specifier: 15.11.1
- version: 15.11.1(babel-core@7.0.0-bridge.0(@babel/core@7.28.0))(css-loader@7.1.2(webpack@5.99.9))(ejs@3.1.10)(lodash@4.17.21)(vue-template-compiler@2.7.16)(webpack@5.99.9)
+ version: 15.11.1(babel-core@7.0.0-bridge.0(@babel/core@7.29.0))(css-loader@7.1.2(webpack@5.104.1))(ejs@3.1.10)(lodash@4.17.23)(vue-template-compiler@2.7.16)(webpack@5.104.1)
vue-style-loader:
specifier: ^4.1.3
version: 4.1.3
@@ -361,20 +352,20 @@ importers:
specifier: ^4.0.0
version: 4.1.0
webpack:
- specifier: ^5.97.1
- version: 5.99.9(webpack-cli@6.0.1)
+ specifier: ^5.104.1
+ version: 5.104.1(webpack-cli@6.0.1)
webpack-cli:
specifier: ^6.0.1
- version: 6.0.1(webpack-dev-server@5.2.2)(webpack@5.99.9)
+ version: 6.0.1(webpack-dev-server@5.2.2)(webpack@5.104.1)
webpack-dev-server:
specifier: ^5.2.2
- version: 5.2.2(webpack-cli@6.0.1)(webpack@5.99.9)
+ version: 5.2.2(webpack-cli@6.0.1)(webpack@5.104.1)
webpack-merge:
specifier: ^6.0.1
version: 6.0.1
workbox-webpack-plugin:
- specifier: ^7.3.0
- version: 7.3.0(@types/babel__core@7.20.5)(webpack@5.99.9)
+ specifier: ^7.4.0
+ version: 7.4.0(@types/babel__core@7.20.5)(webpack@5.104.1)
packages:
@@ -384,10 +375,6 @@ packages:
'@adobe/css-tools@4.4.3':
resolution: {integrity: sha512-VQKMkwriZbaOgVCby1UDY/LDk5fIjhQicCvVPFqfe+69fWaPWydbWJ3wRt59/YzIwda1I81loas3oCoHxnqvdA==}
- '@ampproject/remapping@2.3.0':
- resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==}
- engines: {node: '>=6.0.0'}
-
'@apideck/better-ajv-errors@0.3.6':
resolution: {integrity: sha512-P+ZygBLZtkp0qqOAJJVX4oX/sFo5JR3eBWwwuqHHhK0GIgQOKWrAfiAaWX0aArHkRWHMuggFEgAZNxVPwPZYaA==}
engines: {node: '>=10'}
@@ -397,44 +384,44 @@ packages:
'@babel/code-frame@7.12.11':
resolution: {integrity: sha512-Zt1yodBx1UcyiePMSkWnU4hPqhwq7hGi2nFL1LeA3EUl+q2LQx16MISgJ0+z7dnmgvP9QtIleuETGOiOH1RcIw==}
- '@babel/code-frame@7.27.1':
- resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==}
+ '@babel/code-frame@7.29.0':
+ resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==}
engines: {node: '>=6.9.0'}
- '@babel/compat-data@7.28.0':
- resolution: {integrity: sha512-60X7qkglvrap8mn1lh2ebxXdZYtUcpd7gsmy9kLaBJ4i/WdY8PqTSdxyA8qraikqKQK5C1KRBKXqznrVapyNaw==}
+ '@babel/compat-data@7.29.0':
+ resolution: {integrity: sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==}
engines: {node: '>=6.9.0'}
- '@babel/core@7.28.0':
- resolution: {integrity: sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ==}
+ '@babel/core@7.29.0':
+ resolution: {integrity: sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==}
engines: {node: '>=6.9.0'}
- '@babel/generator@7.28.0':
- resolution: {integrity: sha512-lJjzvrbEeWrhB4P3QBsH7tey117PjLZnDbLiQEKjQ/fNJTjuq4HSqgFA+UNSwZT8D7dxxbnuSBMsa1lrWzKlQg==}
+ '@babel/generator@7.29.1':
+ resolution: {integrity: sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==}
engines: {node: '>=6.9.0'}
'@babel/helper-annotate-as-pure@7.27.3':
resolution: {integrity: sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==}
engines: {node: '>=6.9.0'}
- '@babel/helper-compilation-targets@7.27.2':
- resolution: {integrity: sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==}
+ '@babel/helper-compilation-targets@7.28.6':
+ resolution: {integrity: sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==}
engines: {node: '>=6.9.0'}
- '@babel/helper-create-class-features-plugin@7.27.1':
- resolution: {integrity: sha512-QwGAmuvM17btKU5VqXfb+Giw4JcN0hjuufz3DYnpeVDvZLAObloM77bhMXiqry3Iio+Ai4phVRDwl6WU10+r5A==}
+ '@babel/helper-create-class-features-plugin@7.28.6':
+ resolution: {integrity: sha512-dTOdvsjnG3xNT9Y0AUg1wAl38y+4Rl4sf9caSQZOXdNqVn+H+HbbJ4IyyHaIqNR6SW9oJpA/RuRjsjCw2IdIow==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0
- '@babel/helper-create-regexp-features-plugin@7.27.1':
- resolution: {integrity: sha512-uVDC72XVf8UbrH5qQTc18Agb8emwjTiZrQE11Nv3CuBEZmVvTwwE9CBUEvHku06gQCAyYf8Nv6ja1IN+6LMbxQ==}
+ '@babel/helper-create-regexp-features-plugin@7.28.5':
+ resolution: {integrity: sha512-N1EhvLtHzOvj7QQOUCCS3NrPJP8c5W6ZXCHDn7Yialuy1iu4r5EmIYkXlKNqT99Ciw+W0mDqWoR6HWMZlFP3hw==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0
- '@babel/helper-define-polyfill-provider@0.6.5':
- resolution: {integrity: sha512-uJnGFcPsWQK8fvjgGP5LZUZZsYGIoPeRjSF5PGwrelYgq7Q15/Ft9NGFp1zglwgIv//W0uG4BevRuSJRyylZPg==}
+ '@babel/helper-define-polyfill-provider@0.6.6':
+ resolution: {integrity: sha512-mOAsxeeKkUKayvZR3HeTYD/fICpCPLJrU5ZjelT/PA6WHtNDBOE436YiaEUvHN454bRM3CebhDsIpieCc4texA==}
peerDependencies:
'@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0
@@ -442,16 +429,16 @@ packages:
resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==}
engines: {node: '>=6.9.0'}
- '@babel/helper-member-expression-to-functions@7.27.1':
- resolution: {integrity: sha512-E5chM8eWjTp/aNoVpcbfM7mLxu9XGLWYise2eBKGQomAk/Mb4XoxyqXTZbuTohbsl8EKqdlMhnDI2CCLfcs9wA==}
+ '@babel/helper-member-expression-to-functions@7.28.5':
+ resolution: {integrity: sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg==}
engines: {node: '>=6.9.0'}
- '@babel/helper-module-imports@7.27.1':
- resolution: {integrity: sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==}
+ '@babel/helper-module-imports@7.28.6':
+ resolution: {integrity: sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==}
engines: {node: '>=6.9.0'}
- '@babel/helper-module-transforms@7.27.3':
- resolution: {integrity: sha512-dSOvYwvyLsWBeIRyOeHXp5vPj5l1I011r52FM1+r1jCERv+aFXYk4whgQccYEGYxK2H3ZAIA8nuPkQ0HaUo3qg==}
+ '@babel/helper-module-transforms@7.28.6':
+ resolution: {integrity: sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0
@@ -460,8 +447,8 @@ packages:
resolution: {integrity: sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw==}
engines: {node: '>=6.9.0'}
- '@babel/helper-plugin-utils@7.27.1':
- resolution: {integrity: sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==}
+ '@babel/helper-plugin-utils@7.28.6':
+ resolution: {integrity: sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==}
engines: {node: '>=6.9.0'}
'@babel/helper-remap-async-to-generator@7.27.1':
@@ -470,8 +457,8 @@ packages:
peerDependencies:
'@babel/core': ^7.0.0
- '@babel/helper-replace-supers@7.27.1':
- resolution: {integrity: sha512-7EHz6qDZc8RYS5ElPoShMheWvEgERonFCs7IAonWLLUTXW59DP14bCZt89/GKyreYn8g3S83m21FelHKbeDCKA==}
+ '@babel/helper-replace-supers@7.28.6':
+ resolution: {integrity: sha512-mq8e+laIk94/yFec3DxSjCRD2Z0TAjhVbEJY3UQrlwVo15Lmt7C2wAUbK4bjnTs4APkwsYLTahXRraQXhb1WCg==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0
@@ -484,33 +471,33 @@ packages:
resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==}
engines: {node: '>=6.9.0'}
- '@babel/helper-validator-identifier@7.27.1':
- resolution: {integrity: sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==}
+ '@babel/helper-validator-identifier@7.28.5':
+ resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==}
engines: {node: '>=6.9.0'}
'@babel/helper-validator-option@7.27.1':
resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==}
engines: {node: '>=6.9.0'}
- '@babel/helper-wrap-function@7.27.1':
- resolution: {integrity: sha512-NFJK2sHUvrjo8wAU/nQTWU890/zB2jj0qBcCbZbbf+005cAsv6tMjXz31fBign6M5ov1o0Bllu+9nbqkfsjjJQ==}
+ '@babel/helper-wrap-function@7.28.6':
+ resolution: {integrity: sha512-z+PwLziMNBeSQJonizz2AGnndLsP2DeGHIxDAn+wdHOGuo4Fo1x1HBPPXeE9TAOPHNNWQKCSlA2VZyYyyibDnQ==}
engines: {node: '>=6.9.0'}
- '@babel/helpers@7.27.6':
- resolution: {integrity: sha512-muE8Tt8M22638HU31A3CgfSUciwz1fhATfoVai05aPXGor//CdWDCbnlY1yvBPo07njuVOCNGCSp/GTt12lIug==}
+ '@babel/helpers@7.28.6':
+ resolution: {integrity: sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==}
engines: {node: '>=6.9.0'}
'@babel/highlight@7.25.9':
resolution: {integrity: sha512-llL88JShoCsth8fF8R4SJnIn+WLvR6ccFxu1H3FlMhDontdcmZWf2HgIZ7AIqV3Xcck1idlohrN4EUBQz6klbw==}
engines: {node: '>=6.9.0'}
- '@babel/parser@7.28.0':
- resolution: {integrity: sha512-jVZGvOxOuNSsuQuLRTh13nU0AogFlw32w/MT+LV6D3sP5WdbW61E77RnkbaO2dUvmPAYrBDJXGn5gGS6tH4j8g==}
+ '@babel/parser@7.29.0':
+ resolution: {integrity: sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==}
engines: {node: '>=6.0.0'}
hasBin: true
- '@babel/plugin-bugfix-firefox-class-in-computed-class-key@7.27.1':
- resolution: {integrity: sha512-QPG3C9cCVRQLxAVwmefEmwdTanECuUBMQZ/ym5kiw3XKCGA7qkuQLcjWWHcrD/GKbn/WmJwaezfuuAOcyKlRPA==}
+ '@babel/plugin-bugfix-firefox-class-in-computed-class-key@7.28.5':
+ resolution: {integrity: sha512-87GDMS3tsmMSi/3bWOte1UblL+YUTFMV8SZPZ2eSEL17s74Cw/l63rR6NmGVKMYW2GYi85nE+/d6Hw5N0bEk2Q==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0
@@ -533,8 +520,8 @@ packages:
peerDependencies:
'@babel/core': ^7.13.0
- '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@7.27.1':
- resolution: {integrity: sha512-6BpaYGDavZqkI6yT+KSPdpZFfpnd68UKXbcjI9pJ13pvHhPrCKWOOLp+ysvMeA+DxnhuPpgIaRpxRxo5A9t5jw==}
+ '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@7.28.6':
+ resolution: {integrity: sha512-a0aBScVTlNaiUe35UtfxAN7A/tehvvG4/ByO6+46VPKTRSlfnAFsgKy0FUh+qAkQrDTmhDkT+IBOKlOoMUxQ0g==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0
@@ -572,14 +559,14 @@ packages:
peerDependencies:
'@babel/core': ^7.0.0-0
- '@babel/plugin-syntax-import-assertions@7.27.1':
- resolution: {integrity: sha512-UT/Jrhw57xg4ILHLFnzFpPDlMbcdEicaAtjPQpbj9wa8T4r5KVWCimHcL/460g8Ht0DMxDyjsLgiWSkVjnwPFg==}
+ '@babel/plugin-syntax-import-assertions@7.28.6':
+ resolution: {integrity: sha512-pSJUpFHdx9z5nqTSirOCMtYVP2wFgoWhP0p3g8ONK/4IHhLIBd0B9NYqAvIUAhq+OkhO4VM1tENCt0cjlsNShw==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0-0
- '@babel/plugin-syntax-import-attributes@7.27.1':
- resolution: {integrity: sha512-oFT0FrKHgF53f4vOsZGi2Hh3I35PfSmVs4IBFLFj4dnafP+hIWDLg3VyKmUHfLoLHlyxY4C7DGtmHuJgn+IGww==}
+ '@babel/plugin-syntax-import-attributes@7.28.6':
+ resolution: {integrity: sha512-jiLC0ma9XkQT3TKJ9uYvlakm66Pamywo+qwL+oL8HJOvc6TWdZXVfhqJr8CCzbSGUAbDOzlGHJC1U+vRfLQDvw==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0-0
@@ -660,14 +647,14 @@ packages:
peerDependencies:
'@babel/core': ^7.0.0-0
- '@babel/plugin-transform-async-generator-functions@7.28.0':
- resolution: {integrity: sha512-BEOdvX4+M765icNPZeidyADIvQ1m1gmunXufXxvRESy/jNNyfovIqUyE7MVgGBjWktCoJlzvFA1To2O4ymIO3Q==}
+ '@babel/plugin-transform-async-generator-functions@7.29.0':
+ resolution: {integrity: sha512-va0VdWro4zlBr2JsXC+ofCPB2iG12wPtVGTWFx2WLDOM3nYQZZIGP82qku2eW/JR83sD+k2k+CsNtyEbUqhU6w==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0-0
- '@babel/plugin-transform-async-to-generator@7.27.1':
- resolution: {integrity: sha512-NREkZsZVJS4xmTr8qzE5y8AfIPqsdQfRuUiLRTEzb7Qii8iFWCyDKaUV2c0rCuh4ljDZ98ALHP/PetiBV2nddA==}
+ '@babel/plugin-transform-async-to-generator@7.28.6':
+ resolution: {integrity: sha512-ilTRcmbuXjsMmcZ3HASTe4caH5Tpo93PkTxF9oG2VZsSWsahydmcEHhix9Ik122RcTnZnUzPbmux4wh1swfv7g==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0-0
@@ -678,44 +665,44 @@ packages:
peerDependencies:
'@babel/core': ^7.0.0-0
- '@babel/plugin-transform-block-scoping@7.28.0':
- resolution: {integrity: sha512-gKKnwjpdx5sER/wl0WN0efUBFzF/56YZO0RJrSYP4CljXnP31ByY7fol89AzomdlLNzI36AvOTmYHsnZTCkq8Q==}
+ '@babel/plugin-transform-block-scoping@7.28.6':
+ resolution: {integrity: sha512-tt/7wOtBmwHPNMPu7ax4pdPz6shjFrmHDghvNC+FG9Qvj7D6mJcoRQIF5dy4njmxR941l6rgtvfSB2zX3VlUIw==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0-0
- '@babel/plugin-transform-class-properties@7.27.1':
- resolution: {integrity: sha512-D0VcalChDMtuRvJIu3U/fwWjf8ZMykz5iZsg77Nuj821vCKI3zCyRLwRdWbsuJ/uRwZhZ002QtCqIkwC/ZkvbA==}
+ '@babel/plugin-transform-class-properties@7.28.6':
+ resolution: {integrity: sha512-dY2wS3I2G7D697VHndN91TJr8/AAfXQNt5ynCTI/MpxMsSzHp+52uNivYT5wCPax3whc47DR8Ba7cmlQMg24bw==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0-0
- '@babel/plugin-transform-class-static-block@7.27.1':
- resolution: {integrity: sha512-s734HmYU78MVzZ++joYM+NkJusItbdRcbm+AGRgJCt3iA+yux0QpD9cBVdz3tKyrjVYWRl7j0mHSmv4lhV0aoA==}
+ '@babel/plugin-transform-class-static-block@7.28.6':
+ resolution: {integrity: sha512-rfQ++ghVwTWTqQ7w8qyDxL1XGihjBss4CmTgGRCTAC9RIbhVpyp4fOeZtta0Lbf+dTNIVJer6ych2ibHwkZqsQ==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.12.0
- '@babel/plugin-transform-classes@7.28.0':
- resolution: {integrity: sha512-IjM1IoJNw72AZFlj33Cu8X0q2XK/6AaVC3jQu+cgQ5lThWD5ajnuUAml80dqRmOhmPkTH8uAwnpMu9Rvj0LTRA==}
+ '@babel/plugin-transform-classes@7.28.6':
+ resolution: {integrity: sha512-EF5KONAqC5zAqT783iMGuM2ZtmEBy+mJMOKl2BCvPZ2lVrwvXnB6o+OBWCS+CoeCCpVRF2sA2RBKUxvT8tQT5Q==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0-0
- '@babel/plugin-transform-computed-properties@7.27.1':
- resolution: {integrity: sha512-lj9PGWvMTVksbWiDT2tW68zGS/cyo4AkZ/QTp0sQT0mjPopCmrSkzxeXkznjqBxzDI6TclZhOJbBmbBLjuOZUw==}
+ '@babel/plugin-transform-computed-properties@7.28.6':
+ resolution: {integrity: sha512-bcc3k0ijhHbc2lEfpFHgx7eYw9KNXqOerKWfzbxEHUGKnS3sz9C4CNL9OiFN1297bDNfUiSO7DaLzbvHQQQ1BQ==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0-0
- '@babel/plugin-transform-destructuring@7.28.0':
- resolution: {integrity: sha512-v1nrSMBiKcodhsyJ4Gf+Z0U/yawmJDBOTpEB3mcQY52r9RIyPneGyAS/yM6seP/8I+mWI3elOMtT5dB8GJVs+A==}
+ '@babel/plugin-transform-destructuring@7.28.5':
+ resolution: {integrity: sha512-Kl9Bc6D0zTUcFUvkNuQh4eGXPKKNDOJQXVyyM4ZAQPMveniJdxi8XMJwLo+xSoW3MIq81bD33lcUe9kZpl0MCw==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0-0
- '@babel/plugin-transform-dotall-regex@7.27.1':
- resolution: {integrity: sha512-gEbkDVGRvjj7+T1ivxrfgygpT7GUd4vmODtYpbs0gZATdkX8/iSnOtZSxiZnsgm1YjTgjI6VKBGSJJevkrclzw==}
+ '@babel/plugin-transform-dotall-regex@7.28.6':
+ resolution: {integrity: sha512-SljjowuNKB7q5Oayv4FoPzeB74g3QgLt8IVJw9ADvWy3QnUb/01aw8I4AVv8wYnPvQz2GDDZ/g3GhcNyDBI4Bg==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0-0
@@ -726,8 +713,8 @@ packages:
peerDependencies:
'@babel/core': ^7.0.0-0
- '@babel/plugin-transform-duplicate-named-capturing-groups-regex@7.27.1':
- resolution: {integrity: sha512-hkGcueTEzuhB30B3eJCbCYeCaaEQOmQR0AdvzpD4LoN0GXMWzzGSuRrxR2xTnCrvNbVwK9N6/jQ92GSLfiZWoQ==}
+ '@babel/plugin-transform-duplicate-named-capturing-groups-regex@7.29.0':
+ resolution: {integrity: sha512-zBPcW2lFGxdiD8PUnPwJjag2J9otbcLQzvbiOzDxpYXyCuYX9agOwMPGn1prVH0a4qzhCKu24rlH4c1f7yA8rw==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0
@@ -738,14 +725,14 @@ packages:
peerDependencies:
'@babel/core': ^7.0.0-0
- '@babel/plugin-transform-explicit-resource-management@7.28.0':
- resolution: {integrity: sha512-K8nhUcn3f6iB+P3gwCv/no7OdzOZQcKchW6N389V6PD8NUWKZHzndOd9sPDVbMoBsbmjMqlB4L9fm+fEFNVlwQ==}
+ '@babel/plugin-transform-explicit-resource-management@7.28.6':
+ resolution: {integrity: sha512-Iao5Konzx2b6g7EPqTy40UZbcdXE126tTxVFr/nAIj+WItNxjKSYTEw3RC+A2/ZetmdJsgueL1KhaMCQHkLPIg==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0-0
- '@babel/plugin-transform-exponentiation-operator@7.27.1':
- resolution: {integrity: sha512-uspvXnhHvGKf2r4VVtBpeFnuDWsJLQ6MF6lGJLC89jBR1uoVeqM416AZtTuhTezOfgHicpJQmoD5YUakO/YmXQ==}
+ '@babel/plugin-transform-exponentiation-operator@7.28.6':
+ resolution: {integrity: sha512-WitabqiGjV/vJ0aPOLSFfNY1u9U3R7W36B03r5I2KoNix+a3sOhJ3pKFB3R5It9/UiK78NiO0KE9P21cMhlPkw==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0-0
@@ -774,8 +761,8 @@ packages:
peerDependencies:
'@babel/core': ^7.0.0-0
- '@babel/plugin-transform-json-strings@7.27.1':
- resolution: {integrity: sha512-6WVLVJiTjqcQauBhn1LkICsR2H+zm62I3h9faTDKt1qP4jn2o72tSvqMwtGFKGTpojce0gJs+76eZ2uCHRZh0Q==}
+ '@babel/plugin-transform-json-strings@7.28.6':
+ resolution: {integrity: sha512-Nr+hEN+0geQkzhbdgQVPoqr47lZbm+5fCUmO70722xJZd0Mvb59+33QLImGj6F+DkK3xgDi1YVysP8whD6FQAw==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0-0
@@ -786,8 +773,8 @@ packages:
peerDependencies:
'@babel/core': ^7.0.0-0
- '@babel/plugin-transform-logical-assignment-operators@7.27.1':
- resolution: {integrity: sha512-SJvDs5dXxiae4FbSL1aBJlG4wvl594N6YEVVn9e3JGulwioy6z3oPjx/sQBO3Y4NwUu5HNix6KJ3wBZoewcdbw==}
+ '@babel/plugin-transform-logical-assignment-operators@7.28.6':
+ resolution: {integrity: sha512-+anKKair6gpi8VsM/95kmomGNMD0eLz1NQ8+Pfw5sAwWH9fGYXT50E55ZpV0pHUHWf6IUTWPM+f/7AAff+wr9A==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0-0
@@ -804,14 +791,14 @@ packages:
peerDependencies:
'@babel/core': ^7.0.0-0
- '@babel/plugin-transform-modules-commonjs@7.27.1':
- resolution: {integrity: sha512-OJguuwlTYlN0gBZFRPqwOGNWssZjfIUdS7HMYtN8c1KmwpwHFBwTeFZrg9XZa+DFTitWOW5iTAG7tyCUPsCCyw==}
+ '@babel/plugin-transform-modules-commonjs@7.28.6':
+ resolution: {integrity: sha512-jppVbf8IV9iWWwWTQIxJMAJCWBuuKx71475wHwYytrRGQ2CWiDvYlADQno3tcYpS/T2UUWFQp3nVtYfK/YBQrA==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0-0
- '@babel/plugin-transform-modules-systemjs@7.27.1':
- resolution: {integrity: sha512-w5N1XzsRbc0PQStASMksmUeqECuzKuTJer7kFagK8AXgpCMkeDMO5S+aaFb7A51ZYDF7XI34qsTX+fkHiIm5yA==}
+ '@babel/plugin-transform-modules-systemjs@7.29.0':
+ resolution: {integrity: sha512-PrujnVFbOdUpw4UHiVwKvKRLMMic8+eC0CuNlxjsyZUiBjhFdPsewdXCkveh2KqBA9/waD0W1b4hXSOBQJezpQ==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0-0
@@ -822,8 +809,8 @@ packages:
peerDependencies:
'@babel/core': ^7.0.0-0
- '@babel/plugin-transform-named-capturing-groups-regex@7.27.1':
- resolution: {integrity: sha512-SstR5JYy8ddZvD6MhV0tM/j16Qds4mIpJTOd1Yu9J9pJjH93bxHECF7pgtc28XvkzTD6Pxcm/0Z73Hvk7kb3Ng==}
+ '@babel/plugin-transform-named-capturing-groups-regex@7.29.0':
+ resolution: {integrity: sha512-1CZQA5KNAD6ZYQLPw7oi5ewtDNxH/2vuCh+6SmvgDfhumForvs8a1o9n0UrEoBD8HU4djO2yWngTQlXl1NDVEQ==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0
@@ -834,20 +821,20 @@ packages:
peerDependencies:
'@babel/core': ^7.0.0-0
- '@babel/plugin-transform-nullish-coalescing-operator@7.27.1':
- resolution: {integrity: sha512-aGZh6xMo6q9vq1JGcw58lZ1Z0+i0xB2x0XaauNIUXd6O1xXc3RwoWEBlsTQrY4KQ9Jf0s5rgD6SiNkaUdJegTA==}
+ '@babel/plugin-transform-nullish-coalescing-operator@7.28.6':
+ resolution: {integrity: sha512-3wKbRgmzYbw24mDJXT7N+ADXw8BC/imU9yo9c9X9NKaLF1fW+e5H1U5QjMUBe4Qo4Ox/o++IyUkl1sVCLgevKg==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0-0
- '@babel/plugin-transform-numeric-separator@7.27.1':
- resolution: {integrity: sha512-fdPKAcujuvEChxDBJ5c+0BTaS6revLV7CJL08e4m3de8qJfNIuCc2nc7XJYOjBoTMJeqSmwXJ0ypE14RCjLwaw==}
+ '@babel/plugin-transform-numeric-separator@7.28.6':
+ resolution: {integrity: sha512-SJR8hPynj8outz+SlStQSwvziMN4+Bq99it4tMIf5/Caq+3iOc0JtKyse8puvyXkk3eFRIA5ID/XfunGgO5i6w==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0-0
- '@babel/plugin-transform-object-rest-spread@7.28.0':
- resolution: {integrity: sha512-9VNGikXxzu5eCiQjdE4IZn8sb9q7Xsk5EXLDBKUYg1e/Tve8/05+KJEtcxGxAgCY5t/BpKQM+JEL/yT4tvgiUA==}
+ '@babel/plugin-transform-object-rest-spread@7.28.6':
+ resolution: {integrity: sha512-5rh+JR4JBC4pGkXLAcYdLHZjXudVxWMXbB6u6+E9lRL5TrGVbHt1TjxGbZ8CkmYw9zjkB7jutzOROArsqtncEA==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0-0
@@ -858,14 +845,14 @@ packages:
peerDependencies:
'@babel/core': ^7.0.0-0
- '@babel/plugin-transform-optional-catch-binding@7.27.1':
- resolution: {integrity: sha512-txEAEKzYrHEX4xSZN4kJ+OfKXFVSWKB2ZxM9dpcE3wT7smwkNmXo5ORRlVzMVdJbD+Q8ILTgSD7959uj+3Dm3Q==}
+ '@babel/plugin-transform-optional-catch-binding@7.28.6':
+ resolution: {integrity: sha512-R8ja/Pyrv0OGAvAXQhSTmWyPJPml+0TMqXlO5w+AsMEiwb2fg3WkOvob7UxFSL3OIttFSGSRFKQsOhJ/X6HQdQ==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0-0
- '@babel/plugin-transform-optional-chaining@7.27.1':
- resolution: {integrity: sha512-BQmKPPIuc8EkZgNKsv0X4bPmOoayeu4F1YCwx2/CfmDSXDbp7GnzlUH+/ul5VGfRg1AoFPsrIThlEBj2xb4CAg==}
+ '@babel/plugin-transform-optional-chaining@7.28.6':
+ resolution: {integrity: sha512-A4zobikRGJTsX9uqVFdafzGkqD30t26ck2LmOzAuLL8b2x6k3TIqRiT2xVvA9fNmFeTX484VpsdgmKNA0bS23w==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0-0
@@ -876,14 +863,14 @@ packages:
peerDependencies:
'@babel/core': ^7.0.0-0
- '@babel/plugin-transform-private-methods@7.27.1':
- resolution: {integrity: sha512-10FVt+X55AjRAYI9BrdISN9/AQWHqldOeZDUoLyif1Kn05a56xVBXb8ZouL8pZ9jem8QpXaOt8TS7RHUIS+GPA==}
+ '@babel/plugin-transform-private-methods@7.28.6':
+ resolution: {integrity: sha512-piiuapX9CRv7+0st8lmuUlRSmX6mBcVeNQ1b4AYzJxfCMuBfB0vBXDiGSmm03pKJw1v6cZ8KSeM+oUnM6yAExg==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0-0
- '@babel/plugin-transform-private-property-in-object@7.27.1':
- resolution: {integrity: sha512-5J+IhqTi1XPa0DXF83jYOaARrX+41gOewWbkPyjMNRDqgOCqdffGh8L3f/Ek5utaEBZExjSAzcyjmV9SSAWObQ==}
+ '@babel/plugin-transform-private-property-in-object@7.28.6':
+ resolution: {integrity: sha512-b97jvNSOb5+ehyQmBpmhOCiUC5oVK4PMnpRvO7+ymFBoqYjeDHIU9jnrNUuwHOiL9RpGDoKBpSViarV+BU+eVA==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0-0
@@ -894,14 +881,14 @@ packages:
peerDependencies:
'@babel/core': ^7.0.0-0
- '@babel/plugin-transform-regenerator@7.28.0':
- resolution: {integrity: sha512-LOAozRVbqxEVjSKfhGnuLoE4Kz4Oc5UJzuvFUhSsQzdCdaAQu06mG8zDv2GFSerM62nImUZ7K92vxnQcLSDlCQ==}
+ '@babel/plugin-transform-regenerator@7.29.0':
+ resolution: {integrity: sha512-FijqlqMA7DmRdg/aINBSs04y8XNTYw/lr1gJ2WsmBnnaNw1iS43EPkJW+zK7z65auG3AWRFXWj+NcTQwYptUog==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0-0
- '@babel/plugin-transform-regexp-modifiers@7.27.1':
- resolution: {integrity: sha512-TtEciroaiODtXvLZv4rmfMhkCv8jx3wgKpL68PuiPh2M4fvz5jhsA7697N1gMvkvr/JTF13DrFYyEbY9U7cVPA==}
+ '@babel/plugin-transform-regexp-modifiers@7.28.6':
+ resolution: {integrity: sha512-QGWAepm9qxpaIs7UM9FvUSnCGlb8Ua1RhyM4/veAxLwt3gMat/LSGrZixyuj4I6+Kn9iwvqCyPTtbdxanYoWYg==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0
@@ -912,8 +899,8 @@ packages:
peerDependencies:
'@babel/core': ^7.0.0-0
- '@babel/plugin-transform-runtime@7.28.0':
- resolution: {integrity: sha512-dGopk9nZrtCs2+nfIem25UuHyt5moSJamArzIoh9/vezUQPmYDOzjaHDCkAzuGJibCIkPup8rMT2+wYB6S73cA==}
+ '@babel/plugin-transform-runtime@7.29.0':
+ resolution: {integrity: sha512-jlaRT5dJtMaMCV6fAuLbsQMSwz/QkvaHOHOSXRitGGwSpR1blCY4KUKoyP2tYO8vJcqYe8cEj96cqSztv3uF9w==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0-0
@@ -924,8 +911,8 @@ packages:
peerDependencies:
'@babel/core': ^7.0.0-0
- '@babel/plugin-transform-spread@7.27.1':
- resolution: {integrity: sha512-kpb3HUqaILBJcRFVhFUs6Trdd4mkrzcGXss+6/mxUd273PfbWqSDHRzMT2234gIg2QYfAjvXLSquP1xECSg09Q==}
+ '@babel/plugin-transform-spread@7.28.6':
+ resolution: {integrity: sha512-9U4QObUC0FtJl05AsUcodau/RWDytrU6uKgkxu09mLR9HLDAtUMoPuuskm5huQsoktmsYpI+bGmq+iapDcriKA==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0-0
@@ -960,8 +947,8 @@ packages:
peerDependencies:
'@babel/core': ^7.0.0-0
- '@babel/plugin-transform-unicode-property-regex@7.27.1':
- resolution: {integrity: sha512-uW20S39PnaTImxp39O5qFlHLS9LJEmANjMG7SxIhap8rCHqu0Ik+tLEPX5DKmHn6CsWQ7j3lix2tFOa5YtL12Q==}
+ '@babel/plugin-transform-unicode-property-regex@7.28.6':
+ resolution: {integrity: sha512-4Wlbdl/sIZjzi/8St0evF0gEZrgOswVO6aOzqxh1kDZOl9WmLrHq2HtGhnOJZmHZYKP8WZ1MDLCt5DAWwRo57A==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0-0
@@ -972,14 +959,14 @@ packages:
peerDependencies:
'@babel/core': ^7.0.0-0
- '@babel/plugin-transform-unicode-sets-regex@7.27.1':
- resolution: {integrity: sha512-EtkOujbc4cgvb0mlpQefi4NTPBzhSIevblFevACNLUspmrALgmEBdL/XfnyyITfd8fKBZrZys92zOWcik7j9Tw==}
+ '@babel/plugin-transform-unicode-sets-regex@7.28.6':
+ resolution: {integrity: sha512-/wHc/paTUmsDYN7SZkpWxogTOBNnlx7nBQYfy6JJlCT7G3mVhltk3e++N7zV0XfgGsrqBxd4rJQt9H16I21Y1Q==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0
- '@babel/preset-env@7.28.0':
- resolution: {integrity: sha512-VmaxeGOwuDqzLl5JUkIRM1X2Qu2uKGxHEQWh+cvvbl7JuJRgKGJSfsEF/bUaxFhJl/XAyxBe7q7qSuTbKFuCyg==}
+ '@babel/preset-env@7.29.0':
+ resolution: {integrity: sha512-fNEdfc0yi16lt6IZo2Qxk3knHVdfMYX33czNb4v8yWhemoBhibCpQK/uYHtSKIiO+p/zd3+8fYVXhQdOVV608w==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0-0
@@ -1007,31 +994,39 @@ packages:
peerDependencies:
'@babel/core': ^7.0.0-0
- '@babel/runtime@7.23.2':
- resolution: {integrity: sha512-mM8eg4yl5D6i3lu2QKPuPH4FArvJ8KhTofbE7jwMUv9KX5mBvwPAqnV3MlyBNqdp9RyRKP6Yck8TrfYrPvX3bg==}
+ '@babel/runtime@7.27.0':
+ resolution: {integrity: sha512-VtPOkrdPHZsKc/clNqyi9WUA8TINkZ4cGk63UUE3u4pmB2k+ZMQRDuIOagv8UVd6j7k0T3+RRIb7beKTebNbcw==}
engines: {node: '>=6.9.0'}
'@babel/runtime@7.27.6':
resolution: {integrity: sha512-vbavdySgbTTrmFE+EsiqUTzlOr5bzlnJtUv9PynGCAKvfQqjIXbvFdumPM/GxMDfyuGMJaJAU6TO4zc1Jf1i8Q==}
engines: {node: '>=6.9.0'}
- '@babel/template@7.27.2':
- resolution: {integrity: sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==}
+ '@babel/runtime@7.28.4':
+ resolution: {integrity: sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/runtime@7.28.6':
+ resolution: {integrity: sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/template@7.28.6':
+ resolution: {integrity: sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==}
engines: {node: '>=6.9.0'}
- '@babel/traverse@7.28.0':
- resolution: {integrity: sha512-mGe7UK5wWyh0bKRfupsUchrQGqvDbZDbKJw+kcRGSmdHVYrv+ltd0pnpDTVpiTqnaBru9iEvA8pz8W46v0Amwg==}
+ '@babel/traverse@7.29.0':
+ resolution: {integrity: sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==}
engines: {node: '>=6.9.0'}
- '@babel/types@7.28.0':
- resolution: {integrity: sha512-jYnje+JyZG5YThjHiF28oT4SIZLnYOcSBb6+SDaFIyzDVSkXQmQQYclJ2R+YxcdmK0AX6x1E5OQNtuh3jHDrUg==}
+ '@babel/types@7.29.0':
+ resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==}
engines: {node: '>=6.9.0'}
'@bcoe/v8-coverage@0.2.3':
resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==}
- '@cortex-js/compute-engine@0.28.0':
- resolution: {integrity: sha512-kGs74P4KVNLSqu+iVhLSAvodScZdVGoZI2kOsUjnBEFCFjlPJ1Nj5TpBz/4nwPT+viguB+g7VseXsmcxWRx23Q==}
+ '@cortex-js/compute-engine@0.30.2':
+ resolution: {integrity: sha512-Zx+iisk9WWdbxjm8EYsneIBszvjfUs7BHNwf1jBtSINIgfWGpHrTTq9vW0J59iGCFt6bOFxbmWyxNMRSmksHMA==}
engines: {node: '>=21.7.3', npm: '>=10.5.0'}
'@csstools/css-parser-algorithms@2.7.1':
@@ -1067,6 +1062,12 @@ packages:
peerDependencies:
eslint: ^6.0.0 || ^7.0.0 || >=8.0.0
+ '@eslint-community/eslint-utils@4.9.0':
+ resolution: {integrity: sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==}
+ engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
+ peerDependencies:
+ eslint: ^6.0.0 || ^7.0.0 || >=8.0.0
+
'@eslint-community/regexpp@4.12.1':
resolution: {integrity: sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==}
engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0}
@@ -1083,6 +1084,15 @@ packages:
resolution: {integrity: sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
+ '@floating-ui/core@1.7.3':
+ resolution: {integrity: sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==}
+
+ '@floating-ui/dom@1.7.4':
+ resolution: {integrity: sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==}
+
+ '@floating-ui/utils@0.2.10':
+ resolution: {integrity: sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==}
+
'@gar/promisify@1.1.3':
resolution: {integrity: sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==}
@@ -1112,6 +1122,10 @@ packages:
resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==}
engines: {node: '>=12'}
+ '@isaacs/cliui@9.0.0':
+ resolution: {integrity: sha512-AokJm4tuBHillT+FpMtxQ60n8ObyXBatq7jD2/JA9dxbDDokKQm8KMht5ibGzLVU9IJDIKK4TPKgMHEYMn3lMg==}
+ engines: {node: '>=18'}
+
'@istanbuljs/load-nyc-config@1.1.0':
resolution: {integrity: sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==}
engines: {node: '>=8'}
@@ -1246,8 +1260,11 @@ packages:
'@jimp/utils@0.22.12':
resolution: {integrity: sha512-yJ5cWUknGnilBq97ZXOyOS0HhsHOyAyjHwYfHxGbSyMTohgQI6sVyE8KPgDwH8HHW/nMKXk8TrSwAE71zt716Q==}
- '@jridgewell/gen-mapping@0.3.12':
- resolution: {integrity: sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==}
+ '@jridgewell/gen-mapping@0.3.13':
+ resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==}
+
+ '@jridgewell/remapping@2.3.5':
+ resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==}
'@jridgewell/resolve-uri@3.1.2':
resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==}
@@ -1256,12 +1273,21 @@ packages:
'@jridgewell/source-map@0.3.10':
resolution: {integrity: sha512-0pPkgz9dY+bijgistcTTJ5mR+ocqRXLuhXHYdzoMmmoJ2C9S46RCm2GMUbatPEUK9Yjy26IrAy8D/M00lLkv+Q==}
+ '@jridgewell/source-map@0.3.11':
+ resolution: {integrity: sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==}
+
'@jridgewell/sourcemap-codec@1.5.4':
resolution: {integrity: sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw==}
+ '@jridgewell/sourcemap-codec@1.5.5':
+ resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==}
+
'@jridgewell/trace-mapping@0.3.29':
resolution: {integrity: sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==}
+ '@jridgewell/trace-mapping@0.3.31':
+ resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==}
+
'@jsonjoy.com/base64@1.1.2':
resolution: {integrity: sha512-q6XAnWQDIMA3+FTiOYajoYqySkO+JSat0ytXGSuRdq9uXE7o92gzuQwQM14xaCRlBLGq3v5miDGC4vkVTn54xA==}
engines: {node: '>=10.0'}
@@ -1319,9 +1345,6 @@ packages:
resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==}
engines: {node: '>=14'}
- '@popperjs/core@2.11.8':
- resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==}
-
'@remirror/core-constants@3.0.0':
resolution: {integrity: sha512-42aWfPrimMfDKDi4YegyS7x+/0tlzaqwPQCULLanv3DMIlu96KTJR0fM5isWX2UViOqlGnX6YFgqWepcX+XMNg==}
@@ -1365,8 +1388,8 @@ packages:
peerDependencies:
rollup: ^1.20.0||^2.0.0
- '@rollup/pluginutils@5.2.0':
- resolution: {integrity: sha512-qWJ2ZTbmumwiLFomfzTyt5Kng4hwPi9rwCYN4SHb6eaRU1KNO4ccxINHr/VhH4GgPlt1XfSTLX2LBTme8ne4Zw==}
+ '@rollup/pluginutils@5.3.0':
+ resolution: {integrity: sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==}
engines: {node: '>=14.0.0'}
peerDependencies:
rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0
@@ -1380,32 +1403,32 @@ packages:
'@rushstack/eslint-patch@1.12.0':
resolution: {integrity: sha512-5EwMtOqvJMMa3HbmxLlF74e+3/HhwBTMcvt3nqVJgGCozO6hzIPOBlwm8mGVNR9SN2IJpxSnlxczyDjcn7qIyw==}
- '@sentry-internal/browser-utils@9.40.0':
- resolution: {integrity: sha512-Ajvz6jN+EEMKrOHcUv2+HlhbRUh69uXhhRoBjJw8sc61uqA2vv3QWyBSmTRoHdTnLGboT5bKEhHIkzVXb+YgEw==}
+ '@sentry-internal/browser-utils@10.6.0':
+ resolution: {integrity: sha512-pU0aUW1+wBQF4N4cd4aKX61GkiT2wKynsmNOUPR3vhhruuo4WG+GE8mSItO5+5ZyYCevzJ0aPl2QApQlfpnLvw==}
engines: {node: '>=18'}
- '@sentry-internal/feedback@9.40.0':
- resolution: {integrity: sha512-39UbLdGWGvSJ7bAzRnkv91cBdd6fLbdkLVVvqE2ZUfegm7+rH1mRPglmEhw4VE4mQfKZM1zWr/xus2+XPqJcYw==}
+ '@sentry-internal/feedback@10.6.0':
+ resolution: {integrity: sha512-oz5oG0R8/vaCImhLFA9jlSZFNlCwtpfyIUXuFW/pzR7OBQ8bnfxoZoM9U+vLpNNcChKojIPbs1/Vugg7MMxFzg==}
engines: {node: '>=18'}
- '@sentry-internal/replay-canvas@9.40.0':
- resolution: {integrity: sha512-GLoJ4R4Uipd7Vb+0LzSJA2qCyN1J6YalQIoDuOJTfYyykHvKltds5D8a/5S3Q6d8PcL/nxTn93fynauGEZt2Ow==}
+ '@sentry-internal/replay-canvas@10.6.0':
+ resolution: {integrity: sha512-ekRRvpKWW88vefQEx7EqBLnX+uSfHc6ovDLHdbYtYcT7sc4oWoDqwUN9FrDZxqzwdO0CBgC+iktKKewE5OFOzg==}
engines: {node: '>=18'}
- '@sentry-internal/replay@9.40.0':
- resolution: {integrity: sha512-WrmCvqbLJQC45IFRVN3k0J5pU5NkdX0e9o6XxjcmDiATKk00RHnW4yajnCJ8J1cPR4918yqiJHPX5xpG08BZNA==}
+ '@sentry-internal/replay@10.6.0':
+ resolution: {integrity: sha512-w5M+11k0WKT22h5M+Mio+H2sl3WYhRQkntlBrxt0SeevnNVbRODkmZOqTIUhvbGtBg3/yj1otqnsVEKanb0fSA==}
engines: {node: '>=18'}
- '@sentry/browser@9.40.0':
- resolution: {integrity: sha512-qz/1Go817vcsbcIwgrz4/T34vi3oQ4UIqikosuaCTI9wjZvK0HyW3QmLvTbAnsE7G7h6+UZsVkpO5R16IQvQhQ==}
+ '@sentry/browser@10.6.0':
+ resolution: {integrity: sha512-Nc50U5Zoyrw/Miz1gwvlyJsIYEdHWwperilM52cPvhYzPdeY31g5XPO/tCpcsgpfYdCjNYWcSwIrD/DCYn6YSA==}
engines: {node: '>=18'}
- '@sentry/core@9.40.0':
- resolution: {integrity: sha512-cZkuz6BDna6VXSqvlWnrRsaDx4QBKq1PcfQrqhVz8ljs0M7Gcl+Mtj8dCzUxx12fkYM62hQXG72DEGNlAQpH/Q==}
+ '@sentry/core@10.6.0':
+ resolution: {integrity: sha512-9i0Yf0Px8sScUpGg5KPnn0PwrsO6zoPgp5W5dPp8j+dTmYAxPApoADP4IDF547lsXrm3oKEwEeqQ675xStOiwA==}
engines: {node: '>=18'}
- '@sentry/vue@9.40.0':
- resolution: {integrity: sha512-riYkgwaFhFiRr5EqL2+PWFPGlgSOlMe9LVqYKQepBbfT88sS82KJTcqafYAR5HOXJKvIz1IRbFmOJkQOILMayg==}
+ '@sentry/vue@10.6.0':
+ resolution: {integrity: sha512-fcN4kNYwTVROoeDwI0a8xiTLC9Iw5qB9r9dEo0odst/92Q7H2x84vjyC2i+eWmcbn4MLjoRvNqVnZRxeOD7JDw==}
engines: {node: '>=18'}
peerDependencies:
pinia: 2.x || 3.x
@@ -1450,169 +1473,178 @@ packages:
vue: ^2.6.10
vue-template-compiler: ^2.6.10
- '@tiptap/core@2.23.1':
- resolution: {integrity: sha512-EURGKGsEPrwxvOPi9gA+BsczvsECJNV+xgTAGWHmEtU4YJ0AulYrCX3b7FK+aiduVhThIHDoG/Mmvmb/HPLRhQ==}
+ '@tiptap/core@3.13.0':
+ resolution: {integrity: sha512-iUelgiTMgPVMpY5ZqASUpk8mC8HuR9FWKaDzK27w9oWip9tuB54Z8mePTxNcQaSPb6ErzEaC8x8egrRt7OsdGQ==}
peerDependencies:
- '@tiptap/pm': ^2.7.0
+ '@tiptap/pm': ^3.13.0
- '@tiptap/extension-blockquote@2.23.1':
- resolution: {integrity: sha512-GI3s+uFU88LWRaDG20Z9yIu2av3Usn8kw2lkm2ntwX1K6/mQBS/zkGhWr/FSwWOlMtTzYFxF4Ttb0e+hn67A/A==}
+ '@tiptap/extension-blockquote@3.13.0':
+ resolution: {integrity: sha512-K1z/PAIIwEmiWbzrP//4cC7iG1TZknDlF1yb42G7qkx2S2X4P0NiqX7sKOej3yqrPjKjGwPujLMSuDnCF87QkQ==}
peerDependencies:
- '@tiptap/core': ^2.7.0
+ '@tiptap/core': ^3.13.0
- '@tiptap/extension-bold@2.23.1':
- resolution: {integrity: sha512-OM4RxuZeOqpYRN1G/YpXSE8tZ3sVtT2XlO3qKa74qf+htWz8W3x4X0oQCrHrRTDSAA1wbmeZU3QghAIHnbvP/A==}
+ '@tiptap/extension-bold@3.13.0':
+ resolution: {integrity: sha512-VYiDN9EEwR6ShaDLclG8mphkb/wlIzqfk7hxaKboq1G+NSDj8PcaSI9hldKKtTCLeaSNu6UR5nkdu/YHdzYWTw==}
peerDependencies:
- '@tiptap/core': ^2.7.0
+ '@tiptap/core': ^3.13.0
- '@tiptap/extension-bubble-menu@2.23.1':
- resolution: {integrity: sha512-tupuvrlZMTziVZXJuCVjLwllUnux/an9BtTYHpoRyLX9Hg0v7Kh39k9x58zJaoW8Q/Oc/qxPhbJpyOqhE1rLeg==}
+ '@tiptap/extension-bubble-menu@3.13.0':
+ resolution: {integrity: sha512-qZ3j2DBsqP9DjG2UlExQ+tHMRhAnWlCKNreKddKocb/nAFrPdBCtvkqIEu+68zPlbLD4ukpoyjUklRJg+NipFg==}
peerDependencies:
- '@tiptap/core': ^2.7.0
- '@tiptap/pm': ^2.7.0
+ '@tiptap/core': ^3.13.0
+ '@tiptap/pm': ^3.13.0
- '@tiptap/extension-bullet-list@2.23.1':
- resolution: {integrity: sha512-0g9U42m+boLJZP3x9KoJHDCp9WD5abaVdqNbTg9sFPDNsepb7Zaeu8AEB+yZLP/fuTI1I4ko6qkdr3UaaIYcmA==}
+ '@tiptap/extension-bullet-list@3.13.0':
+ resolution: {integrity: sha512-fFQmmEUoPzRGiQJ/KKutG35ZX21GE+1UCDo8Q6PoWH7Al9lex47nvyeU1BiDYOhcTKgIaJRtEH5lInsOsRJcSA==}
peerDependencies:
- '@tiptap/core': ^2.7.0
+ '@tiptap/extension-list': ^3.13.0
- '@tiptap/extension-code-block-lowlight@2.23.1':
- resolution: {integrity: sha512-EQwLyO1zKViSXcwdsnzurWOdVS1W3oiNuFnkxxSGtU8hlRbkDP4+dY8c/jRE2XejfJuxz7zY7iGg594d8+5/SA==}
+ '@tiptap/extension-code-block-lowlight@3.13.0':
+ resolution: {integrity: sha512-CesjEVUkDelSfRxauTkcrrVzzQAxEp5HYuGIXZtoFDmt2F2lYimlgJyvSxIbRqEw8sFQxvTEKlBg0EtF/BvCJg==}
peerDependencies:
- '@tiptap/core': ^2.7.0
- '@tiptap/extension-code-block': ^2.7.0
- '@tiptap/pm': ^2.7.0
+ '@tiptap/core': ^3.13.0
+ '@tiptap/extension-code-block': ^3.13.0
+ '@tiptap/pm': ^3.13.0
highlight.js: ^11
lowlight: ^2 || ^3
- '@tiptap/extension-code-block@2.23.1':
- resolution: {integrity: sha512-eYzJVUR13BhSE/TYAMZihGBId+XiwhnTPqGcSFo+zx89It/vxwDLvAUn0PReMNI7ULKPTw8orUt2fVKSarb2DQ==}
+ '@tiptap/extension-code-block@3.13.0':
+ resolution: {integrity: sha512-kIwfQ4iqootsWg9e74iYJK54/YMIj6ahUxEltjZRML5z/h4gTDcQt2eTpnEC8yjDjHeUVOR94zH9auCySyk9CQ==}
+ peerDependencies:
+ '@tiptap/core': ^3.13.0
+ '@tiptap/pm': ^3.13.0
+
+ '@tiptap/extension-code@3.13.0':
+ resolution: {integrity: sha512-sF5raBni6iSVpXWvwJCAcOXw5/kZ+djDHx1YSGWhopm4+fsj0xW7GvVO+VTwiFjZGKSw+K5NeAxzcQTJZd3Vhw==}
peerDependencies:
- '@tiptap/core': ^2.7.0
- '@tiptap/pm': ^2.7.0
+ '@tiptap/core': ^3.13.0
- '@tiptap/extension-code@2.23.1':
- resolution: {integrity: sha512-3IOdE40m0UTR2+UXui69o/apLtutAbtzfgmMxD6q0qlRvVqz99QEfk9RPHDNlUqJtYCL4TD+sj7UclBsDdgVXA==}
+ '@tiptap/extension-document@3.13.0':
+ resolution: {integrity: sha512-RjU7hTJwjKXIdY57o/Pc+Yr8swLkrwT7PBQ/m+LCX5oO/V2wYoWCjoBYnK5KSHrWlNy/aLzC33BvLeqZZ9nzlQ==}
peerDependencies:
- '@tiptap/core': ^2.7.0
+ '@tiptap/core': ^3.13.0
- '@tiptap/extension-document@2.23.1':
- resolution: {integrity: sha512-2nkIkGVsaMJkpd024E6vXK+5XNz8VOVWp/pM6bbXpuv0HnGPrfLdh4ruuFc+xTQ3WPOmpSu8ygtujt4I1o9/6g==}
+ '@tiptap/extension-dropcursor@3.13.0':
+ resolution: {integrity: sha512-m7GPT3c/83ni+bbU8c+3dpNa8ug+aQ4phNB1Q52VQG3oTonDJnZS7WCtn3lB/Hi1LqoqMtEHwhepU2eD+JeXqQ==}
peerDependencies:
- '@tiptap/core': ^2.7.0
+ '@tiptap/extensions': ^3.13.0
- '@tiptap/extension-dropcursor@2.23.1':
- resolution: {integrity: sha512-GyVp+o/RVrKlLdrQvtIpJGphFGogiPjcPCkAFcrfY1vDY1EYxfVZELC96gG1mUT1BO8FUD3hmbpkWi9l8/6O4A==}
+ '@tiptap/extension-floating-menu@3.13.0':
+ resolution: {integrity: sha512-OsezV2cMofZM4c13gvgi93IEYBUzZgnu8BXTYZQiQYekz4bX4uulBmLa1KOA9EN71FzS+SoLkXHU0YzlbLjlxA==}
peerDependencies:
- '@tiptap/core': ^2.7.0
- '@tiptap/pm': ^2.7.0
+ '@floating-ui/dom': ^1.0.0
+ '@tiptap/core': ^3.13.0
+ '@tiptap/pm': ^3.13.0
- '@tiptap/extension-floating-menu@2.23.1':
- resolution: {integrity: sha512-GMWkpH+p/OUOk1Y5UGOnKuHSDEVBN7DhYIJiWt5g9LK/mpPeuqoCmQg3RQDgjtZXb74SlxLK2pS/3YcAnemdfQ==}
+ '@tiptap/extension-gapcursor@3.13.0':
+ resolution: {integrity: sha512-KVxjQKkd964nin+1IdM2Dvej/Jy4JTMcMgq5seusUhJ9T9P8F9s2D5Iefwgkps3OCzub/aF+eAsZe+1P5KSIgA==}
peerDependencies:
- '@tiptap/core': ^2.7.0
- '@tiptap/pm': ^2.7.0
+ '@tiptap/extensions': ^3.13.0
- '@tiptap/extension-gapcursor@2.23.1':
- resolution: {integrity: sha512-iP+TiFIGZEbOvYAs04pI14mLI4xqbt64Da91TgMF1FNZUrG+9eWKjqbcHLQREuK3Qnjn5f0DI4nOBv61FlnPmA==}
+ '@tiptap/extension-hard-break@3.13.0':
+ resolution: {integrity: sha512-nH1OBaO+/pakhu+P1jF208mPgB70IKlrR/9d46RMYoYbqJTNf4KVLx5lHAOHytIhjcNg+MjyTfJWfkK+dyCCyg==}
peerDependencies:
- '@tiptap/core': ^2.7.0
- '@tiptap/pm': ^2.7.0
+ '@tiptap/core': ^3.13.0
- '@tiptap/extension-hard-break@2.23.1':
- resolution: {integrity: sha512-YF66EVxnBxt1bHPx6fUUSSXK1Vg+/9baJ0AfJ12hCSPCgSjUclRuNmWIH5ikVfByOmPV1xlrN9wryLoSEBcNRQ==}
+ '@tiptap/extension-heading@3.13.0':
+ resolution: {integrity: sha512-8VKWX8waYPtUWN97J89em9fOtxNteh6pvUEd0htcOAtoxjt2uZjbW5N4lKyWhNKifZBrVhH2Cc2NUPuftCVgxw==}
peerDependencies:
- '@tiptap/core': ^2.7.0
+ '@tiptap/core': ^3.13.0
- '@tiptap/extension-heading@2.23.1':
- resolution: {integrity: sha512-5BPoli9wudiAOgSyK8309jyRhFyu5vd02lNChfpHwxUudzIJ/L+0E6FcwrDcw+yXh23cx7F5SSjtFQ7AobxlDQ==}
+ '@tiptap/extension-horizontal-rule@3.13.0':
+ resolution: {integrity: sha512-ZUFyORtjj22ib8ykbxRhWFQOTZjNKqOsMQjaAGof30cuD2DN5J5pMz7Haj2fFRtLpugWYH+f0Mi+WumQXC3hCw==}
peerDependencies:
- '@tiptap/core': ^2.7.0
+ '@tiptap/core': ^3.13.0
+ '@tiptap/pm': ^3.13.0
- '@tiptap/extension-history@2.23.1':
- resolution: {integrity: sha512-1rp2CRjM+P58oGEgeUUDSk0ch67ngIGbGJOOjiBGKU9GIVhI2j4uSwsYTAa9qYMjMUI6IyH1xJpsY2hLKcBOtg==}
+ '@tiptap/extension-italic@3.13.0':
+ resolution: {integrity: sha512-XbVTgmzk1kgUMTirA6AGdLTcKHUvEJoh3R4qMdPtwwygEOe7sBuvKuLtF6AwUtpnOM+Y3tfWUTNEDWv9AcEdww==}
peerDependencies:
- '@tiptap/core': ^2.7.0
- '@tiptap/pm': ^2.7.0
+ '@tiptap/core': ^3.13.0
- '@tiptap/extension-horizontal-rule@2.23.1':
- resolution: {integrity: sha512-uHEF0jpmhtgAxjKw8/s5ipEeTnu99f9RVMGAlmcthJ5Fx9TzH0MvtH4dtBNEu5MXC7+0bNsnncWo125AAbCohg==}
+ '@tiptap/extension-link@3.13.0':
+ resolution: {integrity: sha512-LuFPJ5GoL12GHW4A+USsj60O90pLcwUPdvEUSWewl9USyG6gnLnY/j5ZOXPYH7LiwYW8+lhq7ABwrDF2PKyBbA==}
peerDependencies:
- '@tiptap/core': ^2.7.0
- '@tiptap/pm': ^2.7.0
+ '@tiptap/core': ^3.13.0
+ '@tiptap/pm': ^3.13.0
- '@tiptap/extension-italic@2.23.1':
- resolution: {integrity: sha512-a+cPzffaC/1AKMmZ1Ka6l81xmTgcalf8NXfBuFCUTf5r7uI9NIgXnLo9hg+jR9F4K+bwhC4/UbMvQQzAjh0c0A==}
+ '@tiptap/extension-list-item@3.13.0':
+ resolution: {integrity: sha512-63NbcS/XeQP2jcdDEnEAE3rjJICDj8y1SN1h/MsJmSt1LusnEo8WQ2ub86QELO6XnD3M04V03cY6Knf6I5mTkw==}
peerDependencies:
- '@tiptap/core': ^2.7.0
+ '@tiptap/extension-list': ^3.13.0
- '@tiptap/extension-link@2.23.1':
- resolution: {integrity: sha512-zMD0V8djkvwRYACzd8EvFXXNLQH5poJt6aHC9/8uest6njRhlrRjSjwG5oa+xHW4A76XfAH0A5BPj6ZxZnAUQg==}
+ '@tiptap/extension-list-keymap@3.13.0':
+ resolution: {integrity: sha512-P+HtIa1iwosb1feFc8B/9MN5EAwzS+/dZ0UH0CTF2E4wnp5Z9OMxKl1IYjfiCwHzZrU5Let+S/maOvJR/EmV0g==}
peerDependencies:
- '@tiptap/core': ^2.7.0
- '@tiptap/pm': ^2.7.0
+ '@tiptap/extension-list': ^3.13.0
- '@tiptap/extension-list-item@2.23.1':
- resolution: {integrity: sha512-wVrRp6KAiyjFVFGmn+ojisP64Bsd+ZPdqQBYVbebBx1skZeW0uhG60d7vUkWHi0gCgxHZDfvDbXpfnOD0INRWw==}
+ '@tiptap/extension-list@3.13.0':
+ resolution: {integrity: sha512-MMFH0jQ4LeCPkJJFyZ77kt6eM/vcKujvTbMzW1xSHCIEA6s4lEcx9QdZMPpfmnOvTzeoVKR4nsu2t2qT9ZXzAw==}
peerDependencies:
- '@tiptap/core': ^2.7.0
+ '@tiptap/core': ^3.13.0
+ '@tiptap/pm': ^3.13.0
- '@tiptap/extension-ordered-list@2.23.1':
- resolution: {integrity: sha512-Zp+qognyNgoaJ9bxkBwIuWJEnQ67RdsHXzv3YOdeGRbkUhd8LT6OL7P0mAuNbMBU8MwHxyJ7C7NsyzwzuVbFzA==}
+ '@tiptap/extension-ordered-list@3.13.0':
+ resolution: {integrity: sha512-QuDyLzuK/3vCvx9GeKhgvHWrGECBzmJyAx6gli2HY+Iil7XicbfltV4nvhIxgxzpx3LDHLKzJN9pBi+2MzX60g==}
peerDependencies:
- '@tiptap/core': ^2.7.0
+ '@tiptap/extension-list': ^3.13.0
- '@tiptap/extension-paragraph@2.23.1':
- resolution: {integrity: sha512-LLEPizt1ALE7Ek6prlJ1uhoUCT8C/a3PdZpCh3DshM1L3Kv9TENlaJL2GhFl8SVUCwHmWHvXg30+4tIRFBedaQ==}
+ '@tiptap/extension-paragraph@3.13.0':
+ resolution: {integrity: sha512-9csQde1i0yeZI5oQQ9e1GYNtGL2JcC2d8Fwtw9FsGC8yz2W0h+Fmk+3bc2kobbtO5LGqupSc1fKM8fAg5rSRDg==}
peerDependencies:
- '@tiptap/core': ^2.7.0
+ '@tiptap/core': ^3.13.0
- '@tiptap/extension-strike@2.23.1':
- resolution: {integrity: sha512-hAT9peYkKezRGp/EcPQKtyYQT+2XGUbb26toTr9XIBQIeQCuCpT+FirPrDMrMVWPwcJt7Rv+AzoVjDuBs9wE0A==}
+ '@tiptap/extension-strike@3.13.0':
+ resolution: {integrity: sha512-VHhWNqTAMOfrC48m2FcPIZB0nhl6XHQviAV16SBc+EFznKNv9tQUsqQrnuQ2y6ZVfqq5UxvZ3hKF/JlN/Ff7xw==}
peerDependencies:
- '@tiptap/core': ^2.7.0
+ '@tiptap/core': ^3.13.0
- '@tiptap/extension-subscript@2.23.1':
- resolution: {integrity: sha512-LFUwe90E90f38aES6ka0Jg7kUG3WTMq3M+7qRu6skEx4+izVB6ub5RTvA56trQlWefWiYeJZptf8xfIKdcwGSw==}
+ '@tiptap/extension-subscript@3.13.0':
+ resolution: {integrity: sha512-8Lq1ATTDUyolue42UbWXAotHPY4Y0r6pMTJyZ9Dqxbv5VrlBk6XeApkGwq6etBXMUsENJycLHlBk3PVqhzGrfw==}
peerDependencies:
- '@tiptap/core': ^2.7.0
+ '@tiptap/core': ^3.13.0
+ '@tiptap/pm': ^3.13.0
- '@tiptap/extension-superscript@2.26.1':
- resolution: {integrity: sha512-YTUmppwJchqXxE4nf+wTMuZuUU9/9ibg8p73rif6WxldjuH0RGZQRY8ad5Ha1c5clG+60e0nrXthqqLgvWfjtw==}
+ '@tiptap/extension-superscript@3.13.0':
+ resolution: {integrity: sha512-ljeaxgPy85IyRCYItKtd23fKmKlHbABq/sP4QGZ5D0PRYX5jF1dt8SEVVkDaoUu7YATRVa7MKl/NzKmTuVStjQ==}
peerDependencies:
- '@tiptap/core': ^2.7.0
+ '@tiptap/core': ^3.13.0
+ '@tiptap/pm': ^3.13.0
- '@tiptap/extension-text-style@2.23.1':
- resolution: {integrity: sha512-fZn1GePlL27pUFfKXKoRZo4L4pZP9dUjNNiS/eltLpbi/SenJ15UKhAoHtN1KQvNGJsWkYN49FjnnltU8qvQ+Q==}
+ '@tiptap/extension-text-align@3.18.0':
+ resolution: {integrity: sha512-NEd2IUgOymKPmGOnxum4hLRbdQyBlK1Cmkt8QGIrmatovPrw2PtWmHVZ6foNChsi/r932dKVfqZ/uMUh8QUppQ==}
peerDependencies:
- '@tiptap/core': ^2.7.0
+ '@tiptap/core': ^3.18.0
- '@tiptap/extension-text@2.23.1':
- resolution: {integrity: sha512-XK0D/eyS1Vm5yUrCtkS0AfgyKLJqpi8nJivCOux/JLhhC4x87R1+mI8NoFDYZJ5ic/afREPSBB8jORqOi0qIHg==}
+ '@tiptap/extension-text@3.13.0':
+ resolution: {integrity: sha512-VcZIna93rixw7hRkHGCxDbL3kvJWi80vIT25a2pXg0WP1e7Pi3nBYvZIL4SQtkbBCji9EHrbZx3p8nNPzfazYw==}
peerDependencies:
- '@tiptap/core': ^2.7.0
+ '@tiptap/core': ^3.13.0
- '@tiptap/extension-underline@2.23.1':
- resolution: {integrity: sha512-MTG+VlGStXD3uj7iPzZU8aJrqxoQyX45WX6xTouezaZzh/NQtTyVWwJqNGE7fsMhxirpJ+at0IZmqlDTjAhEDQ==}
+ '@tiptap/extension-underline@3.13.0':
+ resolution: {integrity: sha512-VDQi+UYw0tFnfghpthJTFmtJ3yx90kXeDwFvhmT8G+O+si5VmP05xYDBYBmYCix5jqKigJxEASiBL0gYOgMDEg==}
peerDependencies:
- '@tiptap/core': ^2.7.0
+ '@tiptap/core': ^3.13.0
- '@tiptap/pm@2.23.1':
- resolution: {integrity: sha512-iAx4rP0k4Xd9Ywh+Gpaz5IWfY2CYRpiwVXWekTHLlNRFtrVIWVpMxaQr2mvRU2g0Ca6rz5w3KzkHBMqrI3dIBA==}
+ '@tiptap/extensions@3.13.0':
+ resolution: {integrity: sha512-i7O0ptSibEtTy+2PIPsNKEvhTvMaFJg1W4Oxfnbuxvaigs7cJV9Q0lwDUcc7CPsNw2T1+44wcxg431CzTvdYoA==}
+ peerDependencies:
+ '@tiptap/core': ^3.13.0
+ '@tiptap/pm': ^3.13.0
- '@tiptap/starter-kit@2.23.1':
- resolution: {integrity: sha512-rrImwzJbKSHoFa+WdNU4I0evXcMiQ4yRm737sxvNJwYItT6fXIxrbRT7nJDmtYu2TflcfT1KklEnSrzz1hhYRw==}
+ '@tiptap/pm@3.13.0':
+ resolution: {integrity: sha512-WKR4ucALq+lwx0WJZW17CspeTpXorbIOpvKv5mulZica6QxqfMhn8n1IXCkDws/mCoLRx4Drk5d377tIjFNsvQ==}
- '@tiptap/vue-2@2.23.1':
- resolution: {integrity: sha512-AgbBm0PjYj+amuZZ4fZui3hrBexg1YVTw6ppDhZJL5pyY2PeTo4ag4tRoj+Z/CPS0Cxj3hC6OHgxrMdr6rYeCw==}
+ '@tiptap/starter-kit@3.13.0':
+ resolution: {integrity: sha512-Ojn6sRub04CRuyQ+9wqN62JUOMv+rG1vXhc2s6DCBCpu28lkCMMW+vTe7kXJcEdbot82+5swPbERw9vohswFzg==}
+
+ '@tiptap/vue-2@3.13.0':
+ resolution: {integrity: sha512-z0ptTUMhZIGPQSqjKXRxTjFxioBpv5BYcN5S2o7AVGa6P2vBecfkmRXg84Lxb3/USdF0k5Ru1zmVqweazNCCpA==}
peerDependencies:
- '@tiptap/core': ^2.7.0
- '@tiptap/pm': ^2.7.0
+ '@tiptap/core': ^3.13.0
+ '@tiptap/pm': ^3.13.0
vue: ^2.6.0
- '@toast-ui/editor@2.5.4':
- resolution: {integrity: sha512-XsuYlPQxhec9dHQREFAigjE4enHSuGMF7D0YQ6wW7phmusvAu0FnJfZUPjJBoU/GKz7WP5U6fKU9/P+8j65D8A==}
-
'@tokenizer/token@0.3.0':
resolution: {integrity: sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==}
@@ -1649,9 +1681,6 @@ packages:
'@types/bonjour@3.5.13':
resolution: {integrity: sha512-z9fJ5Im06zvUL548KvYNecEVlA7cVDkGUi6kZusb04mpyEFKCIZJvloCcmpmLaIahDpOQGHaHmG6imtPMmPXGQ==}
- '@types/codemirror@0.0.71':
- resolution: {integrity: sha512-b2oEEnno1LIGKMR7uBEsr40al1UijF1HEpRn0+Yf1xOLl24iQgB7DBpZVMM7y54G5wCNoclDrRO65E6KHPNO2w==}
-
'@types/connect-history-api-fallback@1.5.4':
resolution: {integrity: sha512-n6Cr2xS1h4uAulPRdlw6Jl6s1oG8KrVilPN2yUITEs+K48EzMJJ3W1xy8K5eWuFvjp3R74AOIGSmp2UfBJ8HFw==}
@@ -1740,6 +1769,9 @@ packages:
'@types/node@24.1.0':
resolution: {integrity: sha512-ut5FthK5moxFKH2T1CUOC6ctR67rQRvvHdFLCD2Ql6KXmMuCrjsSsRI9UsLCm9M18BMwClv4pn327UvB7eeO1w==}
+ '@types/node@25.2.1':
+ resolution: {integrity: sha512-CPrnr8voK8vC6eEtyRzvMpgp3VyVRhgclonE7qYi6P9sXwYb59ucfrnmFBTaP0yUi8Gk4yZg/LlTJULGxvTNsg==}
+
'@types/normalize-package-data@2.4.4':
resolution: {integrity: sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==}
@@ -1776,9 +1808,6 @@ packages:
'@types/strip-json-comments@0.0.30':
resolution: {integrity: sha512-7NQmHra/JILCd1QqpSzl8+mJRc8ZHz3uDm8YV1Ks9IhK0epEiTw8aIErbvH9PI+6XbqhyIQy3462nEsn7UVzjQ==}
- '@types/tern@0.23.9':
- resolution: {integrity: sha512-ypzHFE/wBzh+BlH6rrBgS5I/Z7RD21pGhZ2rltb/+ZrVM1awdZwjx7hE5XfuYgHWk9uvV5HLZN3SloevCAp3Bw==}
-
'@types/tough-cookie@4.0.5':
resolution: {integrity: sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==}
@@ -1800,41 +1829,41 @@ packages:
'@types/yargs@17.0.33':
resolution: {integrity: sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==}
- '@typescript-eslint/project-service@8.35.1':
- resolution: {integrity: sha512-VYxn/5LOpVxADAuP3NrnxxHYfzVtQzLKeldIhDhzC8UHaiQvYlXvKuVho1qLduFbJjjy5U5bkGwa3rUGUb1Q6Q==}
+ '@typescript-eslint/project-service@8.47.0':
+ resolution: {integrity: sha512-2X4BX8hUeB5JcA1TQJ7GjcgulXQ+5UkNb0DL8gHsHUHdFoiCTJoYLTpib3LtSDPZsRET5ygN4qqIWrHyYIKERA==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
- typescript: '>=4.8.4 <5.9.0'
+ typescript: '>=4.8.4 <6.0.0'
- '@typescript-eslint/scope-manager@8.35.1':
- resolution: {integrity: sha512-s/Bpd4i7ht2934nG+UoSPlYXd08KYz3bmjLEb7Ye1UVob0d1ENiT3lY8bsCmik4RqfSbPw9xJJHbugpPpP5JUg==}
+ '@typescript-eslint/scope-manager@8.47.0':
+ resolution: {integrity: sha512-a0TTJk4HXMkfpFkL9/WaGTNuv7JWfFTQFJd6zS9dVAjKsojmv9HT55xzbEpnZoY+VUb+YXLMp+ihMLz/UlZfDg==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
- '@typescript-eslint/tsconfig-utils@8.35.1':
- resolution: {integrity: sha512-K5/U9VmT9dTHoNowWZpz+/TObS3xqC5h0xAIjXPw+MNcKV9qg6eSatEnmeAwkjHijhACH0/N7bkhKvbt1+DXWQ==}
+ '@typescript-eslint/tsconfig-utils@8.47.0':
+ resolution: {integrity: sha512-ybUAvjy4ZCL11uryalkKxuT3w3sXJAuWhOoGS3T/Wu+iUu1tGJmk5ytSY8gbdACNARmcYEB0COksD2j6hfGK2g==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
- typescript: '>=4.8.4 <5.9.0'
+ typescript: '>=4.8.4 <6.0.0'
- '@typescript-eslint/types@8.35.1':
- resolution: {integrity: sha512-q/O04vVnKHfrrhNAscndAn1tuQhIkwqnaW+eu5waD5IPts2eX1dgJxgqcPx5BX109/qAz7IG6VrEPTOYKCNfRQ==}
+ '@typescript-eslint/types@8.47.0':
+ resolution: {integrity: sha512-nHAE6bMKsizhA2uuYZbEbmp5z2UpffNrPEqiKIeN7VsV6UY/roxanWfoRrf6x/k9+Obf+GQdkm0nPU+vnMXo9A==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
- '@typescript-eslint/typescript-estree@8.35.1':
- resolution: {integrity: sha512-Vvpuvj4tBxIka7cPs6Y1uvM7gJgdF5Uu9F+mBJBPY4MhvjrjWGK4H0lVgLJd/8PWZ23FTqsaJaLEkBCFUk8Y9g==}
+ '@typescript-eslint/typescript-estree@8.47.0':
+ resolution: {integrity: sha512-k6ti9UepJf5NpzCjH31hQNLHQWupTRPhZ+KFF8WtTuTpy7uHPfeg2NM7cP27aCGajoEplxJDFVCEm9TGPYyiVg==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
- typescript: '>=4.8.4 <5.9.0'
+ typescript: '>=4.8.4 <6.0.0'
- '@typescript-eslint/utils@8.35.1':
- resolution: {integrity: sha512-lhnwatFmOFcazAsUm3ZnZFpXSxiwoa1Lj50HphnDe1Et01NF4+hrdXONSUHIcbVu2eFb1bAf+5yjXkGVkXBKAQ==}
+ '@typescript-eslint/utils@8.47.0':
+ resolution: {integrity: sha512-g7XrNf25iL4TJOiPqatNuaChyqt49a/onq5YsJ9+hXeugK+41LVg7AxikMfM02PC6jbNtZLCJj6AUcQXJS/jGQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
eslint: ^8.57.0 || ^9.0.0
- typescript: '>=4.8.4 <5.9.0'
+ typescript: '>=4.8.4 <6.0.0'
- '@typescript-eslint/visitor-keys@8.35.1':
- resolution: {integrity: sha512-VRwixir4zBWCSTP/ljEo091lbpypz57PoeAQ9imjG+vbeof9LplljsL1mos4ccG6H9IjfrVGM359RozUnuFhpw==}
+ '@typescript-eslint/visitor-keys@8.47.0':
+ resolution: {integrity: sha512-SIV3/6eftCy1bNzCQoPmbWsRLujS8t5iDIZ4spZOBHqrM+yfX2ogg8Tt3PDTAVKw3sSCiUgg30uOAvK2r9zGjQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@ungap/structured-clone@1.3.0':
@@ -2008,6 +2037,12 @@ packages:
acorn-globals@7.0.1:
resolution: {integrity: sha512-umOSDSDrfHbTNPuNpC2NSnnA3LUrqpevPb4T9jRx4MagXNS0rs+gwiTcAvqCRmsD6utzsrzNt+ebm00SNWiC3Q==}
+ acorn-import-phases@1.0.4:
+ resolution: {integrity: sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==}
+ engines: {node: '>=10.13.0'}
+ peerDependencies:
+ acorn: ^8.14.0
+
acorn-jsx@5.3.2:
resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==}
peerDependencies:
@@ -2089,8 +2124,8 @@ packages:
resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==}
engines: {node: '>=8'}
- ansi-regex@6.1.0:
- resolution: {integrity: sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==}
+ ansi-regex@6.2.2:
+ resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==}
engines: {node: '>=12'}
ansi-styles@2.2.1:
@@ -2109,8 +2144,8 @@ packages:
resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==}
engines: {node: '>=10'}
- ansi-styles@6.2.1:
- resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==}
+ ansi-styles@6.2.3:
+ resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==}
engines: {node: '>=12'}
any-base@1.1.0:
@@ -2249,8 +2284,8 @@ packages:
aws4@1.13.2:
resolution: {integrity: sha512-lHe62zvbTB5eEABUVi/AwVh0ZKY9rMMDhmm+eeyuuUQbQ3+J+fONVQOZyj+DdrvD4BY33uYniyRJ4UJIaSKAfw==}
- axios@1.11.0:
- resolution: {integrity: sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA==}
+ axios@1.13.5:
+ resolution: {integrity: sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q==}
babel-code-frame@6.26.0:
resolution: {integrity: sha512-XqYMR2dfdGMW+hd0IUZ2PwK+fGeFkOxZJ0wY+JaQAHzt1Zx8LcvpiZD2NiGkEG8qx0CfkAOr5xt76d1e8vG90g==}
@@ -2273,13 +2308,6 @@ packages:
'@babel/core': ^7.12.0
webpack: '>=5.61.0'
- babel-loader@9.2.1:
- resolution: {integrity: sha512-fqe8naHt46e0yIdkjUZYqddSXfej3AHajX+CSO5X7oy0EmPc6o5Xh+RClNoHjnieWz9AW4kZxW9yyFMhVB1QLA==}
- engines: {node: '>= 14.15.0'}
- peerDependencies:
- '@babel/core': ^7.12.0
- webpack: '>=5'
-
babel-messages@6.23.0:
resolution: {integrity: sha512-Bl3ZiA+LjqaMtNYopA9TYE9HP1tQ+E5dLxE0XrAzcIJeK2UqF0/EaqXwBn9esd4UmTfEab+P+UYQ1GnioFIb/w==}
@@ -2291,8 +2319,8 @@ packages:
resolution: {integrity: sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==}
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
- babel-plugin-polyfill-corejs2@0.4.14:
- resolution: {integrity: sha512-Co2Y9wX854ts6U8gAAPXfn0GmAyctHuK8n0Yhfjd6t30g7yvKjspvvOo9yG+z52PZRgFErt7Ka2pYnXCjLKEpg==}
+ babel-plugin-polyfill-corejs2@0.4.15:
+ resolution: {integrity: sha512-hR3GwrRwHUfYwGfrisXPIDP3JcYfBrW7wKE7+Au6wDYl7fm/ka1NEII6kORzxNU556JjfidZeBsO10kYvtV1aw==}
peerDependencies:
'@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0
@@ -2301,8 +2329,13 @@ packages:
peerDependencies:
'@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0
- babel-plugin-polyfill-regenerator@0.6.5:
- resolution: {integrity: sha512-ISqQ2frbiNU9vIJkzg7dlPpznPZ4jOiUQ1uSmB0fEHeowtN3COYRsXr/xexn64NpU13P06jc/L5TgiJXOgrbEg==}
+ babel-plugin-polyfill-corejs3@0.14.0:
+ resolution: {integrity: sha512-AvDcMxJ34W4Wgy4KBIIePQTAOP1Ie2WFwkQp3dB7FQ/f0lI5+nM96zUnYEOE1P9sEg0es5VCP0HxiWu5fUHZAQ==}
+ peerDependencies:
+ '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0
+
+ babel-plugin-polyfill-regenerator@0.6.6:
+ resolution: {integrity: sha512-hYm+XLYRMvupxiQzrvXUj7YyvFFVfv5gI0R71AJzudg1g2AI2vyCPPIFEBjk162/wFzti3inBHo7isWFuEVS/A==}
peerDependencies:
'@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0
@@ -2345,6 +2378,10 @@ packages:
balanced-match@2.0.0:
resolution: {integrity: sha512-1ugUSr8BHXRnK23KfuYS+gVMC3LB8QGH9W1iGtDPsNWoQbgtXSExkBu2aDR4epiGWZOjZsj6lDl/N/AqqTC3UA==}
+ balanced-match@4.0.2:
+ resolution: {integrity: sha512-x0K50QvKQ97fdEz2kPehIerj+YTeptKF9hyYkKf6egnwmMWAkADiO0QCzSp0R5xN8FTZgYaBfSaue46Ej62nMg==}
+ engines: {node: 20 || >=22}
+
base64-arraybuffer@1.0.2:
resolution: {integrity: sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==}
engines: {node: '>= 0.6.0'}
@@ -2352,6 +2389,10 @@ packages:
base64-js@1.5.1:
resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==}
+ baseline-browser-mapping@2.9.19:
+ resolution: {integrity: sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg==}
+ hasBin: true
+
batch@0.6.1:
resolution: {integrity: sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw==}
@@ -2397,12 +2438,16 @@ packages:
brace-expansion@2.0.2:
resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==}
+ brace-expansion@5.0.2:
+ resolution: {integrity: sha512-Pdk8c9poy+YhOgVWw1JNN22/HcivgKWwpxKq04M/jTmHyCZn12WPJebZxdjSa5TmBqISrUSgNYU3eRORljfCCw==}
+ engines: {node: 20 || >=22}
+
braces@3.0.3:
resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==}
engines: {node: '>=8'}
- broadcast-channel@5.5.1:
- resolution: {integrity: sha512-C7LtMmJCIIU07xtJngYE2OxaGTGBsG+wOa0mBSPRpbTF36kqtsXQhpxtCVDTkpe8gpZMn9C6PhH+mZ/js4IabA==}
+ broadcast-channel@7.1.0:
+ resolution: {integrity: sha512-InJljddsYWbEL8LBnopnCg+qMQp9KcowvYWOt4YWrjD5HmxzDYKdVbDS1w/ji5rFZdRD58V5UxJPtBdpEbEJYw==}
browserslist-config-kolibri@0.18.0:
resolution: {integrity: sha512-d8JCoLUG8XlgfaE/wB7P58ok6ddiGGjChm6dVsL3W2702ibgFjOx+NtFHrLKCpkSuiaR2/gevvaQsIFwkVNNiA==}
@@ -2412,6 +2457,11 @@ packages:
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
hasBin: true
+ browserslist@4.28.1:
+ resolution: {integrity: sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==}
+ engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
+ hasBin: true
+
bser@2.1.1:
resolution: {integrity: sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==}
@@ -2483,6 +2533,9 @@ packages:
caniuse-lite@1.0.30001726:
resolution: {integrity: sha512-VQAUIUzBiZ/UnlM28fSp2CRF3ivUn1BWEvxMcVTNwpw91Py1pGbPIyIKtd+tzct9C3ouceCVdGAXxZOpZAsgdw==}
+ caniuse-lite@1.0.30001769:
+ resolution: {integrity: sha512-BCfFL1sHijQlBGWBMuJyhZUhzo7wer5sVj9hqekB/7xn0Ypy+pER/edCYQm4exbXj4WiySGp40P8UuTh6w1srg==}
+
canvas-exif-orientation@0.4.0:
resolution: {integrity: sha512-1NjYRG+44oKnY5Ou6NtaRoHchLHYlIzxfzTNBAToTiWOO7BkCW4ays709sYIdD+Wg6DReDAAAcHzfrMgZjyiRg==}
@@ -2570,9 +2623,6 @@ packages:
resolution: {integrity: sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==}
engines: {iojs: '>= 1.0.0', node: '>= 0.12.0'}
- codemirror@5.58.2:
- resolution: {integrity: sha512-K/hOh24cCwRutd1Mk3uLtjWzNISOkm4fvXiMO7LucCrqbh6aJDdtqUziim3MZUI6wOY0rvY1SlL1Ork01uMy6w==}
-
collect-v8-coverage@1.0.2:
resolution: {integrity: sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==}
@@ -2633,13 +2683,6 @@ packages:
resolution: {integrity: sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==}
engines: {node: '>= 10'}
- commander@9.5.0:
- resolution: {integrity: sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==}
- engines: {node: ^12.20.0 || >=14}
-
- common-path-prefix@3.0.0:
- resolution: {integrity: sha512-QE33hToZseCH3jS0qN96O/bSh3kaw/h+Tq7ngyY9eWDUnTlTNUyqfqvCXioLe5Na5jFsL78ra/wuBU4iuEgd4w==}
-
common-tags@1.8.2:
resolution: {integrity: sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA==}
engines: {node: '>=4.0.0'}
@@ -2872,15 +2915,15 @@ packages:
resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==}
engines: {node: '>= 0.6'}
- core-js-compat@3.43.0:
- resolution: {integrity: sha512-2GML2ZsCc5LR7hZYz4AXmjQw8zuy2T//2QntwdnpuYI7jteT6GVYJL7F6C2C57R7gSYrcqVW3lAALefdbhBLDA==}
+ core-js-compat@3.48.0:
+ resolution: {integrity: sha512-OM4cAF3D6VtH/WkLtWvyNC56EZVXsZdU3iqaMG2B4WvYrlqU831pc4UtG5yp0sE9z8Y02wVN7PjW5Zf9Gt0f1Q==}
core-js@2.6.12:
resolution: {integrity: sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ==}
deprecated: core-js@<3.23.3 is no longer maintained and not recommended for usage due to the number of issues. Because of the V8 engine whims, feature detection in old core-js versions could cause a slowdown up to 100x even if nothing is polyfilled. Some versions have web compatibility issues. Please, upgrade your dependencies to the actual version of core-js.
- core-js@3.44.0:
- resolution: {integrity: sha512-aFCtd4l6GvAXwVEh3XbbVqJGHDJt0OZRa+5ePGx3LLwi12WfexqQxcsohb2wgsa/92xtl19Hd66G/L+TaAxDMw==}
+ core-js@3.47.0:
+ resolution: {integrity: sha512-c3Q2VVkGAUyupsjRnaNX6u8Dq2vAdzm9iuPj5FW0fRxzlxgq9Q39MDq10IvmQSpLgHQNyQzQmOo6bgGHmH3NNg==}
core-util-is@1.0.2:
resolution: {integrity: sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==}
@@ -3120,6 +3163,15 @@ packages:
supports-color:
optional: true
+ debug@4.4.3:
+ resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==}
+ engines: {node: '>=6.0'}
+ peerDependencies:
+ supports-color: '*'
+ peerDependenciesMeta:
+ supports-color:
+ optional: true
+
decamelize-keys@1.1.1:
resolution: {integrity: sha512-WiPxgEirIV0/eIOMcnFBA3/IJZAZqKnwAwWyvvdi4lsr1WCN22nhdf/3db3DoZcUjTV2SqfzIwNyp6y2xs3nmg==}
engines: {node: '>=0.10.0'}
@@ -3135,6 +3187,9 @@ packages:
decimal.js@10.5.0:
resolution: {integrity: sha512-8vDa8Qxvr/+d94hSh5P3IJwI5t8/c0KsMp+g8bNw9cY2icONa5aPfvKeieW1WlG0WQYwwhJ7mjui2xtiePQSXw==}
+ decimal.js@10.6.0:
+ resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==}
+
decode-uri-component@0.2.2:
resolution: {integrity: sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==}
engines: {node: '>=0.10'}
@@ -3304,6 +3359,9 @@ packages:
electron-to-chromium@1.5.178:
resolution: {integrity: sha512-wObbz/ar3Bc6e4X5vf0iO8xTN8YAjN/tgiAOJLr7yjYFtP9wAjq8Mb5h0yn6kResir+VYx2DXBj9NNobs0ETSA==}
+ electron-to-chromium@1.5.286:
+ resolution: {integrity: sha512-9tfDXhJ4RKFNerfjdCcZfufu49vg620741MNs26a9+bhLThdB+plgMeou98CAaHu/WATj2iHOOHTp1hWtABj2A==}
+
emittery@0.13.1:
resolution: {integrity: sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==}
engines: {node: '>=12'}
@@ -3337,6 +3395,10 @@ packages:
resolution: {integrity: sha512-6Jw4sE1maoRJo3q8MsSIn2onJFbLTOjY9hlx4DZXmOKvLRd1Ok2kXmAGXaafL2+ijsJZ1ClYbl/pmqr9+k4iUQ==}
engines: {node: '>=10.13.0'}
+ enhanced-resolve@5.19.0:
+ resolution: {integrity: sha512-phv3E1Xl4tQOShqSte26C7Fl84EwUdZsyOuSSk9qtAGyyQs2s3jJzComh+Abf4g187lUUAvH+H26omrqia2aGg==}
+ engines: {node: '>=10.13.0'}
+
enquirer@2.4.1:
resolution: {integrity: sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==}
engines: {node: '>=8.6'}
@@ -3371,6 +3433,10 @@ packages:
resolution: {integrity: sha512-WSzPgsdLtTcQwm4CROfS5ju2Wa1QQcVeT37jFjYzdFz1r9ahadC8B8/a4qxJxM+09F18iumCdRmlr96ZYkQvEg==}
engines: {node: '>= 0.4'}
+ es-abstract@1.24.1:
+ resolution: {integrity: sha512-zHXBLhP+QehSSbsS9Pt23Gg964240DPd6QCf8WpkqEXxQ7fhdZzYsocOr5u7apWonsS5EjZDmTF+/slGMyasvw==}
+ engines: {node: '>= 0.4'}
+
es-define-property@1.0.1:
resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==}
engines: {node: '>= 0.4'}
@@ -3382,8 +3448,8 @@ packages:
es-get-iterator@1.1.3:
resolution: {integrity: sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw==}
- es-module-lexer@1.7.0:
- resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==}
+ es-module-lexer@2.0.0:
+ resolution: {integrity: sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==}
es-object-atoms@1.1.1:
resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==}
@@ -3514,6 +3580,19 @@ packages:
jest:
optional: true
+ eslint-plugin-jest@29.1.0:
+ resolution: {integrity: sha512-LabxXbASXVjguqL+kBHTPMf3gUeSqwH4fsrEyHTY/MCs42I/p9+ctg09SJpYiD8eGaIsP6GwYr5xW6xWS9XgZg==}
+ engines: {node: ^20.12.0 || ^22.0.0 || >=24.0.0}
+ peerDependencies:
+ '@typescript-eslint/eslint-plugin': ^8.0.0
+ eslint: ^8.57.0 || ^9.0.0
+ jest: '*'
+ peerDependenciesMeta:
+ '@typescript-eslint/eslint-plugin':
+ optional: true
+ jest:
+ optional: true
+
eslint-plugin-kolibri@0.18.0:
resolution: {integrity: sha512-gAWycVvKBxc+uSCckt7luKxoPLFZ0yK9623hAeZhyn5CE47ACBLCUMGiYg34LtxO46AMJGP6ZS6Y02X6zCW9FQ==}
engines: {node: '>=0.10.0'}
@@ -3666,8 +3745,8 @@ packages:
resolution: {integrity: sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==}
engines: {'0': node >=0.6.0}
- fake-indexeddb@5.0.2:
- resolution: {integrity: sha512-cB507r5T3D55DfclY01GLkninZLfU7HXV/mhVRTnTRm5k2u+fY7Fof2dBkr80p5t7G7dlA/G5dI87QiMdPpMCQ==}
+ fake-indexeddb@6.2.5:
+ resolution: {integrity: sha512-CGnyrvbhPlWYMngksqrSSUT1BAVP49dZocrHuK0SvtR0D5TMs5wP0o3j7jexDJW01KSadjBp1M/71o/KR3nD1w==}
engines: {node: '>=18'}
fast-deep-equal@3.1.3:
@@ -3750,10 +3829,6 @@ packages:
resolution: {integrity: sha512-Tq6PixE0w/VMFfCgbONnkiQIVol/JJL7nRMi20fqzA4NRs9AfeqMGeRdPi3wIhYkxjeBaWh2rxwapn5Tu3IqOQ==}
engines: {node: '>=6'}
- find-cache-dir@4.0.0:
- resolution: {integrity: sha512-9ZonPT4ZAK4a+1pUPVPZJapbi7O5qbbJPdYw/NOQWZZbVLdDTYM3A4R9z/DpAM08IDaFGsvPgiGZ82WEwUDWjg==}
- engines: {node: '>=14.16'}
-
find-root@1.1.0:
resolution: {integrity: sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==}
@@ -3769,10 +3844,6 @@ packages:
resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==}
engines: {node: '>=10'}
- find-up@6.3.0:
- resolution: {integrity: sha512-v2ZsoEuVHYy8ZIlYqwPe/39Cy+cFDzp4dXPaxNvkEuouymu+2Jbz0PxpKarJHYJTmv2HWT3O382qY8l4jMWthw==}
- engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
-
flat-cache@3.2.0:
resolution: {integrity: sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==}
engines: {node: ^10.12.0 || >=12.0.0}
@@ -3791,8 +3862,8 @@ packages:
flush-promises@1.0.2:
resolution: {integrity: sha512-G0sYfLQERwKz4+4iOZYQEZVpOt9zQrlItIxQAAYAWpfby3gbHrx0osCHz5RLl/XoXevXk0xoN4hDFky/VV9TrA==}
- follow-redirects@1.15.9:
- resolution: {integrity: sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==}
+ follow-redirects@1.15.11:
+ resolution: {integrity: sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==}
engines: {node: '>=4.0'}
peerDependencies:
debug: '*'
@@ -3818,8 +3889,8 @@ packages:
resolution: {integrity: sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==}
engines: {node: '>= 0.12'}
- form-data@4.0.4:
- resolution: {integrity: sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==}
+ form-data@4.0.5:
+ resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==}
engines: {node: '>= 6'}
forwarded@0.2.0:
@@ -3938,20 +4009,27 @@ packages:
glob@10.4.5:
resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==}
+ deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me
+ hasBin: true
+
+ glob@11.1.0:
+ resolution: {integrity: sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw==}
+ engines: {node: 20 || >=22}
+ deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me
hasBin: true
glob@7.1.7:
resolution: {integrity: sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==}
- deprecated: Glob versions prior to v9 are no longer supported
+ deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me
glob@7.2.3:
resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==}
- deprecated: Glob versions prior to v9 are no longer supported
+ deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me
glob@8.1.0:
resolution: {integrity: sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==}
engines: {node: '>=12'}
- deprecated: Glob versions prior to v9 are no longer supported
+ deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me
global-modules@2.0.0:
resolution: {integrity: sha512-NGbfmJBp9x8IxyJSd1P+otYK8vonoJactOogrVfFRIAEY1ukil8RSKDz2Yo7wh1oihl51l/r6W4epkeKJHqL8A==}
@@ -4547,8 +4625,12 @@ packages:
jackspeak@3.4.3:
resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==}
- jake@10.9.2:
- resolution: {integrity: sha512-2P4SQ0HrLQ+fw6llpLnOaGAvN2Zu6778SJMrCUwns4fOoG9ayrTiZk3VV8sCPkVZF8ab0zksVpS8FDY5pRCNBA==}
+ jackspeak@4.2.3:
+ resolution: {integrity: sha512-ykkVRwrYvFm1nb2AJfKKYPr0emF6IiXDYUaFx4Zn9ZuIH7MrzEZ3sD5RlqGXNRpHtvUHJyOnCEFxOlNDtGo7wg==}
+ engines: {node: 20 || >=22}
+
+ jake@10.9.4:
+ resolution: {integrity: sha512-wpHYzhxiVQL+IV05BLE2Xn34zW1S223hvjtqk0+gsPrwd/8JNLXJgZZM/iPFsYc1xyphF+6M6EvdE5E9MBGkDA==}
engines: {node: '>=10'}
hasBin: true
@@ -4716,10 +4798,6 @@ packages:
jpeg-js@0.4.4:
resolution: {integrity: sha512-WZzeDOEtTOBK4Mdsar0IqEU5sMr3vSV2RqkAIzUEV2BHnUfKGyswWFPFwK5EeDo93K3FohSHbLAjj0s1Wzd+dg==}
- jquery@2.2.4:
- resolution: {integrity: sha512-lBHj60ezci2u1v2FqnZIraShGgEXq35qCzMv4lITyHGppTnA13rwR0MgwyNJh9TnDs3aXUvd1xjAotfraMHX/Q==}
- deprecated: This version is deprecated. Please upgrade to the latest version or find support at https://www.herodevs.com/support/jquery-nes.
-
js-base64@2.6.4:
resolution: {integrity: sha512-pZe//GGmwJndub7ZghVHz7vjb2LgC1m8B07Au3eYqeqv9emhESByMXxaEgkUkEqJe87oBbSniGYoQNIBklc7IQ==}
@@ -4777,11 +4855,6 @@ packages:
jsdom@8.5.0:
resolution: {integrity: sha512-rvWfcn2O8SrXPaX5fTYIfPVwvnbU8DnZkjAXK305wfP67csyaJBhgg0F2aU6imqJ+lZmj9EmrBAXy6rWHf2/9Q==}
- jsesc@3.0.2:
- resolution: {integrity: sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==}
- engines: {node: '>=6'}
- hasBin: true
-
jsesc@3.1.0:
resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==}
engines: {node: '>=6'}
@@ -4820,8 +4893,8 @@ packages:
engines: {node: '>=6'}
hasBin: true
- jsonfile@6.1.0:
- resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==}
+ jsonfile@6.2.0:
+ resolution: {integrity: sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==}
jsonpointer@5.0.1:
resolution: {integrity: sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ==}
@@ -4869,8 +4942,9 @@ packages:
kolibri-design-system@5.0.1:
resolution: {integrity: sha512-oz5gEFUj7NYZbrqr89gPjSfRFVuuilSqILA4bbWqLWwkI9ay/uVMdsh8cY2Xajhtzccjwqa1c/NxUETr5kMfHQ==}
- kolibri-design-system@5.3.0:
- resolution: {integrity: sha512-vqlqNaI200yWZOq6gwADZf79e+7Hq3wKz7l2idwNLt6UsihT/dRlodBN68aU4w5Wq3zqK/9bctnDLCcXVJx/rw==}
+ kolibri-design-system@https://codeload.github.com/MisRob/kolibri-design-system/tar.gz/ec3efbe080efdda3fabf8d05fe85a1d8d9fdffc9:
+ resolution: {tarball: https://codeload.github.com/MisRob/kolibri-design-system/tar.gz/ec3efbe080efdda3fabf8d05fe85a1d8d9fdffc9}
+ version: 5.5.2
kolibri-format@1.0.1:
resolution: {integrity: sha512-yGQpsJkBAzmRueAq6MG1UOuDl9pbhEtMWNxq9ObG5pPVkG8uhWJAS1L71GCuNAeaV1XG2IWo2565Ov4yXnudeA==}
@@ -4922,15 +4996,15 @@ packages:
linkify-it@5.0.0:
resolution: {integrity: sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==}
- linkifyjs@4.3.1:
- resolution: {integrity: sha512-DRSlB9DKVW04c4SUdGvKK5FR6be45lTU9M76JnngqPeeGDqPwYc0zdUErtsNVMtxPXgUWV4HbXbnC4sNyBxkYg==}
+ linkifyjs@4.3.2:
+ resolution: {integrity: sha512-NT1CJtq3hHIreOianA8aSXn6Cw0JzYOuDQbOrSPe7gqFnCpKP++MQe3ODgO3oh2GJFORkAAdqredOa60z63GbA==}
load-json-file@4.0.0:
resolution: {integrity: sha512-Kx8hMakjX03tiGTLAIdJ+lL0htKnXjEZN6hk/tozf/WOuYGdZBJrZ+rCJRbVCugsjB3jMLn9746NsQIf5VjBMw==}
engines: {node: '>=4'}
- loader-runner@4.3.0:
- resolution: {integrity: sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==}
+ loader-runner@4.3.1:
+ resolution: {integrity: sha512-IWqP2SCPhyVFTBtRcgMHdzlf9ul25NwaFx4wCEH/KjAXuuHY4yNjvPXsBokp8jCB936PyWRaPKUNh8NvylLp2Q==}
engines: {node: '>=6.11.5'}
loader-utils@1.4.2:
@@ -4956,10 +5030,6 @@ packages:
resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==}
engines: {node: '>=10'}
- locate-path@7.2.0:
- resolution: {integrity: sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA==}
- engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
-
lockr@0.8.5:
resolution: {integrity: sha512-PyWX+NYcJtk+12cARV5qaR0I2cfpHDplpOOI4KKoJtJufdnXo4sJPmfWhbZUAT5rCMgszzU0DovlSjEKp2u12A==}
engines: {node: '>= 0.8.0'}
@@ -4997,6 +5067,9 @@ packages:
lodash@4.17.21:
resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==}
+ lodash@4.17.23:
+ resolution: {integrity: sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==}
+
loglevel@1.9.2:
resolution: {integrity: sha512-HgMmCqIJSAKqo68l0rS2AanEWfkxaZ5wNiEFb5ggm08lDs9Xl2KxBlX3PTcaD2chBM1gXAYf491/M2Rv8Jwayg==}
engines: {node: '>= 0.6.0'}
@@ -5011,6 +5084,10 @@ packages:
lru-cache@10.4.3:
resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==}
+ lru-cache@11.2.6:
+ resolution: {integrity: sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==}
+ engines: {node: 20 || >=22}
+
lru-cache@4.1.5:
resolution: {integrity: sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==}
@@ -5066,6 +5143,11 @@ packages:
resolution: {integrity: sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==}
hasBin: true
+ marked@16.1.1:
+ resolution: {integrity: sha512-ij/2lXfCRT71L6u0M29tJPhP0bM5shLL3u5BePhFwPELj2blMJ6GDtD7PfJhRLhJ/c2UwrK17ySVcDzy2YHjHQ==}
+ engines: {node: '>= 20'}
+ hasBin: true
+
marks-pane@1.0.9:
resolution: {integrity: sha512-Ahs4oeG90tbdPWwAJkAAoHg2lRR8lAs9mZXETNPO9hYg3AkjUJBKi1NQ4aaIQZVGrig7c/3NUV1jANl8rFTeMg==}
@@ -5076,8 +5158,8 @@ packages:
resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
engines: {node: '>= 0.4'}
- mathlive@0.105.3:
- resolution: {integrity: sha512-eEnJIlRm1ga18ymY79di5Iuc161CzHs3PTXIg8WUHeEt/jpxFHs2CPVYRNxAzOo+5t4S7lA+HDretW4i5dsTmg==}
+ mathlive@0.108.2:
+ resolution: {integrity: sha512-GIZkfprGTxrbHckOvwo92ZmOOxdD018BHDzlrEwYUU+pzR5KabhqI1s43lxe/vqXdF5RLiQKgDcuk5jxEjhkYg==}
mathml-tag-names@2.1.3:
resolution: {integrity: sha512-APMBEanjybaPzUrfqU0IMU5I0AswKMH7k8OTLs0vvV4KZpExkTkY87nR/zpbuTPj+gARop7aGUbl11pnDfW6xg==}
@@ -5161,9 +5243,9 @@ packages:
resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==}
engines: {node: '>= 0.6'}
- mime-types@3.0.1:
- resolution: {integrity: sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==}
- engines: {node: '>= 0.6'}
+ mime-types@3.0.2:
+ resolution: {integrity: sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==}
+ engines: {node: '>=18'}
mime@1.6.0:
resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==}
@@ -5178,8 +5260,8 @@ packages:
resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==}
engines: {node: '>=4'}
- mini-css-extract-plugin@2.9.2:
- resolution: {integrity: sha512-GJuACcS//jtq4kCtd5ii/M0SZf7OZRH+BxdqXZHaJfb8TJiVl+NgQRPwiYt2EuqeSkNydn/7vP+bcE27C5mb9w==}
+ mini-css-extract-plugin@2.9.4:
+ resolution: {integrity: sha512-ZWYT7ln73Hptxqxk2DxPU9MmapXRhxkJD6tkSR04dnQxm8BGu2hzgKLugK5yySD97u/8yy7Ma7E76k9ZdvtjkQ==}
engines: {node: '>= 12.13.0'}
peerDependencies:
webpack: ^5.0.0
@@ -5187,6 +5269,10 @@ packages:
minimalistic-assert@1.0.1:
resolution: {integrity: sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==}
+ minimatch@10.2.0:
+ resolution: {integrity: sha512-ugkC31VaVg9cF0DFVoADH12k6061zNZkZON+aX8AWsR9GhPcErkcMBceb6znR8wLERM2AkkOxy2nWRLpT9Jq5w==}
+ engines: {node: 20 || >=22}
+
minimatch@3.0.8:
resolution: {integrity: sha512-6FsRAQsxQ61mw+qP1ZzbL9Bc78x2p5OqNgNpnoAFLTrX8n5Kxph0CsnhmKKNXTWjXqU5L0pGPR7hYk+XWZr60Q==}
@@ -5336,6 +5422,9 @@ packages:
node-releases@2.0.19:
resolution: {integrity: sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==}
+ node-releases@2.0.27:
+ resolution: {integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==}
+
node-sass@9.0.0:
resolution: {integrity: sha512-yltEuuLrfH6M7Pq2gAj5B6Zm7m+gdZoG66wTqG6mIZV/zijq3M2OO2HswtT6oBspPyFhHDcaxWpsBm0fRNDHPg==}
engines: {node: '>=16'}
@@ -5431,8 +5520,9 @@ packages:
resolution: {integrity: sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==}
engines: {node: '>= 0.4'}
- oblivious-set@1.1.1:
- resolution: {integrity: sha512-Oh+8fK09mgGmAshFdH6hSVco6KZmd1tTwNFWj35OvzdmJTMZtAkbn05zar2iG3v6sDs1JLEtOiBGNb6BHwkb2w==}
+ oblivious-set@1.4.0:
+ resolution: {integrity: sha512-szyd0ou0T8nsAqHtprRcP3WidfsN1TnAR5yWXf2mFCEr5ek3LEOkT6EZ/92Xfs74HIdyhG5WkGxIssMU0jBaeg==}
+ engines: {node: '>=16'}
obuf@1.1.2:
resolution: {integrity: sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==}
@@ -5489,10 +5579,6 @@ packages:
resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==}
engines: {node: '>=10'}
- p-limit@4.0.0:
- resolution: {integrity: sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==}
- engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
-
p-locate@3.0.0:
resolution: {integrity: sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==}
engines: {node: '>=6'}
@@ -5505,10 +5591,6 @@ packages:
resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==}
engines: {node: '>=10'}
- p-locate@6.0.0:
- resolution: {integrity: sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw==}
- engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
-
p-map@4.0.0:
resolution: {integrity: sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==}
engines: {node: '>=10'}
@@ -5571,10 +5653,6 @@ packages:
resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==}
engines: {node: '>=8'}
- path-exists@5.0.0:
- resolution: {integrity: sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ==}
- engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
-
path-is-absolute@1.0.1:
resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==}
engines: {node: '>=0.10.0'}
@@ -5594,6 +5672,10 @@ packages:
resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==}
engines: {node: '>=16 || 14 >=14.18'}
+ path-scurry@2.0.1:
+ resolution: {integrity: sha512-oWyT4gICAu+kaA7QWk/jvCHWarMKNs6pXOGWKDTr7cw4IGcUbW+PeTfbaQiLGheFRpjo6O9J0PmyMfQPjH71oA==}
+ engines: {node: 20 || >=22}
+
path-to-regexp@0.1.12:
resolution: {integrity: sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==}
@@ -5673,10 +5755,6 @@ packages:
resolution: {integrity: sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==}
engines: {node: '>=8'}
- pkg-dir@7.0.0:
- resolution: {integrity: sha512-Ie9z/WINcxxLp27BKOCHGde4ITq9UklYKDzVo1nhk5sqGEXU3FpkwP5GM2voTGJkGd9B3Otl+Q4uwSOeSUtOBA==}
- engines: {node: '>=14.16'}
-
pngjs@3.4.0:
resolution: {integrity: sha512-NCrCHhWmnQklfH4MtJMRjZ2a8c80qXeMlQMv2uVp9ISJMTt562SbGd6n2oq0PaPgKm7Z6pL9E2UlLIhC+SHL3w==}
engines: {node: '>=4.0.0'}
@@ -6025,14 +6103,14 @@ packages:
prosemirror-dropcursor@1.8.2:
resolution: {integrity: sha512-CCk6Gyx9+Tt2sbYk5NK0nB1ukHi2ryaRgadV/LvyNuO3ena1payM2z6Cg0vO1ebK8cxbzo41ku2DE5Axj1Zuiw==}
- prosemirror-gapcursor@1.3.2:
- resolution: {integrity: sha512-wtjswVBd2vaQRrnYZaBCbyDqr232Ed4p2QPtRIUK5FuqHYKGWkEwl08oQM4Tw7DOR0FsasARV5uJFvMZWxdNxQ==}
+ prosemirror-gapcursor@1.4.0:
+ resolution: {integrity: sha512-z00qvurSdCEWUIulij/isHaqu4uLS8r/Fi61IbjdIPJEonQgggbJsLnstW7Lgdk4zQ68/yr6B6bf7sJXowIgdQ==}
- prosemirror-history@1.4.1:
- resolution: {integrity: sha512-2JZD8z2JviJrboD9cPuX/Sv/1ChFng+xh2tChQ2X4bB2HeK+rra/bmJ3xGntCcjhOqIzSDG6Id7e8RJ9QPXLEQ==}
+ prosemirror-history@1.5.0:
+ resolution: {integrity: sha512-zlzTiH01eKA55UAf1MEjtssJeHnGxO0j4K4Dpx+gnmX9n+SHNlDqI2oO1Kv1iPN5B1dm5fsljCfqKF9nFL6HRg==}
- prosemirror-inputrules@1.5.0:
- resolution: {integrity: sha512-K0xJRCmt+uSw7xesnHmcn72yBGTbY45vm8gXI4LZXbx2Z0jwh5aF9xrGQgrVPu0WbyFVFF3E/o9VhJYz6SQWnA==}
+ prosemirror-inputrules@1.5.1:
+ resolution: {integrity: sha512-7wj4uMjKaXWAQ1CDgxNzNtR9AlsuwzHfdFH1ygEHA2KHF2DOEaXl1CJfNPAKCg9qNEh4rum975QLaCiQPyY6Fw==}
prosemirror-keymap@1.2.3:
resolution: {integrity: sha512-4HucRlpiLd1IPQQXNqeo81BGtkY8Ai5smHhKW9jjPKRc2wQIxksg7Hl1tTI2IfT2B/LgX6bfYvXxEpJl7aKYKw==}
@@ -6043,8 +6121,8 @@ packages:
prosemirror-menu@1.2.5:
resolution: {integrity: sha512-qwXzynnpBIeg1D7BAtjOusR+81xCp53j7iWu/IargiRZqRjGIlQuu1f3jFi+ehrHhWMLoyOQTSRx/IWZJqOYtQ==}
- prosemirror-model@1.25.1:
- resolution: {integrity: sha512-AUvbm7qqmpZa5d9fPKMvH1Q5bqYQvAZWOGRvxsB6iFLyycvC9MwNemNVjHVrWgjaoxAfY8XVg7DbvQ/qxvI9Eg==}
+ prosemirror-model@1.25.4:
+ resolution: {integrity: sha512-PIM7E43PBxKce8OQeezAs9j4TP+5yDpZVbuurd1h5phUxEKIu+G2a+EUZzIC5nS1mJktDJWzbqS23n1tsAf5QA==}
prosemirror-schema-basic@1.2.4:
resolution: {integrity: sha512-ELxP4TlX3yr2v5rM7Sb70SqStq5NvI15c0j9j/gjsrO5vaw+fnnpovCLEGIcpeGfifkuqJwl4fon6b+KdrODYQ==}
@@ -6052,11 +6130,11 @@ packages:
prosemirror-schema-list@1.5.1:
resolution: {integrity: sha512-927lFx/uwyQaGwJxLWCZRkjXG0p48KpMj6ueoYiu4JX05GGuGcgzAy62dfiV8eFZftgyBUvLx76RsMe20fJl+Q==}
- prosemirror-state@1.4.3:
- resolution: {integrity: sha512-goFKORVbvPuAQaXhpbemJFRKJ2aixr+AZMGiquiqKxaucC6hlpHNZHWgz5R7dS4roHiwq9vDctE//CZ++o0W1Q==}
+ prosemirror-state@1.4.4:
+ resolution: {integrity: sha512-6jiYHH2CIGbCfnxdHbXZ12gySFY/fz/ulZE333G6bPqIZ4F+TXo9ifiR86nAHpWnfoNjOb3o5ESi7J8Uz1jXHw==}
- prosemirror-tables@1.7.1:
- resolution: {integrity: sha512-eRQ97Bf+i9Eby99QbyAiyov43iOKgWa7QCGly+lrDt7efZ1v8NWolhXiB43hSDGIXT1UXgbs4KJN3a06FGpr1Q==}
+ prosemirror-tables@1.8.3:
+ resolution: {integrity: sha512-wbqCR/RlRPRe41a4LFtmhKElzBEfBTdtAYWNIGHM6X2e24NN/MTNUKyXjjphfAfdQce37Kh/5yf765mLPYDe7Q==}
prosemirror-trailing-node@3.0.0:
resolution: {integrity: sha512-xiun5/3q0w5eRnGYfNlW1uU9W6x5MoFKWwq/0TIRgt09lv7Hcser2QYV8t4muXbEr+Fwo0geYn79Xs4GKywrRQ==}
@@ -6065,11 +6143,11 @@ packages:
prosemirror-state: ^1.4.2
prosemirror-view: ^1.33.8
- prosemirror-transform@1.10.4:
- resolution: {integrity: sha512-pwDy22nAnGqNR1feOQKHxoFkkUtepoFAd3r2hbEDsnf4wp57kKA36hXsB3njA9FtONBEwSDnDeCiJe+ItD+ykw==}
+ prosemirror-transform@1.10.5:
+ resolution: {integrity: sha512-RPDQCxIDhIBb1o36xxwsaeAvivO8VLJcgBtzmOwQ64bMtsVFh5SSuJ6dWSxO1UsHTiTXPCgQm3PDJt7p6IOLbw==}
- prosemirror-view@1.40.0:
- resolution: {integrity: sha512-2G3svX0Cr1sJjkD/DYWSe3cfV5VPVTBOxI9XQEGWJDFEpsZb/gh4MV29ctv+OJx2RFX4BLt09i+6zaGM/ldkCw==}
+ prosemirror-view@1.41.4:
+ resolution: {integrity: sha512-WkKgnyjNncri03Gjaz3IFWvCAE94XoiEgvtr0/r2Xw7R8/IjK3sKLSiDoCHWcsXSAinVaKlGRZDvMCsF1kbzjA==}
proto-list@1.2.4:
resolution: {integrity: sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==}
@@ -6105,12 +6183,12 @@ packages:
resolution: {integrity: sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==}
engines: {node: '>=0.6'}
- qs@6.14.0:
- resolution: {integrity: sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==}
+ qs@6.14.2:
+ resolution: {integrity: sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==}
engines: {node: '>=0.6'}
- qs@6.5.3:
- resolution: {integrity: sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==}
+ qs@6.5.5:
+ resolution: {integrity: sha512-mzR4sElr1bfCaPJe7m8ilJ6ZXdDaGoObcYR0ZHSsktM/Lt21MVHj5De30GQH2eiZ1qGRTO7LCAzQsUeXTNexWQ==}
engines: {node: '>=0.6'}
query-ast@1.0.5:
@@ -6226,8 +6304,8 @@ packages:
resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==}
engines: {node: '>= 0.4'}
- regenerate-unicode-properties@10.2.0:
- resolution: {integrity: sha512-DqHn3DwbmmPVzeKj9woBadqmXxLvQoQIwu7nopMc72ztvxVmVk2SBhSnx67zuye5TP+lJsb/TBQsjLKhnDf3MA==}
+ regenerate-unicode-properties@10.2.2:
+ resolution: {integrity: sha512-m03P+zhBeQd1RGnYxrGyDAPpWX/epKirLrp8e3qevZdVkKtnCrjjWczIbYc8+xd6vcTStVlqfycTx1KR4LOr0g==}
engines: {node: '>=4'}
regenerate@1.4.2:
@@ -6250,15 +6328,15 @@ packages:
resolution: {integrity: sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==}
engines: {node: '>=8'}
- regexpu-core@6.2.0:
- resolution: {integrity: sha512-H66BPQMrv+V16t8xtmq+UC0CBpiTBA60V8ibS1QVReIp8T1z8hwFxqcGzm9K6lgsN7sB5edVH8a+ze6Fqm4weA==}
+ regexpu-core@6.4.0:
+ resolution: {integrity: sha512-0ghuzq67LI9bLXpOX/ISfve/Mq33a4aFRzoQYhnnok1JOFpmE/A2TBGkNVenOGEeSBCjIiWcc6MVOG5HEQv0sA==}
engines: {node: '>=4'}
regjsgen@0.8.0:
resolution: {integrity: sha512-RvwtGe3d7LvWiDQXeQw8p5asZUmfU1G/l6WbUXeHta7Y2PEIvBTwH6E2EfmYUK8pxcxEdEmaomqyp0vZZ7C+3Q==}
- regjsparser@0.12.0:
- resolution: {integrity: sha512-cnE+y8bz4NhMjISKbgeVJtqNbtf5QpjZP+Bslo+UqkIt9QPnX9q095eiRRASJG1/tz6dlNr6Z5NsBiWYokp6EQ==}
+ regjsparser@0.13.0:
+ resolution: {integrity: sha512-NZQZdC5wOE/H3UT28fVGL+ikOZcEzfMGk/c3iN9UGxzWHMa1op7274oyiUVrAG4B2EuFhus8SvkaYnhvW92p9Q==}
hasBin: true
request@2.88.2:
@@ -6306,6 +6384,11 @@ packages:
engines: {node: '>= 0.4'}
hasBin: true
+ resolve@1.22.11:
+ resolution: {integrity: sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==}
+ engines: {node: '>= 0.4'}
+ hasBin: true
+
resolve@2.0.0-next.5:
resolution: {integrity: sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==}
hasBin: true
@@ -6410,9 +6493,6 @@ packages:
webpack:
optional: true
- sax@1.3.0:
- resolution: {integrity: sha512-0s+oAmw9zLl1V1cS9BtZN7JAd0cW5e0QH4W3LWEK6a4LaLEA2OTpGYWDY+6XasBLtz6wkm3u1xRw95mRuJ59WA==}
-
sax@1.4.1:
resolution: {integrity: sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==}
@@ -6428,6 +6508,10 @@ packages:
resolution: {integrity: sha512-Gn/JaSk/Mt9gYubxTtSn/QCV4em9mpAPiR1rqy/Ocu19u/G9J5WWdNoUT4SiV6mFC3y6cxyFcFwdzPM3FgxGAQ==}
engines: {node: '>= 10.13.0'}
+ schema-utils@4.3.3:
+ resolution: {integrity: sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==}
+ engines: {node: '>= 10.13.0'}
+
scss-tokenizer@0.4.3:
resolution: {integrity: sha512-raKLgf1LI5QMQnG+RxHz6oK0sL3x3I4FN2UDLqgLOGO8hodECNnNh5BXn7fAyBxrA8zVzdQizQ6XjNJQ+uBwMw==}
@@ -6451,6 +6535,11 @@ packages:
engines: {node: '>=10'}
hasBin: true
+ semver@7.7.4:
+ resolution: {integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==}
+ engines: {node: '>=10'}
+ hasBin: true
+
send@0.19.0:
resolution: {integrity: sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==}
engines: {node: '>= 0.8.0'}
@@ -6522,10 +6611,6 @@ packages:
resolution: {integrity: sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==}
engines: {node: '>= 0.4'}
- showdown@2.1.0:
- resolution: {integrity: sha512-/6NVYu4U819R2pUIk79n67SYgJHWCce0a5xTP979WbNp0FL9MN1I1QK662IDU1b6JzKTvmhgI7T7JYIxBi3kMQ==}
- hasBin: true
-
side-channel-list@1.0.0:
resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==}
engines: {node: '>= 0.4'}
@@ -6567,8 +6652,9 @@ packages:
resolution: {integrity: sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==}
engines: {node: '>= 6.0.0', npm: '>= 3.0.0'}
- smob@1.5.0:
- resolution: {integrity: sha512-g6T+p7QO8npa+/hNx9ohv1E5pVCmWrVCUzUXJyLdMmftX6ER0oiWY/w9knEonLpnOp6b6FenKnMfR8gqwWdwig==}
+ smob@1.6.1:
+ resolution: {integrity: sha512-KAkBqZl3c2GvNgNhcoyJae1aKldDW0LO279wF9bk1PnluRTETKBq0WyzRXxEhoQLk56yHaOY4JCBEKDuJIET5g==}
+ engines: {node: '>=20.0.0'}
sockjs@0.3.24:
resolution: {integrity: sha512-GJgLTZ7vYb/JtPSSZ10hsOYIvEYsjbNU+zPdIHcUaWVNUEPivzxku31865sSSud0Da0W4lEeOPlmw93zLQchuQ==}
@@ -6614,9 +6700,9 @@ packages:
resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==}
engines: {node: '>=0.10.0'}
- source-map@0.7.4:
- resolution: {integrity: sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==}
- engines: {node: '>= 8'}
+ source-map@0.7.6:
+ resolution: {integrity: sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==}
+ engines: {node: '>= 12'}
source-map@0.8.0-beta.0:
resolution: {integrity: sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==}
@@ -6765,8 +6851,8 @@ packages:
resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==}
engines: {node: '>=8'}
- strip-ansi@7.1.0:
- resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==}
+ strip-ansi@7.1.2:
+ resolution: {integrity: sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==}
engines: {node: '>=12'}
strip-bom@3.0.0:
@@ -6901,8 +6987,8 @@ packages:
engines: {node: ^14.13.1 || >=16.0.0}
hasBin: true
- stylus-loader@8.1.1:
- resolution: {integrity: sha512-Ohe29p3gwJiu1kxq16P80g1qq0FxGtwQevKctLE4su8KUq+Ea06Q6lp7SpcJjaKNrWIuEZQGvESUPt8JpukKVw==}
+ stylus-loader@8.1.2:
+ resolution: {integrity: sha512-zr4ae9vIDs4zslsMtsCAoN3CLkhtUEikwNwL+AyGGtwlo+oRaX2su/6MVy/TbaSND/WIGx5hSTK23G58T8zVyQ==}
engines: {node: '>= 18.12.0'}
peerDependencies:
'@rspack/core': 0.x || 1.x
@@ -6914,8 +7000,9 @@ packages:
webpack:
optional: true
- stylus@0.63.0:
- resolution: {integrity: sha512-OMlgrTCPzE/ibtRMoeLVhOY0RcNuNWh0rhAVqeKnk/QwcuUKQbnqhZ1kg2vzD8VU/6h3FoPTq4RJPHgLBvX6Bw==}
+ stylus@0.64.0:
+ resolution: {integrity: sha512-ZIdT8eUv8tegmqy1tTIdJv9We2DumkNZFdCF5mz/Kpq3OcTaxSuCAYZge6HKK2CmNC02G1eJig2RV7XTw5hQrA==}
+ engines: {node: '>=16'}
hasBin: true
supports-color@2.0.0:
@@ -6961,13 +7048,18 @@ packages:
resolution: {integrity: sha512-jX8Et4hHg57mug1/079yitEKWGB3LCwoxByLsNim89LABq8NqgiX+6iYVOsq0vX8uJHkU+DZ5fnq95f800bEsQ==}
engines: {node: '>=0.6'}
- tapable@2.2.2:
- resolution: {integrity: sha512-Re10+NauLTMCudc7T5WLFLAwDhQ0JWdrMK+9B2M8zR5hRExKmsRDCBA7/aV/pNJFltmBFO5BAMlQFi/vq3nKOg==}
+ tapable@2.2.3:
+ resolution: {integrity: sha512-ZL6DDuAlRlLGghwcfmSn9sK3Hr6ArtyudlSAiCqQ6IfE+b+HHbydbYDIG15IfS5do+7XQQBdBiubF/cV2dnDzg==}
+ engines: {node: '>=6'}
+
+ tapable@2.3.0:
+ resolution: {integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==}
engines: {node: '>=6'}
tar@6.2.1:
resolution: {integrity: sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==}
engines: {node: '>=10'}
+ deprecated: Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me
temp-dir@2.0.0:
resolution: {integrity: sha512-aoBAniQmmwtcKp/7BzsH8Cxzv8OL736p7v1ihGb5e9DJ9kTwGWHrQrVB5+lfVDzfGrdRzXch+ig7LHaY1JTOrg==}
@@ -6997,11 +7089,32 @@ packages:
uglify-js:
optional: true
+ terser-webpack-plugin@5.3.16:
+ resolution: {integrity: sha512-h9oBFCWrq78NyWWVcSwZarJkZ01c2AyGrzs1crmHZO3QUg9D61Wu4NPjBy69n7JqylFF5y+CsUZYmYEIZ3mR+Q==}
+ engines: {node: '>= 10.13.0'}
+ peerDependencies:
+ '@swc/core': '*'
+ esbuild: '*'
+ uglify-js: '*'
+ webpack: ^5.1.0
+ peerDependenciesMeta:
+ '@swc/core':
+ optional: true
+ esbuild:
+ optional: true
+ uglify-js:
+ optional: true
+
terser@5.43.1:
resolution: {integrity: sha512-+6erLbBm0+LROX2sPXlUYx/ux5PyE9K/a92Wrt6oA+WDAoFTdpHE5tCYCI5PNzq2y8df4rA+QgHLJuR4jNymsg==}
engines: {node: '>=10'}
hasBin: true
+ terser@5.46.0:
+ resolution: {integrity: sha512-jTwoImyr/QbOWFFso3YoU3ik0jBBDJ6JTOQiy/J2YxVJdZCc+5u7skhNwiOR3FQIygFqVUPHl7qbbxtjW2K3Qg==}
+ engines: {node: '>=10'}
+ hasBin: true
+
test-exclude@6.0.0:
resolution: {integrity: sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==}
engines: {node: '>=8'}
@@ -7033,9 +7146,6 @@ packages:
tippy.js@4.3.5:
resolution: {integrity: sha512-NDq3efte8nGK6BOJ1dDN1/WelAwfmh3UtIYXXck6+SxLzbIQNZE/cmRSnwScZ/FyiKdIcvFHvYUgqmoGx8CcyA==}
- tippy.js@6.3.7:
- resolution: {integrity: sha512-E1d3oP2emgJ9dRQZdf3Kkn0qJgI6ZLpyS5z6ZkY1DF3kaQaBsGZsndEpHwx+eC+tYM41HaSNvNtLx8tU57FzTQ==}
-
tmp@0.2.3:
resolution: {integrity: sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w==}
engines: {node: '>=14.14'}
@@ -7204,6 +7314,9 @@ packages:
undici-types@5.26.5:
resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==}
+ undici-types@7.16.0:
+ resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==}
+
undici-types@7.8.0:
resolution: {integrity: sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==}
@@ -7215,12 +7328,12 @@ packages:
resolution: {integrity: sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==}
engines: {node: '>=4'}
- unicode-match-property-value-ecmascript@2.2.0:
- resolution: {integrity: sha512-4IehN3V/+kkr5YeSSDDQG8QLqO26XpL2XP3GQtqwlT/QYSECAwFztxVHjlbh0+gjJ3XmNLS0zDsbgs9jWKExLg==}
+ unicode-match-property-value-ecmascript@2.2.1:
+ resolution: {integrity: sha512-JQ84qTuMg4nVkx8ga4A16a1epI9H6uTXAknqxkGF/aFfRLw1xC/Bp24HNLaZhHSkWd3+84t8iXnp1J0kYcZHhg==}
engines: {node: '>=4'}
- unicode-property-aliases-ecmascript@2.1.0:
- resolution: {integrity: sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==}
+ unicode-property-aliases-ecmascript@2.2.0:
+ resolution: {integrity: sha512-hpbDzxUY9BFwX+UeBnxv3Sh1q7HFxj48DTmXchNgRa46lO8uj3/1iEn3MiNUYTg1g9ctIqXCCERn8gYZhHC5lQ==}
engines: {node: '>=4'}
unique-filename@1.1.1:
@@ -7266,6 +7379,12 @@ packages:
peerDependencies:
browserslist: '>= 4.21.0'
+ update-browserslist-db@1.2.3:
+ resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==}
+ hasBin: true
+ peerDependencies:
+ browserslist: '>= 4.21.0'
+
uri-js@4.4.1:
resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
@@ -7443,8 +7562,8 @@ packages:
walker@1.0.8:
resolution: {integrity: sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==}
- watchpack@2.4.4:
- resolution: {integrity: sha512-c5EGNOiyxxV5qmTtAB7rbiXxi1ooX1pQKMLX/MIabJjRA0SJBQOjKF+KSVfHkr9U1cADPon0mRiVe/riyaiDUA==}
+ watchpack@2.5.1:
+ resolution: {integrity: sha512-Zn5uXdcFNIA1+1Ei5McRd+iRzfhENPCe7LeABkJtNulSxjma+l7ltNx55BWZkRlwRnpOgHqxnjyaDgJnNXnqzg==}
engines: {node: '>=10.13.0'}
wbuf@1.7.3:
@@ -7515,8 +7634,8 @@ packages:
resolution: {integrity: sha512-yd1RBzSGanHkitROoPFd6qsrxt+oFhg/129YzheDGqeustzX0vTZJZsSsQjVQC4yzBQ56K55XU8gaNCtIzOnTg==}
engines: {node: '>=10.13.0'}
- webpack@5.99.9:
- resolution: {integrity: sha512-brOPwM3JnmOa+7kd3NsmOUOwbDAj8FT9xDsG3IW0MgbN9yZV7Oi/s/+MNQ/EcSMqw7qfoRyXPoeEWT8zLVdVGg==}
+ webpack@5.104.1:
+ resolution: {integrity: sha512-Qphch25abbMNtekmEGJmeRUhLDbe+QfiWTiqpKYkpCOWY64v9eyl+KRRLmqOFA2AvKPpc9DC6+u2n76tQLBoaA==}
engines: {node: '>=10.13.0'}
hasBin: true
peerDependencies:
@@ -7536,6 +7655,7 @@ packages:
whatwg-encoding@2.0.0:
resolution: {integrity: sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==}
engines: {node: '>=12'}
+ deprecated: Use @exodus/bytes instead for a more spec-conformant and faster implementation
whatwg-fetch@3.6.20:
resolution: {integrity: sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg==}
@@ -7573,6 +7693,10 @@ packages:
resolution: {integrity: sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==}
engines: {node: '>= 0.4'}
+ which-typed-array@1.1.20:
+ resolution: {integrity: sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg==}
+ engines: {node: '>= 0.4'}
+
which@1.3.1:
resolution: {integrity: sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==}
hasBin: true
@@ -7592,61 +7716,76 @@ packages:
resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==}
engines: {node: '>=0.10.0'}
- workbox-background-sync@7.3.0:
- resolution: {integrity: sha512-PCSk3eK7Mxeuyatb22pcSx9dlgWNv3+M8PqPaYDokks8Y5/FX4soaOqj3yhAZr5k6Q5JWTOMYgaJBpbw11G9Eg==}
+ workbox-background-sync@7.4.0:
+ resolution: {integrity: sha512-8CB9OxKAgKZKyNMwfGZ1XESx89GryWTfI+V5yEj8sHjFH8MFelUwYXEyldEK6M6oKMmn807GoJFUEA1sC4XS9w==}
- workbox-broadcast-update@7.3.0:
- resolution: {integrity: sha512-T9/F5VEdJVhwmrIAE+E/kq5at2OY6+OXXgOWQevnubal6sO92Gjo24v6dCVwQiclAF5NS3hlmsifRrpQzZCdUA==}
+ workbox-broadcast-update@7.4.0:
+ resolution: {integrity: sha512-+eZQwoktlvo62cI0b+QBr40v5XjighxPq3Fzo9AWMiAosmpG5gxRHgTbGGhaJv/q/MFVxwFNGh/UwHZ/8K88lA==}
- workbox-build@7.3.0:
- resolution: {integrity: sha512-JGL6vZTPlxnlqZRhR/K/msqg3wKP+m0wfEUVosK7gsYzSgeIxvZLi1ViJJzVL7CEeI8r7rGFV973RiEqkP3lWQ==}
- engines: {node: '>=16.0.0'}
+ workbox-build@7.4.0:
+ resolution: {integrity: sha512-Ntk1pWb0caOFIvwz/hfgrov/OJ45wPEhI5PbTywQcYjyZiVhT3UrwwUPl6TRYbTm4moaFYithYnl1lvZ8UjxcA==}
+ engines: {node: '>=20.0.0'}
- workbox-cacheable-response@7.3.0:
- resolution: {integrity: sha512-eAFERIg6J2LuyELhLlmeRcJFa5e16Mj8kL2yCDbhWE+HUun9skRQrGIFVUagqWj4DMaaPSMWfAolM7XZZxNmxA==}
+ workbox-cacheable-response@7.4.0:
+ resolution: {integrity: sha512-0Fb8795zg/x23ISFkAc7lbWes6vbw34DGFIMw31cwuHPgDEC/5EYm6m/ZkylLX0EnEbbOyOCLjKgFS/Z5g0HeQ==}
workbox-core@7.3.0:
resolution: {integrity: sha512-Z+mYrErfh4t3zi7NVTvOuACB0A/jA3bgxUN3PwtAVHvfEsZxV9Iju580VEETug3zYJRc0Dmii/aixI/Uxj8fmw==}
- workbox-expiration@7.3.0:
- resolution: {integrity: sha512-lpnSSLp2BM+K6bgFCWc5bS1LR5pAwDWbcKt1iL87/eTSJRdLdAwGQznZE+1czLgn/X05YChsrEegTNxjM067vQ==}
+ workbox-core@7.4.0:
+ resolution: {integrity: sha512-6BMfd8tYEnN4baG4emG9U0hdXM4gGuDU3ectXuVHnj71vwxTFI7WOpQJC4siTOlVtGqCUtj0ZQNsrvi6kZZTAQ==}
+
+ workbox-expiration@7.4.0:
+ resolution: {integrity: sha512-V50p4BxYhtA80eOvulu8xVfPBgZbkxJ1Jr8UUn0rvqjGhLDqKNtfrDfjJKnLz2U8fO2xGQJTx/SKXNTzHOjnHw==}
- workbox-google-analytics@7.3.0:
- resolution: {integrity: sha512-ii/tSfFdhjLHZ2BrYgFNTrb/yk04pw2hasgbM70jpZfLk0vdJAXgaiMAWsoE+wfJDNWoZmBYY0hMVI0v5wWDbg==}
+ workbox-google-analytics@7.4.0:
+ resolution: {integrity: sha512-MVPXQslRF6YHkzGoFw1A4GIB8GrKym/A5+jYDUSL+AeJw4ytQGrozYdiZqUW1TPQHW8isBCBtyFJergUXyNoWQ==}
- workbox-navigation-preload@7.3.0:
- resolution: {integrity: sha512-fTJzogmFaTv4bShZ6aA7Bfj4Cewaq5rp30qcxl2iYM45YD79rKIhvzNHiFj1P+u5ZZldroqhASXwwoyusnr2cg==}
+ workbox-navigation-preload@7.4.0:
+ resolution: {integrity: sha512-etzftSgdQfjMcfPgbfaZCfM2QuR1P+4o8uCA2s4rf3chtKTq/Om7g/qvEOcZkG6v7JZOSOxVYQiOu6PbAZgU6w==}
workbox-precaching@7.3.0:
resolution: {integrity: sha512-ckp/3t0msgXclVAYaNndAGeAoWQUv7Rwc4fdhWL69CCAb2UHo3Cef0KIUctqfQj1p8h6aGyz3w8Cy3Ihq9OmIw==}
- workbox-range-requests@7.3.0:
- resolution: {integrity: sha512-EyFmM1KpDzzAouNF3+EWa15yDEenwxoeXu9bgxOEYnFfCxns7eAxA9WSSaVd8kujFFt3eIbShNqa4hLQNFvmVQ==}
+ workbox-precaching@7.4.0:
+ resolution: {integrity: sha512-VQs37T6jDqf1rTxUJZXRl3yjZMf5JX/vDPhmx2CPgDDKXATzEoqyRqhYnRoxl6Kr0rqaQlp32i9rtG5zTzIlNg==}
- workbox-recipes@7.3.0:
- resolution: {integrity: sha512-BJro/MpuW35I/zjZQBcoxsctgeB+kyb2JAP5EB3EYzePg8wDGoQuUdyYQS+CheTb+GhqJeWmVs3QxLI8EBP1sg==}
+ workbox-range-requests@7.4.0:
+ resolution: {integrity: sha512-3Vq854ZNuP6Y0KZOQWLaLC9FfM7ZaE+iuQl4VhADXybwzr4z/sMmnLgTeUZLq5PaDlcJBxYXQ3U91V7dwAIfvw==}
+
+ workbox-recipes@7.4.0:
+ resolution: {integrity: sha512-kOkWvsAn4H8GvAkwfJTbwINdv4voFoiE9hbezgB1sb/0NLyTG4rE7l6LvS8lLk5QIRIto+DjXLuAuG3Vmt3cxQ==}
workbox-routing@7.3.0:
resolution: {integrity: sha512-ZUlysUVn5ZUzMOmQN3bqu+gK98vNfgX/gSTZ127izJg/pMMy4LryAthnYtjuqcjkN4HEAx1mdgxNiKJMZQM76A==}
+ workbox-routing@7.4.0:
+ resolution: {integrity: sha512-C/ooj5uBWYAhAqwmU8HYQJdOjjDKBp9MzTQ+otpMmd+q0eF59K+NuXUek34wbL0RFrIXe/KKT+tUWcZcBqxbHQ==}
+
workbox-strategies@7.3.0:
resolution: {integrity: sha512-tmZydug+qzDFATwX7QiEL5Hdf7FrkhjaF9db1CbB39sDmEZJg3l9ayDvPxy8Y18C3Y66Nrr9kkN1f/RlkDgllg==}
- workbox-streams@7.3.0:
- resolution: {integrity: sha512-SZnXucyg8x2Y61VGtDjKPO5EgPUG5NDn/v86WYHX+9ZqvAsGOytP0Jxp1bl663YUuMoXSAtsGLL+byHzEuMRpw==}
+ workbox-strategies@7.4.0:
+ resolution: {integrity: sha512-T4hVqIi5A4mHi92+5EppMX3cLaVywDp8nsyUgJhOZxcfSV/eQofcOA6/EMo5rnTNmNTpw0rUgjAI6LaVullPpg==}
- workbox-sw@7.3.0:
- resolution: {integrity: sha512-aCUyoAZU9IZtH05mn0ACUpyHzPs0lMeJimAYkQkBsOWiqaJLgusfDCR+yllkPkFRxWpZKF8vSvgHYeG7LwhlmA==}
+ workbox-streams@7.4.0:
+ resolution: {integrity: sha512-QHPBQrey7hQbnTs5GrEVoWz7RhHJXnPT+12qqWM378orDMo5VMJLCkCM1cnCk+8Eq92lccx/VgRZ7WAzZWbSLg==}
- workbox-webpack-plugin@7.3.0:
- resolution: {integrity: sha512-EC8lmSAuNmPli04+a5r5lTgv8ab+f5l+XjdYuYpbGnxDT15kH6DBeBazVslpffqTDHt+wkdBMnBCu8GdkKrTSA==}
- engines: {node: '>=16.0.0'}
+ workbox-sw@7.4.0:
+ resolution: {integrity: sha512-ltU+Kr3qWR6BtbdlMnCjobZKzeV1hN+S6UvDywBrwM19TTyqA03X66dzw1tEIdJvQ4lYKkBFox6IAEhoSEZ8Xw==}
+
+ workbox-webpack-plugin@7.4.0:
+ resolution: {integrity: sha512-NRgx4lYe4JP5I8qqiROmngbc38WyyN3BZh48lUir2XYJ63EuHWN0KpDxgcYQ/fJtQQIBoswwUPmpqwQmaupnxQ==}
+ engines: {node: '>=20.0.0'}
peerDependencies:
webpack: ^4.4.0 || ^5.91.0
workbox-window@7.3.0:
resolution: {integrity: sha512-qW8PDy16OV1UBaUNGlTVcepzrlzyzNW/ZJvFQQs2j2TzGsg6IKjcpZC1RSquqQnTOafl5pCj5bGfAHlCjOOjdA==}
+ workbox-window@7.4.0:
+ resolution: {integrity: sha512-/bIYdBLAVsNR3v7gYGaV4pQW3M3kEPx5E8vDxGvxo6khTrGtSSCS7QiFKv9ogzBgZiy0OXLP9zO28U/1nF1mfw==}
+
wrap-ansi@7.0.0:
resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==}
engines: {node: '>=10'}
@@ -7725,21 +7864,12 @@ packages:
resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
engines: {node: '>=10'}
- yocto-queue@1.2.1:
- resolution: {integrity: sha512-AyeEbWOu/TAXdxlV9wmGcR0+yh2j3vYPGOECcIj2S7MkrLyC7ne+oye2BKTItt0ii2PHk4cDy+95+LshzbXnGg==}
- engines: {node: '>=12.20'}
-
snapshots:
'@adobe/css-tools@4.3.3': {}
'@adobe/css-tools@4.4.3': {}
- '@ampproject/remapping@2.3.0':
- dependencies:
- '@jridgewell/gen-mapping': 0.3.12
- '@jridgewell/trace-mapping': 0.3.29
-
'@apideck/better-ajv-errors@0.3.6(ajv@8.17.1)':
dependencies:
ajv: 8.17.1
@@ -7751,818 +7881,822 @@ snapshots:
dependencies:
'@babel/highlight': 7.25.9
- '@babel/code-frame@7.27.1':
+ '@babel/code-frame@7.29.0':
dependencies:
- '@babel/helper-validator-identifier': 7.27.1
+ '@babel/helper-validator-identifier': 7.28.5
js-tokens: 4.0.0
picocolors: 1.1.1
- '@babel/compat-data@7.28.0': {}
+ '@babel/compat-data@7.29.0': {}
- '@babel/core@7.28.0':
+ '@babel/core@7.29.0':
dependencies:
- '@ampproject/remapping': 2.3.0
- '@babel/code-frame': 7.27.1
- '@babel/generator': 7.28.0
- '@babel/helper-compilation-targets': 7.27.2
- '@babel/helper-module-transforms': 7.27.3(@babel/core@7.28.0)
- '@babel/helpers': 7.27.6
- '@babel/parser': 7.28.0
- '@babel/template': 7.27.2
- '@babel/traverse': 7.28.0
- '@babel/types': 7.28.0
+ '@babel/code-frame': 7.29.0
+ '@babel/generator': 7.29.1
+ '@babel/helper-compilation-targets': 7.28.6
+ '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0)
+ '@babel/helpers': 7.28.6
+ '@babel/parser': 7.29.0
+ '@babel/template': 7.28.6
+ '@babel/traverse': 7.29.0
+ '@babel/types': 7.29.0
+ '@jridgewell/remapping': 2.3.5
convert-source-map: 2.0.0
- debug: 4.4.1
+ debug: 4.4.3
gensync: 1.0.0-beta.2
json5: 2.2.3
semver: 6.3.1
transitivePeerDependencies:
- supports-color
- '@babel/generator@7.28.0':
+ '@babel/generator@7.29.1':
dependencies:
- '@babel/parser': 7.28.0
- '@babel/types': 7.28.0
- '@jridgewell/gen-mapping': 0.3.12
- '@jridgewell/trace-mapping': 0.3.29
+ '@babel/parser': 7.29.0
+ '@babel/types': 7.29.0
+ '@jridgewell/gen-mapping': 0.3.13
+ '@jridgewell/trace-mapping': 0.3.31
jsesc: 3.1.0
'@babel/helper-annotate-as-pure@7.27.3':
dependencies:
- '@babel/types': 7.28.0
+ '@babel/types': 7.29.0
- '@babel/helper-compilation-targets@7.27.2':
+ '@babel/helper-compilation-targets@7.28.6':
dependencies:
- '@babel/compat-data': 7.28.0
+ '@babel/compat-data': 7.29.0
'@babel/helper-validator-option': 7.27.1
- browserslist: 4.25.1
+ browserslist: 4.28.1
lru-cache: 5.1.1
semver: 6.3.1
- '@babel/helper-create-class-features-plugin@7.27.1(@babel/core@7.28.0)':
+ '@babel/helper-create-class-features-plugin@7.28.6(@babel/core@7.29.0)':
dependencies:
- '@babel/core': 7.28.0
+ '@babel/core': 7.29.0
'@babel/helper-annotate-as-pure': 7.27.3
- '@babel/helper-member-expression-to-functions': 7.27.1
+ '@babel/helper-member-expression-to-functions': 7.28.5
'@babel/helper-optimise-call-expression': 7.27.1
- '@babel/helper-replace-supers': 7.27.1(@babel/core@7.28.0)
+ '@babel/helper-replace-supers': 7.28.6(@babel/core@7.29.0)
'@babel/helper-skip-transparent-expression-wrappers': 7.27.1
- '@babel/traverse': 7.28.0
+ '@babel/traverse': 7.29.0
semver: 6.3.1
transitivePeerDependencies:
- supports-color
- '@babel/helper-create-regexp-features-plugin@7.27.1(@babel/core@7.28.0)':
+ '@babel/helper-create-regexp-features-plugin@7.28.5(@babel/core@7.29.0)':
dependencies:
- '@babel/core': 7.28.0
+ '@babel/core': 7.29.0
'@babel/helper-annotate-as-pure': 7.27.3
- regexpu-core: 6.2.0
+ regexpu-core: 6.4.0
semver: 6.3.1
- '@babel/helper-define-polyfill-provider@0.6.5(@babel/core@7.28.0)':
+ '@babel/helper-define-polyfill-provider@0.6.6(@babel/core@7.29.0)':
dependencies:
- '@babel/core': 7.28.0
- '@babel/helper-compilation-targets': 7.27.2
- '@babel/helper-plugin-utils': 7.27.1
- debug: 4.4.1
+ '@babel/core': 7.29.0
+ '@babel/helper-compilation-targets': 7.28.6
+ '@babel/helper-plugin-utils': 7.28.6
+ debug: 4.4.3
lodash.debounce: 4.0.8
- resolve: 1.22.10
+ resolve: 1.22.11
transitivePeerDependencies:
- supports-color
'@babel/helper-globals@7.28.0': {}
- '@babel/helper-member-expression-to-functions@7.27.1':
+ '@babel/helper-member-expression-to-functions@7.28.5':
dependencies:
- '@babel/traverse': 7.28.0
- '@babel/types': 7.28.0
+ '@babel/traverse': 7.29.0
+ '@babel/types': 7.29.0
transitivePeerDependencies:
- supports-color
- '@babel/helper-module-imports@7.27.1':
+ '@babel/helper-module-imports@7.28.6':
dependencies:
- '@babel/traverse': 7.28.0
- '@babel/types': 7.28.0
+ '@babel/traverse': 7.29.0
+ '@babel/types': 7.29.0
transitivePeerDependencies:
- supports-color
- '@babel/helper-module-transforms@7.27.3(@babel/core@7.28.0)':
+ '@babel/helper-module-transforms@7.28.6(@babel/core@7.29.0)':
dependencies:
- '@babel/core': 7.28.0
- '@babel/helper-module-imports': 7.27.1
- '@babel/helper-validator-identifier': 7.27.1
- '@babel/traverse': 7.28.0
+ '@babel/core': 7.29.0
+ '@babel/helper-module-imports': 7.28.6
+ '@babel/helper-validator-identifier': 7.28.5
+ '@babel/traverse': 7.29.0
transitivePeerDependencies:
- supports-color
'@babel/helper-optimise-call-expression@7.27.1':
dependencies:
- '@babel/types': 7.28.0
+ '@babel/types': 7.29.0
- '@babel/helper-plugin-utils@7.27.1': {}
+ '@babel/helper-plugin-utils@7.28.6': {}
- '@babel/helper-remap-async-to-generator@7.27.1(@babel/core@7.28.0)':
+ '@babel/helper-remap-async-to-generator@7.27.1(@babel/core@7.29.0)':
dependencies:
- '@babel/core': 7.28.0
+ '@babel/core': 7.29.0
'@babel/helper-annotate-as-pure': 7.27.3
- '@babel/helper-wrap-function': 7.27.1
- '@babel/traverse': 7.28.0
+ '@babel/helper-wrap-function': 7.28.6
+ '@babel/traverse': 7.29.0
transitivePeerDependencies:
- supports-color
- '@babel/helper-replace-supers@7.27.1(@babel/core@7.28.0)':
+ '@babel/helper-replace-supers@7.28.6(@babel/core@7.29.0)':
dependencies:
- '@babel/core': 7.28.0
- '@babel/helper-member-expression-to-functions': 7.27.1
+ '@babel/core': 7.29.0
+ '@babel/helper-member-expression-to-functions': 7.28.5
'@babel/helper-optimise-call-expression': 7.27.1
- '@babel/traverse': 7.28.0
+ '@babel/traverse': 7.29.0
transitivePeerDependencies:
- supports-color
'@babel/helper-skip-transparent-expression-wrappers@7.27.1':
dependencies:
- '@babel/traverse': 7.28.0
- '@babel/types': 7.28.0
+ '@babel/traverse': 7.29.0
+ '@babel/types': 7.29.0
transitivePeerDependencies:
- supports-color
'@babel/helper-string-parser@7.27.1': {}
- '@babel/helper-validator-identifier@7.27.1': {}
+ '@babel/helper-validator-identifier@7.28.5': {}
'@babel/helper-validator-option@7.27.1': {}
- '@babel/helper-wrap-function@7.27.1':
+ '@babel/helper-wrap-function@7.28.6':
dependencies:
- '@babel/template': 7.27.2
- '@babel/traverse': 7.28.0
- '@babel/types': 7.28.0
+ '@babel/template': 7.28.6
+ '@babel/traverse': 7.29.0
+ '@babel/types': 7.29.0
transitivePeerDependencies:
- supports-color
- '@babel/helpers@7.27.6':
+ '@babel/helpers@7.28.6':
dependencies:
- '@babel/template': 7.27.2
- '@babel/types': 7.28.0
+ '@babel/template': 7.28.6
+ '@babel/types': 7.29.0
'@babel/highlight@7.25.9':
dependencies:
- '@babel/helper-validator-identifier': 7.27.1
+ '@babel/helper-validator-identifier': 7.28.5
chalk: 2.4.2
js-tokens: 4.0.0
picocolors: 1.1.1
- '@babel/parser@7.28.0':
+ '@babel/parser@7.29.0':
dependencies:
- '@babel/types': 7.28.0
+ '@babel/types': 7.29.0
- '@babel/plugin-bugfix-firefox-class-in-computed-class-key@7.27.1(@babel/core@7.28.0)':
+ '@babel/plugin-bugfix-firefox-class-in-computed-class-key@7.28.5(@babel/core@7.29.0)':
dependencies:
- '@babel/core': 7.28.0
- '@babel/helper-plugin-utils': 7.27.1
- '@babel/traverse': 7.28.0
+ '@babel/core': 7.29.0
+ '@babel/helper-plugin-utils': 7.28.6
+ '@babel/traverse': 7.29.0
transitivePeerDependencies:
- supports-color
- '@babel/plugin-bugfix-safari-class-field-initializer-scope@7.27.1(@babel/core@7.28.0)':
+ '@babel/plugin-bugfix-safari-class-field-initializer-scope@7.27.1(@babel/core@7.29.0)':
dependencies:
- '@babel/core': 7.28.0
- '@babel/helper-plugin-utils': 7.27.1
+ '@babel/core': 7.29.0
+ '@babel/helper-plugin-utils': 7.28.6
- '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.27.1(@babel/core@7.28.0)':
+ '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.27.1(@babel/core@7.29.0)':
dependencies:
- '@babel/core': 7.28.0
- '@babel/helper-plugin-utils': 7.27.1
+ '@babel/core': 7.29.0
+ '@babel/helper-plugin-utils': 7.28.6
- '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@7.27.1(@babel/core@7.28.0)':
+ '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@7.27.1(@babel/core@7.29.0)':
dependencies:
- '@babel/core': 7.28.0
- '@babel/helper-plugin-utils': 7.27.1
+ '@babel/core': 7.29.0
+ '@babel/helper-plugin-utils': 7.28.6
'@babel/helper-skip-transparent-expression-wrappers': 7.27.1
- '@babel/plugin-transform-optional-chaining': 7.27.1(@babel/core@7.28.0)
+ '@babel/plugin-transform-optional-chaining': 7.28.6(@babel/core@7.29.0)
transitivePeerDependencies:
- supports-color
- '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@7.27.1(@babel/core@7.28.0)':
+ '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@7.28.6(@babel/core@7.29.0)':
dependencies:
- '@babel/core': 7.28.0
- '@babel/helper-plugin-utils': 7.27.1
- '@babel/traverse': 7.28.0
+ '@babel/core': 7.29.0
+ '@babel/helper-plugin-utils': 7.28.6
+ '@babel/traverse': 7.29.0
transitivePeerDependencies:
- supports-color
- '@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2(@babel/core@7.28.0)':
+ '@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2(@babel/core@7.29.0)':
dependencies:
- '@babel/core': 7.28.0
+ '@babel/core': 7.29.0
- '@babel/plugin-syntax-async-generators@7.8.4(@babel/core@7.28.0)':
+ '@babel/plugin-syntax-async-generators@7.8.4(@babel/core@7.29.0)':
dependencies:
- '@babel/core': 7.28.0
- '@babel/helper-plugin-utils': 7.27.1
+ '@babel/core': 7.29.0
+ '@babel/helper-plugin-utils': 7.28.6
- '@babel/plugin-syntax-bigint@7.8.3(@babel/core@7.28.0)':
+ '@babel/plugin-syntax-bigint@7.8.3(@babel/core@7.29.0)':
dependencies:
- '@babel/core': 7.28.0
- '@babel/helper-plugin-utils': 7.27.1
+ '@babel/core': 7.29.0
+ '@babel/helper-plugin-utils': 7.28.6
- '@babel/plugin-syntax-class-properties@7.12.13(@babel/core@7.28.0)':
+ '@babel/plugin-syntax-class-properties@7.12.13(@babel/core@7.29.0)':
dependencies:
- '@babel/core': 7.28.0
- '@babel/helper-plugin-utils': 7.27.1
+ '@babel/core': 7.29.0
+ '@babel/helper-plugin-utils': 7.28.6
- '@babel/plugin-syntax-class-static-block@7.14.5(@babel/core@7.28.0)':
+ '@babel/plugin-syntax-class-static-block@7.14.5(@babel/core@7.29.0)':
dependencies:
- '@babel/core': 7.28.0
- '@babel/helper-plugin-utils': 7.27.1
+ '@babel/core': 7.29.0
+ '@babel/helper-plugin-utils': 7.28.6
- '@babel/plugin-syntax-flow@7.27.1(@babel/core@7.28.0)':
+ '@babel/plugin-syntax-flow@7.27.1(@babel/core@7.29.0)':
dependencies:
- '@babel/core': 7.28.0
- '@babel/helper-plugin-utils': 7.27.1
+ '@babel/core': 7.29.0
+ '@babel/helper-plugin-utils': 7.28.6
- '@babel/plugin-syntax-import-assertions@7.27.1(@babel/core@7.28.0)':
+ '@babel/plugin-syntax-import-assertions@7.28.6(@babel/core@7.29.0)':
dependencies:
- '@babel/core': 7.28.0
- '@babel/helper-plugin-utils': 7.27.1
+ '@babel/core': 7.29.0
+ '@babel/helper-plugin-utils': 7.28.6
- '@babel/plugin-syntax-import-attributes@7.27.1(@babel/core@7.28.0)':
+ '@babel/plugin-syntax-import-attributes@7.28.6(@babel/core@7.29.0)':
dependencies:
- '@babel/core': 7.28.0
- '@babel/helper-plugin-utils': 7.27.1
+ '@babel/core': 7.29.0
+ '@babel/helper-plugin-utils': 7.28.6
- '@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.28.0)':
+ '@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.29.0)':
dependencies:
- '@babel/core': 7.28.0
- '@babel/helper-plugin-utils': 7.27.1
+ '@babel/core': 7.29.0
+ '@babel/helper-plugin-utils': 7.28.6
- '@babel/plugin-syntax-json-strings@7.8.3(@babel/core@7.28.0)':
+ '@babel/plugin-syntax-json-strings@7.8.3(@babel/core@7.29.0)':
dependencies:
- '@babel/core': 7.28.0
- '@babel/helper-plugin-utils': 7.27.1
+ '@babel/core': 7.29.0
+ '@babel/helper-plugin-utils': 7.28.6
- '@babel/plugin-syntax-jsx@7.27.1(@babel/core@7.28.0)':
+ '@babel/plugin-syntax-jsx@7.27.1(@babel/core@7.29.0)':
dependencies:
- '@babel/core': 7.28.0
- '@babel/helper-plugin-utils': 7.27.1
+ '@babel/core': 7.29.0
+ '@babel/helper-plugin-utils': 7.28.6
- '@babel/plugin-syntax-logical-assignment-operators@7.10.4(@babel/core@7.28.0)':
+ '@babel/plugin-syntax-logical-assignment-operators@7.10.4(@babel/core@7.29.0)':
dependencies:
- '@babel/core': 7.28.0
- '@babel/helper-plugin-utils': 7.27.1
+ '@babel/core': 7.29.0
+ '@babel/helper-plugin-utils': 7.28.6
- '@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.28.0)':
+ '@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.29.0)':
dependencies:
- '@babel/core': 7.28.0
- '@babel/helper-plugin-utils': 7.27.1
+ '@babel/core': 7.29.0
+ '@babel/helper-plugin-utils': 7.28.6
- '@babel/plugin-syntax-numeric-separator@7.10.4(@babel/core@7.28.0)':
+ '@babel/plugin-syntax-numeric-separator@7.10.4(@babel/core@7.29.0)':
dependencies:
- '@babel/core': 7.28.0
- '@babel/helper-plugin-utils': 7.27.1
+ '@babel/core': 7.29.0
+ '@babel/helper-plugin-utils': 7.28.6
- '@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.28.0)':
+ '@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.29.0)':
dependencies:
- '@babel/core': 7.28.0
- '@babel/helper-plugin-utils': 7.27.1
+ '@babel/core': 7.29.0
+ '@babel/helper-plugin-utils': 7.28.6
- '@babel/plugin-syntax-optional-catch-binding@7.8.3(@babel/core@7.28.0)':
+ '@babel/plugin-syntax-optional-catch-binding@7.8.3(@babel/core@7.29.0)':
dependencies:
- '@babel/core': 7.28.0
- '@babel/helper-plugin-utils': 7.27.1
+ '@babel/core': 7.29.0
+ '@babel/helper-plugin-utils': 7.28.6
- '@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.28.0)':
+ '@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.29.0)':
dependencies:
- '@babel/core': 7.28.0
- '@babel/helper-plugin-utils': 7.27.1
+ '@babel/core': 7.29.0
+ '@babel/helper-plugin-utils': 7.28.6
- '@babel/plugin-syntax-private-property-in-object@7.14.5(@babel/core@7.28.0)':
+ '@babel/plugin-syntax-private-property-in-object@7.14.5(@babel/core@7.29.0)':
dependencies:
- '@babel/core': 7.28.0
- '@babel/helper-plugin-utils': 7.27.1
+ '@babel/core': 7.29.0
+ '@babel/helper-plugin-utils': 7.28.6
- '@babel/plugin-syntax-top-level-await@7.14.5(@babel/core@7.28.0)':
+ '@babel/plugin-syntax-top-level-await@7.14.5(@babel/core@7.29.0)':
dependencies:
- '@babel/core': 7.28.0
- '@babel/helper-plugin-utils': 7.27.1
+ '@babel/core': 7.29.0
+ '@babel/helper-plugin-utils': 7.28.6
- '@babel/plugin-syntax-typescript@7.27.1(@babel/core@7.28.0)':
+ '@babel/plugin-syntax-typescript@7.27.1(@babel/core@7.29.0)':
dependencies:
- '@babel/core': 7.28.0
- '@babel/helper-plugin-utils': 7.27.1
+ '@babel/core': 7.29.0
+ '@babel/helper-plugin-utils': 7.28.6
- '@babel/plugin-syntax-unicode-sets-regex@7.18.6(@babel/core@7.28.0)':
+ '@babel/plugin-syntax-unicode-sets-regex@7.18.6(@babel/core@7.29.0)':
dependencies:
- '@babel/core': 7.28.0
- '@babel/helper-create-regexp-features-plugin': 7.27.1(@babel/core@7.28.0)
- '@babel/helper-plugin-utils': 7.27.1
+ '@babel/core': 7.29.0
+ '@babel/helper-create-regexp-features-plugin': 7.28.5(@babel/core@7.29.0)
+ '@babel/helper-plugin-utils': 7.28.6
- '@babel/plugin-transform-arrow-functions@7.27.1(@babel/core@7.28.0)':
+ '@babel/plugin-transform-arrow-functions@7.27.1(@babel/core@7.29.0)':
dependencies:
- '@babel/core': 7.28.0
- '@babel/helper-plugin-utils': 7.27.1
+ '@babel/core': 7.29.0
+ '@babel/helper-plugin-utils': 7.28.6
- '@babel/plugin-transform-async-generator-functions@7.28.0(@babel/core@7.28.0)':
+ '@babel/plugin-transform-async-generator-functions@7.29.0(@babel/core@7.29.0)':
dependencies:
- '@babel/core': 7.28.0
- '@babel/helper-plugin-utils': 7.27.1
- '@babel/helper-remap-async-to-generator': 7.27.1(@babel/core@7.28.0)
- '@babel/traverse': 7.28.0
+ '@babel/core': 7.29.0
+ '@babel/helper-plugin-utils': 7.28.6
+ '@babel/helper-remap-async-to-generator': 7.27.1(@babel/core@7.29.0)
+ '@babel/traverse': 7.29.0
transitivePeerDependencies:
- supports-color
- '@babel/plugin-transform-async-to-generator@7.27.1(@babel/core@7.28.0)':
+ '@babel/plugin-transform-async-to-generator@7.28.6(@babel/core@7.29.0)':
dependencies:
- '@babel/core': 7.28.0
- '@babel/helper-module-imports': 7.27.1
- '@babel/helper-plugin-utils': 7.27.1
- '@babel/helper-remap-async-to-generator': 7.27.1(@babel/core@7.28.0)
+ '@babel/core': 7.29.0
+ '@babel/helper-module-imports': 7.28.6
+ '@babel/helper-plugin-utils': 7.28.6
+ '@babel/helper-remap-async-to-generator': 7.27.1(@babel/core@7.29.0)
transitivePeerDependencies:
- supports-color
- '@babel/plugin-transform-block-scoped-functions@7.27.1(@babel/core@7.28.0)':
+ '@babel/plugin-transform-block-scoped-functions@7.27.1(@babel/core@7.29.0)':
dependencies:
- '@babel/core': 7.28.0
- '@babel/helper-plugin-utils': 7.27.1
+ '@babel/core': 7.29.0
+ '@babel/helper-plugin-utils': 7.28.6
- '@babel/plugin-transform-block-scoping@7.28.0(@babel/core@7.28.0)':
+ '@babel/plugin-transform-block-scoping@7.28.6(@babel/core@7.29.0)':
dependencies:
- '@babel/core': 7.28.0
- '@babel/helper-plugin-utils': 7.27.1
+ '@babel/core': 7.29.0
+ '@babel/helper-plugin-utils': 7.28.6
- '@babel/plugin-transform-class-properties@7.27.1(@babel/core@7.28.0)':
+ '@babel/plugin-transform-class-properties@7.28.6(@babel/core@7.29.0)':
dependencies:
- '@babel/core': 7.28.0
- '@babel/helper-create-class-features-plugin': 7.27.1(@babel/core@7.28.0)
- '@babel/helper-plugin-utils': 7.27.1
+ '@babel/core': 7.29.0
+ '@babel/helper-create-class-features-plugin': 7.28.6(@babel/core@7.29.0)
+ '@babel/helper-plugin-utils': 7.28.6
transitivePeerDependencies:
- supports-color
- '@babel/plugin-transform-class-static-block@7.27.1(@babel/core@7.28.0)':
+ '@babel/plugin-transform-class-static-block@7.28.6(@babel/core@7.29.0)':
dependencies:
- '@babel/core': 7.28.0
- '@babel/helper-create-class-features-plugin': 7.27.1(@babel/core@7.28.0)
- '@babel/helper-plugin-utils': 7.27.1
+ '@babel/core': 7.29.0
+ '@babel/helper-create-class-features-plugin': 7.28.6(@babel/core@7.29.0)
+ '@babel/helper-plugin-utils': 7.28.6
transitivePeerDependencies:
- supports-color
- '@babel/plugin-transform-classes@7.28.0(@babel/core@7.28.0)':
+ '@babel/plugin-transform-classes@7.28.6(@babel/core@7.29.0)':
dependencies:
- '@babel/core': 7.28.0
+ '@babel/core': 7.29.0
'@babel/helper-annotate-as-pure': 7.27.3
- '@babel/helper-compilation-targets': 7.27.2
+ '@babel/helper-compilation-targets': 7.28.6
'@babel/helper-globals': 7.28.0
- '@babel/helper-plugin-utils': 7.27.1
- '@babel/helper-replace-supers': 7.27.1(@babel/core@7.28.0)
- '@babel/traverse': 7.28.0
+ '@babel/helper-plugin-utils': 7.28.6
+ '@babel/helper-replace-supers': 7.28.6(@babel/core@7.29.0)
+ '@babel/traverse': 7.29.0
transitivePeerDependencies:
- supports-color
- '@babel/plugin-transform-computed-properties@7.27.1(@babel/core@7.28.0)':
+ '@babel/plugin-transform-computed-properties@7.28.6(@babel/core@7.29.0)':
dependencies:
- '@babel/core': 7.28.0
- '@babel/helper-plugin-utils': 7.27.1
- '@babel/template': 7.27.2
+ '@babel/core': 7.29.0
+ '@babel/helper-plugin-utils': 7.28.6
+ '@babel/template': 7.28.6
- '@babel/plugin-transform-destructuring@7.28.0(@babel/core@7.28.0)':
+ '@babel/plugin-transform-destructuring@7.28.5(@babel/core@7.29.0)':
dependencies:
- '@babel/core': 7.28.0
- '@babel/helper-plugin-utils': 7.27.1
- '@babel/traverse': 7.28.0
+ '@babel/core': 7.29.0
+ '@babel/helper-plugin-utils': 7.28.6
+ '@babel/traverse': 7.29.0
transitivePeerDependencies:
- supports-color
- '@babel/plugin-transform-dotall-regex@7.27.1(@babel/core@7.28.0)':
+ '@babel/plugin-transform-dotall-regex@7.28.6(@babel/core@7.29.0)':
dependencies:
- '@babel/core': 7.28.0
- '@babel/helper-create-regexp-features-plugin': 7.27.1(@babel/core@7.28.0)
- '@babel/helper-plugin-utils': 7.27.1
+ '@babel/core': 7.29.0
+ '@babel/helper-create-regexp-features-plugin': 7.28.5(@babel/core@7.29.0)
+ '@babel/helper-plugin-utils': 7.28.6
- '@babel/plugin-transform-duplicate-keys@7.27.1(@babel/core@7.28.0)':
+ '@babel/plugin-transform-duplicate-keys@7.27.1(@babel/core@7.29.0)':
dependencies:
- '@babel/core': 7.28.0
- '@babel/helper-plugin-utils': 7.27.1
+ '@babel/core': 7.29.0
+ '@babel/helper-plugin-utils': 7.28.6
- '@babel/plugin-transform-duplicate-named-capturing-groups-regex@7.27.1(@babel/core@7.28.0)':
+ '@babel/plugin-transform-duplicate-named-capturing-groups-regex@7.29.0(@babel/core@7.29.0)':
dependencies:
- '@babel/core': 7.28.0
- '@babel/helper-create-regexp-features-plugin': 7.27.1(@babel/core@7.28.0)
- '@babel/helper-plugin-utils': 7.27.1
+ '@babel/core': 7.29.0
+ '@babel/helper-create-regexp-features-plugin': 7.28.5(@babel/core@7.29.0)
+ '@babel/helper-plugin-utils': 7.28.6
- '@babel/plugin-transform-dynamic-import@7.27.1(@babel/core@7.28.0)':
+ '@babel/plugin-transform-dynamic-import@7.27.1(@babel/core@7.29.0)':
dependencies:
- '@babel/core': 7.28.0
- '@babel/helper-plugin-utils': 7.27.1
+ '@babel/core': 7.29.0
+ '@babel/helper-plugin-utils': 7.28.6
- '@babel/plugin-transform-explicit-resource-management@7.28.0(@babel/core@7.28.0)':
+ '@babel/plugin-transform-explicit-resource-management@7.28.6(@babel/core@7.29.0)':
dependencies:
- '@babel/core': 7.28.0
- '@babel/helper-plugin-utils': 7.27.1
- '@babel/plugin-transform-destructuring': 7.28.0(@babel/core@7.28.0)
+ '@babel/core': 7.29.0
+ '@babel/helper-plugin-utils': 7.28.6
+ '@babel/plugin-transform-destructuring': 7.28.5(@babel/core@7.29.0)
transitivePeerDependencies:
- supports-color
- '@babel/plugin-transform-exponentiation-operator@7.27.1(@babel/core@7.28.0)':
+ '@babel/plugin-transform-exponentiation-operator@7.28.6(@babel/core@7.29.0)':
dependencies:
- '@babel/core': 7.28.0
- '@babel/helper-plugin-utils': 7.27.1
+ '@babel/core': 7.29.0
+ '@babel/helper-plugin-utils': 7.28.6
- '@babel/plugin-transform-export-namespace-from@7.27.1(@babel/core@7.28.0)':
+ '@babel/plugin-transform-export-namespace-from@7.27.1(@babel/core@7.29.0)':
dependencies:
- '@babel/core': 7.28.0
- '@babel/helper-plugin-utils': 7.27.1
+ '@babel/core': 7.29.0
+ '@babel/helper-plugin-utils': 7.28.6
- '@babel/plugin-transform-flow-strip-types@7.27.1(@babel/core@7.28.0)':
+ '@babel/plugin-transform-flow-strip-types@7.27.1(@babel/core@7.29.0)':
dependencies:
- '@babel/core': 7.28.0
- '@babel/helper-plugin-utils': 7.27.1
- '@babel/plugin-syntax-flow': 7.27.1(@babel/core@7.28.0)
+ '@babel/core': 7.29.0
+ '@babel/helper-plugin-utils': 7.28.6
+ '@babel/plugin-syntax-flow': 7.27.1(@babel/core@7.29.0)
- '@babel/plugin-transform-for-of@7.27.1(@babel/core@7.28.0)':
+ '@babel/plugin-transform-for-of@7.27.1(@babel/core@7.29.0)':
dependencies:
- '@babel/core': 7.28.0
- '@babel/helper-plugin-utils': 7.27.1
+ '@babel/core': 7.29.0
+ '@babel/helper-plugin-utils': 7.28.6
'@babel/helper-skip-transparent-expression-wrappers': 7.27.1
transitivePeerDependencies:
- supports-color
- '@babel/plugin-transform-function-name@7.27.1(@babel/core@7.28.0)':
+ '@babel/plugin-transform-function-name@7.27.1(@babel/core@7.29.0)':
dependencies:
- '@babel/core': 7.28.0
- '@babel/helper-compilation-targets': 7.27.2
- '@babel/helper-plugin-utils': 7.27.1
- '@babel/traverse': 7.28.0
+ '@babel/core': 7.29.0
+ '@babel/helper-compilation-targets': 7.28.6
+ '@babel/helper-plugin-utils': 7.28.6
+ '@babel/traverse': 7.29.0
transitivePeerDependencies:
- supports-color
- '@babel/plugin-transform-json-strings@7.27.1(@babel/core@7.28.0)':
+ '@babel/plugin-transform-json-strings@7.28.6(@babel/core@7.29.0)':
dependencies:
- '@babel/core': 7.28.0
- '@babel/helper-plugin-utils': 7.27.1
+ '@babel/core': 7.29.0
+ '@babel/helper-plugin-utils': 7.28.6
- '@babel/plugin-transform-literals@7.27.1(@babel/core@7.28.0)':
+ '@babel/plugin-transform-literals@7.27.1(@babel/core@7.29.0)':
dependencies:
- '@babel/core': 7.28.0
- '@babel/helper-plugin-utils': 7.27.1
+ '@babel/core': 7.29.0
+ '@babel/helper-plugin-utils': 7.28.6
- '@babel/plugin-transform-logical-assignment-operators@7.27.1(@babel/core@7.28.0)':
+ '@babel/plugin-transform-logical-assignment-operators@7.28.6(@babel/core@7.29.0)':
dependencies:
- '@babel/core': 7.28.0
- '@babel/helper-plugin-utils': 7.27.1
+ '@babel/core': 7.29.0
+ '@babel/helper-plugin-utils': 7.28.6
- '@babel/plugin-transform-member-expression-literals@7.27.1(@babel/core@7.28.0)':
+ '@babel/plugin-transform-member-expression-literals@7.27.1(@babel/core@7.29.0)':
dependencies:
- '@babel/core': 7.28.0
- '@babel/helper-plugin-utils': 7.27.1
+ '@babel/core': 7.29.0
+ '@babel/helper-plugin-utils': 7.28.6
- '@babel/plugin-transform-modules-amd@7.27.1(@babel/core@7.28.0)':
+ '@babel/plugin-transform-modules-amd@7.27.1(@babel/core@7.29.0)':
dependencies:
- '@babel/core': 7.28.0
- '@babel/helper-module-transforms': 7.27.3(@babel/core@7.28.0)
- '@babel/helper-plugin-utils': 7.27.1
+ '@babel/core': 7.29.0
+ '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0)
+ '@babel/helper-plugin-utils': 7.28.6
transitivePeerDependencies:
- supports-color
- '@babel/plugin-transform-modules-commonjs@7.27.1(@babel/core@7.28.0)':
+ '@babel/plugin-transform-modules-commonjs@7.28.6(@babel/core@7.29.0)':
dependencies:
- '@babel/core': 7.28.0
- '@babel/helper-module-transforms': 7.27.3(@babel/core@7.28.0)
- '@babel/helper-plugin-utils': 7.27.1
+ '@babel/core': 7.29.0
+ '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0)
+ '@babel/helper-plugin-utils': 7.28.6
transitivePeerDependencies:
- supports-color
- '@babel/plugin-transform-modules-systemjs@7.27.1(@babel/core@7.28.0)':
+ '@babel/plugin-transform-modules-systemjs@7.29.0(@babel/core@7.29.0)':
dependencies:
- '@babel/core': 7.28.0
- '@babel/helper-module-transforms': 7.27.3(@babel/core@7.28.0)
- '@babel/helper-plugin-utils': 7.27.1
- '@babel/helper-validator-identifier': 7.27.1
- '@babel/traverse': 7.28.0
+ '@babel/core': 7.29.0
+ '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0)
+ '@babel/helper-plugin-utils': 7.28.6
+ '@babel/helper-validator-identifier': 7.28.5
+ '@babel/traverse': 7.29.0
transitivePeerDependencies:
- supports-color
- '@babel/plugin-transform-modules-umd@7.27.1(@babel/core@7.28.0)':
+ '@babel/plugin-transform-modules-umd@7.27.1(@babel/core@7.29.0)':
dependencies:
- '@babel/core': 7.28.0
- '@babel/helper-module-transforms': 7.27.3(@babel/core@7.28.0)
- '@babel/helper-plugin-utils': 7.27.1
+ '@babel/core': 7.29.0
+ '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0)
+ '@babel/helper-plugin-utils': 7.28.6
transitivePeerDependencies:
- supports-color
- '@babel/plugin-transform-named-capturing-groups-regex@7.27.1(@babel/core@7.28.0)':
+ '@babel/plugin-transform-named-capturing-groups-regex@7.29.0(@babel/core@7.29.0)':
dependencies:
- '@babel/core': 7.28.0
- '@babel/helper-create-regexp-features-plugin': 7.27.1(@babel/core@7.28.0)
- '@babel/helper-plugin-utils': 7.27.1
+ '@babel/core': 7.29.0
+ '@babel/helper-create-regexp-features-plugin': 7.28.5(@babel/core@7.29.0)
+ '@babel/helper-plugin-utils': 7.28.6
- '@babel/plugin-transform-new-target@7.27.1(@babel/core@7.28.0)':
+ '@babel/plugin-transform-new-target@7.27.1(@babel/core@7.29.0)':
dependencies:
- '@babel/core': 7.28.0
- '@babel/helper-plugin-utils': 7.27.1
+ '@babel/core': 7.29.0
+ '@babel/helper-plugin-utils': 7.28.6
- '@babel/plugin-transform-nullish-coalescing-operator@7.27.1(@babel/core@7.28.0)':
+ '@babel/plugin-transform-nullish-coalescing-operator@7.28.6(@babel/core@7.29.0)':
dependencies:
- '@babel/core': 7.28.0
- '@babel/helper-plugin-utils': 7.27.1
+ '@babel/core': 7.29.0
+ '@babel/helper-plugin-utils': 7.28.6
- '@babel/plugin-transform-numeric-separator@7.27.1(@babel/core@7.28.0)':
+ '@babel/plugin-transform-numeric-separator@7.28.6(@babel/core@7.29.0)':
dependencies:
- '@babel/core': 7.28.0
- '@babel/helper-plugin-utils': 7.27.1
+ '@babel/core': 7.29.0
+ '@babel/helper-plugin-utils': 7.28.6
- '@babel/plugin-transform-object-rest-spread@7.28.0(@babel/core@7.28.0)':
+ '@babel/plugin-transform-object-rest-spread@7.28.6(@babel/core@7.29.0)':
dependencies:
- '@babel/core': 7.28.0
- '@babel/helper-compilation-targets': 7.27.2
- '@babel/helper-plugin-utils': 7.27.1
- '@babel/plugin-transform-destructuring': 7.28.0(@babel/core@7.28.0)
- '@babel/plugin-transform-parameters': 7.27.7(@babel/core@7.28.0)
- '@babel/traverse': 7.28.0
+ '@babel/core': 7.29.0
+ '@babel/helper-compilation-targets': 7.28.6
+ '@babel/helper-plugin-utils': 7.28.6
+ '@babel/plugin-transform-destructuring': 7.28.5(@babel/core@7.29.0)
+ '@babel/plugin-transform-parameters': 7.27.7(@babel/core@7.29.0)
+ '@babel/traverse': 7.29.0
transitivePeerDependencies:
- supports-color
- '@babel/plugin-transform-object-super@7.27.1(@babel/core@7.28.0)':
+ '@babel/plugin-transform-object-super@7.27.1(@babel/core@7.29.0)':
dependencies:
- '@babel/core': 7.28.0
- '@babel/helper-plugin-utils': 7.27.1
- '@babel/helper-replace-supers': 7.27.1(@babel/core@7.28.0)
+ '@babel/core': 7.29.0
+ '@babel/helper-plugin-utils': 7.28.6
+ '@babel/helper-replace-supers': 7.28.6(@babel/core@7.29.0)
transitivePeerDependencies:
- supports-color
- '@babel/plugin-transform-optional-catch-binding@7.27.1(@babel/core@7.28.0)':
+ '@babel/plugin-transform-optional-catch-binding@7.28.6(@babel/core@7.29.0)':
dependencies:
- '@babel/core': 7.28.0
- '@babel/helper-plugin-utils': 7.27.1
+ '@babel/core': 7.29.0
+ '@babel/helper-plugin-utils': 7.28.6
- '@babel/plugin-transform-optional-chaining@7.27.1(@babel/core@7.28.0)':
+ '@babel/plugin-transform-optional-chaining@7.28.6(@babel/core@7.29.0)':
dependencies:
- '@babel/core': 7.28.0
- '@babel/helper-plugin-utils': 7.27.1
+ '@babel/core': 7.29.0
+ '@babel/helper-plugin-utils': 7.28.6
'@babel/helper-skip-transparent-expression-wrappers': 7.27.1
transitivePeerDependencies:
- supports-color
- '@babel/plugin-transform-parameters@7.27.7(@babel/core@7.28.0)':
+ '@babel/plugin-transform-parameters@7.27.7(@babel/core@7.29.0)':
dependencies:
- '@babel/core': 7.28.0
- '@babel/helper-plugin-utils': 7.27.1
+ '@babel/core': 7.29.0
+ '@babel/helper-plugin-utils': 7.28.6
- '@babel/plugin-transform-private-methods@7.27.1(@babel/core@7.28.0)':
+ '@babel/plugin-transform-private-methods@7.28.6(@babel/core@7.29.0)':
dependencies:
- '@babel/core': 7.28.0
- '@babel/helper-create-class-features-plugin': 7.27.1(@babel/core@7.28.0)
- '@babel/helper-plugin-utils': 7.27.1
+ '@babel/core': 7.29.0
+ '@babel/helper-create-class-features-plugin': 7.28.6(@babel/core@7.29.0)
+ '@babel/helper-plugin-utils': 7.28.6
transitivePeerDependencies:
- supports-color
- '@babel/plugin-transform-private-property-in-object@7.27.1(@babel/core@7.28.0)':
+ '@babel/plugin-transform-private-property-in-object@7.28.6(@babel/core@7.29.0)':
dependencies:
- '@babel/core': 7.28.0
+ '@babel/core': 7.29.0
'@babel/helper-annotate-as-pure': 7.27.3
- '@babel/helper-create-class-features-plugin': 7.27.1(@babel/core@7.28.0)
- '@babel/helper-plugin-utils': 7.27.1
+ '@babel/helper-create-class-features-plugin': 7.28.6(@babel/core@7.29.0)
+ '@babel/helper-plugin-utils': 7.28.6
transitivePeerDependencies:
- supports-color
- '@babel/plugin-transform-property-literals@7.27.1(@babel/core@7.28.0)':
+ '@babel/plugin-transform-property-literals@7.27.1(@babel/core@7.29.0)':
dependencies:
- '@babel/core': 7.28.0
- '@babel/helper-plugin-utils': 7.27.1
+ '@babel/core': 7.29.0
+ '@babel/helper-plugin-utils': 7.28.6
- '@babel/plugin-transform-regenerator@7.28.0(@babel/core@7.28.0)':
+ '@babel/plugin-transform-regenerator@7.29.0(@babel/core@7.29.0)':
dependencies:
- '@babel/core': 7.28.0
- '@babel/helper-plugin-utils': 7.27.1
+ '@babel/core': 7.29.0
+ '@babel/helper-plugin-utils': 7.28.6
- '@babel/plugin-transform-regexp-modifiers@7.27.1(@babel/core@7.28.0)':
+ '@babel/plugin-transform-regexp-modifiers@7.28.6(@babel/core@7.29.0)':
dependencies:
- '@babel/core': 7.28.0
- '@babel/helper-create-regexp-features-plugin': 7.27.1(@babel/core@7.28.0)
- '@babel/helper-plugin-utils': 7.27.1
+ '@babel/core': 7.29.0
+ '@babel/helper-create-regexp-features-plugin': 7.28.5(@babel/core@7.29.0)
+ '@babel/helper-plugin-utils': 7.28.6
- '@babel/plugin-transform-reserved-words@7.27.1(@babel/core@7.28.0)':
+ '@babel/plugin-transform-reserved-words@7.27.1(@babel/core@7.29.0)':
dependencies:
- '@babel/core': 7.28.0
- '@babel/helper-plugin-utils': 7.27.1
+ '@babel/core': 7.29.0
+ '@babel/helper-plugin-utils': 7.28.6
- '@babel/plugin-transform-runtime@7.28.0(@babel/core@7.28.0)':
+ '@babel/plugin-transform-runtime@7.29.0(@babel/core@7.29.0)':
dependencies:
- '@babel/core': 7.28.0
- '@babel/helper-module-imports': 7.27.1
- '@babel/helper-plugin-utils': 7.27.1
- babel-plugin-polyfill-corejs2: 0.4.14(@babel/core@7.28.0)
- babel-plugin-polyfill-corejs3: 0.13.0(@babel/core@7.28.0)
- babel-plugin-polyfill-regenerator: 0.6.5(@babel/core@7.28.0)
+ '@babel/core': 7.29.0
+ '@babel/helper-module-imports': 7.28.6
+ '@babel/helper-plugin-utils': 7.28.6
+ babel-plugin-polyfill-corejs2: 0.4.15(@babel/core@7.29.0)
+ babel-plugin-polyfill-corejs3: 0.13.0(@babel/core@7.29.0)
+ babel-plugin-polyfill-regenerator: 0.6.6(@babel/core@7.29.0)
semver: 6.3.1
transitivePeerDependencies:
- supports-color
- '@babel/plugin-transform-shorthand-properties@7.27.1(@babel/core@7.28.0)':
+ '@babel/plugin-transform-shorthand-properties@7.27.1(@babel/core@7.29.0)':
dependencies:
- '@babel/core': 7.28.0
- '@babel/helper-plugin-utils': 7.27.1
+ '@babel/core': 7.29.0
+ '@babel/helper-plugin-utils': 7.28.6
- '@babel/plugin-transform-spread@7.27.1(@babel/core@7.28.0)':
+ '@babel/plugin-transform-spread@7.28.6(@babel/core@7.29.0)':
dependencies:
- '@babel/core': 7.28.0
- '@babel/helper-plugin-utils': 7.27.1
+ '@babel/core': 7.29.0
+ '@babel/helper-plugin-utils': 7.28.6
'@babel/helper-skip-transparent-expression-wrappers': 7.27.1
transitivePeerDependencies:
- supports-color
- '@babel/plugin-transform-sticky-regex@7.27.1(@babel/core@7.28.0)':
+ '@babel/plugin-transform-sticky-regex@7.27.1(@babel/core@7.29.0)':
dependencies:
- '@babel/core': 7.28.0
- '@babel/helper-plugin-utils': 7.27.1
+ '@babel/core': 7.29.0
+ '@babel/helper-plugin-utils': 7.28.6
- '@babel/plugin-transform-template-literals@7.27.1(@babel/core@7.28.0)':
+ '@babel/plugin-transform-template-literals@7.27.1(@babel/core@7.29.0)':
dependencies:
- '@babel/core': 7.28.0
- '@babel/helper-plugin-utils': 7.27.1
+ '@babel/core': 7.29.0
+ '@babel/helper-plugin-utils': 7.28.6
- '@babel/plugin-transform-typeof-symbol@7.27.1(@babel/core@7.28.0)':
+ '@babel/plugin-transform-typeof-symbol@7.27.1(@babel/core@7.29.0)':
dependencies:
- '@babel/core': 7.28.0
- '@babel/helper-plugin-utils': 7.27.1
+ '@babel/core': 7.29.0
+ '@babel/helper-plugin-utils': 7.28.6
- '@babel/plugin-transform-typescript@7.28.0(@babel/core@7.28.0)':
+ '@babel/plugin-transform-typescript@7.28.0(@babel/core@7.29.0)':
dependencies:
- '@babel/core': 7.28.0
+ '@babel/core': 7.29.0
'@babel/helper-annotate-as-pure': 7.27.3
- '@babel/helper-create-class-features-plugin': 7.27.1(@babel/core@7.28.0)
- '@babel/helper-plugin-utils': 7.27.1
+ '@babel/helper-create-class-features-plugin': 7.28.6(@babel/core@7.29.0)
+ '@babel/helper-plugin-utils': 7.28.6
'@babel/helper-skip-transparent-expression-wrappers': 7.27.1
- '@babel/plugin-syntax-typescript': 7.27.1(@babel/core@7.28.0)
+ '@babel/plugin-syntax-typescript': 7.27.1(@babel/core@7.29.0)
transitivePeerDependencies:
- supports-color
- '@babel/plugin-transform-unicode-escapes@7.27.1(@babel/core@7.28.0)':
+ '@babel/plugin-transform-unicode-escapes@7.27.1(@babel/core@7.29.0)':
dependencies:
- '@babel/core': 7.28.0
- '@babel/helper-plugin-utils': 7.27.1
+ '@babel/core': 7.29.0
+ '@babel/helper-plugin-utils': 7.28.6
- '@babel/plugin-transform-unicode-property-regex@7.27.1(@babel/core@7.28.0)':
+ '@babel/plugin-transform-unicode-property-regex@7.28.6(@babel/core@7.29.0)':
dependencies:
- '@babel/core': 7.28.0
- '@babel/helper-create-regexp-features-plugin': 7.27.1(@babel/core@7.28.0)
- '@babel/helper-plugin-utils': 7.27.1
+ '@babel/core': 7.29.0
+ '@babel/helper-create-regexp-features-plugin': 7.28.5(@babel/core@7.29.0)
+ '@babel/helper-plugin-utils': 7.28.6
- '@babel/plugin-transform-unicode-regex@7.27.1(@babel/core@7.28.0)':
+ '@babel/plugin-transform-unicode-regex@7.27.1(@babel/core@7.29.0)':
dependencies:
- '@babel/core': 7.28.0
- '@babel/helper-create-regexp-features-plugin': 7.27.1(@babel/core@7.28.0)
- '@babel/helper-plugin-utils': 7.27.1
+ '@babel/core': 7.29.0
+ '@babel/helper-create-regexp-features-plugin': 7.28.5(@babel/core@7.29.0)
+ '@babel/helper-plugin-utils': 7.28.6
- '@babel/plugin-transform-unicode-sets-regex@7.27.1(@babel/core@7.28.0)':
+ '@babel/plugin-transform-unicode-sets-regex@7.28.6(@babel/core@7.29.0)':
dependencies:
- '@babel/core': 7.28.0
- '@babel/helper-create-regexp-features-plugin': 7.27.1(@babel/core@7.28.0)
- '@babel/helper-plugin-utils': 7.27.1
+ '@babel/core': 7.29.0
+ '@babel/helper-create-regexp-features-plugin': 7.28.5(@babel/core@7.29.0)
+ '@babel/helper-plugin-utils': 7.28.6
- '@babel/preset-env@7.28.0(@babel/core@7.28.0)':
+ '@babel/preset-env@7.29.0(@babel/core@7.29.0)':
dependencies:
- '@babel/compat-data': 7.28.0
- '@babel/core': 7.28.0
- '@babel/helper-compilation-targets': 7.27.2
- '@babel/helper-plugin-utils': 7.27.1
+ '@babel/compat-data': 7.29.0
+ '@babel/core': 7.29.0
+ '@babel/helper-compilation-targets': 7.28.6
+ '@babel/helper-plugin-utils': 7.28.6
'@babel/helper-validator-option': 7.27.1
- '@babel/plugin-bugfix-firefox-class-in-computed-class-key': 7.27.1(@babel/core@7.28.0)
- '@babel/plugin-bugfix-safari-class-field-initializer-scope': 7.27.1(@babel/core@7.28.0)
- '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression': 7.27.1(@babel/core@7.28.0)
- '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining': 7.27.1(@babel/core@7.28.0)
- '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly': 7.27.1(@babel/core@7.28.0)
- '@babel/plugin-proposal-private-property-in-object': 7.21.0-placeholder-for-preset-env.2(@babel/core@7.28.0)
- '@babel/plugin-syntax-import-assertions': 7.27.1(@babel/core@7.28.0)
- '@babel/plugin-syntax-import-attributes': 7.27.1(@babel/core@7.28.0)
- '@babel/plugin-syntax-unicode-sets-regex': 7.18.6(@babel/core@7.28.0)
- '@babel/plugin-transform-arrow-functions': 7.27.1(@babel/core@7.28.0)
- '@babel/plugin-transform-async-generator-functions': 7.28.0(@babel/core@7.28.0)
- '@babel/plugin-transform-async-to-generator': 7.27.1(@babel/core@7.28.0)
- '@babel/plugin-transform-block-scoped-functions': 7.27.1(@babel/core@7.28.0)
- '@babel/plugin-transform-block-scoping': 7.28.0(@babel/core@7.28.0)
- '@babel/plugin-transform-class-properties': 7.27.1(@babel/core@7.28.0)
- '@babel/plugin-transform-class-static-block': 7.27.1(@babel/core@7.28.0)
- '@babel/plugin-transform-classes': 7.28.0(@babel/core@7.28.0)
- '@babel/plugin-transform-computed-properties': 7.27.1(@babel/core@7.28.0)
- '@babel/plugin-transform-destructuring': 7.28.0(@babel/core@7.28.0)
- '@babel/plugin-transform-dotall-regex': 7.27.1(@babel/core@7.28.0)
- '@babel/plugin-transform-duplicate-keys': 7.27.1(@babel/core@7.28.0)
- '@babel/plugin-transform-duplicate-named-capturing-groups-regex': 7.27.1(@babel/core@7.28.0)
- '@babel/plugin-transform-dynamic-import': 7.27.1(@babel/core@7.28.0)
- '@babel/plugin-transform-explicit-resource-management': 7.28.0(@babel/core@7.28.0)
- '@babel/plugin-transform-exponentiation-operator': 7.27.1(@babel/core@7.28.0)
- '@babel/plugin-transform-export-namespace-from': 7.27.1(@babel/core@7.28.0)
- '@babel/plugin-transform-for-of': 7.27.1(@babel/core@7.28.0)
- '@babel/plugin-transform-function-name': 7.27.1(@babel/core@7.28.0)
- '@babel/plugin-transform-json-strings': 7.27.1(@babel/core@7.28.0)
- '@babel/plugin-transform-literals': 7.27.1(@babel/core@7.28.0)
- '@babel/plugin-transform-logical-assignment-operators': 7.27.1(@babel/core@7.28.0)
- '@babel/plugin-transform-member-expression-literals': 7.27.1(@babel/core@7.28.0)
- '@babel/plugin-transform-modules-amd': 7.27.1(@babel/core@7.28.0)
- '@babel/plugin-transform-modules-commonjs': 7.27.1(@babel/core@7.28.0)
- '@babel/plugin-transform-modules-systemjs': 7.27.1(@babel/core@7.28.0)
- '@babel/plugin-transform-modules-umd': 7.27.1(@babel/core@7.28.0)
- '@babel/plugin-transform-named-capturing-groups-regex': 7.27.1(@babel/core@7.28.0)
- '@babel/plugin-transform-new-target': 7.27.1(@babel/core@7.28.0)
- '@babel/plugin-transform-nullish-coalescing-operator': 7.27.1(@babel/core@7.28.0)
- '@babel/plugin-transform-numeric-separator': 7.27.1(@babel/core@7.28.0)
- '@babel/plugin-transform-object-rest-spread': 7.28.0(@babel/core@7.28.0)
- '@babel/plugin-transform-object-super': 7.27.1(@babel/core@7.28.0)
- '@babel/plugin-transform-optional-catch-binding': 7.27.1(@babel/core@7.28.0)
- '@babel/plugin-transform-optional-chaining': 7.27.1(@babel/core@7.28.0)
- '@babel/plugin-transform-parameters': 7.27.7(@babel/core@7.28.0)
- '@babel/plugin-transform-private-methods': 7.27.1(@babel/core@7.28.0)
- '@babel/plugin-transform-private-property-in-object': 7.27.1(@babel/core@7.28.0)
- '@babel/plugin-transform-property-literals': 7.27.1(@babel/core@7.28.0)
- '@babel/plugin-transform-regenerator': 7.28.0(@babel/core@7.28.0)
- '@babel/plugin-transform-regexp-modifiers': 7.27.1(@babel/core@7.28.0)
- '@babel/plugin-transform-reserved-words': 7.27.1(@babel/core@7.28.0)
- '@babel/plugin-transform-shorthand-properties': 7.27.1(@babel/core@7.28.0)
- '@babel/plugin-transform-spread': 7.27.1(@babel/core@7.28.0)
- '@babel/plugin-transform-sticky-regex': 7.27.1(@babel/core@7.28.0)
- '@babel/plugin-transform-template-literals': 7.27.1(@babel/core@7.28.0)
- '@babel/plugin-transform-typeof-symbol': 7.27.1(@babel/core@7.28.0)
- '@babel/plugin-transform-unicode-escapes': 7.27.1(@babel/core@7.28.0)
- '@babel/plugin-transform-unicode-property-regex': 7.27.1(@babel/core@7.28.0)
- '@babel/plugin-transform-unicode-regex': 7.27.1(@babel/core@7.28.0)
- '@babel/plugin-transform-unicode-sets-regex': 7.27.1(@babel/core@7.28.0)
- '@babel/preset-modules': 0.1.6-no-external-plugins(@babel/core@7.28.0)
- babel-plugin-polyfill-corejs2: 0.4.14(@babel/core@7.28.0)
- babel-plugin-polyfill-corejs3: 0.13.0(@babel/core@7.28.0)
- babel-plugin-polyfill-regenerator: 0.6.5(@babel/core@7.28.0)
- core-js-compat: 3.43.0
+ '@babel/plugin-bugfix-firefox-class-in-computed-class-key': 7.28.5(@babel/core@7.29.0)
+ '@babel/plugin-bugfix-safari-class-field-initializer-scope': 7.27.1(@babel/core@7.29.0)
+ '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression': 7.27.1(@babel/core@7.29.0)
+ '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining': 7.27.1(@babel/core@7.29.0)
+ '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly': 7.28.6(@babel/core@7.29.0)
+ '@babel/plugin-proposal-private-property-in-object': 7.21.0-placeholder-for-preset-env.2(@babel/core@7.29.0)
+ '@babel/plugin-syntax-import-assertions': 7.28.6(@babel/core@7.29.0)
+ '@babel/plugin-syntax-import-attributes': 7.28.6(@babel/core@7.29.0)
+ '@babel/plugin-syntax-unicode-sets-regex': 7.18.6(@babel/core@7.29.0)
+ '@babel/plugin-transform-arrow-functions': 7.27.1(@babel/core@7.29.0)
+ '@babel/plugin-transform-async-generator-functions': 7.29.0(@babel/core@7.29.0)
+ '@babel/plugin-transform-async-to-generator': 7.28.6(@babel/core@7.29.0)
+ '@babel/plugin-transform-block-scoped-functions': 7.27.1(@babel/core@7.29.0)
+ '@babel/plugin-transform-block-scoping': 7.28.6(@babel/core@7.29.0)
+ '@babel/plugin-transform-class-properties': 7.28.6(@babel/core@7.29.0)
+ '@babel/plugin-transform-class-static-block': 7.28.6(@babel/core@7.29.0)
+ '@babel/plugin-transform-classes': 7.28.6(@babel/core@7.29.0)
+ '@babel/plugin-transform-computed-properties': 7.28.6(@babel/core@7.29.0)
+ '@babel/plugin-transform-destructuring': 7.28.5(@babel/core@7.29.0)
+ '@babel/plugin-transform-dotall-regex': 7.28.6(@babel/core@7.29.0)
+ '@babel/plugin-transform-duplicate-keys': 7.27.1(@babel/core@7.29.0)
+ '@babel/plugin-transform-duplicate-named-capturing-groups-regex': 7.29.0(@babel/core@7.29.0)
+ '@babel/plugin-transform-dynamic-import': 7.27.1(@babel/core@7.29.0)
+ '@babel/plugin-transform-explicit-resource-management': 7.28.6(@babel/core@7.29.0)
+ '@babel/plugin-transform-exponentiation-operator': 7.28.6(@babel/core@7.29.0)
+ '@babel/plugin-transform-export-namespace-from': 7.27.1(@babel/core@7.29.0)
+ '@babel/plugin-transform-for-of': 7.27.1(@babel/core@7.29.0)
+ '@babel/plugin-transform-function-name': 7.27.1(@babel/core@7.29.0)
+ '@babel/plugin-transform-json-strings': 7.28.6(@babel/core@7.29.0)
+ '@babel/plugin-transform-literals': 7.27.1(@babel/core@7.29.0)
+ '@babel/plugin-transform-logical-assignment-operators': 7.28.6(@babel/core@7.29.0)
+ '@babel/plugin-transform-member-expression-literals': 7.27.1(@babel/core@7.29.0)
+ '@babel/plugin-transform-modules-amd': 7.27.1(@babel/core@7.29.0)
+ '@babel/plugin-transform-modules-commonjs': 7.28.6(@babel/core@7.29.0)
+ '@babel/plugin-transform-modules-systemjs': 7.29.0(@babel/core@7.29.0)
+ '@babel/plugin-transform-modules-umd': 7.27.1(@babel/core@7.29.0)
+ '@babel/plugin-transform-named-capturing-groups-regex': 7.29.0(@babel/core@7.29.0)
+ '@babel/plugin-transform-new-target': 7.27.1(@babel/core@7.29.0)
+ '@babel/plugin-transform-nullish-coalescing-operator': 7.28.6(@babel/core@7.29.0)
+ '@babel/plugin-transform-numeric-separator': 7.28.6(@babel/core@7.29.0)
+ '@babel/plugin-transform-object-rest-spread': 7.28.6(@babel/core@7.29.0)
+ '@babel/plugin-transform-object-super': 7.27.1(@babel/core@7.29.0)
+ '@babel/plugin-transform-optional-catch-binding': 7.28.6(@babel/core@7.29.0)
+ '@babel/plugin-transform-optional-chaining': 7.28.6(@babel/core@7.29.0)
+ '@babel/plugin-transform-parameters': 7.27.7(@babel/core@7.29.0)
+ '@babel/plugin-transform-private-methods': 7.28.6(@babel/core@7.29.0)
+ '@babel/plugin-transform-private-property-in-object': 7.28.6(@babel/core@7.29.0)
+ '@babel/plugin-transform-property-literals': 7.27.1(@babel/core@7.29.0)
+ '@babel/plugin-transform-regenerator': 7.29.0(@babel/core@7.29.0)
+ '@babel/plugin-transform-regexp-modifiers': 7.28.6(@babel/core@7.29.0)
+ '@babel/plugin-transform-reserved-words': 7.27.1(@babel/core@7.29.0)
+ '@babel/plugin-transform-shorthand-properties': 7.27.1(@babel/core@7.29.0)
+ '@babel/plugin-transform-spread': 7.28.6(@babel/core@7.29.0)
+ '@babel/plugin-transform-sticky-regex': 7.27.1(@babel/core@7.29.0)
+ '@babel/plugin-transform-template-literals': 7.27.1(@babel/core@7.29.0)
+ '@babel/plugin-transform-typeof-symbol': 7.27.1(@babel/core@7.29.0)
+ '@babel/plugin-transform-unicode-escapes': 7.27.1(@babel/core@7.29.0)
+ '@babel/plugin-transform-unicode-property-regex': 7.28.6(@babel/core@7.29.0)
+ '@babel/plugin-transform-unicode-regex': 7.27.1(@babel/core@7.29.0)
+ '@babel/plugin-transform-unicode-sets-regex': 7.28.6(@babel/core@7.29.0)
+ '@babel/preset-modules': 0.1.6-no-external-plugins(@babel/core@7.29.0)
+ babel-plugin-polyfill-corejs2: 0.4.15(@babel/core@7.29.0)
+ babel-plugin-polyfill-corejs3: 0.14.0(@babel/core@7.29.0)
+ babel-plugin-polyfill-regenerator: 0.6.6(@babel/core@7.29.0)
+ core-js-compat: 3.48.0
semver: 6.3.1
transitivePeerDependencies:
- supports-color
- '@babel/preset-flow@7.27.1(@babel/core@7.28.0)':
+ '@babel/preset-flow@7.27.1(@babel/core@7.29.0)':
dependencies:
- '@babel/core': 7.28.0
- '@babel/helper-plugin-utils': 7.27.1
+ '@babel/core': 7.29.0
+ '@babel/helper-plugin-utils': 7.28.6
'@babel/helper-validator-option': 7.27.1
- '@babel/plugin-transform-flow-strip-types': 7.27.1(@babel/core@7.28.0)
+ '@babel/plugin-transform-flow-strip-types': 7.27.1(@babel/core@7.29.0)
- '@babel/preset-modules@0.1.6-no-external-plugins(@babel/core@7.28.0)':
+ '@babel/preset-modules@0.1.6-no-external-plugins(@babel/core@7.29.0)':
dependencies:
- '@babel/core': 7.28.0
- '@babel/helper-plugin-utils': 7.27.1
- '@babel/types': 7.28.0
+ '@babel/core': 7.29.0
+ '@babel/helper-plugin-utils': 7.28.6
+ '@babel/types': 7.29.0
esutils: 2.0.3
- '@babel/preset-typescript@7.27.1(@babel/core@7.28.0)':
+ '@babel/preset-typescript@7.27.1(@babel/core@7.29.0)':
dependencies:
- '@babel/core': 7.28.0
- '@babel/helper-plugin-utils': 7.27.1
+ '@babel/core': 7.29.0
+ '@babel/helper-plugin-utils': 7.28.6
'@babel/helper-validator-option': 7.27.1
- '@babel/plugin-syntax-jsx': 7.27.1(@babel/core@7.28.0)
- '@babel/plugin-transform-modules-commonjs': 7.27.1(@babel/core@7.28.0)
- '@babel/plugin-transform-typescript': 7.28.0(@babel/core@7.28.0)
+ '@babel/plugin-syntax-jsx': 7.27.1(@babel/core@7.29.0)
+ '@babel/plugin-transform-modules-commonjs': 7.28.6(@babel/core@7.29.0)
+ '@babel/plugin-transform-typescript': 7.28.0(@babel/core@7.29.0)
transitivePeerDependencies:
- supports-color
- '@babel/register@7.27.1(@babel/core@7.28.0)':
+ '@babel/register@7.27.1(@babel/core@7.29.0)':
dependencies:
- '@babel/core': 7.28.0
+ '@babel/core': 7.29.0
clone-deep: 4.0.1
find-cache-dir: 2.1.0
make-dir: 2.1.0
pirates: 4.0.7
source-map-support: 0.5.21
- '@babel/runtime@7.23.2':
+ '@babel/runtime@7.27.0':
dependencies:
regenerator-runtime: 0.14.1
'@babel/runtime@7.27.6': {}
- '@babel/template@7.27.2':
+ '@babel/runtime@7.28.4': {}
+
+ '@babel/runtime@7.28.6': {}
+
+ '@babel/template@7.28.6':
dependencies:
- '@babel/code-frame': 7.27.1
- '@babel/parser': 7.28.0
- '@babel/types': 7.28.0
+ '@babel/code-frame': 7.29.0
+ '@babel/parser': 7.29.0
+ '@babel/types': 7.29.0
- '@babel/traverse@7.28.0':
+ '@babel/traverse@7.29.0':
dependencies:
- '@babel/code-frame': 7.27.1
- '@babel/generator': 7.28.0
+ '@babel/code-frame': 7.29.0
+ '@babel/generator': 7.29.1
'@babel/helper-globals': 7.28.0
- '@babel/parser': 7.28.0
- '@babel/template': 7.27.2
- '@babel/types': 7.28.0
- debug: 4.4.1
+ '@babel/parser': 7.29.0
+ '@babel/template': 7.28.6
+ '@babel/types': 7.29.0
+ debug: 4.4.3
transitivePeerDependencies:
- supports-color
- '@babel/types@7.28.0':
+ '@babel/types@7.29.0':
dependencies:
'@babel/helper-string-parser': 7.27.1
- '@babel/helper-validator-identifier': 7.27.1
+ '@babel/helper-validator-identifier': 7.28.5
'@bcoe/v8-coverage@0.2.3': {}
- '@cortex-js/compute-engine@0.28.0':
+ '@cortex-js/compute-engine@0.30.2':
dependencies:
complex-esm: 2.1.1-esm1
- decimal.js: 10.5.0
+ decimal.js: 10.6.0
'@csstools/css-parser-algorithms@2.7.1(@csstools/css-tokenizer@2.4.1)':
dependencies:
@@ -8586,12 +8720,17 @@ snapshots:
eslint: 8.57.1
eslint-visitor-keys: 3.4.3
+ '@eslint-community/eslint-utils@4.9.0(eslint@8.57.1)':
+ dependencies:
+ eslint: 8.57.1
+ eslint-visitor-keys: 3.4.3
+
'@eslint-community/regexpp@4.12.1': {}
'@eslint/eslintrc@0.4.3':
dependencies:
ajv: 6.12.6
- debug: 4.4.1
+ debug: 4.4.3
espree: 7.3.1
globals: 13.24.0
ignore: 4.0.6
@@ -8618,6 +8757,20 @@ snapshots:
'@eslint/js@8.57.1': {}
+ '@floating-ui/core@1.7.3':
+ dependencies:
+ '@floating-ui/utils': 0.2.10
+ optional: true
+
+ '@floating-ui/dom@1.7.4':
+ dependencies:
+ '@floating-ui/core': 1.7.3
+ '@floating-ui/utils': 0.2.10
+ optional: true
+
+ '@floating-ui/utils@0.2.10':
+ optional: true
+
'@gar/promisify@1.1.3': {}
'@humanwhocodes/config-array@0.13.0':
@@ -8631,7 +8784,7 @@ snapshots:
'@humanwhocodes/config-array@0.5.0':
dependencies:
'@humanwhocodes/object-schema': 1.2.1
- debug: 4.4.1
+ debug: 4.4.3
minimatch: 3.1.2
transitivePeerDependencies:
- supports-color
@@ -8646,11 +8799,13 @@ snapshots:
dependencies:
string-width: 5.1.2
string-width-cjs: string-width@4.2.3
- strip-ansi: 7.1.0
+ strip-ansi: 7.1.2
strip-ansi-cjs: strip-ansi@6.0.1
wrap-ansi: 8.1.0
wrap-ansi-cjs: wrap-ansi@7.0.0
+ '@isaacs/cliui@9.0.0': {}
+
'@istanbuljs/load-nyc-config@1.1.0':
dependencies:
camelcase: 5.3.1
@@ -8755,7 +8910,7 @@ snapshots:
'@jest/test-result': 29.7.0
'@jest/transform': 29.7.0
'@jest/types': 29.6.3
- '@jridgewell/trace-mapping': 0.3.29
+ '@jridgewell/trace-mapping': 0.3.31
'@types/node': 24.1.0
chalk: 4.1.2
collect-v8-coverage: 1.0.2
@@ -8787,7 +8942,7 @@ snapshots:
'@jest/source-map@29.6.3':
dependencies:
- '@jridgewell/trace-mapping': 0.3.29
+ '@jridgewell/trace-mapping': 0.3.31
callsites: 3.1.0
graceful-fs: 4.2.11
@@ -8807,7 +8962,7 @@ snapshots:
'@jest/transform@29.7.0':
dependencies:
- '@babel/core': 7.28.0
+ '@babel/core': 7.29.0
'@jest/types': 29.6.3
'@jridgewell/trace-mapping': 0.3.29
babel-plugin-istanbul: 6.1.1
@@ -8912,25 +9067,42 @@ snapshots:
dependencies:
regenerator-runtime: 0.13.11
- '@jridgewell/gen-mapping@0.3.12':
+ '@jridgewell/gen-mapping@0.3.13':
dependencies:
- '@jridgewell/sourcemap-codec': 1.5.4
- '@jridgewell/trace-mapping': 0.3.29
+ '@jridgewell/sourcemap-codec': 1.5.5
+ '@jridgewell/trace-mapping': 0.3.31
+
+ '@jridgewell/remapping@2.3.5':
+ dependencies:
+ '@jridgewell/gen-mapping': 0.3.13
+ '@jridgewell/trace-mapping': 0.3.31
'@jridgewell/resolve-uri@3.1.2': {}
'@jridgewell/source-map@0.3.10':
dependencies:
- '@jridgewell/gen-mapping': 0.3.12
+ '@jridgewell/gen-mapping': 0.3.13
'@jridgewell/trace-mapping': 0.3.29
+ '@jridgewell/source-map@0.3.11':
+ dependencies:
+ '@jridgewell/gen-mapping': 0.3.13
+ '@jridgewell/trace-mapping': 0.3.31
+
'@jridgewell/sourcemap-codec@1.5.4': {}
+ '@jridgewell/sourcemap-codec@1.5.5': {}
+
'@jridgewell/trace-mapping@0.3.29':
dependencies:
'@jridgewell/resolve-uri': 3.1.2
'@jridgewell/sourcemap-codec': 1.5.4
+ '@jridgewell/trace-mapping@0.3.31':
+ dependencies:
+ '@jridgewell/resolve-uri': 3.1.2
+ '@jridgewell/sourcemap-codec': 1.5.5
+
'@jsonjoy.com/base64@1.1.2(tslib@2.8.1)':
dependencies:
tslib: 2.8.1
@@ -8964,12 +9136,12 @@ snapshots:
'@npmcli/fs@1.1.1':
dependencies:
'@gar/promisify': 1.1.3
- semver: 7.7.2
+ semver: 7.7.4
'@npmcli/fs@2.1.2':
dependencies:
'@gar/promisify': 1.1.3
- semver: 7.7.2
+ semver: 7.7.4
'@npmcli/move-file@1.1.2':
dependencies:
@@ -8986,14 +9158,12 @@ snapshots:
'@pkgjs/parseargs@0.11.0':
optional: true
- '@popperjs/core@2.11.8': {}
-
'@remirror/core-constants@3.0.0': {}
- '@rollup/plugin-babel@5.3.1(@babel/core@7.28.0)(@types/babel__core@7.20.5)(rollup@2.79.2)':
+ '@rollup/plugin-babel@5.3.1(@babel/core@7.29.0)(@types/babel__core@7.20.5)(rollup@2.79.2)':
dependencies:
- '@babel/core': 7.28.0
- '@babel/helper-module-imports': 7.27.1
+ '@babel/core': 7.29.0
+ '@babel/helper-module-imports': 7.28.6
'@rollup/pluginutils': 3.1.0(rollup@2.79.2)
rollup: 2.79.2
optionalDependencies:
@@ -9003,11 +9173,11 @@ snapshots:
'@rollup/plugin-node-resolve@15.3.1(rollup@2.79.2)':
dependencies:
- '@rollup/pluginutils': 5.2.0(rollup@2.79.2)
+ '@rollup/pluginutils': 5.3.0(rollup@2.79.2)
'@types/resolve': 1.20.2
deepmerge: 4.3.1
is-module: 1.0.0
- resolve: 1.22.10
+ resolve: 1.22.11
optionalDependencies:
rollup: 2.79.2
@@ -9020,8 +9190,8 @@ snapshots:
'@rollup/plugin-terser@0.4.4(rollup@2.79.2)':
dependencies:
serialize-javascript: 6.0.2
- smob: 1.5.0
- terser: 5.43.1
+ smob: 1.6.1
+ terser: 5.46.0
optionalDependencies:
rollup: 2.79.2
@@ -9032,7 +9202,7 @@ snapshots:
picomatch: 2.3.1
rollup: 2.79.2
- '@rollup/pluginutils@5.2.0(rollup@2.79.2)':
+ '@rollup/pluginutils@5.3.0(rollup@2.79.2)':
dependencies:
'@types/estree': 1.0.8
estree-walker: 2.0.2
@@ -9044,38 +9214,38 @@ snapshots:
'@rushstack/eslint-patch@1.12.0': {}
- '@sentry-internal/browser-utils@9.40.0':
+ '@sentry-internal/browser-utils@10.6.0':
dependencies:
- '@sentry/core': 9.40.0
+ '@sentry/core': 10.6.0
- '@sentry-internal/feedback@9.40.0':
+ '@sentry-internal/feedback@10.6.0':
dependencies:
- '@sentry/core': 9.40.0
+ '@sentry/core': 10.6.0
- '@sentry-internal/replay-canvas@9.40.0':
+ '@sentry-internal/replay-canvas@10.6.0':
dependencies:
- '@sentry-internal/replay': 9.40.0
- '@sentry/core': 9.40.0
+ '@sentry-internal/replay': 10.6.0
+ '@sentry/core': 10.6.0
- '@sentry-internal/replay@9.40.0':
+ '@sentry-internal/replay@10.6.0':
dependencies:
- '@sentry-internal/browser-utils': 9.40.0
- '@sentry/core': 9.40.0
+ '@sentry-internal/browser-utils': 10.6.0
+ '@sentry/core': 10.6.0
- '@sentry/browser@9.40.0':
+ '@sentry/browser@10.6.0':
dependencies:
- '@sentry-internal/browser-utils': 9.40.0
- '@sentry-internal/feedback': 9.40.0
- '@sentry-internal/replay': 9.40.0
- '@sentry-internal/replay-canvas': 9.40.0
- '@sentry/core': 9.40.0
+ '@sentry-internal/browser-utils': 10.6.0
+ '@sentry-internal/feedback': 10.6.0
+ '@sentry-internal/replay': 10.6.0
+ '@sentry-internal/replay-canvas': 10.6.0
+ '@sentry/core': 10.6.0
- '@sentry/core@9.40.0': {}
+ '@sentry/core@10.6.0': {}
- '@sentry/vue@9.40.0(vue@2.7.16)':
+ '@sentry/vue@10.6.0(vue@2.7.16)':
dependencies:
- '@sentry/browser': 9.40.0
- '@sentry/core': 9.40.0
+ '@sentry/browser': 10.6.0
+ '@sentry/core': 10.6.0
vue: 2.7.16
'@sinclair/typebox@0.27.8': {}
@@ -9099,7 +9269,7 @@ snapshots:
'@testing-library/dom@9.3.4':
dependencies:
- '@babel/code-frame': 7.27.1
+ '@babel/code-frame': 7.29.0
'@babel/runtime': 7.27.6
'@types/aria-query': 5.0.4
aria-query: 5.1.3
@@ -9115,7 +9285,7 @@ snapshots:
chalk: 3.0.0
css.escape: 1.5.1
dom-accessibility-api: 0.6.3
- lodash: 4.17.21
+ lodash: 4.17.23
redent: 3.0.0
'@testing-library/user-event@14.6.1(@testing-library/dom@9.3.4)':
@@ -9130,187 +9300,199 @@ snapshots:
vue: 2.7.16
vue-template-compiler: 2.7.16
- '@tiptap/core@2.23.1(@tiptap/pm@2.23.1)':
+ '@tiptap/core@3.13.0(@tiptap/pm@3.13.0)':
dependencies:
- '@tiptap/pm': 2.23.1
+ '@tiptap/pm': 3.13.0
- '@tiptap/extension-blockquote@2.23.1(@tiptap/core@2.23.1(@tiptap/pm@2.23.1))':
+ '@tiptap/extension-blockquote@3.13.0(@tiptap/core@3.13.0(@tiptap/pm@3.13.0))':
dependencies:
- '@tiptap/core': 2.23.1(@tiptap/pm@2.23.1)
+ '@tiptap/core': 3.13.0(@tiptap/pm@3.13.0)
- '@tiptap/extension-bold@2.23.1(@tiptap/core@2.23.1(@tiptap/pm@2.23.1))':
+ '@tiptap/extension-bold@3.13.0(@tiptap/core@3.13.0(@tiptap/pm@3.13.0))':
dependencies:
- '@tiptap/core': 2.23.1(@tiptap/pm@2.23.1)
+ '@tiptap/core': 3.13.0(@tiptap/pm@3.13.0)
- '@tiptap/extension-bubble-menu@2.23.1(@tiptap/core@2.23.1(@tiptap/pm@2.23.1))(@tiptap/pm@2.23.1)':
+ '@tiptap/extension-bubble-menu@3.13.0(@tiptap/core@3.13.0(@tiptap/pm@3.13.0))(@tiptap/pm@3.13.0)':
dependencies:
- '@tiptap/core': 2.23.1(@tiptap/pm@2.23.1)
- '@tiptap/pm': 2.23.1
- tippy.js: 6.3.7
+ '@floating-ui/dom': 1.7.4
+ '@tiptap/core': 3.13.0(@tiptap/pm@3.13.0)
+ '@tiptap/pm': 3.13.0
+ optional: true
- '@tiptap/extension-bullet-list@2.23.1(@tiptap/core@2.23.1(@tiptap/pm@2.23.1))':
+ '@tiptap/extension-bullet-list@3.13.0(@tiptap/extension-list@3.13.0(@tiptap/core@3.13.0(@tiptap/pm@3.13.0))(@tiptap/pm@3.13.0))':
dependencies:
- '@tiptap/core': 2.23.1(@tiptap/pm@2.23.1)
+ '@tiptap/extension-list': 3.13.0(@tiptap/core@3.13.0(@tiptap/pm@3.13.0))(@tiptap/pm@3.13.0)
- '@tiptap/extension-code-block-lowlight@2.23.1(@tiptap/core@2.23.1(@tiptap/pm@2.23.1))(@tiptap/extension-code-block@2.23.1(@tiptap/core@2.23.1(@tiptap/pm@2.23.1))(@tiptap/pm@2.23.1))(@tiptap/pm@2.23.1)(highlight.js@11.11.1)(lowlight@3.3.0)':
+ '@tiptap/extension-code-block-lowlight@3.13.0(@tiptap/core@3.13.0(@tiptap/pm@3.13.0))(@tiptap/extension-code-block@3.13.0(@tiptap/core@3.13.0(@tiptap/pm@3.13.0))(@tiptap/pm@3.13.0))(@tiptap/pm@3.13.0)(highlight.js@11.11.1)(lowlight@3.3.0)':
dependencies:
- '@tiptap/core': 2.23.1(@tiptap/pm@2.23.1)
- '@tiptap/extension-code-block': 2.23.1(@tiptap/core@2.23.1(@tiptap/pm@2.23.1))(@tiptap/pm@2.23.1)
- '@tiptap/pm': 2.23.1
+ '@tiptap/core': 3.13.0(@tiptap/pm@3.13.0)
+ '@tiptap/extension-code-block': 3.13.0(@tiptap/core@3.13.0(@tiptap/pm@3.13.0))(@tiptap/pm@3.13.0)
+ '@tiptap/pm': 3.13.0
highlight.js: 11.11.1
lowlight: 3.3.0
- '@tiptap/extension-code-block@2.23.1(@tiptap/core@2.23.1(@tiptap/pm@2.23.1))(@tiptap/pm@2.23.1)':
+ '@tiptap/extension-code-block@3.13.0(@tiptap/core@3.13.0(@tiptap/pm@3.13.0))(@tiptap/pm@3.13.0)':
+ dependencies:
+ '@tiptap/core': 3.13.0(@tiptap/pm@3.13.0)
+ '@tiptap/pm': 3.13.0
+
+ '@tiptap/extension-code@3.13.0(@tiptap/core@3.13.0(@tiptap/pm@3.13.0))':
+ dependencies:
+ '@tiptap/core': 3.13.0(@tiptap/pm@3.13.0)
+
+ '@tiptap/extension-document@3.13.0(@tiptap/core@3.13.0(@tiptap/pm@3.13.0))':
dependencies:
- '@tiptap/core': 2.23.1(@tiptap/pm@2.23.1)
- '@tiptap/pm': 2.23.1
+ '@tiptap/core': 3.13.0(@tiptap/pm@3.13.0)
- '@tiptap/extension-code@2.23.1(@tiptap/core@2.23.1(@tiptap/pm@2.23.1))':
+ '@tiptap/extension-dropcursor@3.13.0(@tiptap/extensions@3.13.0(@tiptap/core@3.13.0(@tiptap/pm@3.13.0))(@tiptap/pm@3.13.0))':
dependencies:
- '@tiptap/core': 2.23.1(@tiptap/pm@2.23.1)
+ '@tiptap/extensions': 3.13.0(@tiptap/core@3.13.0(@tiptap/pm@3.13.0))(@tiptap/pm@3.13.0)
- '@tiptap/extension-document@2.23.1(@tiptap/core@2.23.1(@tiptap/pm@2.23.1))':
+ '@tiptap/extension-floating-menu@3.13.0(@floating-ui/dom@1.7.4)(@tiptap/core@3.13.0(@tiptap/pm@3.13.0))(@tiptap/pm@3.13.0)':
dependencies:
- '@tiptap/core': 2.23.1(@tiptap/pm@2.23.1)
+ '@floating-ui/dom': 1.7.4
+ '@tiptap/core': 3.13.0(@tiptap/pm@3.13.0)
+ '@tiptap/pm': 3.13.0
+ optional: true
- '@tiptap/extension-dropcursor@2.23.1(@tiptap/core@2.23.1(@tiptap/pm@2.23.1))(@tiptap/pm@2.23.1)':
+ '@tiptap/extension-gapcursor@3.13.0(@tiptap/extensions@3.13.0(@tiptap/core@3.13.0(@tiptap/pm@3.13.0))(@tiptap/pm@3.13.0))':
dependencies:
- '@tiptap/core': 2.23.1(@tiptap/pm@2.23.1)
- '@tiptap/pm': 2.23.1
+ '@tiptap/extensions': 3.13.0(@tiptap/core@3.13.0(@tiptap/pm@3.13.0))(@tiptap/pm@3.13.0)
- '@tiptap/extension-floating-menu@2.23.1(@tiptap/core@2.23.1(@tiptap/pm@2.23.1))(@tiptap/pm@2.23.1)':
+ '@tiptap/extension-hard-break@3.13.0(@tiptap/core@3.13.0(@tiptap/pm@3.13.0))':
dependencies:
- '@tiptap/core': 2.23.1(@tiptap/pm@2.23.1)
- '@tiptap/pm': 2.23.1
- tippy.js: 6.3.7
+ '@tiptap/core': 3.13.0(@tiptap/pm@3.13.0)
- '@tiptap/extension-gapcursor@2.23.1(@tiptap/core@2.23.1(@tiptap/pm@2.23.1))(@tiptap/pm@2.23.1)':
+ '@tiptap/extension-heading@3.13.0(@tiptap/core@3.13.0(@tiptap/pm@3.13.0))':
dependencies:
- '@tiptap/core': 2.23.1(@tiptap/pm@2.23.1)
- '@tiptap/pm': 2.23.1
+ '@tiptap/core': 3.13.0(@tiptap/pm@3.13.0)
- '@tiptap/extension-hard-break@2.23.1(@tiptap/core@2.23.1(@tiptap/pm@2.23.1))':
+ '@tiptap/extension-horizontal-rule@3.13.0(@tiptap/core@3.13.0(@tiptap/pm@3.13.0))(@tiptap/pm@3.13.0)':
dependencies:
- '@tiptap/core': 2.23.1(@tiptap/pm@2.23.1)
+ '@tiptap/core': 3.13.0(@tiptap/pm@3.13.0)
+ '@tiptap/pm': 3.13.0
- '@tiptap/extension-heading@2.23.1(@tiptap/core@2.23.1(@tiptap/pm@2.23.1))':
+ '@tiptap/extension-italic@3.13.0(@tiptap/core@3.13.0(@tiptap/pm@3.13.0))':
dependencies:
- '@tiptap/core': 2.23.1(@tiptap/pm@2.23.1)
+ '@tiptap/core': 3.13.0(@tiptap/pm@3.13.0)
- '@tiptap/extension-history@2.23.1(@tiptap/core@2.23.1(@tiptap/pm@2.23.1))(@tiptap/pm@2.23.1)':
+ '@tiptap/extension-link@3.13.0(@tiptap/core@3.13.0(@tiptap/pm@3.13.0))(@tiptap/pm@3.13.0)':
dependencies:
- '@tiptap/core': 2.23.1(@tiptap/pm@2.23.1)
- '@tiptap/pm': 2.23.1
+ '@tiptap/core': 3.13.0(@tiptap/pm@3.13.0)
+ '@tiptap/pm': 3.13.0
+ linkifyjs: 4.3.2
- '@tiptap/extension-horizontal-rule@2.23.1(@tiptap/core@2.23.1(@tiptap/pm@2.23.1))(@tiptap/pm@2.23.1)':
+ '@tiptap/extension-list-item@3.13.0(@tiptap/extension-list@3.13.0(@tiptap/core@3.13.0(@tiptap/pm@3.13.0))(@tiptap/pm@3.13.0))':
dependencies:
- '@tiptap/core': 2.23.1(@tiptap/pm@2.23.1)
- '@tiptap/pm': 2.23.1
+ '@tiptap/extension-list': 3.13.0(@tiptap/core@3.13.0(@tiptap/pm@3.13.0))(@tiptap/pm@3.13.0)
- '@tiptap/extension-italic@2.23.1(@tiptap/core@2.23.1(@tiptap/pm@2.23.1))':
+ '@tiptap/extension-list-keymap@3.13.0(@tiptap/extension-list@3.13.0(@tiptap/core@3.13.0(@tiptap/pm@3.13.0))(@tiptap/pm@3.13.0))':
dependencies:
- '@tiptap/core': 2.23.1(@tiptap/pm@2.23.1)
+ '@tiptap/extension-list': 3.13.0(@tiptap/core@3.13.0(@tiptap/pm@3.13.0))(@tiptap/pm@3.13.0)
- '@tiptap/extension-link@2.23.1(@tiptap/core@2.23.1(@tiptap/pm@2.23.1))(@tiptap/pm@2.23.1)':
+ '@tiptap/extension-list@3.13.0(@tiptap/core@3.13.0(@tiptap/pm@3.13.0))(@tiptap/pm@3.13.0)':
dependencies:
- '@tiptap/core': 2.23.1(@tiptap/pm@2.23.1)
- '@tiptap/pm': 2.23.1
- linkifyjs: 4.3.1
+ '@tiptap/core': 3.13.0(@tiptap/pm@3.13.0)
+ '@tiptap/pm': 3.13.0
- '@tiptap/extension-list-item@2.23.1(@tiptap/core@2.23.1(@tiptap/pm@2.23.1))':
+ '@tiptap/extension-ordered-list@3.13.0(@tiptap/extension-list@3.13.0(@tiptap/core@3.13.0(@tiptap/pm@3.13.0))(@tiptap/pm@3.13.0))':
dependencies:
- '@tiptap/core': 2.23.1(@tiptap/pm@2.23.1)
+ '@tiptap/extension-list': 3.13.0(@tiptap/core@3.13.0(@tiptap/pm@3.13.0))(@tiptap/pm@3.13.0)
- '@tiptap/extension-ordered-list@2.23.1(@tiptap/core@2.23.1(@tiptap/pm@2.23.1))':
+ '@tiptap/extension-paragraph@3.13.0(@tiptap/core@3.13.0(@tiptap/pm@3.13.0))':
dependencies:
- '@tiptap/core': 2.23.1(@tiptap/pm@2.23.1)
+ '@tiptap/core': 3.13.0(@tiptap/pm@3.13.0)
- '@tiptap/extension-paragraph@2.23.1(@tiptap/core@2.23.1(@tiptap/pm@2.23.1))':
+ '@tiptap/extension-strike@3.13.0(@tiptap/core@3.13.0(@tiptap/pm@3.13.0))':
dependencies:
- '@tiptap/core': 2.23.1(@tiptap/pm@2.23.1)
+ '@tiptap/core': 3.13.0(@tiptap/pm@3.13.0)
- '@tiptap/extension-strike@2.23.1(@tiptap/core@2.23.1(@tiptap/pm@2.23.1))':
+ '@tiptap/extension-subscript@3.13.0(@tiptap/core@3.13.0(@tiptap/pm@3.13.0))(@tiptap/pm@3.13.0)':
dependencies:
- '@tiptap/core': 2.23.1(@tiptap/pm@2.23.1)
+ '@tiptap/core': 3.13.0(@tiptap/pm@3.13.0)
+ '@tiptap/pm': 3.13.0
- '@tiptap/extension-subscript@2.23.1(@tiptap/core@2.23.1(@tiptap/pm@2.23.1))':
+ '@tiptap/extension-superscript@3.13.0(@tiptap/core@3.13.0(@tiptap/pm@3.13.0))(@tiptap/pm@3.13.0)':
dependencies:
- '@tiptap/core': 2.23.1(@tiptap/pm@2.23.1)
+ '@tiptap/core': 3.13.0(@tiptap/pm@3.13.0)
+ '@tiptap/pm': 3.13.0
- '@tiptap/extension-superscript@2.26.1(@tiptap/core@2.23.1(@tiptap/pm@2.23.1))':
+ '@tiptap/extension-text-align@3.18.0(@tiptap/core@3.13.0(@tiptap/pm@3.13.0))':
dependencies:
- '@tiptap/core': 2.23.1(@tiptap/pm@2.23.1)
+ '@tiptap/core': 3.13.0(@tiptap/pm@3.13.0)
- '@tiptap/extension-text-style@2.23.1(@tiptap/core@2.23.1(@tiptap/pm@2.23.1))':
+ '@tiptap/extension-text@3.13.0(@tiptap/core@3.13.0(@tiptap/pm@3.13.0))':
dependencies:
- '@tiptap/core': 2.23.1(@tiptap/pm@2.23.1)
+ '@tiptap/core': 3.13.0(@tiptap/pm@3.13.0)
- '@tiptap/extension-text@2.23.1(@tiptap/core@2.23.1(@tiptap/pm@2.23.1))':
+ '@tiptap/extension-underline@3.13.0(@tiptap/core@3.13.0(@tiptap/pm@3.13.0))':
dependencies:
- '@tiptap/core': 2.23.1(@tiptap/pm@2.23.1)
+ '@tiptap/core': 3.13.0(@tiptap/pm@3.13.0)
- '@tiptap/extension-underline@2.23.1(@tiptap/core@2.23.1(@tiptap/pm@2.23.1))':
+ '@tiptap/extensions@3.13.0(@tiptap/core@3.13.0(@tiptap/pm@3.13.0))(@tiptap/pm@3.13.0)':
dependencies:
- '@tiptap/core': 2.23.1(@tiptap/pm@2.23.1)
+ '@tiptap/core': 3.13.0(@tiptap/pm@3.13.0)
+ '@tiptap/pm': 3.13.0
- '@tiptap/pm@2.23.1':
+ '@tiptap/pm@3.13.0':
dependencies:
prosemirror-changeset: 2.3.1
prosemirror-collab: 1.3.1
prosemirror-commands: 1.7.1
prosemirror-dropcursor: 1.8.2
- prosemirror-gapcursor: 1.3.2
- prosemirror-history: 1.4.1
- prosemirror-inputrules: 1.5.0
+ prosemirror-gapcursor: 1.4.0
+ prosemirror-history: 1.5.0
+ prosemirror-inputrules: 1.5.1
prosemirror-keymap: 1.2.3
prosemirror-markdown: 1.13.2
prosemirror-menu: 1.2.5
- prosemirror-model: 1.25.1
+ prosemirror-model: 1.25.4
prosemirror-schema-basic: 1.2.4
prosemirror-schema-list: 1.5.1
- prosemirror-state: 1.4.3
- prosemirror-tables: 1.7.1
- prosemirror-trailing-node: 3.0.0(prosemirror-model@1.25.1)(prosemirror-state@1.4.3)(prosemirror-view@1.40.0)
- prosemirror-transform: 1.10.4
- prosemirror-view: 1.40.0
-
- '@tiptap/starter-kit@2.23.1':
- dependencies:
- '@tiptap/core': 2.23.1(@tiptap/pm@2.23.1)
- '@tiptap/extension-blockquote': 2.23.1(@tiptap/core@2.23.1(@tiptap/pm@2.23.1))
- '@tiptap/extension-bold': 2.23.1(@tiptap/core@2.23.1(@tiptap/pm@2.23.1))
- '@tiptap/extension-bullet-list': 2.23.1(@tiptap/core@2.23.1(@tiptap/pm@2.23.1))
- '@tiptap/extension-code': 2.23.1(@tiptap/core@2.23.1(@tiptap/pm@2.23.1))
- '@tiptap/extension-code-block': 2.23.1(@tiptap/core@2.23.1(@tiptap/pm@2.23.1))(@tiptap/pm@2.23.1)
- '@tiptap/extension-document': 2.23.1(@tiptap/core@2.23.1(@tiptap/pm@2.23.1))
- '@tiptap/extension-dropcursor': 2.23.1(@tiptap/core@2.23.1(@tiptap/pm@2.23.1))(@tiptap/pm@2.23.1)
- '@tiptap/extension-gapcursor': 2.23.1(@tiptap/core@2.23.1(@tiptap/pm@2.23.1))(@tiptap/pm@2.23.1)
- '@tiptap/extension-hard-break': 2.23.1(@tiptap/core@2.23.1(@tiptap/pm@2.23.1))
- '@tiptap/extension-heading': 2.23.1(@tiptap/core@2.23.1(@tiptap/pm@2.23.1))
- '@tiptap/extension-history': 2.23.1(@tiptap/core@2.23.1(@tiptap/pm@2.23.1))(@tiptap/pm@2.23.1)
- '@tiptap/extension-horizontal-rule': 2.23.1(@tiptap/core@2.23.1(@tiptap/pm@2.23.1))(@tiptap/pm@2.23.1)
- '@tiptap/extension-italic': 2.23.1(@tiptap/core@2.23.1(@tiptap/pm@2.23.1))
- '@tiptap/extension-list-item': 2.23.1(@tiptap/core@2.23.1(@tiptap/pm@2.23.1))
- '@tiptap/extension-ordered-list': 2.23.1(@tiptap/core@2.23.1(@tiptap/pm@2.23.1))
- '@tiptap/extension-paragraph': 2.23.1(@tiptap/core@2.23.1(@tiptap/pm@2.23.1))
- '@tiptap/extension-strike': 2.23.1(@tiptap/core@2.23.1(@tiptap/pm@2.23.1))
- '@tiptap/extension-text': 2.23.1(@tiptap/core@2.23.1(@tiptap/pm@2.23.1))
- '@tiptap/extension-text-style': 2.23.1(@tiptap/core@2.23.1(@tiptap/pm@2.23.1))
- '@tiptap/pm': 2.23.1
-
- '@tiptap/vue-2@2.23.1(@tiptap/core@2.23.1(@tiptap/pm@2.23.1))(@tiptap/pm@2.23.1)(vue@2.7.16)':
- dependencies:
- '@tiptap/core': 2.23.1(@tiptap/pm@2.23.1)
- '@tiptap/extension-bubble-menu': 2.23.1(@tiptap/core@2.23.1(@tiptap/pm@2.23.1))(@tiptap/pm@2.23.1)
- '@tiptap/extension-floating-menu': 2.23.1(@tiptap/core@2.23.1(@tiptap/pm@2.23.1))(@tiptap/pm@2.23.1)
- '@tiptap/pm': 2.23.1
+ prosemirror-state: 1.4.4
+ prosemirror-tables: 1.8.3
+ prosemirror-trailing-node: 3.0.0(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.4)
+ prosemirror-transform: 1.10.5
+ prosemirror-view: 1.41.4
+
+ '@tiptap/starter-kit@3.13.0':
+ dependencies:
+ '@tiptap/core': 3.13.0(@tiptap/pm@3.13.0)
+ '@tiptap/extension-blockquote': 3.13.0(@tiptap/core@3.13.0(@tiptap/pm@3.13.0))
+ '@tiptap/extension-bold': 3.13.0(@tiptap/core@3.13.0(@tiptap/pm@3.13.0))
+ '@tiptap/extension-bullet-list': 3.13.0(@tiptap/extension-list@3.13.0(@tiptap/core@3.13.0(@tiptap/pm@3.13.0))(@tiptap/pm@3.13.0))
+ '@tiptap/extension-code': 3.13.0(@tiptap/core@3.13.0(@tiptap/pm@3.13.0))
+ '@tiptap/extension-code-block': 3.13.0(@tiptap/core@3.13.0(@tiptap/pm@3.13.0))(@tiptap/pm@3.13.0)
+ '@tiptap/extension-document': 3.13.0(@tiptap/core@3.13.0(@tiptap/pm@3.13.0))
+ '@tiptap/extension-dropcursor': 3.13.0(@tiptap/extensions@3.13.0(@tiptap/core@3.13.0(@tiptap/pm@3.13.0))(@tiptap/pm@3.13.0))
+ '@tiptap/extension-gapcursor': 3.13.0(@tiptap/extensions@3.13.0(@tiptap/core@3.13.0(@tiptap/pm@3.13.0))(@tiptap/pm@3.13.0))
+ '@tiptap/extension-hard-break': 3.13.0(@tiptap/core@3.13.0(@tiptap/pm@3.13.0))
+ '@tiptap/extension-heading': 3.13.0(@tiptap/core@3.13.0(@tiptap/pm@3.13.0))
+ '@tiptap/extension-horizontal-rule': 3.13.0(@tiptap/core@3.13.0(@tiptap/pm@3.13.0))(@tiptap/pm@3.13.0)
+ '@tiptap/extension-italic': 3.13.0(@tiptap/core@3.13.0(@tiptap/pm@3.13.0))
+ '@tiptap/extension-link': 3.13.0(@tiptap/core@3.13.0(@tiptap/pm@3.13.0))(@tiptap/pm@3.13.0)
+ '@tiptap/extension-list': 3.13.0(@tiptap/core@3.13.0(@tiptap/pm@3.13.0))(@tiptap/pm@3.13.0)
+ '@tiptap/extension-list-item': 3.13.0(@tiptap/extension-list@3.13.0(@tiptap/core@3.13.0(@tiptap/pm@3.13.0))(@tiptap/pm@3.13.0))
+ '@tiptap/extension-list-keymap': 3.13.0(@tiptap/extension-list@3.13.0(@tiptap/core@3.13.0(@tiptap/pm@3.13.0))(@tiptap/pm@3.13.0))
+ '@tiptap/extension-ordered-list': 3.13.0(@tiptap/extension-list@3.13.0(@tiptap/core@3.13.0(@tiptap/pm@3.13.0))(@tiptap/pm@3.13.0))
+ '@tiptap/extension-paragraph': 3.13.0(@tiptap/core@3.13.0(@tiptap/pm@3.13.0))
+ '@tiptap/extension-strike': 3.13.0(@tiptap/core@3.13.0(@tiptap/pm@3.13.0))
+ '@tiptap/extension-text': 3.13.0(@tiptap/core@3.13.0(@tiptap/pm@3.13.0))
+ '@tiptap/extension-underline': 3.13.0(@tiptap/core@3.13.0(@tiptap/pm@3.13.0))
+ '@tiptap/extensions': 3.13.0(@tiptap/core@3.13.0(@tiptap/pm@3.13.0))(@tiptap/pm@3.13.0)
+ '@tiptap/pm': 3.13.0
+
+ '@tiptap/vue-2@3.13.0(@floating-ui/dom@1.7.4)(@tiptap/core@3.13.0(@tiptap/pm@3.13.0))(@tiptap/pm@3.13.0)(vue@2.7.16)':
+ dependencies:
+ '@tiptap/core': 3.13.0(@tiptap/pm@3.13.0)
+ '@tiptap/pm': 3.13.0
vue: 2.7.16
vue-ts-types: 1.6.2(vue@2.7.16)
-
- '@toast-ui/editor@2.5.4':
- dependencies:
- '@types/codemirror': 0.0.71
- codemirror: 5.58.2
+ optionalDependencies:
+ '@tiptap/extension-bubble-menu': 3.13.0(@tiptap/core@3.13.0(@tiptap/pm@3.13.0))(@tiptap/pm@3.13.0)
+ '@tiptap/extension-floating-menu': 3.13.0(@floating-ui/dom@1.7.4)(@tiptap/core@3.13.0(@tiptap/pm@3.13.0))(@tiptap/pm@3.13.0)
+ transitivePeerDependencies:
+ - '@floating-ui/dom'
'@tokenizer/token@0.3.0': {}
@@ -9324,24 +9506,24 @@ snapshots:
'@types/babel__core@7.20.5':
dependencies:
- '@babel/parser': 7.28.0
- '@babel/types': 7.28.0
+ '@babel/parser': 7.29.0
+ '@babel/types': 7.29.0
'@types/babel__generator': 7.27.0
'@types/babel__template': 7.4.4
'@types/babel__traverse': 7.20.7
'@types/babel__generator@7.27.0':
dependencies:
- '@babel/types': 7.28.0
+ '@babel/types': 7.29.0
'@types/babel__template@7.4.4':
dependencies:
- '@babel/parser': 7.28.0
- '@babel/types': 7.28.0
+ '@babel/parser': 7.29.0
+ '@babel/types': 7.29.0
'@types/babel__traverse@7.20.7':
dependencies:
- '@babel/types': 7.28.0
+ '@babel/types': 7.29.0
'@types/body-parser@1.19.6':
dependencies:
@@ -9352,10 +9534,6 @@ snapshots:
dependencies:
'@types/node': 24.1.0
- '@types/codemirror@0.0.71':
- dependencies:
- '@types/tern': 0.23.9
-
'@types/connect-history-api-fallback@1.5.4':
dependencies:
'@types/express-serve-static-core': 4.19.6
@@ -9462,6 +9640,10 @@ snapshots:
dependencies:
undici-types: 7.8.0
+ '@types/node@25.2.1':
+ dependencies:
+ undici-types: 7.16.0
+
'@types/normalize-package-data@2.4.4': {}
'@types/qs@6.14.0': {}
@@ -9497,10 +9679,6 @@ snapshots:
'@types/strip-json-comments@0.0.30': {}
- '@types/tern@0.23.9':
- dependencies:
- '@types/estree': 1.0.8
-
'@types/tough-cookie@4.0.5': {}
'@types/trusted-types@2.0.7': {}
@@ -9519,56 +9697,56 @@ snapshots:
dependencies:
'@types/yargs-parser': 21.0.3
- '@typescript-eslint/project-service@8.35.1(typescript@5.8.3)':
+ '@typescript-eslint/project-service@8.47.0(typescript@5.8.3)':
dependencies:
- '@typescript-eslint/tsconfig-utils': 8.35.1(typescript@5.8.3)
- '@typescript-eslint/types': 8.35.1
- debug: 4.4.1
+ '@typescript-eslint/tsconfig-utils': 8.47.0(typescript@5.8.3)
+ '@typescript-eslint/types': 8.47.0
+ debug: 4.4.3
typescript: 5.8.3
transitivePeerDependencies:
- supports-color
- '@typescript-eslint/scope-manager@8.35.1':
+ '@typescript-eslint/scope-manager@8.47.0':
dependencies:
- '@typescript-eslint/types': 8.35.1
- '@typescript-eslint/visitor-keys': 8.35.1
+ '@typescript-eslint/types': 8.47.0
+ '@typescript-eslint/visitor-keys': 8.47.0
- '@typescript-eslint/tsconfig-utils@8.35.1(typescript@5.8.3)':
+ '@typescript-eslint/tsconfig-utils@8.47.0(typescript@5.8.3)':
dependencies:
typescript: 5.8.3
- '@typescript-eslint/types@8.35.1': {}
+ '@typescript-eslint/types@8.47.0': {}
- '@typescript-eslint/typescript-estree@8.35.1(typescript@5.8.3)':
+ '@typescript-eslint/typescript-estree@8.47.0(typescript@5.8.3)':
dependencies:
- '@typescript-eslint/project-service': 8.35.1(typescript@5.8.3)
- '@typescript-eslint/tsconfig-utils': 8.35.1(typescript@5.8.3)
- '@typescript-eslint/types': 8.35.1
- '@typescript-eslint/visitor-keys': 8.35.1
- debug: 4.4.1
+ '@typescript-eslint/project-service': 8.47.0(typescript@5.8.3)
+ '@typescript-eslint/tsconfig-utils': 8.47.0(typescript@5.8.3)
+ '@typescript-eslint/types': 8.47.0
+ '@typescript-eslint/visitor-keys': 8.47.0
+ debug: 4.4.3
fast-glob: 3.3.3
is-glob: 4.0.3
minimatch: 9.0.5
- semver: 7.7.2
+ semver: 7.7.4
ts-api-utils: 2.1.0(typescript@5.8.3)
typescript: 5.8.3
transitivePeerDependencies:
- supports-color
- '@typescript-eslint/utils@8.35.1(eslint@8.57.1)(typescript@5.8.3)':
+ '@typescript-eslint/utils@8.47.0(eslint@8.57.1)(typescript@5.8.3)':
dependencies:
- '@eslint-community/eslint-utils': 4.7.0(eslint@8.57.1)
- '@typescript-eslint/scope-manager': 8.35.1
- '@typescript-eslint/types': 8.35.1
- '@typescript-eslint/typescript-estree': 8.35.1(typescript@5.8.3)
+ '@eslint-community/eslint-utils': 4.9.0(eslint@8.57.1)
+ '@typescript-eslint/scope-manager': 8.47.0
+ '@typescript-eslint/types': 8.47.0
+ '@typescript-eslint/typescript-estree': 8.47.0(typescript@5.8.3)
eslint: 8.57.1
typescript: 5.8.3
transitivePeerDependencies:
- supports-color
- '@typescript-eslint/visitor-keys@8.35.1':
+ '@typescript-eslint/visitor-keys@8.47.0':
dependencies:
- '@typescript-eslint/types': 8.35.1
+ '@typescript-eslint/types': 8.47.0
eslint-visitor-keys: 4.2.1
'@ungap/structured-clone@1.3.0': {}
@@ -9630,15 +9808,15 @@ snapshots:
'@vue/compiler-sfc@2.7.16':
dependencies:
- '@babel/parser': 7.28.0
+ '@babel/parser': 7.29.0
postcss: 8.5.6
source-map: 0.6.1
optionalDependencies:
prettier: 2.8.8
- '@vue/component-compiler-utils@3.3.0(babel-core@7.0.0-bridge.0(@babel/core@7.28.0))(ejs@3.1.10)(lodash@4.17.21)':
+ '@vue/component-compiler-utils@3.3.0(babel-core@7.0.0-bridge.0(@babel/core@7.29.0))(ejs@3.1.10)(lodash@4.17.23)':
dependencies:
- consolidate: 0.15.1(babel-core@7.0.0-bridge.0(@babel/core@7.28.0))(ejs@3.1.10)(lodash@4.17.21)
+ consolidate: 0.15.1(babel-core@7.0.0-bridge.0(@babel/core@7.29.0))(ejs@3.1.10)(lodash@4.17.23)
hash-sum: 1.0.2
lru-cache: 4.1.5
merge-source-map: 1.1.0
@@ -9706,7 +9884,7 @@ snapshots:
'@vue/test-utils@1.3.6(vue-template-compiler@2.7.16)(vue@2.7.16)':
dependencies:
dom-event-types: 1.1.0
- lodash: 4.17.21
+ lodash: 4.17.23
pretty: 2.0.0
vue: 2.7.16
vue-template-compiler: 2.7.16
@@ -9806,22 +9984,22 @@ snapshots:
'@webassemblyjs/ast': 1.14.1
'@xtuc/long': 4.2.2
- '@webpack-cli/configtest@3.0.1(webpack-cli@6.0.1)(webpack@5.99.9)':
+ '@webpack-cli/configtest@3.0.1(webpack-cli@6.0.1)(webpack@5.104.1)':
dependencies:
- webpack: 5.99.9(webpack-cli@6.0.1)
- webpack-cli: 6.0.1(webpack-dev-server@5.2.2)(webpack@5.99.9)
+ webpack: 5.104.1(webpack-cli@6.0.1)
+ webpack-cli: 6.0.1(webpack-dev-server@5.2.2)(webpack@5.104.1)
- '@webpack-cli/info@3.0.1(webpack-cli@6.0.1)(webpack@5.99.9)':
+ '@webpack-cli/info@3.0.1(webpack-cli@6.0.1)(webpack@5.104.1)':
dependencies:
- webpack: 5.99.9(webpack-cli@6.0.1)
- webpack-cli: 6.0.1(webpack-dev-server@5.2.2)(webpack@5.99.9)
+ webpack: 5.104.1(webpack-cli@6.0.1)
+ webpack-cli: 6.0.1(webpack-dev-server@5.2.2)(webpack@5.104.1)
- '@webpack-cli/serve@3.0.1(webpack-cli@6.0.1)(webpack-dev-server@5.2.2)(webpack@5.99.9)':
+ '@webpack-cli/serve@3.0.1(webpack-cli@6.0.1)(webpack-dev-server@5.2.2)(webpack@5.104.1)':
dependencies:
- webpack: 5.99.9(webpack-cli@6.0.1)
- webpack-cli: 6.0.1(webpack-dev-server@5.2.2)(webpack@5.99.9)
+ webpack: 5.104.1(webpack-cli@6.0.1)
+ webpack-cli: 6.0.1(webpack-dev-server@5.2.2)(webpack@5.104.1)
optionalDependencies:
- webpack-dev-server: 5.2.2(webpack-cli@6.0.1)(webpack@5.99.9)
+ webpack-dev-server: 5.2.2(webpack-cli@6.0.1)(webpack@5.104.1)
'@xmldom/xmldom@0.7.13': {}
@@ -9848,7 +10026,7 @@ snapshots:
accepts@2.0.0:
dependencies:
- mime-types: 3.0.1
+ mime-types: 3.0.2
negotiator: 1.0.0
acorn-globals@1.0.9:
@@ -9860,6 +10038,10 @@ snapshots:
acorn: 8.15.0
acorn-walk: 8.3.4
+ acorn-import-phases@1.0.4(acorn@8.15.0):
+ dependencies:
+ acorn: 8.15.0
+
acorn-jsx@5.3.2(acorn@7.4.1):
dependencies:
acorn: 7.4.1
@@ -9880,7 +10062,7 @@ snapshots:
agent-base@6.0.2:
dependencies:
- debug: 4.4.1
+ debug: 4.4.3
transitivePeerDependencies:
- supports-color
@@ -9932,7 +10114,7 @@ snapshots:
ansi-regex@5.0.1: {}
- ansi-regex@6.1.0: {}
+ ansi-regex@6.2.2: {}
ansi-styles@2.2.1: {}
@@ -9946,7 +10128,7 @@ snapshots:
ansi-styles@5.2.0: {}
- ansi-styles@6.2.1: {}
+ ansi-styles@6.2.3: {}
any-base@1.1.0: {}
@@ -10088,10 +10270,10 @@ snapshots:
aws4@1.13.2: {}
- axios@1.11.0:
+ axios@1.13.5:
dependencies:
- follow-redirects: 1.15.9
- form-data: 4.0.4
+ follow-redirects: 1.15.11
+ form-data: 4.0.5
proxy-from-env: 1.1.0
transitivePeerDependencies:
- debug
@@ -10102,35 +10284,28 @@ snapshots:
esutils: 2.0.3
js-tokens: 3.0.2
- babel-core@7.0.0-bridge.0(@babel/core@7.28.0):
+ babel-core@7.0.0-bridge.0(@babel/core@7.29.0):
dependencies:
- '@babel/core': 7.28.0
+ '@babel/core': 7.29.0
- babel-jest@29.7.0(@babel/core@7.28.0):
+ babel-jest@29.7.0(@babel/core@7.29.0):
dependencies:
- '@babel/core': 7.28.0
+ '@babel/core': 7.29.0
'@jest/transform': 29.7.0
'@types/babel__core': 7.20.5
babel-plugin-istanbul: 6.1.1
- babel-preset-jest: 29.6.3(@babel/core@7.28.0)
+ babel-preset-jest: 29.6.3(@babel/core@7.29.0)
chalk: 4.1.2
graceful-fs: 4.2.11
slash: 3.0.0
transitivePeerDependencies:
- supports-color
- babel-loader@10.0.0(@babel/core@7.28.0)(webpack@5.99.9):
+ babel-loader@10.0.0(@babel/core@7.29.0)(webpack@5.104.1):
dependencies:
- '@babel/core': 7.28.0
+ '@babel/core': 7.29.0
find-up: 5.0.0
- webpack: 5.99.9(webpack-cli@6.0.1)
-
- babel-loader@9.2.1(@babel/core@7.28.0)(webpack@5.99.9):
- dependencies:
- '@babel/core': 7.28.0
- find-cache-dir: 4.0.0
- schema-utils: 4.3.2
- webpack: 5.99.9(webpack-cli@6.0.1)
+ webpack: 5.104.1(webpack-cli@6.0.1)
babel-messages@6.23.0:
dependencies:
@@ -10138,7 +10313,7 @@ snapshots:
babel-plugin-istanbul@6.1.1:
dependencies:
- '@babel/helper-plugin-utils': 7.27.1
+ '@babel/helper-plugin-utils': 7.28.6
'@istanbuljs/load-nyc-config': 1.1.0
'@istanbuljs/schema': 0.1.3
istanbul-lib-instrument: 5.2.1
@@ -10148,32 +10323,40 @@ snapshots:
babel-plugin-jest-hoist@29.6.3:
dependencies:
- '@babel/template': 7.27.2
- '@babel/types': 7.28.0
+ '@babel/template': 7.28.6
+ '@babel/types': 7.29.0
'@types/babel__core': 7.20.5
'@types/babel__traverse': 7.20.7
- babel-plugin-polyfill-corejs2@0.4.14(@babel/core@7.28.0):
+ babel-plugin-polyfill-corejs2@0.4.15(@babel/core@7.29.0):
dependencies:
- '@babel/compat-data': 7.28.0
- '@babel/core': 7.28.0
- '@babel/helper-define-polyfill-provider': 0.6.5(@babel/core@7.28.0)
+ '@babel/compat-data': 7.29.0
+ '@babel/core': 7.29.0
+ '@babel/helper-define-polyfill-provider': 0.6.6(@babel/core@7.29.0)
semver: 6.3.1
transitivePeerDependencies:
- supports-color
- babel-plugin-polyfill-corejs3@0.13.0(@babel/core@7.28.0):
+ babel-plugin-polyfill-corejs3@0.13.0(@babel/core@7.29.0):
+ dependencies:
+ '@babel/core': 7.29.0
+ '@babel/helper-define-polyfill-provider': 0.6.6(@babel/core@7.29.0)
+ core-js-compat: 3.48.0
+ transitivePeerDependencies:
+ - supports-color
+
+ babel-plugin-polyfill-corejs3@0.14.0(@babel/core@7.29.0):
dependencies:
- '@babel/core': 7.28.0
- '@babel/helper-define-polyfill-provider': 0.6.5(@babel/core@7.28.0)
- core-js-compat: 3.43.0
+ '@babel/core': 7.29.0
+ '@babel/helper-define-polyfill-provider': 0.6.6(@babel/core@7.29.0)
+ core-js-compat: 3.48.0
transitivePeerDependencies:
- supports-color
- babel-plugin-polyfill-regenerator@0.6.5(@babel/core@7.28.0):
+ babel-plugin-polyfill-regenerator@0.6.6(@babel/core@7.29.0):
dependencies:
- '@babel/core': 7.28.0
- '@babel/helper-define-polyfill-provider': 0.6.5(@babel/core@7.28.0)
+ '@babel/core': 7.29.0
+ '@babel/helper-define-polyfill-provider': 0.6.6(@babel/core@7.29.0)
transitivePeerDependencies:
- supports-color
@@ -10191,30 +10374,30 @@ snapshots:
babel-runtime: 6.26.0
babel-types: 6.26.0
- babel-preset-current-node-syntax@1.1.0(@babel/core@7.28.0):
- dependencies:
- '@babel/core': 7.28.0
- '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.28.0)
- '@babel/plugin-syntax-bigint': 7.8.3(@babel/core@7.28.0)
- '@babel/plugin-syntax-class-properties': 7.12.13(@babel/core@7.28.0)
- '@babel/plugin-syntax-class-static-block': 7.14.5(@babel/core@7.28.0)
- '@babel/plugin-syntax-import-attributes': 7.27.1(@babel/core@7.28.0)
- '@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.28.0)
- '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.28.0)
- '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.28.0)
- '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.28.0)
- '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.28.0)
- '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.28.0)
- '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.28.0)
- '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.28.0)
- '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.28.0)
- '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.28.0)
-
- babel-preset-jest@29.6.3(@babel/core@7.28.0):
- dependencies:
- '@babel/core': 7.28.0
+ babel-preset-current-node-syntax@1.1.0(@babel/core@7.29.0):
+ dependencies:
+ '@babel/core': 7.29.0
+ '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.29.0)
+ '@babel/plugin-syntax-bigint': 7.8.3(@babel/core@7.29.0)
+ '@babel/plugin-syntax-class-properties': 7.12.13(@babel/core@7.29.0)
+ '@babel/plugin-syntax-class-static-block': 7.14.5(@babel/core@7.29.0)
+ '@babel/plugin-syntax-import-attributes': 7.28.6(@babel/core@7.29.0)
+ '@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.29.0)
+ '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.29.0)
+ '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.29.0)
+ '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.29.0)
+ '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.29.0)
+ '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.29.0)
+ '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.29.0)
+ '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.29.0)
+ '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.29.0)
+ '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.29.0)
+
+ babel-preset-jest@29.6.3(@babel/core@7.29.0):
+ dependencies:
+ '@babel/core': 7.29.0
babel-plugin-jest-hoist: 29.6.3
- babel-preset-current-node-syntax: 1.1.0(@babel/core@7.28.0)
+ babel-preset-current-node-syntax: 1.1.0(@babel/core@7.29.0)
babel-runtime@6.26.0:
dependencies:
@@ -10227,7 +10410,7 @@ snapshots:
babel-traverse: 6.26.0
babel-types: 6.26.0
babylon: 6.18.0
- lodash: 4.17.21
+ lodash: 4.17.23
transitivePeerDependencies:
- supports-color
@@ -10241,7 +10424,7 @@ snapshots:
debug: 2.6.9
globals: 9.18.0
invariant: 2.2.4
- lodash: 4.17.21
+ lodash: 4.17.23
transitivePeerDependencies:
- supports-color
@@ -10249,7 +10432,7 @@ snapshots:
dependencies:
babel-runtime: 6.26.0
esutils: 2.0.3
- lodash: 4.17.21
+ lodash: 4.17.23
to-fast-properties: 1.0.3
babylon@6.18.0: {}
@@ -10258,10 +10441,16 @@ snapshots:
balanced-match@2.0.0: {}
+ balanced-match@4.0.2:
+ dependencies:
+ jackspeak: 4.2.3
+
base64-arraybuffer@1.0.2: {}
base64-js@1.5.1: {}
+ baseline-browser-mapping@2.9.19: {}
+
batch@0.6.1: {}
bcrypt-pbkdf@1.0.2:
@@ -10301,11 +10490,11 @@ snapshots:
dependencies:
bytes: 3.1.2
content-type: 1.0.5
- debug: 4.4.1
+ debug: 4.4.3
http-errors: 2.0.0
iconv-lite: 0.6.3
on-finished: 2.4.1
- qs: 6.14.0
+ qs: 6.14.2
raw-body: 3.0.0
type-is: 2.0.1
transitivePeerDependencies:
@@ -10329,14 +10518,18 @@ snapshots:
dependencies:
balanced-match: 1.0.2
+ brace-expansion@5.0.2:
+ dependencies:
+ balanced-match: 4.0.2
+
braces@3.0.3:
dependencies:
fill-range: 7.1.1
- broadcast-channel@5.5.1:
+ broadcast-channel@7.1.0:
dependencies:
- '@babel/runtime': 7.23.2
- oblivious-set: 1.1.1
+ '@babel/runtime': 7.27.0
+ oblivious-set: 1.4.0
p-queue: 6.6.2
unload: 2.4.1
@@ -10349,6 +10542,14 @@ snapshots:
node-releases: 2.0.19
update-browserslist-db: 1.1.3(browserslist@4.25.1)
+ browserslist@4.28.1:
+ dependencies:
+ baseline-browser-mapping: 2.9.19
+ caniuse-lite: 1.0.30001769
+ electron-to-chromium: 1.5.286
+ node-releases: 2.0.27
+ update-browserslist-db: 1.2.3(browserslist@4.28.1)
+
bser@2.1.1:
dependencies:
node-int64: 0.4.0
@@ -10457,13 +10658,15 @@ snapshots:
caniuse-api@3.0.0:
dependencies:
- browserslist: 4.25.1
- caniuse-lite: 1.0.30001726
+ browserslist: 4.28.1
+ caniuse-lite: 1.0.30001769
lodash.memoize: 4.1.2
lodash.uniq: 4.5.0
caniuse-lite@1.0.30001726: {}
+ caniuse-lite@1.0.30001769: {}
+
canvas-exif-orientation@0.4.0: {}
canvg@1.5.3:
@@ -10530,9 +10733,9 @@ snapshots:
ci-info@4.3.0: {}
- circular-dependency-plugin@5.2.2(webpack@5.99.9):
+ circular-dependency-plugin@5.2.2(webpack@5.104.1):
dependencies:
- webpack: 5.99.9(webpack-cli@6.0.1)
+ webpack: 5.104.1(webpack-cli@6.0.1)
cjs-module-lexer@1.4.3: {}
@@ -10558,8 +10761,6 @@ snapshots:
co@4.6.0: {}
- codemirror@5.58.2: {}
-
collect-v8-coverage@1.0.2: {}
color-convert@1.9.3:
@@ -10606,10 +10807,6 @@ snapshots:
commander@7.2.0: {}
- commander@9.5.0: {}
-
- common-path-prefix@3.0.0: {}
-
common-tags@1.8.2: {}
commondir@1.0.1: {}
@@ -10649,13 +10846,13 @@ snapshots:
console-control-strings@1.1.0: {}
- consolidate@0.15.1(babel-core@7.0.0-bridge.0(@babel/core@7.28.0))(ejs@3.1.10)(lodash@4.17.21):
+ consolidate@0.15.1(babel-core@7.0.0-bridge.0(@babel/core@7.29.0))(ejs@3.1.10)(lodash@4.17.23):
dependencies:
bluebird: 3.7.2
optionalDependencies:
- babel-core: 7.0.0-bridge.0(@babel/core@7.28.0)
+ babel-core: 7.0.0-bridge.0(@babel/core@7.29.0)
ejs: 3.1.10
- lodash: 4.17.21
+ lodash: 4.17.23
content-disposition@0.5.4:
dependencies:
@@ -10677,13 +10874,13 @@ snapshots:
cookie@0.7.2: {}
- core-js-compat@3.43.0:
+ core-js-compat@3.48.0:
dependencies:
- browserslist: 4.25.1
+ browserslist: 4.28.1
core-js@2.6.12: {}
- core-js@3.44.0: {}
+ core-js@3.47.0: {}
core-util-is@1.0.2: {}
@@ -10759,7 +10956,7 @@ snapshots:
dependencies:
utrie: 1.0.2
- css-loader@7.1.2(webpack@5.99.9):
+ css-loader@7.1.2(webpack@5.104.1):
dependencies:
icss-utils: 5.1.0(postcss@8.5.6)
postcss: 8.5.6
@@ -10770,17 +10967,17 @@ snapshots:
postcss-value-parser: 4.2.0
semver: 7.7.2
optionalDependencies:
- webpack: 5.99.9(webpack-cli@6.0.1)
+ webpack: 5.104.1(webpack-cli@6.0.1)
- css-minimizer-webpack-plugin@7.0.2(webpack@5.99.9):
+ css-minimizer-webpack-plugin@7.0.2(webpack@5.104.1):
dependencies:
'@jridgewell/trace-mapping': 0.3.29
cssnano: 7.0.7(postcss@8.5.6)
jest-worker: 29.7.0
postcss: 8.5.6
- schema-utils: 4.3.2
+ schema-utils: 4.3.3
serialize-javascript: 6.0.2
- webpack: 5.99.9(webpack-cli@6.0.1)
+ webpack: 5.104.1(webpack-cli@6.0.1)
css-select@5.2.2:
dependencies:
@@ -10820,7 +11017,7 @@ snapshots:
cssnano-preset-default@7.0.7(postcss@8.5.6):
dependencies:
- browserslist: 4.25.1
+ browserslist: 4.28.1
css-declaration-sorter: 7.2.0(postcss@8.5.6)
cssnano-utils: 5.0.1(postcss@8.5.6)
postcss: 8.5.6
@@ -10938,6 +11135,10 @@ snapshots:
dependencies:
ms: 2.1.3
+ debug@4.4.3:
+ dependencies:
+ ms: 2.1.3
+
decamelize-keys@1.1.1:
dependencies:
decamelize: 1.2.0
@@ -10949,6 +11150,8 @@ snapshots:
decimal.js@10.5.0: {}
+ decimal.js@10.6.0: {}
+
decode-uri-component@0.2.2: {}
dedent@1.6.0: {}
@@ -11104,16 +11307,18 @@ snapshots:
'@one-ini/wasm': 0.1.1
commander: 10.0.1
minimatch: 9.0.1
- semver: 7.7.2
+ semver: 7.7.4
ee-first@1.1.1: {}
ejs@3.1.10:
dependencies:
- jake: 10.9.2
+ jake: 10.9.4
electron-to-chromium@1.5.178: {}
+ electron-to-chromium@1.5.286: {}
+
emittery@0.13.1: {}
emoji-regex@8.0.0: {}
@@ -11140,7 +11345,12 @@ snapshots:
enhanced-resolve@5.18.2:
dependencies:
graceful-fs: 4.2.11
- tapable: 2.2.2
+ tapable: 2.3.0
+
+ enhanced-resolve@5.19.0:
+ dependencies:
+ graceful-fs: 4.2.11
+ tapable: 2.3.0
enquirer@2.4.1:
dependencies:
@@ -11159,11 +11369,11 @@ snapshots:
dependencies:
'@types/localforage': 0.0.34
'@xmldom/xmldom': 0.7.13
- core-js: 3.44.0
+ core-js: 3.47.0
event-emitter: 0.3.5
jszip: 3.10.1
localforage: 1.10.0
- lodash: 4.17.21
+ lodash: 4.17.23
marks-pane: 1.0.9
path-webpack: 0.0.3
@@ -11230,6 +11440,63 @@ snapshots:
unbox-primitive: 1.1.0
which-typed-array: 1.1.19
+ es-abstract@1.24.1:
+ dependencies:
+ array-buffer-byte-length: 1.0.2
+ arraybuffer.prototype.slice: 1.0.4
+ available-typed-arrays: 1.0.7
+ call-bind: 1.0.8
+ call-bound: 1.0.4
+ data-view-buffer: 1.0.2
+ data-view-byte-length: 1.0.2
+ data-view-byte-offset: 1.0.1
+ es-define-property: 1.0.1
+ es-errors: 1.3.0
+ es-object-atoms: 1.1.1
+ es-set-tostringtag: 2.1.0
+ es-to-primitive: 1.3.0
+ function.prototype.name: 1.1.8
+ get-intrinsic: 1.3.0
+ get-proto: 1.0.1
+ get-symbol-description: 1.1.0
+ globalthis: 1.0.4
+ gopd: 1.2.0
+ has-property-descriptors: 1.0.2
+ has-proto: 1.2.0
+ has-symbols: 1.1.0
+ hasown: 2.0.2
+ internal-slot: 1.1.0
+ is-array-buffer: 3.0.5
+ is-callable: 1.2.7
+ is-data-view: 1.0.2
+ is-negative-zero: 2.0.3
+ is-regex: 1.2.1
+ is-set: 2.0.3
+ is-shared-array-buffer: 1.0.4
+ is-string: 1.1.1
+ is-typed-array: 1.1.15
+ is-weakref: 1.1.1
+ math-intrinsics: 1.1.0
+ object-inspect: 1.13.4
+ object-keys: 1.1.1
+ object.assign: 4.1.7
+ own-keys: 1.0.1
+ regexp.prototype.flags: 1.5.4
+ safe-array-concat: 1.1.3
+ safe-push-apply: 1.0.0
+ safe-regex-test: 1.1.0
+ set-proto: 1.0.0
+ stop-iteration-iterator: 1.1.0
+ string.prototype.trim: 1.2.10
+ string.prototype.trimend: 1.0.9
+ string.prototype.trimstart: 1.0.8
+ typed-array-buffer: 1.0.3
+ typed-array-byte-length: 1.0.3
+ typed-array-byte-offset: 1.0.4
+ typed-array-length: 1.0.7
+ unbox-primitive: 1.1.0
+ which-typed-array: 1.1.20
+
es-define-property@1.0.1: {}
es-errors@1.3.0: {}
@@ -11246,7 +11513,7 @@ snapshots:
isarray: 2.0.5
stop-iteration-iterator: 1.1.0
- es-module-lexer@1.7.0: {}
+ es-module-lexer@2.0.0: {}
es-object-atoms@1.1.1:
dependencies:
@@ -11328,7 +11595,7 @@ snapshots:
transitivePeerDependencies:
- supports-color
- eslint-import-resolver-webpack@0.13.10(eslint-plugin-import@2.32.0)(webpack@5.99.9):
+ eslint-import-resolver-webpack@0.13.10(eslint-plugin-import@2.32.0)(webpack@5.104.1):
dependencies:
debug: 3.2.7
enhanced-resolve: 0.9.1
@@ -11338,10 +11605,10 @@ snapshots:
interpret: 1.4.0
is-core-module: 2.16.1
is-regex: 1.2.1
- lodash: 4.17.21
+ lodash: 4.17.23
resolve: 2.0.0-next.5
semver: 5.7.2
- webpack: 5.99.9(webpack-cli@6.0.1)
+ webpack: 5.104.1(webpack-cli@6.0.1)
transitivePeerDependencies:
- supports-color
@@ -11351,7 +11618,7 @@ snapshots:
optionalDependencies:
eslint: 8.57.1
eslint-import-resolver-node: 0.3.9
- eslint-import-resolver-webpack: 0.13.10(eslint-plugin-import@2.32.0)(webpack@5.99.9)
+ eslint-import-resolver-webpack: 0.13.10(eslint-plugin-import@2.32.0)(webpack@5.104.1)
transitivePeerDependencies:
- supports-color
@@ -11392,7 +11659,17 @@ snapshots:
eslint-plugin-jest@28.14.0(eslint@8.57.1)(jest@29.7.0(@types/node@24.1.0))(typescript@5.8.3):
dependencies:
- '@typescript-eslint/utils': 8.35.1(eslint@8.57.1)(typescript@5.8.3)
+ '@typescript-eslint/utils': 8.47.0(eslint@8.57.1)(typescript@5.8.3)
+ eslint: 8.57.1
+ optionalDependencies:
+ jest: 29.7.0(@types/node@24.1.0)
+ transitivePeerDependencies:
+ - supports-color
+ - typescript
+
+ eslint-plugin-jest@29.1.0(eslint@8.57.1)(jest@29.7.0(@types/node@24.1.0))(typescript@5.8.3):
+ dependencies:
+ '@typescript-eslint/utils': 8.47.0(eslint@8.57.1)(typescript@5.8.3)
eslint: 8.57.1
optionalDependencies:
jest: 29.7.0(@types/node@24.1.0)
@@ -11448,7 +11725,7 @@ snapshots:
ajv: 6.12.6
chalk: 4.1.2
cross-spawn: 7.0.6
- debug: 4.4.1
+ debug: 4.4.3
doctrine: 3.0.0
enquirer: 2.4.1
escape-string-regexp: 4.0.0
@@ -11648,7 +11925,7 @@ snapshots:
content-type: 1.0.5
cookie: 0.7.2
cookie-signature: 1.2.2
- debug: 4.4.1
+ debug: 4.4.3
encodeurl: 2.0.0
escape-html: 1.0.3
etag: 1.8.1
@@ -11656,12 +11933,12 @@ snapshots:
fresh: 2.0.0
http-errors: 2.0.0
merge-descriptors: 2.0.0
- mime-types: 3.0.1
+ mime-types: 3.0.2
on-finished: 2.4.1
once: 1.4.0
parseurl: 1.3.3
proxy-addr: 2.0.7
- qs: 6.14.0
+ qs: 6.14.2
range-parser: 1.2.1
router: 2.2.0
send: 1.2.0
@@ -11688,7 +11965,7 @@ snapshots:
extsprintf@1.3.0: {}
- fake-indexeddb@5.0.2: {}
+ fake-indexeddb@6.2.5: {}
fast-deep-equal@3.1.3: {}
@@ -11728,11 +12005,11 @@ snapshots:
dependencies:
flat-cache: 3.2.0
- file-loader@6.2.0(webpack@5.99.9):
+ file-loader@6.2.0(webpack@5.104.1):
dependencies:
loader-utils: 2.0.4
schema-utils: 3.3.0
- webpack: 5.99.9(webpack-cli@6.0.1)
+ webpack: 5.104.1(webpack-cli@6.0.1)
file-saver@2.0.1: {}
@@ -11768,7 +12045,7 @@ snapshots:
finalhandler@2.1.0:
dependencies:
- debug: 4.4.1
+ debug: 4.4.3
encodeurl: 2.0.0
escape-html: 1.0.3
on-finished: 2.4.1
@@ -11788,11 +12065,6 @@ snapshots:
make-dir: 2.1.0
pkg-dir: 3.0.0
- find-cache-dir@4.0.0:
- dependencies:
- common-path-prefix: 3.0.0
- pkg-dir: 7.0.0
-
find-root@1.1.0: {}
find-up@3.0.0:
@@ -11809,11 +12081,6 @@ snapshots:
locate-path: 6.0.0
path-exists: 4.0.0
- find-up@6.3.0:
- dependencies:
- locate-path: 7.2.0
- path-exists: 5.0.0
-
flat-cache@3.2.0:
dependencies:
flatted: 3.3.3
@@ -11828,7 +12095,7 @@ snapshots:
flush-promises@1.0.2: {}
- follow-redirects@1.15.9: {}
+ follow-redirects@1.15.11: {}
fontfaceobserver@2.3.0: {}
@@ -11849,7 +12116,7 @@ snapshots:
combined-stream: 1.0.8
mime-types: 2.1.35
- form-data@4.0.4:
+ form-data@4.0.5:
dependencies:
asynckit: 0.4.0
combined-stream: 1.0.8
@@ -11871,7 +12138,7 @@ snapshots:
dependencies:
at-least-node: 1.0.0
graceful-fs: 4.2.11
- jsonfile: 6.1.0
+ jsonfile: 6.2.0
universalify: 2.0.1
fs-minipass@2.1.0:
@@ -11981,6 +12248,15 @@ snapshots:
package-json-from-dist: 1.0.1
path-scurry: 1.11.1
+ glob@11.1.0:
+ dependencies:
+ foreground-child: 3.3.1
+ jackspeak: 4.2.3
+ minimatch: 10.2.0
+ minipass: 7.1.2
+ package-json-from-dist: 1.0.1
+ path-scurry: 2.0.1
+
glob@7.1.7:
dependencies:
fs.realpath: 1.0.0
@@ -12042,7 +12318,7 @@ snapshots:
globule@1.3.4:
dependencies:
glob: 7.1.7
- lodash: 4.17.21
+ lodash: 4.17.23
minimatch: 3.0.8
gonzales-pe@4.3.0:
@@ -12164,7 +12440,7 @@ snapshots:
dependencies:
'@tootallnate/once': 1.1.2
agent-base: 6.0.2
- debug: 4.4.1
+ debug: 4.4.3
transitivePeerDependencies:
- supports-color
@@ -12172,7 +12448,7 @@ snapshots:
dependencies:
'@tootallnate/once': 2.0.0
agent-base: 6.0.2
- debug: 4.4.1
+ debug: 4.4.3
transitivePeerDependencies:
- supports-color
@@ -12191,7 +12467,7 @@ snapshots:
http-proxy@1.18.1:
dependencies:
eventemitter3: 4.0.7
- follow-redirects: 1.15.9
+ follow-redirects: 1.15.11
requires-port: 1.0.0
transitivePeerDependencies:
- debug
@@ -12205,7 +12481,7 @@ snapshots:
https-proxy-agent@5.0.1:
dependencies:
agent-base: 6.0.2
- debug: 4.4.1
+ debug: 4.4.3
transitivePeerDependencies:
- supports-color
@@ -12525,8 +12801,8 @@ snapshots:
istanbul-lib-instrument@5.2.1:
dependencies:
- '@babel/core': 7.28.0
- '@babel/parser': 7.28.0
+ '@babel/core': 7.29.0
+ '@babel/parser': 7.29.0
'@istanbuljs/schema': 0.1.3
istanbul-lib-coverage: 3.2.2
semver: 6.3.1
@@ -12535,11 +12811,11 @@ snapshots:
istanbul-lib-instrument@6.0.3:
dependencies:
- '@babel/core': 7.28.0
- '@babel/parser': 7.28.0
+ '@babel/core': 7.29.0
+ '@babel/parser': 7.29.0
'@istanbuljs/schema': 0.1.3
istanbul-lib-coverage: 3.2.2
- semver: 7.7.2
+ semver: 7.7.4
transitivePeerDependencies:
- supports-color
@@ -12551,7 +12827,7 @@ snapshots:
istanbul-lib-source-maps@4.0.1:
dependencies:
- debug: 4.4.1
+ debug: 4.4.3
istanbul-lib-coverage: 3.2.2
source-map: 0.6.1
transitivePeerDependencies:
@@ -12568,12 +12844,15 @@ snapshots:
optionalDependencies:
'@pkgjs/parseargs': 0.11.0
- jake@10.9.2:
+ jackspeak@4.2.3:
+ dependencies:
+ '@isaacs/cliui': 9.0.0
+
+ jake@10.9.4:
dependencies:
async: 3.2.6
- chalk: 4.1.2
filelist: 1.0.4
- minimatch: 3.1.2
+ picocolors: 1.1.1
jest-changed-files@29.7.0:
dependencies:
@@ -12628,10 +12907,10 @@ snapshots:
jest-config@29.7.0(@types/node@24.1.0):
dependencies:
- '@babel/core': 7.28.0
+ '@babel/core': 7.29.0
'@jest/test-sequencer': 29.7.0
'@jest/types': 29.6.3
- babel-jest: 29.7.0(@babel/core@7.28.0)
+ babel-jest: 29.7.0(@babel/core@7.29.0)
chalk: 4.1.2
ci-info: 3.9.0
deepmerge: 4.3.1
@@ -12739,7 +13018,7 @@ snapshots:
jest-message-util@29.7.0:
dependencies:
- '@babel/code-frame': 7.27.1
+ '@babel/code-frame': 7.29.0
'@jest/types': 29.6.3
'@types/stack-utils': 2.0.3
chalk: 4.1.2
@@ -12841,15 +13120,15 @@ snapshots:
jest-snapshot@29.7.0:
dependencies:
- '@babel/core': 7.28.0
- '@babel/generator': 7.28.0
- '@babel/plugin-syntax-jsx': 7.27.1(@babel/core@7.28.0)
- '@babel/plugin-syntax-typescript': 7.27.1(@babel/core@7.28.0)
- '@babel/types': 7.28.0
+ '@babel/core': 7.29.0
+ '@babel/generator': 7.29.1
+ '@babel/plugin-syntax-jsx': 7.27.1(@babel/core@7.29.0)
+ '@babel/plugin-syntax-typescript': 7.27.1(@babel/core@7.29.0)
+ '@babel/types': 7.29.0
'@jest/expect-utils': 29.7.0
'@jest/transform': 29.7.0
'@jest/types': 29.6.3
- babel-preset-current-node-syntax: 1.1.0(@babel/core@7.28.0)
+ babel-preset-current-node-syntax: 1.1.0(@babel/core@7.29.0)
chalk: 4.1.2
expect: 29.7.0
graceful-fs: 4.2.11
@@ -12860,7 +13139,7 @@ snapshots:
jest-util: 29.7.0
natural-compare: 1.4.0
pretty-format: 29.7.0
- semver: 7.7.2
+ semver: 7.7.4
transitivePeerDependencies:
- supports-color
@@ -12904,7 +13183,7 @@ snapshots:
jest-worker@27.5.1:
dependencies:
- '@types/node': 24.1.0
+ '@types/node': 25.2.1
merge-stream: 2.0.0
supports-color: 8.1.1
@@ -12931,8 +13210,6 @@ snapshots:
jpeg-js@0.4.4: {}
- jquery@2.2.4: {}
-
js-base64@2.6.4: {}
js-beautify@1.15.4:
@@ -12964,18 +13241,18 @@ snapshots:
jsbn@1.1.0: {}
- jscodeshift@17.3.0(@babel/preset-env@7.28.0(@babel/core@7.28.0)):
- dependencies:
- '@babel/core': 7.28.0
- '@babel/parser': 7.28.0
- '@babel/plugin-transform-class-properties': 7.27.1(@babel/core@7.28.0)
- '@babel/plugin-transform-modules-commonjs': 7.27.1(@babel/core@7.28.0)
- '@babel/plugin-transform-nullish-coalescing-operator': 7.27.1(@babel/core@7.28.0)
- '@babel/plugin-transform-optional-chaining': 7.27.1(@babel/core@7.28.0)
- '@babel/plugin-transform-private-methods': 7.27.1(@babel/core@7.28.0)
- '@babel/preset-flow': 7.27.1(@babel/core@7.28.0)
- '@babel/preset-typescript': 7.27.1(@babel/core@7.28.0)
- '@babel/register': 7.27.1(@babel/core@7.28.0)
+ jscodeshift@17.3.0(@babel/preset-env@7.29.0(@babel/core@7.29.0)):
+ dependencies:
+ '@babel/core': 7.29.0
+ '@babel/parser': 7.29.0
+ '@babel/plugin-transform-class-properties': 7.28.6(@babel/core@7.29.0)
+ '@babel/plugin-transform-modules-commonjs': 7.28.6(@babel/core@7.29.0)
+ '@babel/plugin-transform-nullish-coalescing-operator': 7.28.6(@babel/core@7.29.0)
+ '@babel/plugin-transform-optional-chaining': 7.28.6(@babel/core@7.29.0)
+ '@babel/plugin-transform-private-methods': 7.28.6(@babel/core@7.29.0)
+ '@babel/preset-flow': 7.27.1(@babel/core@7.29.0)
+ '@babel/preset-typescript': 7.27.1(@babel/core@7.29.0)
+ '@babel/register': 7.27.1(@babel/core@7.29.0)
flow-parser: 0.274.2
graceful-fs: 4.2.11
micromatch: 4.0.8
@@ -12985,7 +13262,7 @@ snapshots:
tmp: 0.2.3
write-file-atomic: 5.0.1
optionalDependencies:
- '@babel/preset-env': 7.28.0(@babel/core@7.28.0)
+ '@babel/preset-env': 7.29.0(@babel/core@7.29.0)
transitivePeerDependencies:
- supports-color
@@ -13000,7 +13277,7 @@ snapshots:
decimal.js: 10.5.0
domexception: 4.0.0
escodegen: 2.1.0
- form-data: 4.0.4
+ form-data: 4.0.5
html-encoding-sniffer: 3.0.0
http-proxy-agent: 5.0.0
https-proxy-agent: 5.0.1
@@ -13042,8 +13319,6 @@ snapshots:
whatwg-url: 2.0.1
xml-name-validator: 2.0.1
- jsesc@3.0.2: {}
-
jsesc@3.1.0: {}
json-buffer@3.0.1: {}
@@ -13068,7 +13343,7 @@ snapshots:
json5@2.2.3: {}
- jsonfile@6.1.0:
+ jsonfile@6.2.0:
dependencies:
universalify: 2.0.1
optionalDependencies:
@@ -13140,7 +13415,7 @@ snapshots:
vue2-teleport: 1.1.4
xstate: 4.38.3
- kolibri-design-system@5.3.0:
+ kolibri-design-system@https://codeload.github.com/MisRob/kolibri-design-system/tar.gz/ec3efbe080efdda3fabf8d05fe85a1d8d9fdffc9:
dependencies:
aphrodite: https://codeload.github.com/learningequality/aphrodite/tar.gz/fdc8d7be8912a5cf17f74ff10f124013c52c3e32
autosize: 3.0.21
@@ -13202,12 +13477,12 @@ snapshots:
chalk: 4.1.2
loglevel: 1.9.2
- kolibri-tools@0.18.2(@testing-library/dom@9.3.4)(@types/node@24.1.0)(ejs@3.1.10)(eslint-import-resolver-webpack@0.13.10)(file-loader@6.2.0(webpack@5.99.9))(postcss@8.5.6)(typescript@5.8.3)(vue@2.7.16):
+ kolibri-tools@0.18.2(@testing-library/dom@9.3.4)(@types/node@24.1.0)(ejs@3.1.10)(eslint-import-resolver-webpack@0.13.10)(file-loader@6.2.0(webpack@5.104.1))(postcss@8.5.6)(typescript@5.8.3)(vue@2.7.16):
dependencies:
- '@babel/core': 7.28.0
- '@babel/plugin-syntax-import-assertions': 7.27.1(@babel/core@7.28.0)
- '@babel/plugin-transform-runtime': 7.28.0(@babel/core@7.28.0)
- '@babel/preset-env': 7.28.0(@babel/core@7.28.0)
+ '@babel/core': 7.29.0
+ '@babel/plugin-syntax-import-assertions': 7.28.6(@babel/core@7.29.0)
+ '@babel/plugin-transform-runtime': 7.29.0(@babel/core@7.29.0)
+ '@babel/preset-env': 7.29.0(@babel/core@7.29.0)
'@rushstack/eslint-patch': 1.12.0
'@testing-library/jest-dom': 6.6.3
'@testing-library/user-event': 14.6.1(@testing-library/dom@9.3.4)
@@ -13215,17 +13490,17 @@ snapshots:
'@vue/test-utils': 1.3.6(vue-template-compiler@2.7.16)(vue@2.7.16)
ast-traverse: 0.1.1
autoprefixer: 10.4.21(postcss@8.5.6)
- babel-core: 7.0.0-bridge.0(@babel/core@7.28.0)
- babel-jest: 29.7.0(@babel/core@7.28.0)
- babel-loader: 10.0.0(@babel/core@7.28.0)(webpack@5.99.9)
+ babel-core: 7.0.0-bridge.0(@babel/core@7.29.0)
+ babel-jest: 29.7.0(@babel/core@7.29.0)
+ babel-loader: 10.0.0(@babel/core@7.29.0)(webpack@5.104.1)
browserslist-config-kolibri: 0.18.0
chalk: 4.1.2
check-node-version: 4.2.1
cli-table: 0.3.11
commander: 13.1.0
- core-js: 3.44.0
- css-loader: 7.1.2(webpack@5.99.9)
- css-minimizer-webpack-plugin: 7.0.2(webpack@5.99.9)
+ core-js: 3.47.0
+ css-loader: 7.1.2(webpack@5.104.1)
+ css-minimizer-webpack-plugin: 7.0.2(webpack@5.104.1)
csv-parse: 5.6.0
csv-writer: 1.6.0
del: 6.1.1
@@ -13236,36 +13511,36 @@ snapshots:
jest: 29.7.0(@types/node@24.1.0)
jest-environment-jsdom: 29.7.0
jest-serializer-vue: 3.1.0
- jscodeshift: 17.3.0(@babel/preset-env@7.28.0(@babel/core@7.28.0))
+ jscodeshift: 17.3.0(@babel/preset-env@7.29.0(@babel/core@7.29.0))
kolibri: 0.18.0
kolibri-format: 1.0.1(@testing-library/dom@9.3.4)(eslint-import-resolver-webpack@0.13.10)(jest@29.7.0(@types/node@24.1.0))(postcss@8.5.6)(typescript@5.8.3)
launch-editor-middleware: 2.10.0
- lodash: 4.17.21
- mini-css-extract-plugin: 2.9.2(webpack@5.99.9)
+ lodash: 4.17.23
+ mini-css-extract-plugin: 2.9.4(webpack@5.104.1)
node-sass: 9.0.0
- postcss-loader: 8.1.1(postcss@8.5.6)(typescript@5.8.3)(webpack@5.99.9)
+ postcss-loader: 8.1.1(postcss@8.5.6)(typescript@5.8.3)(webpack@5.104.1)
process: 0.11.10
query-ast: 1.0.5
readline-sync: 1.4.10
recast: 0.23.11
rewire: 6.0.0
rtlcss: 4.3.0
- sass-loader: 16.0.5(node-sass@9.0.0)(webpack@5.99.9)
+ sass-loader: 16.0.5(node-sass@9.0.0)(webpack@5.104.1)
semver: 7.7.2
strip-ansi: 6.0.1
- style-loader: 4.0.0(webpack@5.99.9)
+ style-loader: 4.0.0(webpack@5.104.1)
temp: 0.8.4
- terser-webpack-plugin: 5.3.14(webpack@5.99.9)
+ terser-webpack-plugin: 5.3.14(webpack@5.104.1)
toml: 3.0.0
- url-loader: 4.1.1(file-loader@6.2.0(webpack@5.99.9))(webpack@5.99.9)
- vue-jest: 3.0.7(babel-core@7.0.0-bridge.0(@babel/core@7.28.0))(vue-template-compiler@2.7.16)(vue@2.7.16)
- vue-loader: 15.11.1(babel-core@7.0.0-bridge.0(@babel/core@7.28.0))(css-loader@7.1.2(webpack@5.99.9))(ejs@3.1.10)(lodash@4.17.21)(vue-template-compiler@2.7.16)(webpack@5.99.9)
+ url-loader: 4.1.1(file-loader@6.2.0(webpack@5.104.1))(webpack@5.104.1)
+ vue-jest: 3.0.7(babel-core@7.0.0-bridge.0(@babel/core@7.29.0))(vue-template-compiler@2.7.16)(vue@2.7.16)
+ vue-loader: 15.11.1(babel-core@7.0.0-bridge.0(@babel/core@7.29.0))(css-loader@7.1.2(webpack@5.104.1))(ejs@3.1.10)(lodash@4.17.23)(vue-template-compiler@2.7.16)(webpack@5.104.1)
vue-sfc-descriptor-to-string: 1.0.0
vue-style-loader: 4.1.3
vue-template-compiler: 2.7.16
- webpack: 5.99.9(webpack-cli@6.0.1)
- webpack-cli: 6.0.1(webpack-dev-server@5.2.2)(webpack@5.99.9)
- webpack-dev-server: 5.2.2(webpack-cli@6.0.1)(webpack@5.99.9)
+ webpack: 5.104.1(webpack-cli@6.0.1)
+ webpack-cli: 6.0.1(webpack-dev-server@5.2.2)(webpack@5.104.1)
+ webpack-dev-server: 5.2.2(webpack-cli@6.0.1)(webpack@5.104.1)
webpack-merge: 6.0.1
transitivePeerDependencies:
- '@parcel/css'
@@ -13358,14 +13633,14 @@ snapshots:
kolibri@0.18.0:
dependencies:
'@vueuse/core': 11.3.0(vue@2.7.16)
- axios: 1.11.0
+ axios: 1.13.5
fontfaceobserver: 2.3.0
frame-throttle: 3.0.0
intl: 1.2.5
kolibri-constants: 0.2.9
kolibri-design-system: 5.0.1
lockr: 0.8.5
- lodash: 4.17.21
+ lodash: 4.17.23
path-to-regexp: 1.9.0
ua-parser-js: 1.0.40
vue: 2.7.16
@@ -13414,7 +13689,7 @@ snapshots:
dependencies:
uc.micro: 2.1.0
- linkifyjs@4.3.1: {}
+ linkifyjs@4.3.2: {}
load-json-file@4.0.0:
dependencies:
@@ -13423,7 +13698,7 @@ snapshots:
pify: 3.0.0
strip-bom: 3.0.0
- loader-runner@4.3.0: {}
+ loader-runner@4.3.1: {}
loader-utils@1.4.2:
dependencies:
@@ -13454,10 +13729,6 @@ snapshots:
dependencies:
p-locate: 5.0.0
- locate-path@7.2.0:
- dependencies:
- p-locate: 6.0.0
-
lockr@0.8.5: {}
lodash.clonedeep@4.5.0: {}
@@ -13482,6 +13753,8 @@ snapshots:
lodash@4.17.21: {}
+ lodash@4.17.23: {}
+
loglevel@1.9.2: {}
loose-envify@1.4.0:
@@ -13496,6 +13769,8 @@ snapshots:
lru-cache@10.4.3: {}
+ lru-cache@11.2.6: {}
+
lru-cache@4.1.5:
dependencies:
pseudomap: 1.0.2
@@ -13524,7 +13799,7 @@ snapshots:
make-dir@4.0.0:
dependencies:
- semver: 7.7.2
+ semver: 7.7.4
make-fetch-happen@10.2.1:
dependencies:
@@ -13589,15 +13864,17 @@ snapshots:
punycode.js: 2.3.1
uc.micro: 2.1.0
+ marked@16.1.1: {}
+
marks-pane@1.0.9: {}
material-icons@0.3.1: {}
math-intrinsics@1.1.0: {}
- mathlive@0.105.3:
+ mathlive@0.108.2:
dependencies:
- '@cortex-js/compute-engine': 0.28.0
+ '@cortex-js/compute-engine': 0.30.2
mathml-tag-names@2.1.3: {}
@@ -13683,7 +13960,7 @@ snapshots:
dependencies:
mime-db: 1.52.0
- mime-types@3.0.1:
+ mime-types@3.0.2:
dependencies:
mime-db: 1.54.0
@@ -13693,14 +13970,18 @@ snapshots:
min-indent@1.0.1: {}
- mini-css-extract-plugin@2.9.2(webpack@5.99.9):
+ mini-css-extract-plugin@2.9.4(webpack@5.104.1):
dependencies:
schema-utils: 4.3.2
- tapable: 2.2.2
- webpack: 5.99.9(webpack-cli@6.0.1)
+ tapable: 2.2.3
+ webpack: 5.104.1(webpack-cli@6.0.1)
minimalistic-assert@1.0.1: {}
+ minimatch@10.2.0:
+ dependencies:
+ brace-expansion: 5.0.2
+
minimatch@3.0.8:
dependencies:
brace-expansion: 1.1.12
@@ -13810,7 +14091,7 @@ snapshots:
node-cache@4.2.1:
dependencies:
clone: 2.1.2
- lodash: 4.17.21
+ lodash: 4.17.23
node-fetch@2.7.0(encoding@0.1.13):
dependencies:
@@ -13834,7 +14115,7 @@ snapshots:
nopt: 5.0.0
npmlog: 6.0.2
rimraf: 3.0.2
- semver: 7.7.2
+ semver: 7.7.4
tar: 6.2.1
which: 2.0.2
transitivePeerDependencies:
@@ -13845,6 +14126,8 @@ snapshots:
node-releases@2.0.19: {}
+ node-releases@2.0.27: {}
+
node-sass@9.0.0:
dependencies:
async-foreach: 0.1.3
@@ -13853,7 +14136,7 @@ snapshots:
gaze: 1.1.3
get-stdin: 4.0.1
glob: 7.2.3
- lodash: 4.17.21
+ lodash: 4.17.23
make-fetch-happen: 10.2.1
meow: 9.0.0
nan: 2.22.2
@@ -13895,7 +14178,7 @@ snapshots:
dependencies:
hosted-git-info: 4.1.0
is-core-module: 2.16.1
- semver: 7.7.2
+ semver: 7.7.4
validate-npm-package-license: 3.0.4
normalize-path@3.0.0: {}
@@ -13977,7 +14260,7 @@ snapshots:
define-properties: 1.2.1
es-object-atoms: 1.1.1
- oblivious-set@1.1.1: {}
+ oblivious-set@1.4.0: {}
obuf@1.1.2: {}
@@ -14042,10 +14325,6 @@ snapshots:
dependencies:
yocto-queue: 0.1.0
- p-limit@4.0.0:
- dependencies:
- yocto-queue: 1.2.1
-
p-locate@3.0.0:
dependencies:
p-limit: 2.3.0
@@ -14058,10 +14337,6 @@ snapshots:
dependencies:
p-limit: 3.1.0
- p-locate@6.0.0:
- dependencies:
- p-limit: 4.0.0
-
p-map@4.0.0:
dependencies:
aggregate-error: 3.1.0
@@ -14102,7 +14377,7 @@ snapshots:
parse-json@5.2.0:
dependencies:
- '@babel/code-frame': 7.27.1
+ '@babel/code-frame': 7.29.0
error-ex: 1.3.2
json-parse-even-better-errors: 2.3.1
lines-and-columns: 1.2.4
@@ -14119,8 +14394,6 @@ snapshots:
path-exists@4.0.0: {}
- path-exists@5.0.0: {}
-
path-is-absolute@1.0.1: {}
path-key@2.0.1: {}
@@ -14134,6 +14407,11 @@ snapshots:
lru-cache: 10.4.3
minipass: 7.1.2
+ path-scurry@2.0.1:
+ dependencies:
+ lru-cache: 11.2.6
+ minipass: 7.1.2
+
path-to-regexp@0.1.12: {}
path-to-regexp@1.9.0:
@@ -14187,10 +14465,6 @@ snapshots:
dependencies:
find-up: 4.1.0
- pkg-dir@7.0.0:
- dependencies:
- find-up: 6.3.0
-
pngjs@3.4.0: {}
pngjs@6.0.0: {}
@@ -14207,7 +14481,7 @@ snapshots:
postcss-colormin@7.0.3(postcss@8.5.6):
dependencies:
- browserslist: 4.25.1
+ browserslist: 4.28.1
caniuse-api: 3.0.0
colord: 2.9.3
postcss: 8.5.6
@@ -14215,7 +14489,7 @@ snapshots:
postcss-convert-values@7.0.5(postcss@8.5.6):
dependencies:
- browserslist: 4.25.1
+ browserslist: 4.28.1
postcss: 8.5.6
postcss-value-parser: 4.2.0
@@ -14247,14 +14521,14 @@ snapshots:
dependencies:
postcss: 8.5.6
- postcss-loader@8.1.1(postcss@8.5.6)(typescript@5.8.3)(webpack@5.99.9):
+ postcss-loader@8.1.1(postcss@8.5.6)(typescript@5.8.3)(webpack@5.104.1):
dependencies:
cosmiconfig: 9.0.0(typescript@5.8.3)
jiti: 1.21.7
postcss: 8.5.6
semver: 7.7.2
optionalDependencies:
- webpack: 5.99.9(webpack-cli@6.0.1)
+ webpack: 5.104.1(webpack-cli@6.0.1)
transitivePeerDependencies:
- typescript
@@ -14268,7 +14542,7 @@ snapshots:
postcss-merge-rules@7.0.5(postcss@8.5.6):
dependencies:
- browserslist: 4.25.1
+ browserslist: 4.28.1
caniuse-api: 3.0.0
cssnano-utils: 5.0.1(postcss@8.5.6)
postcss: 8.5.6
@@ -14288,7 +14562,7 @@ snapshots:
postcss-minify-params@7.0.3(postcss@8.5.6):
dependencies:
- browserslist: 4.25.1
+ browserslist: 4.28.1
cssnano-utils: 5.0.1(postcss@8.5.6)
postcss: 8.5.6
postcss-value-parser: 4.2.0
@@ -14351,7 +14625,7 @@ snapshots:
postcss-normalize-unicode@7.0.3(postcss@8.5.6):
dependencies:
- browserslist: 4.25.1
+ browserslist: 4.28.1
postcss: 8.5.6
postcss-value-parser: 4.2.0
@@ -14373,7 +14647,7 @@ snapshots:
postcss-reduce-initial@7.0.3(postcss@8.5.6):
dependencies:
- browserslist: 4.25.1
+ browserslist: 4.28.1
caniuse-api: 3.0.0
postcss: 8.5.6
@@ -14490,106 +14764,106 @@ snapshots:
prosemirror-changeset@2.3.1:
dependencies:
- prosemirror-transform: 1.10.4
+ prosemirror-transform: 1.10.5
prosemirror-collab@1.3.1:
dependencies:
- prosemirror-state: 1.4.3
+ prosemirror-state: 1.4.4
prosemirror-commands@1.7.1:
dependencies:
- prosemirror-model: 1.25.1
- prosemirror-state: 1.4.3
- prosemirror-transform: 1.10.4
+ prosemirror-model: 1.25.4
+ prosemirror-state: 1.4.4
+ prosemirror-transform: 1.10.5
prosemirror-dropcursor@1.8.2:
dependencies:
- prosemirror-state: 1.4.3
- prosemirror-transform: 1.10.4
- prosemirror-view: 1.40.0
+ prosemirror-state: 1.4.4
+ prosemirror-transform: 1.10.5
+ prosemirror-view: 1.41.4
- prosemirror-gapcursor@1.3.2:
+ prosemirror-gapcursor@1.4.0:
dependencies:
prosemirror-keymap: 1.2.3
- prosemirror-model: 1.25.1
- prosemirror-state: 1.4.3
- prosemirror-view: 1.40.0
+ prosemirror-model: 1.25.4
+ prosemirror-state: 1.4.4
+ prosemirror-view: 1.41.4
- prosemirror-history@1.4.1:
+ prosemirror-history@1.5.0:
dependencies:
- prosemirror-state: 1.4.3
- prosemirror-transform: 1.10.4
- prosemirror-view: 1.40.0
+ prosemirror-state: 1.4.4
+ prosemirror-transform: 1.10.5
+ prosemirror-view: 1.41.4
rope-sequence: 1.3.4
- prosemirror-inputrules@1.5.0:
+ prosemirror-inputrules@1.5.1:
dependencies:
- prosemirror-state: 1.4.3
- prosemirror-transform: 1.10.4
+ prosemirror-state: 1.4.4
+ prosemirror-transform: 1.10.5
prosemirror-keymap@1.2.3:
dependencies:
- prosemirror-state: 1.4.3
+ prosemirror-state: 1.4.4
w3c-keyname: 2.2.8
prosemirror-markdown@1.13.2:
dependencies:
'@types/markdown-it': 14.1.2
markdown-it: 14.1.0
- prosemirror-model: 1.25.1
+ prosemirror-model: 1.25.4
prosemirror-menu@1.2.5:
dependencies:
crelt: 1.0.6
prosemirror-commands: 1.7.1
- prosemirror-history: 1.4.1
- prosemirror-state: 1.4.3
+ prosemirror-history: 1.5.0
+ prosemirror-state: 1.4.4
- prosemirror-model@1.25.1:
+ prosemirror-model@1.25.4:
dependencies:
orderedmap: 2.1.1
prosemirror-schema-basic@1.2.4:
dependencies:
- prosemirror-model: 1.25.1
+ prosemirror-model: 1.25.4
prosemirror-schema-list@1.5.1:
dependencies:
- prosemirror-model: 1.25.1
- prosemirror-state: 1.4.3
- prosemirror-transform: 1.10.4
+ prosemirror-model: 1.25.4
+ prosemirror-state: 1.4.4
+ prosemirror-transform: 1.10.5
- prosemirror-state@1.4.3:
+ prosemirror-state@1.4.4:
dependencies:
- prosemirror-model: 1.25.1
- prosemirror-transform: 1.10.4
- prosemirror-view: 1.40.0
+ prosemirror-model: 1.25.4
+ prosemirror-transform: 1.10.5
+ prosemirror-view: 1.41.4
- prosemirror-tables@1.7.1:
+ prosemirror-tables@1.8.3:
dependencies:
prosemirror-keymap: 1.2.3
- prosemirror-model: 1.25.1
- prosemirror-state: 1.4.3
- prosemirror-transform: 1.10.4
- prosemirror-view: 1.40.0
+ prosemirror-model: 1.25.4
+ prosemirror-state: 1.4.4
+ prosemirror-transform: 1.10.5
+ prosemirror-view: 1.41.4
- prosemirror-trailing-node@3.0.0(prosemirror-model@1.25.1)(prosemirror-state@1.4.3)(prosemirror-view@1.40.0):
+ prosemirror-trailing-node@3.0.0(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.4):
dependencies:
'@remirror/core-constants': 3.0.0
escape-string-regexp: 4.0.0
- prosemirror-model: 1.25.1
- prosemirror-state: 1.4.3
- prosemirror-view: 1.40.0
+ prosemirror-model: 1.25.4
+ prosemirror-state: 1.4.4
+ prosemirror-view: 1.41.4
- prosemirror-transform@1.10.4:
+ prosemirror-transform@1.10.5:
dependencies:
- prosemirror-model: 1.25.1
+ prosemirror-model: 1.25.4
- prosemirror-view@1.40.0:
+ prosemirror-view@1.41.4:
dependencies:
- prosemirror-model: 1.25.1
- prosemirror-state: 1.4.3
- prosemirror-transform: 1.10.4
+ prosemirror-model: 1.25.4
+ prosemirror-state: 1.4.4
+ prosemirror-transform: 1.10.5
proto-list@1.2.4: {}
@@ -14618,16 +14892,16 @@ snapshots:
dependencies:
side-channel: 1.1.0
- qs@6.14.0:
+ qs@6.14.2:
dependencies:
side-channel: 1.1.0
- qs@6.5.3: {}
+ qs@6.5.5: {}
query-ast@1.0.5:
dependencies:
invariant: 2.2.4
- lodash: 4.17.21
+ lodash: 4.17.23
querystringify@2.2.0: {}
@@ -14645,25 +14919,25 @@ snapshots:
ranges-apply@5.1.0:
dependencies:
- '@babel/runtime': 7.27.6
+ '@babel/runtime': 7.28.4
ranges-merge: 7.1.0
ranges-merge@7.1.0:
dependencies:
- '@babel/runtime': 7.27.6
+ '@babel/runtime': 7.28.6
ranges-push: 5.1.0
ranges-sort: 4.1.0
ranges-push@5.1.0:
dependencies:
- '@babel/runtime': 7.27.6
+ '@babel/runtime': 7.28.4
ranges-merge: 7.1.0
string-collapse-leading-whitespace: 5.1.0
string-trim-spaces-only: 3.1.0
ranges-sort@4.1.0:
dependencies:
- '@babel/runtime': 7.27.6
+ '@babel/runtime': 7.28.6
raw-body@2.5.2:
dependencies:
@@ -14782,7 +15056,7 @@ snapshots:
get-proto: 1.0.1
which-builtin-type: 1.2.1
- regenerate-unicode-properties@10.2.0:
+ regenerate-unicode-properties@10.2.2:
dependencies:
regenerate: 1.4.2
@@ -14805,20 +15079,20 @@ snapshots:
regexpp@3.2.0: {}
- regexpu-core@6.2.0:
+ regexpu-core@6.4.0:
dependencies:
regenerate: 1.4.2
- regenerate-unicode-properties: 10.2.0
+ regenerate-unicode-properties: 10.2.2
regjsgen: 0.8.0
- regjsparser: 0.12.0
+ regjsparser: 0.13.0
unicode-match-property-ecmascript: 2.0.0
- unicode-match-property-value-ecmascript: 2.2.0
+ unicode-match-property-value-ecmascript: 2.2.1
regjsgen@0.8.0: {}
- regjsparser@0.12.0:
+ regjsparser@0.13.0:
dependencies:
- jsesc: 3.0.2
+ jsesc: 3.1.0
request@2.88.2:
dependencies:
@@ -14837,7 +15111,7 @@ snapshots:
mime-types: 2.1.35
oauth-sign: 0.9.0
performance-now: 2.1.0
- qs: 6.5.3
+ qs: 6.5.5
safe-buffer: 5.2.1
tough-cookie: 2.5.0
tunnel-agent: 0.6.0
@@ -14869,6 +15143,12 @@ snapshots:
path-parse: 1.0.7
supports-preserve-symlinks-flag: 1.0.0
+ resolve@1.22.11:
+ dependencies:
+ is-core-module: 2.16.1
+ path-parse: 1.0.7
+ supports-preserve-symlinks-flag: 1.0.0
+
resolve@2.0.0-next.5:
dependencies:
is-core-module: 2.16.1
@@ -14905,7 +15185,7 @@ snapshots:
router@2.2.0:
dependencies:
- debug: 4.4.1
+ debug: 4.4.3
depd: 2.0.0
is-promise: 4.0.0
parseurl: 1.3.3
@@ -14954,18 +15234,16 @@ snapshots:
sass-graph@4.0.1:
dependencies:
glob: 7.2.3
- lodash: 4.17.21
+ lodash: 4.17.23
scss-tokenizer: 0.4.3
yargs: 17.7.2
- sass-loader@16.0.5(node-sass@9.0.0)(webpack@5.99.9):
+ sass-loader@16.0.5(node-sass@9.0.0)(webpack@5.104.1):
dependencies:
neo-async: 2.6.2
optionalDependencies:
node-sass: 9.0.0
- webpack: 5.99.9(webpack-cli@6.0.1)
-
- sax@1.3.0: {}
+ webpack: 5.104.1(webpack-cli@6.0.1)
sax@1.4.1: {}
@@ -14986,10 +15264,17 @@ snapshots:
ajv-formats: 2.1.1(ajv@8.17.1)
ajv-keywords: 5.1.0(ajv@8.17.1)
+ schema-utils@4.3.3:
+ dependencies:
+ '@types/json-schema': 7.0.15
+ ajv: 8.17.1
+ ajv-formats: 2.1.1(ajv@8.17.1)
+ ajv-keywords: 5.1.0(ajv@8.17.1)
+
scss-tokenizer@0.4.3:
dependencies:
js-base64: 2.6.4
- source-map: 0.7.4
+ source-map: 0.7.6
select-hose@2.0.0: {}
@@ -15004,6 +15289,8 @@ snapshots:
semver@7.7.2: {}
+ semver@7.7.4: {}
+
send@0.19.0:
dependencies:
debug: 2.6.9
@@ -15024,13 +15311,13 @@ snapshots:
send@1.2.0:
dependencies:
- debug: 4.4.1
+ debug: 4.4.3
encodeurl: 2.0.0
escape-html: 1.0.3
etag: 1.8.1
fresh: 2.0.0
http-errors: 2.0.0
- mime-types: 3.0.1
+ mime-types: 3.0.2
ms: 2.1.3
on-finished: 2.4.1
range-parser: 1.2.1
@@ -15120,10 +15407,6 @@ snapshots:
shell-quote@1.8.3: {}
- showdown@2.1.0:
- dependencies:
- commander: 9.5.0
-
side-channel-list@1.0.0:
dependencies:
es-errors: 1.3.0
@@ -15172,7 +15455,7 @@ snapshots:
smart-buffer@4.2.0: {}
- smob@1.5.0: {}
+ smob@1.6.1: {}
sockjs@0.3.24:
dependencies:
@@ -15183,7 +15466,7 @@ snapshots:
socks-proxy-agent@6.2.1:
dependencies:
agent-base: 6.0.2
- debug: 4.4.1
+ debug: 4.4.3
socks: 2.8.5
transitivePeerDependencies:
- supports-color
@@ -15191,7 +15474,7 @@ snapshots:
socks-proxy-agent@7.0.0:
dependencies:
agent-base: 6.0.2
- debug: 4.4.1
+ debug: 4.4.3
socks: 2.8.5
transitivePeerDependencies:
- supports-color
@@ -15229,7 +15512,7 @@ snapshots:
source-map@0.6.1: {}
- source-map@0.7.4: {}
+ source-map@0.7.6: {}
source-map@0.8.0-beta.0:
dependencies:
@@ -15255,7 +15538,7 @@ snapshots:
spdy-transport@3.0.0:
dependencies:
- debug: 4.4.1
+ debug: 4.4.3
detect-node: 2.1.0
hpack.js: 2.1.6
obuf: 1.1.2
@@ -15266,7 +15549,7 @@ snapshots:
spdy@4.0.2:
dependencies:
- debug: 4.4.1
+ debug: 4.4.3
handle-thing: 2.0.1
http-deceiver: 1.2.7
select-hose: 2.0.0
@@ -15325,13 +15608,13 @@ snapshots:
string-collapse-leading-whitespace@5.1.0:
dependencies:
- '@babel/runtime': 7.27.6
+ '@babel/runtime': 7.28.6
string-hash@1.1.3: {}
string-left-right@4.1.0:
dependencies:
- '@babel/runtime': 7.27.6
+ '@babel/runtime': 7.28.4
lodash.clonedeep: 4.5.0
lodash.isplainobject: 4.0.6
@@ -15353,7 +15636,7 @@ snapshots:
string-trim-spaces-only@3.1.0:
dependencies:
- '@babel/runtime': 7.27.6
+ '@babel/runtime': 7.28.6
string-width@4.2.3:
dependencies:
@@ -15365,14 +15648,14 @@ snapshots:
dependencies:
eastasianwidth: 0.2.0
emoji-regex: 9.2.2
- strip-ansi: 7.1.0
+ strip-ansi: 7.1.2
string.prototype.matchall@4.0.12:
dependencies:
call-bind: 1.0.8
call-bound: 1.0.4
define-properties: 1.2.1
- es-abstract: 1.24.0
+ es-abstract: 1.24.1
es-errors: 1.3.0
es-object-atoms: 1.1.1
get-intrinsic: 1.3.0
@@ -15435,9 +15718,9 @@ snapshots:
dependencies:
ansi-regex: 5.0.1
- strip-ansi@7.1.0:
+ strip-ansi@7.1.2:
dependencies:
- ansi-regex: 6.1.0
+ ansi-regex: 6.2.2
strip-bom@3.0.0: {}
@@ -15464,15 +15747,15 @@ snapshots:
'@tokenizer/token': 0.3.0
peek-readable: 4.1.0
- style-loader@4.0.0(webpack@5.99.9):
+ style-loader@4.0.0(webpack@5.104.1):
dependencies:
- webpack: 5.99.9(webpack-cli@6.0.1)
+ webpack: 5.104.1(webpack-cli@6.0.1)
style-search@0.1.0: {}
stylehacks@7.0.5(postcss@8.5.6):
dependencies:
- browserslist: 4.25.1
+ browserslist: 4.28.1
postcss: 8.5.6
postcss-selector-parser: 7.1.0
@@ -15597,21 +15880,21 @@ snapshots:
- supports-color
- typescript
- stylus-loader@8.1.1(stylus@0.63.0)(webpack@5.99.9):
+ stylus-loader@8.1.2(stylus@0.64.0)(webpack@5.104.1):
dependencies:
fast-glob: 3.3.3
normalize-path: 3.0.0
- stylus: 0.63.0
+ stylus: 0.64.0
optionalDependencies:
- webpack: 5.99.9(webpack-cli@6.0.1)
+ webpack: 5.104.1(webpack-cli@6.0.1)
- stylus@0.63.0:
+ stylus@0.64.0:
dependencies:
'@adobe/css-tools': 4.3.3
- debug: 4.4.1
- glob: 7.2.3
- sax: 1.3.0
- source-map: 0.7.4
+ debug: 4.4.3
+ glob: 10.4.5
+ sax: 1.4.1
+ source-map: 0.7.6
transitivePeerDependencies:
- supports-color
@@ -15660,7 +15943,9 @@ snapshots:
tapable@0.1.10: {}
- tapable@2.2.2: {}
+ tapable@2.2.3: {}
+
+ tapable@2.3.0: {}
tar@6.2.1:
dependencies:
@@ -15684,14 +15969,23 @@ snapshots:
type-fest: 0.16.0
unique-string: 2.0.0
- terser-webpack-plugin@5.3.14(webpack@5.99.9):
+ terser-webpack-plugin@5.3.14(webpack@5.104.1):
dependencies:
'@jridgewell/trace-mapping': 0.3.29
jest-worker: 27.5.1
- schema-utils: 4.3.2
+ schema-utils: 4.3.3
serialize-javascript: 6.0.2
terser: 5.43.1
- webpack: 5.99.9(webpack-cli@6.0.1)
+ webpack: 5.104.1(webpack-cli@6.0.1)
+
+ terser-webpack-plugin@5.3.16(webpack@5.104.1):
+ dependencies:
+ '@jridgewell/trace-mapping': 0.3.31
+ jest-worker: 27.5.1
+ schema-utils: 4.3.3
+ serialize-javascript: 6.0.2
+ terser: 5.46.0
+ webpack: 5.104.1(webpack-cli@6.0.1)
terser@5.43.1:
dependencies:
@@ -15700,6 +15994,13 @@ snapshots:
commander: 2.20.3
source-map-support: 0.5.21
+ terser@5.46.0:
+ dependencies:
+ '@jridgewell/source-map': 0.3.11
+ acorn: 8.15.0
+ commander: 2.20.3
+ source-map-support: 0.5.21
+
test-exclude@6.0.0:
dependencies:
'@istanbuljs/schema': 0.1.3
@@ -15728,10 +16029,6 @@ snapshots:
dependencies:
popper.js: 1.16.1
- tippy.js@6.3.7:
- dependencies:
- '@popperjs/core': 2.11.8
-
tmp@0.2.3: {}
tmpl@1.0.5: {}
@@ -15842,7 +16139,7 @@ snapshots:
dependencies:
content-type: 1.0.5
media-typer: 1.1.0
- mime-types: 3.0.1
+ mime-types: 3.0.2
type@2.7.3: {}
@@ -15894,6 +16191,8 @@ snapshots:
undici-types@5.26.5: {}
+ undici-types@7.16.0: {}
+
undici-types@7.8.0: {}
unicode-canonical-property-names-ecmascript@2.0.1: {}
@@ -15901,11 +16200,11 @@ snapshots:
unicode-match-property-ecmascript@2.0.0:
dependencies:
unicode-canonical-property-names-ecmascript: 2.0.1
- unicode-property-aliases-ecmascript: 2.1.0
+ unicode-property-aliases-ecmascript: 2.2.0
- unicode-match-property-value-ecmascript@2.2.0: {}
+ unicode-match-property-value-ecmascript@2.2.1: {}
- unicode-property-aliases-ecmascript@2.1.0: {}
+ unicode-property-aliases-ecmascript@2.2.0: {}
unique-filename@1.1.1:
dependencies:
@@ -15943,20 +16242,26 @@ snapshots:
escalade: 3.2.0
picocolors: 1.1.1
+ update-browserslist-db@1.2.3(browserslist@4.28.1):
+ dependencies:
+ browserslist: 4.28.1
+ escalade: 3.2.0
+ picocolors: 1.1.1
+
uri-js@4.4.1:
dependencies:
punycode: 2.3.1
urix@0.1.0: {}
- url-loader@4.1.1(file-loader@6.2.0(webpack@5.99.9))(webpack@5.99.9):
+ url-loader@4.1.1(file-loader@6.2.0(webpack@5.104.1))(webpack@5.104.1):
dependencies:
loader-utils: 2.0.4
mime-types: 2.1.35
schema-utils: 3.3.0
- webpack: 5.99.9(webpack-cli@6.0.1)
+ webpack: 5.104.1(webpack-cli@6.0.1)
optionalDependencies:
- file-loader: 6.2.0(webpack@5.99.9)
+ file-loader: 6.2.0(webpack@5.104.1)
url-parse@1.5.10:
dependencies:
@@ -15985,7 +16290,7 @@ snapshots:
v8-to-istanbul@9.3.0:
dependencies:
- '@jridgewell/trace-mapping': 0.3.29
+ '@jridgewell/trace-mapping': 0.3.31
'@types/istanbul-lib-coverage': 2.0.6
convert-source-map: 2.0.0
@@ -16015,13 +16320,13 @@ snapshots:
vue-eslint-parser@9.4.3(eslint@8.57.1):
dependencies:
- debug: 4.4.1
+ debug: 4.4.3
eslint: 8.57.1
eslint-scope: 7.2.2
eslint-visitor-keys: 3.4.3
espree: 9.6.1
esquery: 1.6.0
- lodash: 4.17.21
+ lodash: 4.17.23
semver: 7.7.2
transitivePeerDependencies:
- supports-color
@@ -16036,9 +16341,9 @@ snapshots:
intl-relativeformat: 1.3.0
vue: 2.7.16
- vue-jest@3.0.7(babel-core@7.0.0-bridge.0(@babel/core@7.28.0))(vue-template-compiler@2.7.16)(vue@2.7.16):
+ vue-jest@3.0.7(babel-core@7.0.0-bridge.0(@babel/core@7.29.0))(vue-template-compiler@2.7.16)(vue@2.7.16):
dependencies:
- babel-core: 7.0.0-bridge.0(@babel/core@7.28.0)
+ babel-core: 7.0.0-bridge.0(@babel/core@7.29.0)
babel-plugin-transform-es2015-modules-commonjs: 6.26.2
chalk: 2.4.2
deasync: 0.1.30
@@ -16055,15 +16360,15 @@ snapshots:
transitivePeerDependencies:
- supports-color
- vue-loader@15.11.1(babel-core@7.0.0-bridge.0(@babel/core@7.28.0))(css-loader@7.1.2(webpack@5.99.9))(ejs@3.1.10)(lodash@4.17.21)(vue-template-compiler@2.7.16)(webpack@5.99.9):
+ vue-loader@15.11.1(babel-core@7.0.0-bridge.0(@babel/core@7.29.0))(css-loader@7.1.2(webpack@5.104.1))(ejs@3.1.10)(lodash@4.17.23)(vue-template-compiler@2.7.16)(webpack@5.104.1):
dependencies:
- '@vue/component-compiler-utils': 3.3.0(babel-core@7.0.0-bridge.0(@babel/core@7.28.0))(ejs@3.1.10)(lodash@4.17.21)
- css-loader: 7.1.2(webpack@5.99.9)
+ '@vue/component-compiler-utils': 3.3.0(babel-core@7.0.0-bridge.0(@babel/core@7.29.0))(ejs@3.1.10)(lodash@4.17.23)
+ css-loader: 7.1.2(webpack@5.104.1)
hash-sum: 1.0.2
loader-utils: 1.4.2
vue-hot-reload-api: 2.3.4
vue-style-loader: 4.1.3
- webpack: 5.99.9(webpack-cli@6.0.1)
+ webpack: 5.104.1(webpack-cli@6.0.1)
optionalDependencies:
vue-template-compiler: 2.7.16
transitivePeerDependencies:
@@ -16174,7 +16479,7 @@ snapshots:
dependencies:
makeerror: 1.0.12
- watchpack@2.4.4:
+ watchpack@2.5.1:
dependencies:
glob-to-regexp: 0.4.1
graceful-fs: 4.2.11
@@ -16193,12 +16498,12 @@ snapshots:
webidl-conversions@7.0.0: {}
- webpack-cli@6.0.1(webpack-dev-server@5.2.2)(webpack@5.99.9):
+ webpack-cli@6.0.1(webpack-dev-server@5.2.2)(webpack@5.104.1):
dependencies:
'@discoveryjs/json-ext': 0.6.3
- '@webpack-cli/configtest': 3.0.1(webpack-cli@6.0.1)(webpack@5.99.9)
- '@webpack-cli/info': 3.0.1(webpack-cli@6.0.1)(webpack@5.99.9)
- '@webpack-cli/serve': 3.0.1(webpack-cli@6.0.1)(webpack-dev-server@5.2.2)(webpack@5.99.9)
+ '@webpack-cli/configtest': 3.0.1(webpack-cli@6.0.1)(webpack@5.104.1)
+ '@webpack-cli/info': 3.0.1(webpack-cli@6.0.1)(webpack@5.104.1)
+ '@webpack-cli/serve': 3.0.1(webpack-cli@6.0.1)(webpack-dev-server@5.2.2)(webpack@5.104.1)
colorette: 2.0.20
commander: 12.1.0
cross-spawn: 7.0.6
@@ -16207,23 +16512,23 @@ snapshots:
import-local: 3.2.0
interpret: 3.1.1
rechoir: 0.8.0
- webpack: 5.99.9(webpack-cli@6.0.1)
+ webpack: 5.104.1(webpack-cli@6.0.1)
webpack-merge: 6.0.1
optionalDependencies:
- webpack-dev-server: 5.2.2(webpack-cli@6.0.1)(webpack@5.99.9)
+ webpack-dev-server: 5.2.2(webpack-cli@6.0.1)(webpack@5.104.1)
- webpack-dev-middleware@7.4.2(webpack@5.99.9):
+ webpack-dev-middleware@7.4.2(webpack@5.104.1):
dependencies:
colorette: 2.0.20
memfs: 4.17.2
mime-types: 2.1.35
on-finished: 2.4.1
range-parser: 1.2.1
- schema-utils: 4.3.2
+ schema-utils: 4.3.3
optionalDependencies:
- webpack: 5.99.9(webpack-cli@6.0.1)
+ webpack: 5.104.1(webpack-cli@6.0.1)
- webpack-dev-server@5.2.2(webpack-cli@6.0.1)(webpack@5.99.9):
+ webpack-dev-server@5.2.2(webpack-cli@6.0.1)(webpack@5.104.1):
dependencies:
'@types/bonjour': 3.5.13
'@types/connect-history-api-fallback': 1.5.4
@@ -16251,11 +16556,11 @@ snapshots:
serve-index: 1.9.1
sockjs: 0.3.24
spdy: 4.0.2
- webpack-dev-middleware: 7.4.2(webpack@5.99.9)
+ webpack-dev-middleware: 7.4.2(webpack@5.104.1)
ws: 8.18.3
optionalDependencies:
- webpack: 5.99.9(webpack-cli@6.0.1)
- webpack-cli: 6.0.1(webpack-dev-server@5.2.2)(webpack@5.99.9)
+ webpack: 5.104.1(webpack-cli@6.0.1)
+ webpack-cli: 6.0.1(webpack-dev-server@5.2.2)(webpack@5.104.1)
transitivePeerDependencies:
- bufferutil
- debug
@@ -16275,7 +16580,7 @@ snapshots:
webpack-sources@3.3.3: {}
- webpack@5.99.9(webpack-cli@6.0.1):
+ webpack@5.104.1(webpack-cli@6.0.1):
dependencies:
'@types/eslint-scope': 3.7.7
'@types/estree': 1.0.8
@@ -16284,25 +16589,26 @@ snapshots:
'@webassemblyjs/wasm-edit': 1.14.1
'@webassemblyjs/wasm-parser': 1.14.1
acorn: 8.15.0
- browserslist: 4.25.1
+ acorn-import-phases: 1.0.4(acorn@8.15.0)
+ browserslist: 4.28.1
chrome-trace-event: 1.0.4
- enhanced-resolve: 5.18.2
- es-module-lexer: 1.7.0
+ enhanced-resolve: 5.19.0
+ es-module-lexer: 2.0.0
eslint-scope: 5.1.1
events: 3.3.0
glob-to-regexp: 0.4.1
graceful-fs: 4.2.11
json-parse-even-better-errors: 2.3.1
- loader-runner: 4.3.0
+ loader-runner: 4.3.1
mime-types: 2.1.35
neo-async: 2.6.2
- schema-utils: 4.3.2
- tapable: 2.2.2
- terser-webpack-plugin: 5.3.14(webpack@5.99.9)
- watchpack: 2.4.4
+ schema-utils: 4.3.3
+ tapable: 2.3.0
+ terser-webpack-plugin: 5.3.16(webpack@5.104.1)
+ watchpack: 2.5.1
webpack-sources: 3.3.3
optionalDependencies:
- webpack-cli: 6.0.1(webpack-dev-server@5.2.2)(webpack@5.99.9)
+ webpack-cli: 6.0.1(webpack-dev-server@5.2.2)(webpack@5.104.1)
transitivePeerDependencies:
- '@swc/core'
- esbuild
@@ -16386,6 +16692,16 @@ snapshots:
gopd: 1.2.0
has-tostringtag: 1.0.2
+ which-typed-array@1.1.20:
+ dependencies:
+ available-typed-arrays: 1.0.7
+ call-bind: 1.0.8
+ call-bound: 1.0.4
+ for-each: 0.3.5
+ get-proto: 1.0.1
+ gopd: 1.2.0
+ has-tostringtag: 1.0.2
+
which@1.3.1:
dependencies:
isexe: 2.0.0
@@ -16402,22 +16718,22 @@ snapshots:
word-wrap@1.2.5: {}
- workbox-background-sync@7.3.0:
+ workbox-background-sync@7.4.0:
dependencies:
idb: 7.1.1
- workbox-core: 7.3.0
+ workbox-core: 7.4.0
- workbox-broadcast-update@7.3.0:
+ workbox-broadcast-update@7.4.0:
dependencies:
- workbox-core: 7.3.0
+ workbox-core: 7.4.0
- workbox-build@7.3.0(@types/babel__core@7.20.5):
+ workbox-build@7.4.0(@types/babel__core@7.20.5):
dependencies:
'@apideck/better-ajv-errors': 0.3.6(ajv@8.17.1)
- '@babel/core': 7.28.0
- '@babel/preset-env': 7.28.0(@babel/core@7.28.0)
- '@babel/runtime': 7.27.6
- '@rollup/plugin-babel': 5.3.1(@babel/core@7.28.0)(@types/babel__core@7.20.5)(rollup@2.79.2)
+ '@babel/core': 7.29.0
+ '@babel/preset-env': 7.29.0(@babel/core@7.29.0)
+ '@babel/runtime': 7.28.6
+ '@rollup/plugin-babel': 5.3.1(@babel/core@7.29.0)(@types/babel__core@7.20.5)(rollup@2.79.2)
'@rollup/plugin-node-resolve': 15.3.1(rollup@2.79.2)
'@rollup/plugin-replace': 2.4.2(rollup@2.79.2)
'@rollup/plugin-terser': 0.4.4(rollup@2.79.2)
@@ -16426,8 +16742,8 @@ snapshots:
common-tags: 1.8.2
fast-json-stable-stringify: 2.1.0
fs-extra: 9.1.0
- glob: 7.2.3
- lodash: 4.17.21
+ glob: 11.1.0
+ lodash: 4.17.23
pretty-bytes: 5.6.0
rollup: 2.79.2
source-map: 0.8.0-beta.0
@@ -16435,46 +16751,48 @@ snapshots:
strip-comments: 2.0.1
tempy: 0.6.0
upath: 1.2.0
- workbox-background-sync: 7.3.0
- workbox-broadcast-update: 7.3.0
- workbox-cacheable-response: 7.3.0
- workbox-core: 7.3.0
- workbox-expiration: 7.3.0
- workbox-google-analytics: 7.3.0
- workbox-navigation-preload: 7.3.0
- workbox-precaching: 7.3.0
- workbox-range-requests: 7.3.0
- workbox-recipes: 7.3.0
- workbox-routing: 7.3.0
- workbox-strategies: 7.3.0
- workbox-streams: 7.3.0
- workbox-sw: 7.3.0
- workbox-window: 7.3.0
+ workbox-background-sync: 7.4.0
+ workbox-broadcast-update: 7.4.0
+ workbox-cacheable-response: 7.4.0
+ workbox-core: 7.4.0
+ workbox-expiration: 7.4.0
+ workbox-google-analytics: 7.4.0
+ workbox-navigation-preload: 7.4.0
+ workbox-precaching: 7.4.0
+ workbox-range-requests: 7.4.0
+ workbox-recipes: 7.4.0
+ workbox-routing: 7.4.0
+ workbox-strategies: 7.4.0
+ workbox-streams: 7.4.0
+ workbox-sw: 7.4.0
+ workbox-window: 7.4.0
transitivePeerDependencies:
- '@types/babel__core'
- supports-color
- workbox-cacheable-response@7.3.0:
+ workbox-cacheable-response@7.4.0:
dependencies:
- workbox-core: 7.3.0
+ workbox-core: 7.4.0
workbox-core@7.3.0: {}
- workbox-expiration@7.3.0:
+ workbox-core@7.4.0: {}
+
+ workbox-expiration@7.4.0:
dependencies:
idb: 7.1.1
- workbox-core: 7.3.0
+ workbox-core: 7.4.0
- workbox-google-analytics@7.3.0:
+ workbox-google-analytics@7.4.0:
dependencies:
- workbox-background-sync: 7.3.0
- workbox-core: 7.3.0
- workbox-routing: 7.3.0
- workbox-strategies: 7.3.0
+ workbox-background-sync: 7.4.0
+ workbox-core: 7.4.0
+ workbox-routing: 7.4.0
+ workbox-strategies: 7.4.0
- workbox-navigation-preload@7.3.0:
+ workbox-navigation-preload@7.4.0:
dependencies:
- workbox-core: 7.3.0
+ workbox-core: 7.4.0
workbox-precaching@7.3.0:
dependencies:
@@ -16482,42 +16800,56 @@ snapshots:
workbox-routing: 7.3.0
workbox-strategies: 7.3.0
- workbox-range-requests@7.3.0:
+ workbox-precaching@7.4.0:
dependencies:
- workbox-core: 7.3.0
+ workbox-core: 7.4.0
+ workbox-routing: 7.4.0
+ workbox-strategies: 7.4.0
- workbox-recipes@7.3.0:
+ workbox-range-requests@7.4.0:
dependencies:
- workbox-cacheable-response: 7.3.0
- workbox-core: 7.3.0
- workbox-expiration: 7.3.0
- workbox-precaching: 7.3.0
- workbox-routing: 7.3.0
- workbox-strategies: 7.3.0
+ workbox-core: 7.4.0
+
+ workbox-recipes@7.4.0:
+ dependencies:
+ workbox-cacheable-response: 7.4.0
+ workbox-core: 7.4.0
+ workbox-expiration: 7.4.0
+ workbox-precaching: 7.4.0
+ workbox-routing: 7.4.0
+ workbox-strategies: 7.4.0
workbox-routing@7.3.0:
dependencies:
workbox-core: 7.3.0
+ workbox-routing@7.4.0:
+ dependencies:
+ workbox-core: 7.4.0
+
workbox-strategies@7.3.0:
dependencies:
workbox-core: 7.3.0
- workbox-streams@7.3.0:
+ workbox-strategies@7.4.0:
dependencies:
- workbox-core: 7.3.0
- workbox-routing: 7.3.0
+ workbox-core: 7.4.0
+
+ workbox-streams@7.4.0:
+ dependencies:
+ workbox-core: 7.4.0
+ workbox-routing: 7.4.0
- workbox-sw@7.3.0: {}
+ workbox-sw@7.4.0: {}
- workbox-webpack-plugin@7.3.0(@types/babel__core@7.20.5)(webpack@5.99.9):
+ workbox-webpack-plugin@7.4.0(@types/babel__core@7.20.5)(webpack@5.104.1):
dependencies:
fast-json-stable-stringify: 2.1.0
pretty-bytes: 5.6.0
upath: 1.2.0
- webpack: 5.99.9(webpack-cli@6.0.1)
+ webpack: 5.104.1(webpack-cli@6.0.1)
webpack-sources: 1.4.3
- workbox-build: 7.3.0(@types/babel__core@7.20.5)
+ workbox-build: 7.4.0(@types/babel__core@7.20.5)
transitivePeerDependencies:
- '@types/babel__core'
- supports-color
@@ -16527,6 +16859,11 @@ snapshots:
'@types/trusted-types': 2.0.7
workbox-core: 7.3.0
+ workbox-window@7.4.0:
+ dependencies:
+ '@types/trusted-types': 2.0.7
+ workbox-core: 7.4.0
+
wrap-ansi@7.0.0:
dependencies:
ansi-styles: 4.3.0
@@ -16535,9 +16872,9 @@ snapshots:
wrap-ansi@8.1.0:
dependencies:
- ansi-styles: 6.2.1
+ ansi-styles: 6.2.3
string-width: 5.1.2
- strip-ansi: 7.1.0
+ strip-ansi: 7.1.2
wrappy@1.0.2: {}
@@ -16586,5 +16923,3 @@ snapshots:
yargs-parser: 21.1.1
yocto-queue@0.1.0: {}
-
- yocto-queue@1.2.1: {}
diff --git a/requirements-dev.in b/requirements-dev.in
index 595ad607ce..8a9224102c 100644
--- a/requirements-dev.in
+++ b/requirements-dev.in
@@ -5,8 +5,6 @@ mixer==7.2.2
pytest
pytest-django
pytest-timeout
-pytest-subtests
-pre-commit==4.2.0
+pre-commit==4.5.1
nodeenv
-pip-tools==7.5.0
drf-yasg==1.21.10
diff --git a/requirements-dev.txt b/requirements-dev.txt
index 295cd1744c..0a95537ba6 100644
--- a/requirements-dev.txt
+++ b/requirements-dev.txt
@@ -1,25 +1,11 @@
-#
-# This file is autogenerated by pip-compile with Python 3.10
-# by the following command:
-#
-# pip-compile requirements-dev.in
-#
+# This file was autogenerated by uv via the following command:
+# uv pip compile requirements-dev.in --output-file requirements-dev.txt
asgiref==3.3.4
# via
# -c requirements.txt
# django
-attrs==23.1.0
- # via
- # -c requirements.txt
- # pytest-subtests
-build==1.2.1
- # via pip-tools
cfgv==3.3.1
# via pre-commit
-click==8.1.3
- # via
- # -c requirements.txt
- # pip-tools
distlib==0.3.9
# via virtualenv
django==3.2.24
@@ -35,11 +21,13 @@ djangorestframework==3.15.1
# drf-yasg
drf-yasg==1.21.10
# via -r requirements-dev.in
-exceptiongroup==1.2.2
- # via pytest
+exceptiongroup==1.3.1
+ # via
+ # -c requirements.txt
+ # pytest
faker==12.0.1
# via mixer
-filelock==3.16.1
+filelock==3.20.3
# via virtualenv
identify==2.4.4
# via pre-commit
@@ -53,40 +41,30 @@ mock==5.2.0
# via
# -r requirements-dev.in
# django-concurrent-test-helper
-nodeenv==1.9.1
+nodeenv==1.10.0
# via
# -r requirements-dev.in
# pre-commit
-packaging==25.0
+packaging==26.0
# via
# -c requirements.txt
- # build
# drf-yasg
# pytest
-pip-tools==7.5.0
- # via -r requirements-dev.in
platformdirs==4.3.6
# via virtualenv
pluggy==1.5.0
# via pytest
-pre-commit==4.2.0
+pre-commit==4.5.1
# via -r requirements-dev.in
pygments==2.19.1
# via pytest
-pyproject-hooks==1.1.0
- # via
- # build
- # pip-tools
-pytest==8.4.1
+pytest==9.0.2
# via
# -r requirements-dev.in
# pytest-django
- # pytest-subtests
# pytest-timeout
pytest-django==4.11.1
# via -r requirements-dev.in
-pytest-subtests==0.14.2
- # via -r requirements-dev.in
pytest-timeout==2.4.0
# via -r requirements-dev.in
python-dateutil==2.9.0.post0
@@ -114,17 +92,13 @@ sqlparse==0.4.1
tblib==1.7.0
# via django-concurrent-test-helper
tomli==1.2.3
+ # via pytest
+typing-extensions==4.15.0
# via
- # build
- # pip-tools
- # pytest
+ # -c requirements.txt
+ # exceptiongroup
+ # virtualenv
uritemplate==3.0.1
# via drf-yasg
-virtualenv==20.26.6
+virtualenv==20.36.1
# via pre-commit
-wheel==0.38.1
- # via pip-tools
-
-# The following packages are considered to be unsafe in a requirements file:
-# pip
-# setuptools
diff --git a/requirements.in b/requirements.in
index 27ca67281c..ff3997361e 100644
--- a/requirements.in
+++ b/requirements.in
@@ -2,38 +2,39 @@ django-cte==1.3.3
django-mptt==0.16.0
django-filter==23.5
djangorestframework==3.15.1
-psycopg2-binary==2.9.10
+psycopg2-binary==2.9.11
django-js-reverse==0.10.2
django-registration==3.4
le-utils>=0.2.12
gunicorn==23.0.0
django-postmark==0.1.6
jsonfield==3.1.0
-celery==5.5.3
+celery==5.6.0
redis
-python-postmark==0.6.1
+python-postmark==0.7.0
Django==3.2.24
django-webpack-loader==0.7.0
google-cloud-error-reporting
-google-cloud-storage
+google-cloud-storage==2.19.0
django-s3-storage==0.15.0
requests>=2.20.0
google-cloud-core
django-db-readonly==0.7.0
-google-cloud-kms==2.10.0
-google-crc32c==1.7.1
+google-cloud-kms==2.24.2
+google-crc32c==1.8.0
backoff
django-model-utils==5.0.0
django-redis
django-prometheus
sentry-sdk
html5lib==1.1
-pillow==11.3.0
+pillow==12.1.1
python-dateutil>=2.8.1
jsonschema>=3.2.0
django-celery-results
packaging>=21.0
-langcodes==3.5.0
-pydantic==2.11.7
-latex2mathml==3.78.0
-markdown-it-py==3.0.0
+pycountry
+langcodes==3.5.1
+pydantic==2.12.5
+latex2mathml==3.78.1
+markdown-it-py==4.0.0
diff --git a/requirements.txt b/requirements.txt
index 9863c77097..fc0ebd636c 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,9 +1,5 @@
-#
-# This file is autogenerated by pip-compile with Python 3.10
-# by the following command:
-#
-# pip-compile requirements.in
-#
+# This file was autogenerated by uv via the following command:
+# uv pip compile requirements.in --output-file requirements.txt
amqp==5.1.1
# via kombu
annotated-types==0.7.0
@@ -26,9 +22,7 @@ botocore==1.20.75
# via
# boto3
# s3transfer
-cachetools==4.2.2
- # via google-auth
-celery==5.5.3
+celery==5.6.0
# via
# -r requirements.in
# django-celery-results
@@ -36,6 +30,8 @@ certifi==2020.12.5
# via
# requests
# sentry-sdk
+cffi==2.0.0
+ # via cryptography
charset-normalizer==3.3.2
# via requests
click==8.1.3
@@ -52,6 +48,8 @@ click-repl==0.2.0
# via celery
confusable-homoglyphs==3.2.0
# via django-registration
+cryptography==46.0.5
+ # via google-auth
django==3.2.24
# via
# -r requirements.in
@@ -95,47 +93,66 @@ django-webpack-loader==0.7.0
# via -r requirements.in
djangorestframework==3.15.1
# via -r requirements.in
-google-api-core[grpc]==1.27.0
+exceptiongroup==1.3.1
+ # via celery
+google-api-core==2.30.0
# via
+ # google-cloud-appengine-logging
# google-cloud-core
# google-cloud-error-reporting
# google-cloud-kms
# google-cloud-logging
-google-auth==1.30.0
+ # google-cloud-storage
+google-auth==2.48.0
# via
# google-api-core
+ # google-cloud-appengine-logging
# google-cloud-core
+ # google-cloud-kms
# google-cloud-storage
-google-cloud-core==1.7.3
+google-cloud-appengine-logging==1.8.0
+ # via google-cloud-logging
+google-cloud-audit-log==0.4.0
+ # via google-cloud-logging
+google-cloud-core==2.5.0
# via
# -r requirements.in
# google-cloud-logging
# google-cloud-storage
google-cloud-error-reporting==1.4.0
# via -r requirements.in
-google-cloud-kms==2.10.0
+google-cloud-kms==2.24.2
# via -r requirements.in
-google-cloud-logging==2.3.1
+google-cloud-logging==2.7.1
# via google-cloud-error-reporting
-google-cloud-storage==1.41.1
+google-cloud-storage==2.19.0
# via -r requirements.in
-google-crc32c==1.7.1
+google-crc32c==1.8.0
# via
# -r requirements.in
+ # google-cloud-storage
# google-resumable-media
-google-resumable-media==1.3.0
+google-resumable-media==2.8.0
# via google-cloud-storage
-googleapis-common-protos[grpc]==1.57.0
+googleapis-common-protos==1.57.0
# via
# google-api-core
+ # google-cloud-audit-log
# grpc-google-iam-v1
+ # grpcio-status
grpc-google-iam-v1==0.12.4
- # via google-cloud-kms
-grpcio==1.53.2
+ # via
+ # google-cloud-kms
+ # google-cloud-logging
+grpcio==1.78.1
# via
# google-api-core
+ # google-cloud-appengine-logging
# googleapis-common-protos
# grpc-google-iam-v1
+ # grpcio-status
+grpcio-status==1.62.3
+ # via google-api-core
gunicorn==23.0.0
# via -r requirements.in
html5lib==1.1
@@ -150,51 +167,52 @@ jmespath==0.10.0
# botocore
jsonfield==3.1.0
# via -r requirements.in
-jsonschema==4.25.0
+jsonschema==4.26.0
# via -r requirements.in
jsonschema-specifications==2024.10.1
# via jsonschema
-kombu==5.5.2
+kombu==5.6.1
# via celery
-langcodes==3.5.0
+langcodes==3.5.1
# via -r requirements.in
-language-data==1.3.0
- # via langcodes
-latex2mathml==3.78.0
+latex2mathml==3.78.1
# via -r requirements.in
le-utils==0.2.12
# via -r requirements.in
-marisa-trie==1.2.1
- # via language-data
-markdown-it-py==3.0.0
+markdown-it-py==4.0.0
# via -r requirements.in
mdurl==0.1.2
# via markdown-it-py
-packaging==25.0
+packaging==26.0
# via
# -r requirements.in
# django-js-reverse
- # google-api-core
# google-cloud-error-reporting
- # google-cloud-kms
# gunicorn
-pillow==11.3.0
+ # kombu
+pillow==12.1.1
# via -r requirements.in
prometheus-client==0.10.1
# via django-prometheus
prompt-toolkit==3.0.23
# via click-repl
-proto-plus==1.18.1
+proto-plus==1.27.1
# via
+ # google-api-core
+ # google-cloud-appengine-logging
# google-cloud-error-reporting
# google-cloud-kms
# google-cloud-logging
protobuf==4.25.8
# via
# google-api-core
+ # google-cloud-appengine-logging
+ # google-cloud-audit-log
+ # google-cloud-kms
# googleapis-common-protos
+ # grpcio-status
# proto-plus
-psycopg2-binary==2.9.10
+psycopg2-binary==2.9.11
# via -r requirements.in
pyasn1==0.4.8
# via
@@ -202,9 +220,13 @@ pyasn1==0.4.8
# rsa
pyasn1-modules==0.2.8
# via google-auth
-pydantic==2.11.7
+pycountry==24.6.1
+ # via -r requirements.in
+pycparser==3.0
+ # via cffi
+pydantic==2.12.5
# via -r requirements.in
-pydantic-core==2.33.2
+pydantic-core==2.41.5
# via pydantic
pyparsing==2.4.7
# via httplib2
@@ -213,14 +235,13 @@ python-dateutil==2.9.0.post0
# -r requirements.in
# botocore
# celery
-python-postmark==0.6.1
+python-postmark==0.7.0
# via -r requirements.in
pytz==2022.1
# via
# django
# django-postmark
- # google-api-core
-redis==6.4.0
+redis==7.1.0
# via
# -r requirements.in
# django-redis
@@ -228,12 +249,12 @@ referencing==0.36.2
# via
# jsonschema
# jsonschema-specifications
-requests==2.32.4
+requests==2.32.5
# via
# -r requirements.in
# google-api-core
# google-cloud-storage
-rpds-py==0.24.0
+rpds-py==0.30.0
# via
# jsonschema
# referencing
@@ -241,29 +262,30 @@ rsa==4.7.2
# via google-auth
s3transfer==0.4.2
# via boto3
-sentry-sdk==2.34.1
+sentry-sdk==2.48.0
# via -r requirements.in
six==1.16.0
# via
# click-repl
- # google-api-core
- # google-auth
- # google-cloud-core
- # google-resumable-media
# html5lib
# python-dateutil
sqlparse==0.4.1
# via django
-typing-extensions==4.13.0
+typing-extensions==4.15.0
# via
+ # cryptography
+ # exceptiongroup
+ # grpcio
# pydantic
# pydantic-core
# referencing
# typing-inspection
-typing-inspection==0.4.1
+typing-inspection==0.4.2
# via pydantic
tzdata==2025.2
# via kombu
+tzlocal==5.3.1
+ # via celery
urllib3==1.26.18
# via
# botocore
@@ -278,6 +300,3 @@ wcwidth==0.2.5
# via prompt-toolkit
webencodings==0.5.1
# via html5lib
-
-# The following packages are considered to be unsafe in a requirements file:
-# setuptools
diff --git a/webpack.config.js b/webpack.config.js
index 04bb82bb5e..d055936b79 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -5,7 +5,6 @@ const process = require('node:process');
const fs = require('node:fs');
const { execSync } = require('node:child_process');
-const { NormalModuleReplacementPlugin } = require('webpack');
const baseConfig = require('kolibri-tools/lib/webpack.config.base');
const { merge } = require('webpack-merge');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
@@ -41,7 +40,6 @@ function getWSLIP() {
const djangoProjectDir = path.resolve('contentcuration');
const staticFilesDir = path.resolve(djangoProjectDir, 'contentcuration', 'static');
const srcDir = path.resolve(djangoProjectDir, 'contentcuration', 'frontend');
-const dummyModule = path.resolve(srcDir, 'shared', 'styles', 'modulePlaceholder.js')
const bundleOutputDir = path.resolve(staticFilesDir, 'studio');
@@ -186,15 +184,5 @@ module.exports = (env = {}) => {
],
stats: 'normal',
});
- if (dev) {
- config.entry.editorDev = './editorDev/index.js';
- } else {
- config.plugins.push(
- new NormalModuleReplacementPlugin(
- /styl$/,
- dummyModule
- ),
- )
- }
return config;
};