-
-
- <%block name="viewtitle">
- %block>
-
-<%block name="viewcontent">%block>
-%block>
diff --git a/cms/templates/maintenance/container.html b/cms/templates/maintenance/container.html
deleted file mode 100644
index 319a57bfe995..000000000000
--- a/cms/templates/maintenance/container.html
+++ /dev/null
@@ -1,25 +0,0 @@
-<%page expression_filter="h"/>
-<%inherit file="base.html" />
-<%namespace name='static' file='../static_content.html'/>
-<%!
-from django.urls import reverse
-from openedx.core.djangolib.js_utils import js_escaped_string
-%>
-<%block name="title">${view['name']}%block>
-<%block name="viewtitle">
-
- ${view['name']}
-
-%block>
-
-<%block name="viewcontent">
-
- <%include file="_${view['slug']}.html"/>
-
-%block>
-
-<%block name="requirejs">
- require(["js/maintenance/${view['slug'] | n, js_escaped_string}"], function(MaintenanceFactory) {
- MaintenanceFactory("${reverse(view['url']) | n, js_escaped_string}");
- });
-%block>
diff --git a/cms/templates/maintenance/index.html b/cms/templates/maintenance/index.html
deleted file mode 100644
index 293cb90b4a9c..000000000000
--- a/cms/templates/maintenance/index.html
+++ /dev/null
@@ -1,20 +0,0 @@
-<%page expression_filter="h"/>
-<%inherit file="base.html" />
-<%namespace name='static' file='../static_content.html'/>
-<%!
-from django.utils.translation import gettext as _
-from django.urls import reverse
-%>
-<%block name="title">${_('Maintenance Dashboard')}%block>
-<%block name="viewcontent">
-
-
- % for view in views.values():
-
- ${view['name']}
- ${view['description']}
-
- % endfor
-
-
-%block>
diff --git a/cms/templates/studio_xblock_wrapper.html b/cms/templates/studio_xblock_wrapper.html
index 4fb3c71803cb..8929b225cc91 100644
--- a/cms/templates/studio_xblock_wrapper.html
+++ b/cms/templates/studio_xblock_wrapper.html
@@ -7,12 +7,11 @@
from openedx.core.djangolib.js_utils import (
dump_js_escaped_json, js_escaped_string
)
-from cms.djangoapps.contentstore.toggles import use_new_text_editor, use_new_problem_editor, use_new_video_editor, use_video_gallery_flow
+from cms.djangoapps.contentstore.toggles import use_new_problem_editor, use_new_video_editor, use_video_gallery_flow
from cms.lib.xblock.upstream_sync import UpstreamLink
from openedx.core.djangoapps.content_tagging.toggles import is_tagging_feature_disabled
%>
<%
-use_new_editor_text = use_new_text_editor(xblock.context_key)
use_new_editor_video = use_new_video_editor(xblock.context_key)
use_new_editor_problem = use_new_problem_editor(xblock.context_key)
use_new_video_gallery_flow = use_video_gallery_flow()
@@ -83,7 +82,6 @@
is-collapsed
% endif
"
- use-new-editor-text = ${use_new_editor_text}
use-new-editor-video = ${use_new_editor_video}
use-new-editor-problem = ${use_new_editor_problem}
use-video-gallery-flow = ${use_new_video_gallery_flow}
@@ -93,7 +91,7 @@
% if upstream_info.upstream_ref:
data-upstream-ref = ${upstream_info.upstream_ref}
data-version-synced = ${upstream_info.version_synced}
- data-is-modified = ${upstream_info.is_modified}
+ data-is-modified = ${len(upstream_info.downstream_customized) > 0}
%endif
>
'
with override_waffle_switch(ENABLE_COURSE_ABOUT_SIDEBAR_HTML, active=waffle_enabled):
meta = self.create_courseware_meta()
if waffle_enabled:
assert meta.about_sidebar_html == '
About Course
'
else:
assert meta.about_sidebar_html is None
+ assert meta.overview == '
About Course
'
@ddt.ddt
diff --git a/openedx/core/djangoapps/courseware_api/views.py b/openedx/core/djangoapps/courseware_api/views.py
index ee37835b4841..a5940d8a132e 100644
--- a/openedx/core/djangoapps/courseware_api/views.py
+++ b/openedx/core/djangoapps/courseware_api/views.py
@@ -63,6 +63,7 @@
from openedx.core.djangoapps.courseware_api.utils import get_celebrations_dict
from openedx.core.djangoapps.enrollments.permissions import ENROLL_IN_COURSE
from openedx.core.djangoapps.programs.utils import ProgramProgressMeter
+from openedx.core.djangolib.markup import clean_dangerous_html
from openedx.core.lib.api.authentication import BearerAuthenticationAllowInactiveUser
from openedx.core.lib.api.view_utils import DeveloperErrorViewMixin
from openedx.core.lib.courses import get_course_by_id
@@ -288,7 +289,7 @@ def linkedin_add_to_profile_url(self):
get_certificate_url(course_id=self.course_key, uuid=user_certificate.verify_uuid)
)
return linkedin_config.add_to_profile_url(
- self.course_overview.display_name, user_certificate.mode, cert_url, certificate=user_certificate,
+ self.course_overview, user_certificate.mode, cert_url, certificate=user_certificate,
)
@property
@@ -516,7 +517,9 @@ def about_sidebar_html(self):
Returns the HTML content for the course about section.
"""
if ENABLE_COURSE_ABOUT_SIDEBAR_HTML.is_enabled():
- return get_course_about_section(self.request, self.course, "about_sidebar_html")
+ return clean_dangerous_html(
+ get_course_about_section(self.request, self.course, "about_sidebar_html")
+ )
return None
@property
@@ -524,7 +527,9 @@ def overview(self):
"""
Returns the overview HTML content for the course.
"""
- return get_course_about_section(self.request, self.course, "overview")
+ return clean_dangerous_html(
+ get_course_about_section(self.request, self.course, "overview")
+ )
@method_decorator(transaction.non_atomic_requests, name='dispatch')
diff --git a/openedx/core/djangoapps/credentials/management/commands/notify_credentials.py b/openedx/core/djangoapps/credentials/management/commands/notify_credentials.py
index 7d28ca5197c1..374975cf3ae7 100644
--- a/openedx/core/djangoapps/credentials/management/commands/notify_credentials.py
+++ b/openedx/core/djangoapps/credentials/management/commands/notify_credentials.py
@@ -12,12 +12,13 @@
import shlex
from datetime import datetime, timedelta
+from zoneinfo import ZoneInfo
+
import dateutil.parser
from django.conf import settings
from django.core.management.base import BaseCommand, CommandError
from opaque_keys import InvalidKeyError
from opaque_keys.edx.keys import CourseKey
-from pytz import UTC
from openedx.core.djangoapps.credentials.models import NotifyCredentialsConfig
from openedx.core.djangoapps.credentials.tasks.v1.tasks import handle_notify_credentials
@@ -32,7 +33,7 @@
def parsetime(timestr):
dt = dateutil.parser.parse(timestr)
if dt.tzinfo is None:
- dt = dt.replace(tzinfo=UTC)
+ dt = dt.replace(tzinfo=ZoneInfo("UTC"))
return dt
diff --git a/openedx/core/djangoapps/credit/api/provider.py b/openedx/core/djangoapps/credit/api/provider.py
index 9875a7d0f515..5628130b37be 100644
--- a/openedx/core/djangoapps/credit/api/provider.py
+++ b/openedx/core/djangoapps/credit/api/provider.py
@@ -7,7 +7,7 @@
import logging
import uuid
-import pytz
+from zoneinfo import ZoneInfo
from django.db import transaction
from edx_proctoring.api import get_last_exam_completion_date
@@ -296,7 +296,7 @@ def create_credit_request(course_key, provider_id, username):
parameters = {
"request_uuid": credit_request.uuid,
- "timestamp": to_timestamp(datetime.datetime.now(pytz.UTC)),
+ "timestamp": to_timestamp(datetime.datetime.now(ZoneInfo("UTC"))),
"course_org": course_key.org,
"course_num": course_key.course,
"course_run": course_key.run,
diff --git a/openedx/core/djangoapps/credit/models.py b/openedx/core/djangoapps/credit/models.py
index 9c14a15104b9..d6be33828117 100644
--- a/openedx/core/djangoapps/credit/models.py
+++ b/openedx/core/djangoapps/credit/models.py
@@ -10,7 +10,7 @@
import logging
from collections import defaultdict
-import pytz
+from zoneinfo import ZoneInfo
from config_models.models import ConfigurationModel
from django.conf import settings
from django.core.cache import cache
@@ -536,7 +536,7 @@ def default_deadline_for_credit_eligibility():
"""
The default deadline to use when creating a new CreditEligibility model.
"""
- return datetime.datetime.now(pytz.UTC) + datetime.timedelta(
+ return datetime.datetime.now(ZoneInfo("UTC")) + datetime.timedelta(
days=getattr(settings, "CREDIT_ELIGIBILITY_EXPIRATION_DAYS", 365)
)
@@ -617,7 +617,7 @@ def get_user_eligibilities(cls, username):
return cls.objects.filter(
username=username,
course__enabled=True,
- deadline__gt=datetime.datetime.now(pytz.UTC)
+ deadline__gt=datetime.datetime.now(ZoneInfo("UTC"))
).select_related('course')
@classmethod
@@ -636,7 +636,7 @@ def is_user_eligible_for_credit(cls, course_key, username):
course__course_key=course_key,
course__enabled=True,
username=username,
- deadline__gt=datetime.datetime.now(pytz.UTC),
+ deadline__gt=datetime.datetime.now(ZoneInfo("UTC")),
).exists()
def __str__(self):
diff --git a/openedx/core/djangoapps/credit/serializers.py b/openedx/core/djangoapps/credit/serializers.py
index 85e8fed44e57..56ffe7a960b2 100644
--- a/openedx/core/djangoapps/credit/serializers.py
+++ b/openedx/core/djangoapps/credit/serializers.py
@@ -4,7 +4,7 @@
import datetime
import logging
-import pytz
+from zoneinfo import ZoneInfo
from django.conf import settings
from rest_framework import serializers
from rest_framework.exceptions import PermissionDenied
@@ -78,7 +78,7 @@ def validate_timestamp(self, value):
log.warning(msg)
raise serializers.ValidationError(msg)
- elapsed = (datetime.datetime.now(pytz.UTC) - date_time).total_seconds()
+ elapsed = (datetime.datetime.now(ZoneInfo("UTC")) - date_time).total_seconds()
if elapsed > settings.CREDIT_PROVIDER_TIMESTAMP_EXPIRATION:
msg = f'[{value}] is too far in the past (over [{elapsed}] seconds).'
log.warning(msg)
diff --git a/openedx/core/djangoapps/credit/tests/factories.py b/openedx/core/djangoapps/credit/tests/factories.py
index cd777bdfe93b..5489b7bb0668 100644
--- a/openedx/core/djangoapps/credit/tests/factories.py
+++ b/openedx/core/djangoapps/credit/tests/factories.py
@@ -7,7 +7,7 @@
import factory
from factory.fuzzy import FuzzyText
-import pytz
+from zoneinfo import ZoneInfo
from django.contrib.auth.models import User # lint-amnesty, pylint: disable=imported-auth-user
from openedx.core.djangoapps.credit.models import (
@@ -80,7 +80,7 @@ def post(obj, create, extracted, **kwargs):
obj.parameters = json.dumps({
"request_uuid": obj.uuid,
- "timestamp": to_timestamp(datetime.datetime.now(pytz.UTC)),
+ "timestamp": to_timestamp(datetime.datetime.now(ZoneInfo("UTC"))),
"course_org": course_key.org,
"course_num": course_key.course,
"course_run": course_key.run,
diff --git a/openedx/core/djangoapps/credit/tests/test_api.py b/openedx/core/djangoapps/credit/tests/test_api.py
index 7dc644dc097a..2ec31641dd1d 100644
--- a/openedx/core/djangoapps/credit/tests/test_api.py
+++ b/openedx/core/djangoapps/credit/tests/test_api.py
@@ -9,7 +9,7 @@
import pytest
import ddt
import httpretty
-import pytz
+from zoneinfo import ZoneInfo
from django.contrib.auth.models import User # lint-amnesty, pylint: disable=imported-auth-user
from django.core import mail
from django.db import connection
@@ -400,7 +400,7 @@ def test_eligibility_expired(self):
CreditEligibility.objects.create(
course=credit_course,
username="staff",
- deadline=datetime.datetime.now(pytz.UTC) - datetime.timedelta(days=1)
+ deadline=datetime.datetime.now(ZoneInfo("UTC")) - datetime.timedelta(days=1)
)
# The user should NOT be eligible for credit
@@ -960,7 +960,7 @@ def test_credit_request(self):
# Validate the timestamp
assert 'timestamp' in parameters
parsed_date = from_timestamp(parameters['timestamp'])
- assert parsed_date < datetime.datetime.now(pytz.UTC)
+ assert parsed_date < datetime.datetime.now(ZoneInfo("UTC"))
# Validate course information
assert parameters['course_org'] == self.course_key.org
diff --git a/openedx/core/djangoapps/credit/tests/test_signals.py b/openedx/core/djangoapps/credit/tests/test_signals.py
index c3331c0ecfc6..592c042a05b9 100644
--- a/openedx/core/djangoapps/credit/tests/test_signals.py
+++ b/openedx/core/djangoapps/credit/tests/test_signals.py
@@ -7,7 +7,7 @@
from uuid import uuid4
import ddt
-import pytz
+from zoneinfo import ZoneInfo
from django.test.client import RequestFactory
from opaque_keys.edx.keys import UsageKey
from openedx_events.data import EventsMetadata
@@ -47,8 +47,8 @@ class TestMinGradedRequirementStatus(ModuleStoreTestCase):
satisfied. But if student grade is less than and deadline is passed then
user will be marked as failed.
"""
- VALID_DUE_DATE = datetime.now(pytz.UTC) + timedelta(days=20)
- EXPIRED_DUE_DATE = datetime.now(pytz.UTC) - timedelta(days=20)
+ VALID_DUE_DATE = datetime.now(ZoneInfo("UTC")) + timedelta(days=20)
+ EXPIRED_DUE_DATE = datetime.now(ZoneInfo("UTC")) - timedelta(days=20)
DATES = {
'valid': VALID_DUE_DATE,
diff --git a/openedx/core/djangoapps/credit/tests/test_views.py b/openedx/core/djangoapps/credit/tests/test_views.py
index deb3c8726aeb..02428557ded0 100644
--- a/openedx/core/djangoapps/credit/tests/test_views.py
+++ b/openedx/core/djangoapps/credit/tests/test_views.py
@@ -7,7 +7,7 @@
import json
import ddt
-import pytz
+from zoneinfo import ZoneInfo
from django.conf import settings
from django.test import Client, TestCase
from django.test.utils import override_settings
@@ -523,7 +523,7 @@ def _credit_provider_callback(self, request_uuid, status, **kwargs):
"""
provider_id = kwargs.get('provider_id', self.provider.provider_id)
secret_key = kwargs.get('secret_key', '931433d583c84ca7ba41784bad3232e6')
- timestamp = kwargs.get('timestamp', to_timestamp(datetime.datetime.now(pytz.UTC)))
+ timestamp = kwargs.get('timestamp', to_timestamp(datetime.datetime.now(ZoneInfo("UTC"))))
keys = kwargs.get('keys', {self.provider.provider_id: secret_key})
url = reverse('credit:provider_callback', args=[provider_id])
@@ -577,7 +577,7 @@ def test_post_with_invalid_timestamp(self, timedelta):
if timedelta == 'invalid':
timestamp = timedelta
else:
- timestamp = to_timestamp(datetime.datetime.now(pytz.UTC) + timedelta)
+ timestamp = to_timestamp(datetime.datetime.now(ZoneInfo("UTC")) + timedelta)
request_uuid = self._create_credit_request_and_get_uuid()
response = self._credit_provider_callback(request_uuid, 'approved', timestamp=timestamp)
assert response.status_code == 400
@@ -585,7 +585,7 @@ def test_post_with_invalid_timestamp(self, timedelta):
def test_post_with_string_timestamp(self):
""" Verify the endpoint supports timestamps transmitted as strings instead of integers. """
request_uuid = self._create_credit_request_and_get_uuid()
- timestamp = str(to_timestamp(datetime.datetime.now(pytz.UTC)))
+ timestamp = str(to_timestamp(datetime.datetime.now(ZoneInfo("UTC"))))
response = self._credit_provider_callback(request_uuid, 'approved', timestamp=timestamp)
assert response.status_code == 200
diff --git a/openedx/core/djangoapps/credit/views.py b/openedx/core/djangoapps/credit/views.py
index 2a06f85a321a..d61316b49c26 100644
--- a/openedx/core/djangoapps/credit/views.py
+++ b/openedx/core/djangoapps/credit/views.py
@@ -6,7 +6,7 @@
import datetime
import logging
-import pytz
+from zoneinfo import ZoneInfo
from django.conf import settings
from django.utils.decorators import method_decorator
from django.views.decorators.csrf import csrf_exempt
@@ -166,7 +166,7 @@ def filter_queryset(self, queryset):
return queryset.filter(
username=username,
course__course_key=course_key,
- deadline__gt=datetime.datetime.now(pytz.UTC)
+ deadline__gt=datetime.datetime.now(ZoneInfo("UTC"))
)
diff --git a/openedx/core/djangoapps/discussions/README.rst b/openedx/core/djangoapps/discussions/README.rst
index 5c1127d324cc..d8a937a07ecc 100644
--- a/openedx/core/djangoapps/discussions/README.rst
+++ b/openedx/core/djangoapps/discussions/README.rst
@@ -3,7 +3,7 @@ Discussions
This Discussions app is responsible for providing support for configuring
discussion tools in the Open edX platform. This includes the in-built forum
-tool that uses the `cs_comments_service`, but also other LTI-based tools.
+tool, but also other LTI-based tools.
Technical Overview
@@ -44,10 +44,9 @@ discussion configuration information such as the course key, the provider type,
whether in-context discussions are enabled, whether graded units are enabled,
when unit level visibility is enabled. Other plugin configuration and a list
of discussion contexts for which discussions are enabled. Each discussion
-context has a usage key, a title (the units name) an external id
-(the cs_comments_service id), it's ordering in the course, and additional
-context. It then sends its own signal that has the discussion configuration
-object attached.
+context has a usage key, a title (the units name) an external id,
+its ordering in the course, and additional context. It then sends its own
+signal that has the discussion configuration object attached.
Finally, the handler for this discussion change signal, takes the information
from the discussion change signal and compares it to the topics in the
diff --git a/openedx/core/djangoapps/discussions/config/waffle.py b/openedx/core/djangoapps/discussions/config/waffle.py
index eca6fc970856..1d4c67e9e17b 100644
--- a/openedx/core/djangoapps/discussions/config/waffle.py
+++ b/openedx/core/djangoapps/discussions/config/waffle.py
@@ -2,8 +2,6 @@
This module contains various configuration settings via
waffle switches for the discussions app.
"""
-from django.conf import settings
-
from openedx.core.djangoapps.waffle_utils import CourseWaffleFlag
WAFFLE_FLAG_NAMESPACE = "discussions"
@@ -45,31 +43,3 @@
ENABLE_NEW_STRUCTURE_DISCUSSIONS = CourseWaffleFlag(
f"{WAFFLE_FLAG_NAMESPACE}.enable_new_structure_discussions", __name__
)
-
-# .. toggle_name: discussions.enable_forum_v2
-# .. toggle_implementation: CourseWaffleFlag
-# .. toggle_default: False
-# .. toggle_description: Waffle flag to use the forum v2 instead of v1(cs_comment_service)
-# .. toggle_use_cases: temporary, open_edx
-# .. toggle_creation_date: 2024-9-26
-# .. toggle_target_removal_date: 2025-12-05
-ENABLE_FORUM_V2 = CourseWaffleFlag(f"{WAFFLE_FLAG_NAMESPACE}.enable_forum_v2", __name__)
-
-
-def is_forum_v2_enabled(course_key):
- """
- Returns whether forum V2 is enabled on the course. This is a 2-step check:
-
- 1. Check value of settings.DISABLE_FORUM_V2: if it exists and is true, this setting overrides any course flag.
- 2. Else, check the value of the corresponding course waffle flag.
- """
- if is_forum_v2_disabled_globally():
- return False
- return ENABLE_FORUM_V2.is_enabled(course_key)
-
-
-def is_forum_v2_disabled_globally() -> bool:
- """
- Return True if DISABLE_FORUM_V2 is defined and true-ish.
- """
- return getattr(settings, "DISABLE_FORUM_V2", False)
diff --git a/openedx/core/djangoapps/django_comment_common/admin.py b/openedx/core/djangoapps/django_comment_common/admin.py
deleted file mode 100644
index 6d2d7a34d46d..000000000000
--- a/openedx/core/djangoapps/django_comment_common/admin.py
+++ /dev/null
@@ -1,10 +0,0 @@
-"""
-Admin for managing the connection to the Forums backend service.
-"""
-
-
-from django.contrib import admin
-
-from .models import ForumsConfig
-
-admin.site.register(ForumsConfig)
diff --git a/openedx/core/djangoapps/django_comment_common/comment_client/comment.py b/openedx/core/djangoapps/django_comment_common/comment_client/comment.py
index dae2088594ae..72bb19da981b 100644
--- a/openedx/core/djangoapps/django_comment_common/comment_client/comment.py
+++ b/openedx/core/djangoapps/django_comment_common/comment_client/comment.py
@@ -139,6 +139,32 @@ def body_text(self):
soup = BeautifulSoup(self.body, "html.parser")
return soup.get_text()
+ @classmethod
+ def retrieve_all(cls, params=None):
+ """
+ Retrieve all comments for a user in a course using Forum v2 API.
+
+ Arguments:
+ params: Dictionary with keys:
+ - user_id: The ID of the user
+ - course_id: The ID of the course
+ - flagged: Boolean for flagged comments
+ - page: Page number
+ - per_page: Items per page
+
+ Returns:
+ Dictionary with collection, comment_count, num_pages, page
+ """
+ if params is None:
+ params = {}
+ return forum_api.get_user_comments(
+ user_id=params.get('user_id'),
+ course_id=params.get('course_id'),
+ flagged=params.get('flagged', False),
+ page=params.get('page', 1),
+ per_page=params.get('per_page', 10),
+ )
+
@classmethod
def get_user_comment_count(cls, user_id, course_ids):
"""
@@ -232,11 +258,3 @@ def _url_for_thread_comments(thread_id):
def _url_for_comment(comment_id):
return f"{settings.PREFIX}/comments/{comment_id}"
-
-
-def _url_for_flag_abuse_comment(comment_id):
- return f"{settings.PREFIX}/comments/{comment_id}/abuse_flag"
-
-
-def _url_for_unflag_abuse_comment(comment_id):
- return f"{settings.PREFIX}/comments/{comment_id}/abuse_unflag"
diff --git a/openedx/core/djangoapps/django_comment_common/comment_client/course.py b/openedx/core/djangoapps/django_comment_common/comment_client/course.py
index 8cbb580e7831..1dad0ca159aa 100644
--- a/openedx/core/djangoapps/django_comment_common/comment_client/course.py
+++ b/openedx/core/djangoapps/django_comment_common/comment_client/course.py
@@ -8,9 +8,6 @@
from opaque_keys.edx.keys import CourseKey
from forum import api as forum_api
-from openedx.core.djangoapps.django_comment_common.comment_client import settings
-from openedx.core.djangoapps.django_comment_common.comment_client.utils import perform_request
-from openedx.core.djangoapps.discussions.config.waffle import is_forum_v2_enabled
def get_course_commentable_counts(course_key: CourseKey) -> Dict[str, Dict[str, int]]:
@@ -31,19 +28,7 @@ def get_course_commentable_counts(course_key: CourseKey) -> Dict[str, Dict[str,
}
"""
- if is_forum_v2_enabled(course_key):
- commentable_stats = forum_api.get_commentables_stats(str(course_key))
- else:
- url = f"{settings.PREFIX}/commentables/{course_key}/counts"
- commentable_stats = perform_request(
- 'get',
- url,
- metric_tags=[
- f"course_key:{course_key}",
- "function:get_course_commentable_counts",
- ],
- metric_action='commentable_stats.retrieve',
- )
+ commentable_stats = forum_api.get_commentables_stats(str(course_key))
return commentable_stats
@@ -81,20 +66,7 @@ def get_course_user_stats(course_key: CourseKey, params: Optional[Dict] = None)
"""
if params is None:
params = {}
- if is_forum_v2_enabled(course_key):
- course_stats = forum_api.get_user_course_stats(str(course_key), **params)
- else:
- url = f"{settings.PREFIX}/users/{course_key}/stats"
- course_stats = perform_request(
- 'get',
- url,
- params,
- metric_action='user.course_stats',
- metric_tags=[
- f"course_key:{course_key}",
- "function:get_course_user_stats",
- ],
- )
+ course_stats = forum_api.get_user_course_stats(str(course_key), **params)
return course_stats
@@ -109,17 +81,5 @@ def update_course_users_stats(course_key: CourseKey) -> Dict:
Returns:
dict: data returned by API. Contains count of users updated.
"""
- if is_forum_v2_enabled(course_key):
- course_stats = forum_api.update_users_in_course(str(course_key))
- else:
- url = f"{settings.PREFIX}/users/{course_key}/update_stats"
- course_stats = perform_request(
- 'post',
- url,
- metric_action='user.update_course_stats',
- metric_tags=[
- f"course_key:{course_key}",
- "function:update_course_users_stats",
- ],
- )
+ course_stats = forum_api.update_users_in_course(str(course_key))
return course_stats
diff --git a/openedx/core/djangoapps/django_comment_common/comment_client/models.py b/openedx/core/djangoapps/django_comment_common/comment_client/models.py
index ddfcc37cc524..ed1ed3f920bb 100644
--- a/openedx/core/djangoapps/django_comment_common/comment_client/models.py
+++ b/openedx/core/djangoapps/django_comment_common/comment_client/models.py
@@ -2,15 +2,10 @@
import logging
-import typing as t
from forum import api as forum_api
-from openedx.core.djangoapps.discussions.config.waffle import (
- is_forum_v2_disabled_globally,
- is_forum_v2_enabled,
-)
-from .utils import CommentClientRequestError, extract, get_course_key, perform_request
+from .utils import CommentClientRequestError, extract, get_course_key
log = logging.getLogger(__name__)
@@ -81,12 +76,11 @@ def retrieve(self, *args, **kwargs):
def _retrieve(self, *args, **kwargs):
course_id = self.attributes.get("course_id") or kwargs.get("course_key")
if not course_id:
- _, course_id = is_forum_v2_enabled_for_comment(self.id)
+ course_id = forum_api.get_course_id_by_comment(self.id)
+ response = None
if self.type == "comment":
- response = forum_api.get_parent_comment(
- comment_id=self.attributes["id"], course_id=course_id
- )
- else:
+ response = forum_api.get_parent_comment(comment_id=self.attributes["id"], course_id=course_id)
+ if response is None:
raise CommentClientRequestError("Forum v2 API call is missing")
self._update_from_response(response)
@@ -111,25 +105,6 @@ def _metric_tags(self):
def find(cls, id): # pylint: disable=redefined-builtin
return cls(id=id)
- @classmethod
- def retrieve_all(cls, params=None):
- """
- Performs a GET request against the resource's listing endpoint.
-
- Arguments:
- params: A dictionary of parameters to be passed as the request's query string.
-
- Returns:
- The parsed JSON response from the backend.
- """
- return perform_request(
- "get",
- cls.url(action="get_all"),
- params,
- metric_tags=[f"model_class:{cls.__name__}"],
- metric_action="model.retrieve_all",
- )
-
def _update_from_response(self, response_data):
for k, v in response_data.items():
if k in self.accessible_fields:
@@ -230,18 +205,15 @@ def handle_update(self, params=None):
request_params.update(params)
course_id = self.attributes.get("course_id") or request_params.get("course_id")
course_key = get_course_key(course_id)
- if is_forum_v2_enabled(course_key):
- response = None
- if self.type == "comment":
- response = self.handle_update_comment(request_params, str(course_key))
- elif self.type == "thread":
- response = self.handle_update_thread(request_params, str(course_key))
- elif self.type == "user":
- response = self.handle_update_user(request_params, str(course_key))
- if response is None:
- raise CommentClientRequestError("Forum v2 API call is missing")
- else:
- response = self.perform_http_put_request(request_params)
+ response = None
+ if self.type == "comment":
+ response = self.handle_update_comment(request_params, str(course_key))
+ elif self.type == "thread":
+ response = self.handle_update_thread(request_params, str(course_key))
+ elif self.type == "user":
+ response = self.handle_update_user(request_params, str(course_key))
+ if response is None:
+ raise CommentClientRequestError("Forum v2 API call is missing")
return response
def handle_update_user(self, request_params, course_id):
@@ -298,28 +270,6 @@ def handle_update_thread(self, request_params, course_id):
response = forum_api.update_thread(**request_data)
return response
- def perform_http_put_request(self, request_params):
- url = self.url(action="put", params=self.attributes)
- response = perform_request(
- "put",
- url,
- request_params,
- metric_tags=self._metric_tags,
- metric_action="model.update",
- )
- return response
-
- def perform_http_post_request(self):
- url = self.url(action="post", params=self.attributes)
- response = perform_request(
- "post",
- url,
- self.initializable_attributes(),
- metric_tags=self._metric_tags,
- metric_action="model.insert",
- )
- return response
-
def handle_create(self, params=None):
course_id = self.attributes.get("course_id") or params.get("course_id")
course_key = str(get_course_key(course_id))
@@ -372,22 +322,3 @@ def handle_create_thread(self, course_id):
response = forum_api.create_thread(**params)
return response
-
-
-def is_forum_v2_enabled_for_comment(comment_id: str) -> tuple[bool, t.Optional[str]]:
- """
- Figure out whether we use forum v2 for a given comment.
-
- See is_forum_v2_enabled_for_thread.
-
- Return:
-
- enabled (bool)
- course_id (str or None)
- """
- if is_forum_v2_disabled_globally():
- return False, None
-
- course_id = forum_api.get_course_id_by_comment(comment_id)
- course_key = get_course_key(course_id)
- return is_forum_v2_enabled(course_key), course_id
diff --git a/openedx/core/djangoapps/django_comment_common/comment_client/thread.py b/openedx/core/djangoapps/django_comment_common/comment_client/thread.py
index 754fe0065f00..8bdf2482e738 100644
--- a/openedx/core/djangoapps/django_comment_common/comment_client/thread.py
+++ b/openedx/core/djangoapps/django_comment_common/comment_client/thread.py
@@ -3,7 +3,6 @@
import logging
import time
-import typing as t
from django.core.exceptions import ObjectDoesNotExist
from eventtracking import tracker
@@ -16,10 +15,6 @@
from forum.backend import get_backend # pylint: disable=import-error
from forum.backends.mongodb.threads import CommentThread # pylint: disable=import-error
from forum.utils import ForumV2RequestError # pylint: disable=import-error
-from openedx.core.djangoapps.discussions.config.waffle import (
- is_forum_v2_disabled_globally,
- is_forum_v2_enabled,
-)
from . import models, settings, utils
@@ -227,7 +222,7 @@ def _retrieve(self, *args, **kwargs):
request_params = utils.clean_forum_params(request_params)
course_id = kwargs.get("course_id")
if not course_id:
- _, course_id = is_forum_v2_enabled_for_thread(self.id)
+ course_id = forum_api.get_course_id_by_thread(self.id)
if user_id := request_params.get("user_id"):
request_params["user_id"] = str(user_id)
response = forum_api.get_thread(
@@ -282,7 +277,7 @@ def un_pin(self, user, thread_id, course_id=None):
@classmethod
def get_user_threads_count(cls, user_id, course_ids):
"""
- Returns threads and responses count of user in the given course_ids.
+ Returns threads count of user in the given course_ids.
TODO: Add support for MySQL backend as well
"""
query_params = {
@@ -428,42 +423,18 @@ def restore_thread(cls, thread_id, course_id=None, restored_by=None):
)
-def _url_for_flag_abuse_thread(thread_id):
- return f"{settings.PREFIX}/threads/{thread_id}/abuse_flag"
-
-
-def _url_for_unflag_abuse_thread(thread_id):
- return f"{settings.PREFIX}/threads/{thread_id}/abuse_unflag"
-
-
-def _url_for_pin_thread(thread_id):
- return f"{settings.PREFIX}/threads/{thread_id}/pin"
-
-
-def _url_for_un_pin_thread(thread_id):
- return f"{settings.PREFIX}/threads/{thread_id}/unpin"
-
-
-def is_forum_v2_enabled_for_thread(thread_id: str) -> tuple[bool, t.Optional[str]]:
- """
- Figure out whether we use forum v2 for a given thread.
-
- This is a complex affair... First, we check the value of the DISABLE_FORUM_V2
- setting, which overrides everything. If this setting does not exist, then we need to
- find the course ID that corresponds to the thread ID. Then, we return the value of
- the course waffle flag for this course ID.
-
- Note that to fetch the course ID associated to a thread ID, we need to connect both
- to mongodb and mysql. As a consequence, when forum v2 needs adequate connection
- strings for both backends.
-
- Return:
-
- enabled (bool)
- course_id (str or None)
- """
- if is_forum_v2_disabled_globally():
- return False, None
- course_id = forum_api.get_course_id_by_thread(thread_id)
- course_key = utils.get_course_key(course_id)
- return is_forum_v2_enabled(course_key), course_id
+def _clean_forum_params(params):
+ """Convert string booleans to actual booleans and remove None values from forum parameters."""
+ result = {}
+ for k, v in params.items():
+ if v is not None:
+ if isinstance(v, str):
+ if v.lower() == 'true':
+ result[k] = True
+ elif v.lower() == 'false':
+ result[k] = False
+ else:
+ result[k] = v
+ else:
+ result[k] = v
+ return result
diff --git a/openedx/core/djangoapps/django_comment_common/comment_client/user.py b/openedx/core/djangoapps/django_comment_common/comment_client/user.py
index 187593e70717..bd208545ce56 100644
--- a/openedx/core/djangoapps/django_comment_common/comment_client/user.py
+++ b/openedx/core/djangoapps/django_comment_common/comment_client/user.py
@@ -4,7 +4,6 @@
from . import models, settings, utils
from forum import api as forum_api
from forum.utils import ForumV2RequestError, str_to_bool
-from openedx.core.djangoapps.discussions.config.waffle import is_forum_v2_enabled
class User(models.Model):
@@ -34,21 +33,11 @@ def from_django_user(cls, user):
def read(self, source):
"""
- Calls cs_comments_service to mark thread as read for the user
+ Calls forum service to mark thread as read for the user
"""
course_id = self.attributes.get("course_id")
course_key = utils.get_course_key(course_id)
- if is_forum_v2_enabled(course_key):
- forum_api.mark_thread_as_read(self.id, source.id, course_id=str(course_id))
- else:
- params = {'source_type': source.type, 'source_id': source.id}
- utils.perform_request(
- 'post',
- _url_for_read(self.id),
- params,
- metric_action='user.read',
- metric_tags=self._metric_tags + [f'target.type:{source.type}'],
- )
+ forum_api.mark_thread_as_read(self.id, source.id, course_id=str(course_id))
def follow(self, source, course_id=None):
course_key = utils.get_course_key(self.attributes.get("course_id") or course_id)
@@ -110,31 +99,21 @@ def active_threads(self, query_params=None):
query_params = {}
if not self.course_id:
raise utils.CommentClientRequestError("Must provide course_id when retrieving active threads for the user")
- url = _url_for_user_active_threads(self.id)
params = {'course_id': str(self.course_id)}
params.update(query_params)
course_key = utils.get_course_key(self.attributes.get("course_id"))
- if is_forum_v2_enabled(course_key):
- if user_id := params.get("user_id"):
- params["user_id"] = str(user_id)
- if page := params.get("page"):
- params["page"] = int(page)
- if per_page := params.get("per_page"):
- params["per_page"] = int(per_page)
- if count_flagged := params.get("count_flagged", False):
- params["count_flagged"] = str_to_bool(count_flagged)
- if not params.get("course_id"):
- params["course_id"] = str(course_key)
- response = forum_api.get_user_active_threads(**params)
- else:
- response = utils.perform_request(
- 'get',
- url,
- params,
- metric_action='user.active_threads',
- metric_tags=self._metric_tags,
- paged_results=True,
- )
+ if user_id := params.get("user_id"):
+ params["user_id"] = str(user_id)
+ if page := params.get("page"):
+ params["page"] = int(page)
+ if per_page := params.get("per_page"):
+ params["per_page"] = int(per_page)
+ if count_flagged := params.get("count_flagged", False):
+ params["count_flagged"] = str_to_bool(count_flagged)
+ if not params.get("course_id"):
+ params["course_id"] = str(course_key)
+ params = _clean_forum_params(params)
+ response = forum_api.get_user_active_threads(**params)
return response.get('collection', []), response.get('page', 1), response.get('num_pages', 1)
def subscribed_threads(self, query_params=None):
@@ -157,9 +136,7 @@ def subscribed_threads(self, query_params=None):
params["count_flagged"] = str_to_bool(count_flagged)
if not params.get("course_id"):
params["course_id"] = str(course_key)
- if 'text' in params:
- params.pop('text')
- params = utils.clean_forum_params(params)
+ params = _clean_forum_params(params)
response = forum_api.get_user_subscriptions(**params)
return utils.CommentClientPaginatedResult(
collection=response.get('collection', []),
@@ -169,7 +146,6 @@ def subscribed_threads(self, query_params=None):
)
def _retrieve(self, *args, **kwargs):
- url = self.url(action='get', params=self.attributes)
retrieve_params = self.default_retrieve_params.copy()
retrieve_params.update(kwargs)
@@ -183,116 +159,43 @@ def _retrieve(self, *args, **kwargs):
if course_id:
course_id = str(course_id)
retrieve_params['course_id'] = course_id
- course_key = utils.get_course_key(course_id) or utils.get_course_key(kwargs.get("course_key"))
- if is_forum_v2_enabled(course_key):
- group_ids = [retrieve_params['group_id']] if 'group_id' in retrieve_params else []
- is_complete = retrieve_params['complete']
- params = utils.clean_forum_params({
- "user_id": self.attributes["id"],
- "group_ids": group_ids,
- "course_id": course_id,
- "complete": is_complete
- })
- try:
- response = forum_api.get_user(**params)
- except ForumV2RequestError as e:
- course_id = str(course_key)
- self.save({"course_id": course_id})
- response = forum_api.get_user(**params)
- else:
- try:
- response = utils.perform_request(
- 'get',
- url,
- retrieve_params,
- metric_action='model.retrieve',
- metric_tags=self._metric_tags,
- )
- except utils.CommentClientRequestError as e:
- if e.status_code == 404:
- # attempt to gracefully recover from a previous failure
- # to sync this user to the comments service.
- self.save()
- response = utils.perform_request(
- 'get',
- url,
- retrieve_params,
- metric_action='model.retrieve',
- metric_tags=self._metric_tags,
- )
- else:
- raise
+ group_ids = [retrieve_params['group_id']] if 'group_id' in retrieve_params else None
+ is_complete = retrieve_params['complete']
+ params = _clean_forum_params({
+ "user_id": self.attributes["id"],
+ "group_ids": group_ids,
+ "course_id": course_id,
+ "complete": is_complete
+ })
+ try:
+ response = forum_api.get_user(**params)
+ except ForumV2RequestError as e:
+ self.save({"course_id": course_id})
+ response = forum_api.get_user(**params)
self._update_from_response(response)
def retire(self, retired_username):
course_key = utils.get_course_key(self.attributes.get("course_id"))
- if is_forum_v2_enabled(course_key):
- forum_api.retire_user(user_id=self.id, retired_username=retired_username, course_id=str(course_key))
- else:
- url = _url_for_retire(self.id)
- params = {'retired_username': retired_username}
- utils.perform_request(
- 'post',
- url,
- params,
- raw=True,
- metric_action='user.retire',
- metric_tags=self._metric_tags
- )
+ forum_api.retire_user(user_id=self.id, retired_username=retired_username, course_id=str(course_key))
def replace_username(self, new_username):
course_key = utils.get_course_key(self.attributes.get("course_id"))
- if is_forum_v2_enabled(course_key):
- forum_api.update_username(user_id=self.id, new_username=new_username, course_id=str(course_key))
- else:
- url = _url_for_username_replacement(self.id)
- params = {"new_username": new_username}
-
- utils.perform_request(
- 'post',
- url,
- params,
- raw=True,
- )
-
-
-def _url_for_vote_comment(comment_id):
- return f"{settings.PREFIX}/comments/{comment_id}/votes"
-
-
-def _url_for_vote_thread(thread_id):
- return f"{settings.PREFIX}/threads/{thread_id}/votes"
-
-
-def _url_for_subscription(user_id):
- return f"{settings.PREFIX}/users/{user_id}/subscriptions"
-
-
-def _url_for_user_active_threads(user_id):
- return f"{settings.PREFIX}/users/{user_id}/active_threads"
-
-
-def _url_for_user_subscribed_threads(user_id):
- return f"{settings.PREFIX}/users/{user_id}/subscribed_threads"
-
-
-def _url_for_read(user_id):
- """
- Returns cs_comments_service url endpoint to mark thread as read for given user_id
- """
- return f"{settings.PREFIX}/users/{user_id}/read"
-
-
-def _url_for_retire(user_id):
- """
- Returns cs_comments_service url endpoint to retire a user (remove all post content, etc.)
- """
- return f"{settings.PREFIX}/users/{user_id}/retire"
-
-
-def _url_for_username_replacement(user_id):
- """
- Returns cs_comments_servuce url endpoint to replace the username of a user
- """
- return f"{settings.PREFIX}/users/{user_id}/replace_username"
+ forum_api.update_username(user_id=self.id, new_username=new_username, course_id=str(course_key))
+
+
+def _clean_forum_params(params):
+ """Convert string booleans to actual booleans and remove None values from forum parameters."""
+ result = {}
+ for k, v in params.items():
+ if v is not None:
+ if isinstance(v, str):
+ if v.lower() == 'true':
+ result[k] = True
+ elif v.lower() == 'false':
+ result[k] = False
+ else:
+ result[k] = v
+ else:
+ result[k] = v
+ return result
diff --git a/openedx/core/djangoapps/django_comment_common/comment_client/utils.py b/openedx/core/djangoapps/django_comment_common/comment_client/utils.py
index 26625ed3a732..ccdced767e00 100644
--- a/openedx/core/djangoapps/django_comment_common/comment_client/utils.py
+++ b/openedx/core/djangoapps/django_comment_common/comment_client/utils.py
@@ -3,14 +3,10 @@
import logging
-from uuid import uuid4
-import requests
-from django.utils.translation import get_language
+import requests # pylint: disable=unused-import
from opaque_keys.edx.keys import CourseKey
-from .settings import SERVICE_HOST as COMMENTS_SERVICE
-
log = logging.getLogger(__name__)
@@ -31,78 +27,6 @@ def extract(dic, keys):
return strip_none({k: dic.get(k) for k in keys})
-def perform_request(method, url, data_or_params=None, raw=False,
- metric_action=None, metric_tags=None, paged_results=False):
- # To avoid dependency conflict
- from openedx.core.djangoapps.django_comment_common.models import ForumsConfig
- config = ForumsConfig.current()
-
- if not config.enabled:
- raise CommentClientMaintenanceError('service disabled')
-
- if metric_tags is None:
- metric_tags = []
-
- metric_tags.append(f'method:{method}')
- if metric_action:
- metric_tags.append(f'action:{metric_action}')
-
- if data_or_params is None:
- data_or_params = {}
- headers = {
- 'X-Edx-Api-Key': config.api_key,
- 'Accept-Language': get_language(),
- }
- request_id = uuid4()
- request_id_dict = {'request_id': request_id}
-
- if method in ['post', 'put', 'patch']:
- data = data_or_params
- params = request_id_dict
- else:
- data = None
- params = data_or_params.copy()
- params.update(request_id_dict)
- response = requests.request(
- method,
- url,
- data=data,
- params=params,
- headers=headers,
- timeout=config.connection_timeout
- )
-
- metric_tags.append(f'status_code:{response.status_code}')
- status_code = int(response.status_code)
- if status_code > 200:
- metric_tags.append('result:failure')
- else:
- metric_tags.append('result:success')
-
- if 200 < status_code < 500: # lint-amnesty, pylint: disable=no-else-raise
- log.info(f'Investigation Log: CommentClientRequestError for request with {method} and params {params}')
- raise CommentClientRequestError(response.text, response.status_code)
- # Heroku returns a 503 when an application is in maintenance mode
- elif status_code == 503:
- raise CommentClientMaintenanceError(response.text)
- elif status_code == 500:
- raise CommentClient500Error(response.text)
- else:
- if raw:
- return response.text
- else:
- try:
- data = response.json()
- except ValueError:
- raise CommentClientError( # lint-amnesty, pylint: disable=raise-missing-from
- "Invalid JSON response for request {request_id}; first 100 characters: '{content}'".format(
- request_id=request_id,
- content=response.text[:100]
- )
- )
- return data
-
-
def clean_forum_params(params):
"""Convert string booleans to actual booleans and remove None values and empty lists from forum parameters."""
result = {}
@@ -160,33 +84,6 @@ def __init__(self, collection, page, num_pages, subscriptions_count=0, corrected
self.corrected_text = corrected_text
-def check_forum_heartbeat():
- """
- Check the forum connection via its built-in heartbeat service and create an answer which can be used in the LMS
- heartbeat django application.
- This function can be connected to the LMS heartbeat checker through the HEARTBEAT_CHECKS variable.
- """
- # To avoid dependency conflict
- from openedx.core.djangoapps.django_comment_common.models import ForumsConfig
- config = ForumsConfig.current()
-
- if not config.enabled:
- # If this check is enabled but forums disabled, don't connect, just report no error
- return 'forum', True, 'OK'
-
- try:
- res = requests.get(
- '%s/heartbeat' % COMMENTS_SERVICE,
- timeout=config.connection_timeout
- ).json()
- if res['OK']:
- return 'forum', True, 'OK'
- else:
- return 'forum', False, res.get('check', 'Forum heartbeat failed')
- except Exception as fail:
- return 'forum', False, str(fail)
-
-
def get_course_key(course_id: CourseKey | str | None) -> CourseKey | None:
"""
Returns a CourseKey if the provided course_id is a valid string representation of a CourseKey.
diff --git a/openedx/core/djangoapps/enrollments/tests/test_data.py b/openedx/core/djangoapps/enrollments/tests/test_data.py
index 93b299d75a3c..491be2cc8cdc 100644
--- a/openedx/core/djangoapps/enrollments/tests/test_data.py
+++ b/openedx/core/djangoapps/enrollments/tests/test_data.py
@@ -8,7 +8,7 @@
import ddt
import pytest
-from pytz import UTC
+from zoneinfo import ZoneInfo
from common.djangoapps.course_modes.models import CourseMode
from common.djangoapps.course_modes.tests.factories import CourseModeFactory
@@ -369,7 +369,7 @@ def test_get_course_without_expired_mode_included(self):
def _update_verified_mode_as_expired(self, course_id):
"""Dry method to change verified mode expiration."""
mode = CourseMode.objects.get(course_id=course_id, mode_slug=CourseMode.VERIFIED)
- mode.expiration_datetime = datetime.datetime(year=1970, month=1, day=1, tzinfo=UTC)
+ mode.expiration_datetime = datetime.datetime(year=1970, month=1, day=1, tzinfo=ZoneInfo("UTC"))
mode.save()
def assert_enrollment_modes(self, expected_modes, include_expired):
diff --git a/openedx/core/djangoapps/enrollments/tests/test_views.py b/openedx/core/djangoapps/enrollments/tests/test_views.py
index a6b34cbfc60b..31510c3b933f 100644
--- a/openedx/core/djangoapps/enrollments/tests/test_views.py
+++ b/openedx/core/djangoapps/enrollments/tests/test_views.py
@@ -12,7 +12,7 @@
import ddt
import httpretty
import pytest
-import pytz
+from zoneinfo import ZoneInfo
from django.conf import settings
from django.core.cache import cache
from django.core.exceptions import ImproperlyConfigured
@@ -356,8 +356,8 @@ def test_enroll_without_user(self):
@ddt.unpack
def test_force_enrollment(self, course_modes, enrollment_mode, force_enrollment):
# Create the course modes (if any) required for this test case
- start_date = datetime.datetime(2021, 12, 1, 5, 0, 0, tzinfo=pytz.UTC)
- end_date = datetime.datetime(2022, 12, 1, 5, 0, 0, tzinfo=pytz.UTC)
+ start_date = datetime.datetime(2021, 12, 1, 5, 0, 0, tzinfo=ZoneInfo("UTC"))
+ end_date = datetime.datetime(2022, 12, 1, 5, 0, 0, tzinfo=ZoneInfo("UTC"))
self.course = CourseFactory.create(
emit_signals=True,
start=start_date,
@@ -658,11 +658,11 @@ def test_get_course_details_with_credit_course(self):
# enforced at the data layer, so we need to handle the case
# in which no dates are specified.
(None, None, None, None),
- (datetime.datetime(2015, 1, 2, 3, 4, 5, tzinfo=pytz.UTC), None, "2015-01-02T03:04:05Z", None),
- (None, datetime.datetime(2015, 1, 2, 3, 4, 5, tzinfo=pytz.UTC), None, "2015-01-02T03:04:05Z"),
+ (datetime.datetime(2015, 1, 2, 3, 4, 5, tzinfo=ZoneInfo("UTC")), None, "2015-01-02T03:04:05Z", None),
+ (None, datetime.datetime(2015, 1, 2, 3, 4, 5, tzinfo=ZoneInfo("UTC")), None, "2015-01-02T03:04:05Z"),
(
- datetime.datetime(2014, 6, 7, 8, 9, 10, tzinfo=pytz.UTC),
- datetime.datetime(2015, 1, 2, 3, 4, 5, tzinfo=pytz.UTC),
+ datetime.datetime(2014, 6, 7, 8, 9, 10, tzinfo=ZoneInfo("UTC")),
+ datetime.datetime(2015, 1, 2, 3, 4, 5, tzinfo=ZoneInfo("UTC")),
"2014-06-07T08:09:10Z",
"2015-01-02T03:04:05Z",
),
@@ -1078,7 +1078,7 @@ def test_deactivate_enrollment_expired_mode(self):
# Change verified mode expiration.
mode = CourseMode.objects.get(course_id=self.course.id, mode_slug=CourseMode.VERIFIED)
- mode.expiration_datetime = datetime.datetime(year=1970, month=1, day=1, tzinfo=pytz.utc)
+ mode.expiration_datetime = datetime.datetime(year=1970, month=1, day=1, tzinfo=ZoneInfo("UTC"))
mode.save()
# Deactivate enrollment.
@@ -1198,7 +1198,7 @@ def test_update_enrollment_with_expired_mode(self, using_api_key, updated_mode):
# Change verified mode expiration.
mode = CourseMode.objects.get(course_id=self.course.id, mode_slug=CourseMode.VERIFIED)
- mode.expiration_datetime = datetime.datetime(year=1970, month=1, day=1, tzinfo=pytz.utc)
+ mode.expiration_datetime = datetime.datetime(year=1970, month=1, day=1, tzinfo=ZoneInfo("UTC"))
mode.save()
self.assert_enrollment_status(
as_server=using_api_key,
@@ -1784,7 +1784,7 @@ class CourseEnrollmentsApiListTest(APITestCase, ModuleStoreTestCase):
"""
Test the course enrollments list API.
"""
- CREATED_DATA = datetime.datetime(2018, 1, 1, 0, 0, 1, tzinfo=pytz.UTC)
+ CREATED_DATA = datetime.datetime(2018, 1, 1, 0, 0, 1, tzinfo=ZoneInfo("UTC"))
def setUp(self):
super().setUp()
diff --git a/openedx/core/djangoapps/external_user_ids/migrations/0009_mariadb_uuid_conversion.py b/openedx/core/djangoapps/external_user_ids/migrations/0009_mariadb_uuid_conversion.py
new file mode 100644
index 000000000000..eddb1608856e
--- /dev/null
+++ b/openedx/core/djangoapps/external_user_ids/migrations/0009_mariadb_uuid_conversion.py
@@ -0,0 +1,86 @@
+# Generated migration for MariaDB UUID field conversion (Django 5.2)
+"""
+Migration to convert UUIDField from char(32) to uuid type for MariaDB compatibility.
+
+This migration is necessary because Django 5 changed the behavior of UUIDField for MariaDB
+databases from using CharField(32) to using a proper UUID type. This change isn't managed
+automatically, so we need to generate migrations to safely convert the columns.
+
+This migration only executes for MariaDB databases and is a no-op for other backends.
+
+See: https://www.albertyw.com/note/django-5-mariadb-uuidfield
+"""
+
+from django.db import migrations
+
+
+def apply_mariadb_migration(apps, schema_editor):
+ """Apply the migration only for MariaDB databases."""
+ connection = schema_editor.connection
+
+ # Check if this is a MariaDB database
+ if connection.vendor != 'mysql':
+ return
+
+ # Additional check for MariaDB specifically (vs MySQL)
+ with connection.cursor() as cursor:
+ cursor.execute("SELECT VERSION()")
+ version = cursor.fetchone()[0]
+ if 'mariadb' not in version.lower():
+ return
+
+ # Apply the field changes for MariaDB
+ with connection.cursor() as cursor:
+ # Convert external_user_id in externalid table
+ cursor.execute(
+ "ALTER TABLE external_user_ids_externalid "
+ "MODIFY external_user_id uuid NOT NULL"
+ )
+ # Convert external_user_id in historicalexternalid table
+ cursor.execute(
+ "ALTER TABLE external_user_ids_historicalexternalid "
+ "MODIFY external_user_id uuid NOT NULL"
+ )
+
+
+def reverse_mariadb_migration(apps, schema_editor):
+ """Reverse the migration only for MariaDB databases."""
+ connection = schema_editor.connection
+
+ # Check if this is a MariaDB database
+ if connection.vendor != 'mysql':
+ return
+
+ # Additional check for MariaDB specifically (vs MySQL)
+ with connection.cursor() as cursor:
+ cursor.execute("SELECT VERSION()")
+ version = cursor.fetchone()[0]
+ if 'mariadb' not in version.lower():
+ return
+
+ # Reverse the field changes for MariaDB
+ with connection.cursor() as cursor:
+ # Revert external_user_id in externalid table
+ cursor.execute(
+ "ALTER TABLE external_user_ids_externalid "
+ "MODIFY external_user_id char(32) NOT NULL"
+ )
+ # Revert external_user_id in historicalexternalid table
+ cursor.execute(
+ "ALTER TABLE external_user_ids_historicalexternalid "
+ "MODIFY external_user_id char(32) NOT NULL"
+ )
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('external_user_ids', '0008_remove_mbcoaching_extid_type'),
+ ]
+
+ operations = [
+ migrations.RunPython(
+ code=apply_mariadb_migration,
+ reverse_code=reverse_mariadb_migration,
+ ),
+ ]
diff --git a/openedx/core/djangoapps/models/tests/test_course_details.py b/openedx/core/djangoapps/models/tests/test_course_details.py
index 41e739ecb4c8..b23b56c88aa3 100644
--- a/openedx/core/djangoapps/models/tests/test_course_details.py
+++ b/openedx/core/djangoapps/models/tests/test_course_details.py
@@ -7,7 +7,7 @@
from django.test import override_settings
import pytest
import ddt
-from pytz import UTC
+from zoneinfo import ZoneInfo
from django.conf import settings
from xmodule.modulestore import ModuleStoreEnum
@@ -86,13 +86,13 @@ def test_update_and_fetch(self):
jsondetails.self_paced = True
assert CourseDetails.update_from_json(self.course.id, jsondetails.__dict__, self.user).self_paced ==\
jsondetails.self_paced
- jsondetails.start_date = datetime.datetime(2010, 10, 1, 0, tzinfo=UTC)
+ jsondetails.start_date = datetime.datetime(2010, 10, 1, 0, tzinfo=ZoneInfo("UTC"))
assert CourseDetails.update_from_json(self.course.id, jsondetails.__dict__, self.user).start_date ==\
jsondetails.start_date
- jsondetails.end_date = datetime.datetime(2011, 10, 1, 0, tzinfo=UTC)
+ jsondetails.end_date = datetime.datetime(2011, 10, 1, 0, tzinfo=ZoneInfo("UTC"))
assert CourseDetails.update_from_json(self.course.id, jsondetails.__dict__, self.user).end_date ==\
jsondetails.end_date
- jsondetails.certificate_available_date = datetime.datetime(2010, 10, 1, 0, tzinfo=UTC)
+ jsondetails.certificate_available_date = datetime.datetime(2010, 10, 1, 0, tzinfo=ZoneInfo("UTC"))
assert CourseDetails.update_from_json(self.course.id, jsondetails.__dict__, self.user)\
.certificate_available_date == jsondetails.certificate_available_date
jsondetails.course_image_name = "an_image.jpg"
@@ -126,7 +126,7 @@ def test_update_and_fetch(self):
jsondetails.instructor_info
def test_toggle_pacing_during_course_run(self):
- self.course.start = datetime.datetime.now(UTC)
+ self.course.start = datetime.datetime.now(ZoneInfo("UTC"))
self.store.update_item(self.course, self.user.id)
details = CourseDetails.fetch(self.course.id)
diff --git a/openedx/core/djangoapps/notifications/base_notification.py b/openedx/core/djangoapps/notifications/base_notification.py
index 5264053ace5a..4b9393830eb9 100644
--- a/openedx/core/djangoapps/notifications/base_notification.py
+++ b/openedx/core/djangoapps/notifications/base_notification.py
@@ -178,7 +178,7 @@
'is_core': False,
'info': '',
'web': True,
- 'email': False,
+ 'email': True,
'push': False,
'email_cadence': EmailCadence.DAILY,
'non_editable': ['push'],
@@ -236,7 +236,7 @@
'is_core': False,
'info': '',
'web': True,
- 'email': False,
+ 'email': True,
'email_cadence': EmailCadence.DAILY,
'push': False,
'non_editable': ['push'],
diff --git a/openedx/core/djangoapps/notifications/tasks.py b/openedx/core/djangoapps/notifications/tasks.py
index fb9f95990d3a..290a7c02b350 100644
--- a/openedx/core/djangoapps/notifications/tasks.py
+++ b/openedx/core/djangoapps/notifications/tasks.py
@@ -10,7 +10,7 @@
from django.core.exceptions import ValidationError
from edx_django_utils.monitoring import set_code_owner_attribute
from opaque_keys.edx.keys import CourseKey
-from pytz import UTC
+from zoneinfo import ZoneInfo
from openedx.core.djangoapps.notifications.audience_filters import NotificationFilter
from openedx.core.djangoapps.notifications.base_notification import (
@@ -75,7 +75,7 @@ def delete_expired_notifications():
This task deletes all expired notifications
"""
batch_size = settings.EXPIRED_NOTIFICATIONS_DELETE_BATCH_SIZE
- expiry_date = datetime.now(UTC) - timedelta(days=settings.NOTIFICATIONS_EXPIRY)
+ expiry_date = datetime.now(ZoneInfo("UTC")) - timedelta(days=settings.NOTIFICATIONS_EXPIRY)
start_time = datetime.now()
total_deleted = 0
delete_count = None
diff --git a/openedx/core/djangoapps/notifications/tests/test_notification_grouping.py b/openedx/core/djangoapps/notifications/tests/test_notification_grouping.py
index debd72d9011f..fd28bb999e9d 100644
--- a/openedx/core/djangoapps/notifications/tests/test_notification_grouping.py
+++ b/openedx/core/djangoapps/notifications/tests/test_notification_grouping.py
@@ -6,7 +6,7 @@
import unittest
from unittest.mock import MagicMock, patch
from datetime import datetime
-from pytz import utc
+from zoneinfo import ZoneInfo
from common.djangoapps.student.tests.factories import UserFactory
from openedx.core.djangoapps.notifications.grouping_notifications import (
@@ -128,7 +128,7 @@ def test_group_user_notifications_no_grouper(self):
self.assertFalse(old_notification.save.called)
- @ddt.data(datetime(2023, 1, 1, tzinfo=utc), None)
+ @ddt.data(datetime(2023, 1, 1, tzinfo=ZoneInfo("UTC")), None)
def test_not_grouped_when_notification_is_seen(self, last_seen):
"""
Notification is not grouped if the notification is marked as seen
@@ -172,11 +172,11 @@ def test_get_user_existing_notifications(self, mock_filter):
# Mock the notification objects returned by the filter
mock_notification1 = MagicMock(spec=Notification)
mock_notification1.user_id = 1
- mock_notification1.created = datetime(2023, 9, 1, tzinfo=utc)
+ mock_notification1.created = datetime(2023, 9, 1, tzinfo=ZoneInfo("UTC"))
mock_notification2 = MagicMock(spec=Notification)
mock_notification2.user_id = 1
- mock_notification2.created = datetime(2023, 9, 2, tzinfo=utc)
+ mock_notification2.created = datetime(2023, 9, 2, tzinfo=ZoneInfo("UTC"))
mock_filter.return_value = [mock_notification1, mock_notification2]
diff --git a/openedx/core/djangoapps/notifications/tests/test_views.py b/openedx/core/djangoapps/notifications/tests/test_views.py
index 06d615f07d7b..5e09a86c5914 100644
--- a/openedx/core/djangoapps/notifications/tests/test_views.py
+++ b/openedx/core/djangoapps/notifications/tests/test_views.py
@@ -11,7 +11,7 @@
from django.test.utils import override_settings
from django.urls import reverse
from edx_toggles.toggles.testutils import override_waffle_flag
-from pytz import UTC
+from zoneinfo import ZoneInfo
from rest_framework import status
from rest_framework.test import APIClient, APITestCase
@@ -192,7 +192,7 @@ def test_list_notifications_with_expiry_date(self):
"""
Test that the view can filter notifications by expiry date.
"""
- today = datetime.now(UTC)
+ today = datetime.now(ZoneInfo("UTC"))
# Create two notifications for the user, one with current date and other with expiry date.
Notification.objects.create(
@@ -604,7 +604,7 @@ def setUp(self):
},
"new_instructor_all_learners_post": {
"web": True,
- "email": False,
+ "email": True,
"push": False,
"email_cadence": "Daily"
},
@@ -628,7 +628,7 @@ def setUp(self):
"notification_types": {
"course_updates": {
"web": True,
- "email": False,
+ "email": True,
"push": False,
"email_cadence": "Daily"
},
diff --git a/openedx/core/djangoapps/notifications/views.py b/openedx/core/djangoapps/notifications/views.py
index 091be365d45f..041f68f95648 100644
--- a/openedx/core/djangoapps/notifications/views.py
+++ b/openedx/core/djangoapps/notifications/views.py
@@ -8,7 +8,7 @@
from django_ratelimit.core import is_ratelimited
from django.shortcuts import get_object_or_404
from django.utils.translation import gettext as _
-from pytz import UTC
+from zoneinfo import ZoneInfo
from rest_framework import generics, status
from rest_framework.decorators import api_view
from rest_framework.generics import UpdateAPIView
@@ -80,7 +80,7 @@ def get_queryset(self):
"""
Override the get_queryset method to filter the queryset by app name, request.user and created
"""
- expiry_date = datetime.now(UTC) - timedelta(days=settings.NOTIFICATIONS_EXPIRY)
+ expiry_date = datetime.now(ZoneInfo("UTC")) - timedelta(days=settings.NOTIFICATIONS_EXPIRY)
app_name = self.request.query_params.get('app_name')
if self.request.query_params.get('tray_opened'):
@@ -212,7 +212,7 @@ def patch(self, request, *args, **kwargs):
- 404: Not Found status code if the notification was not found.
"""
notification_id = request.data.get('notification_id', None)
- read_at = datetime.now(UTC)
+ read_at = datetime.now(ZoneInfo("UTC"))
if notification_id:
notification = get_object_or_404(Notification, pk=notification_id, user=request.user)
diff --git a/openedx/core/djangoapps/oauth_dispatch/dot_overrides/validators.py b/openedx/core/djangoapps/oauth_dispatch/dot_overrides/validators.py
index f8cf0538140d..773c344db0af 100644
--- a/openedx/core/djangoapps/oauth_dispatch/dot_overrides/validators.py
+++ b/openedx/core/djangoapps/oauth_dispatch/dot_overrides/validators.py
@@ -11,7 +11,7 @@
from oauth2_provider.models import AccessToken
from oauth2_provider.oauth2_validators import OAuth2Validator
from oauth2_provider.scopes import get_scopes_backend
-from pytz import utc
+from zoneinfo import ZoneInfo
from ..models import RestrictedApplication
# pylint: disable=W0223
@@ -23,7 +23,7 @@ def on_access_token_presave(sender, instance, *args, **kwargs): # pylint: disab
Mark AccessTokens as expired for 'restricted applications' if required.
"""
if RestrictedApplication.should_expire_access_token(instance.application):
- instance.expires = datetime(1970, 1, 1, tzinfo=utc)
+ instance.expires = datetime(1970, 1, 1, tzinfo=ZoneInfo("UTC"))
class EdxOAuth2Validator(OAuth2Validator):
@@ -152,4 +152,4 @@ def _get_utc_now():
"""
Return current time in UTC.
"""
- return datetime.utcnow().replace(tzinfo=utc)
+ return datetime.utcnow().replace(tzinfo=ZoneInfo("UTC"))
diff --git a/openedx/core/djangoapps/oauth_dispatch/models.py b/openedx/core/djangoapps/oauth_dispatch/models.py
index 2e635167e4c7..3869698af4cb 100644
--- a/openedx/core/djangoapps/oauth_dispatch/models.py
+++ b/openedx/core/djangoapps/oauth_dispatch/models.py
@@ -11,7 +11,7 @@
from django_mysql.models import ListCharField
from oauth2_provider.settings import oauth2_settings
from organizations.models import Organization
-from pytz import utc
+from zoneinfo import ZoneInfo
from openedx.core.djangolib.markup import HTML
from openedx.core.lib.request_utils import get_request_or_stub
@@ -53,7 +53,7 @@ def verify_access_token_as_expired(cls, access_token):
For access_tokens for RestrictedApplications, make sure that the expiry date
is set at the beginning of the epoch which is Jan. 1, 1970
"""
- return access_token.expires == datetime(1970, 1, 1, tzinfo=utc)
+ return access_token.expires == datetime(1970, 1, 1, tzinfo=ZoneInfo("UTC"))
class ApplicationAccess(models.Model):
diff --git a/openedx/core/djangoapps/oauth_dispatch/tests/factories.py b/openedx/core/djangoapps/oauth_dispatch/tests/factories.py
index 473bcd4ced9d..7d7be8dd7fa6 100644
--- a/openedx/core/djangoapps/oauth_dispatch/tests/factories.py
+++ b/openedx/core/djangoapps/oauth_dispatch/tests/factories.py
@@ -4,7 +4,7 @@
from datetime import datetime, timedelta
import factory
-import pytz
+from zoneinfo import ZoneInfo
from factory.django import DjangoModelFactory
from factory.fuzzy import FuzzyText
from oauth2_provider.models import AccessToken, Application, RefreshToken
@@ -39,7 +39,7 @@ class Meta:
django_get_or_create = ('user', 'application')
token = FuzzyText(length=32)
- expires = datetime.now(pytz.UTC) + timedelta(days=1)
+ expires = datetime.now(ZoneInfo("UTC")) + timedelta(days=1)
class RefreshTokenFactory(DjangoModelFactory):
diff --git a/openedx/core/djangoapps/oauth_dispatch/tests/mixins.py b/openedx/core/djangoapps/oauth_dispatch/tests/mixins.py
index d99ac4883b18..407a9aac2b84 100644
--- a/openedx/core/djangoapps/oauth_dispatch/tests/mixins.py
+++ b/openedx/core/djangoapps/oauth_dispatch/tests/mixins.py
@@ -11,6 +11,7 @@
from jwt.exceptions import ExpiredSignatureError
from common.djangoapps.student.models import UserProfile, anonymous_id_for_user
+from common.test.utils import assert_dict_contains_subset
class AccessTokenMixin:
@@ -88,7 +89,7 @@ def _decode_jwt(verify_expiration):
expected['grant_type'] = grant_type or ''
- self.assertDictContainsSubset(expected, payload)
+ assert_dict_contains_subset(self, expected, payload)
if expires_in:
assert payload['exp'] == payload['iat'] + expires_in
diff --git a/openedx/core/djangoapps/oauth_dispatch/tests/test_api.py b/openedx/core/djangoapps/oauth_dispatch/tests/test_api.py
index 3c064cc63c55..5bf1f524bce6 100644
--- a/openedx/core/djangoapps/oauth_dispatch/tests/test_api.py
+++ b/openedx/core/djangoapps/oauth_dispatch/tests/test_api.py
@@ -9,6 +9,7 @@
from oauth2_provider.models import AccessToken
from common.djangoapps.student.tests.factories import UserFactory
+from common.test.utils import assert_dict_contains_subset
OAUTH_PROVIDER_ENABLED = settings.FEATURES.get('ENABLE_OAUTH2_PROVIDER')
if OAUTH_PROVIDER_ENABLED:
@@ -43,7 +44,8 @@ def test_create_token_success(self):
token = api.create_dot_access_token(HttpRequest(), self.user, self.client)
assert token['access_token']
assert token['refresh_token']
- self.assertDictContainsSubset(
+ assert_dict_contains_subset(
+ self,
{
'token_type': 'Bearer',
'expires_in': EXPECTED_DEFAULT_EXPIRES_IN,
@@ -63,5 +65,5 @@ def test_create_token_overrides(self):
token = api.create_dot_access_token(
HttpRequest(), self.user, self.client, expires_in=expires_in, scopes=['profile'],
)
- self.assertDictContainsSubset({'scope': 'profile'}, token)
- self.assertDictContainsSubset({'expires_in': expires_in}, token)
+ assert_dict_contains_subset(self, {'scope': 'profile'}, token)
+ assert_dict_contains_subset(self, {'expires_in': expires_in}, token)
diff --git a/openedx/core/djangoapps/oauth_dispatch/tests/test_jwt.py b/openedx/core/djangoapps/oauth_dispatch/tests/test_jwt.py
index da95fd072ded..647da14f6edd 100644
--- a/openedx/core/djangoapps/oauth_dispatch/tests/test_jwt.py
+++ b/openedx/core/djangoapps/oauth_dispatch/tests/test_jwt.py
@@ -12,6 +12,7 @@
from openedx.core.djangoapps.oauth_dispatch.models import RestrictedApplication
from openedx.core.djangoapps.oauth_dispatch.tests.mixins import AccessTokenMixin
from common.djangoapps.student.tests.factories import UserFactory
+from common.test.utils import assert_dict_contains_subset
@ddt.ddt
@@ -171,7 +172,7 @@ def test_create_jwt_for_user(self, user_email_verified, mock_create_roles):
token_payload = self.assert_valid_jwt_access_token(
jwt_token, self.user, self.default_scopes, aud=aud, secret=secret,
)
- self.assertDictContainsSubset(additional_claims, token_payload)
+ assert_dict_contains_subset(self, additional_claims, token_payload)
assert user_email_verified == token_payload['email_verified']
assert token_payload['roles'] == mock_create_roles.return_value
diff --git a/openedx/core/djangoapps/password_policy/compliance.py b/openedx/core/djangoapps/password_policy/compliance.py
index fdd103d2437d..8601a55d658f 100644
--- a/openedx/core/djangoapps/password_policy/compliance.py
+++ b/openedx/core/djangoapps/password_policy/compliance.py
@@ -4,7 +4,7 @@
from datetime import datetime
-import pytz
+from zoneinfo import ZoneInfo
from django.conf import settings
from django.utils.translation import gettext as _
@@ -69,7 +69,7 @@ def enforce_compliance_on_login(user, password):
if deadline is None:
return
- now = datetime.now(pytz.UTC)
+ now = datetime.now(ZoneInfo("UTC"))
if now >= deadline: # lint-amnesty, pylint: disable=no-else-raise
raise NonCompliantPasswordException(
HTML(_(
diff --git a/openedx/core/djangoapps/password_policy/tests/test_compliance.py b/openedx/core/djangoapps/password_policy/tests/test_compliance.py
index cb803bed99a9..5d8a56d60c80 100644
--- a/openedx/core/djangoapps/password_policy/tests/test_compliance.py
+++ b/openedx/core/djangoapps/password_policy/tests/test_compliance.py
@@ -6,7 +6,7 @@
from unittest.mock import patch
import pytest
-import pytz
+from zoneinfo import ZoneInfo
from dateutil.parser import parse as parse_date
from django.test import TestCase, override_settings
@@ -75,7 +75,7 @@ def test_enforce_compliance_on_login(self):
mock_check_user_compliance.return_value = False
with patch('openedx.core.djangoapps.password_policy.compliance._get_compliance_deadline_for_user') as \
mock_get_compliance_deadline_for_user:
- mock_get_compliance_deadline_for_user.return_value = datetime.now(pytz.UTC) - timedelta(1)
+ mock_get_compliance_deadline_for_user.return_value = datetime.now(ZoneInfo("UTC")) - timedelta(1)
pytest.raises(NonCompliantPasswordException, enforce_compliance_on_login, user, password)
# Test deadline is in the future
@@ -84,7 +84,7 @@ def test_enforce_compliance_on_login(self):
mock_check_user_compliance.return_value = False
with patch('openedx.core.djangoapps.password_policy.compliance._get_compliance_deadline_for_user') as \
mock_get_compliance_deadline_for_user:
- mock_get_compliance_deadline_for_user.return_value = datetime.now(pytz.UTC) + timedelta(1)
+ mock_get_compliance_deadline_for_user.return_value = datetime.now(ZoneInfo("UTC")) + timedelta(1)
assert pytest.raises(NonCompliantPasswordWarning, enforce_compliance_on_login, user, password)
def test_check_user_compliance(self):
diff --git a/openedx/core/djangoapps/profile_images/tests/test_views.py b/openedx/core/djangoapps/profile_images/tests/test_views.py
index 0a276377589b..963c0956c28a 100644
--- a/openedx/core/djangoapps/profile_images/tests/test_views.py
+++ b/openedx/core/djangoapps/profile_images/tests/test_views.py
@@ -7,7 +7,7 @@
import pytest
import datetime # lint-amnesty, pylint: disable=wrong-import-order
-from pytz import UTC
+from zoneinfo import ZoneInfo
from django.urls import reverse
from django.http import HttpResponse
@@ -30,8 +30,8 @@
from .helpers import make_image_file
TEST_PASSWORD = "test"
-TEST_UPLOAD_DT = datetime.datetime(2002, 1, 9, 15, 43, 1, tzinfo=UTC)
-TEST_UPLOAD_DT2 = datetime.datetime(2003, 1, 9, 15, 43, 1, tzinfo=UTC)
+TEST_UPLOAD_DT = datetime.datetime(2002, 1, 9, 15, 43, 1, tzinfo=ZoneInfo("UTC"))
+TEST_UPLOAD_DT2 = datetime.datetime(2003, 1, 9, 15, 43, 1, tzinfo=ZoneInfo("UTC"))
class ProfileImageEndpointMixin(UserSettingsEventTestMixin):
diff --git a/openedx/core/djangoapps/profile_images/views.py b/openedx/core/djangoapps/profile_images/views.py
index b88b3ad32bdb..1c9d4fcf3bf7 100644
--- a/openedx/core/djangoapps/profile_images/views.py
+++ b/openedx/core/djangoapps/profile_images/views.py
@@ -11,7 +11,7 @@
from django.utils.translation import gettext as _
from edx_rest_framework_extensions.auth.jwt.authentication import JwtAuthentication
from edx_rest_framework_extensions.auth.session.authentication import SessionAuthenticationAllowInactiveUser
-from pytz import UTC
+from zoneinfo import ZoneInfo
from rest_framework import permissions, status
from rest_framework.parsers import FormParser, MultiPartParser
from rest_framework.response import Response
@@ -38,7 +38,7 @@ def _make_upload_dt():
Generate a server-side timestamp for the upload. This is in a separate
function so its behavior can be overridden in tests.
"""
- return datetime.datetime.utcnow().replace(tzinfo=UTC)
+ return datetime.datetime.utcnow().replace(tzinfo=ZoneInfo("UTC"))
class ProfileImageView(DeveloperErrorViewMixin, APIView):
diff --git a/openedx/core/djangoapps/programs/tests/test_tasks.py b/openedx/core/djangoapps/programs/tests/test_tasks.py
index e2b1c554c840..943c8c0dd444 100644
--- a/openedx/core/djangoapps/programs/tests/test_tasks.py
+++ b/openedx/core/djangoapps/programs/tests/test_tasks.py
@@ -10,7 +10,7 @@
import ddt
import httpretty
import pytest
-import pytz
+from zoneinfo import ZoneInfo
import requests
from celery.exceptions import MaxRetriesExceededError
from django.conf import settings
@@ -520,7 +520,7 @@ class AwardCourseCertificatesTestCase(CredentialsApiConfigMixin, TestCase):
def setUp(self):
super().setUp()
- self.available_date = datetime.now(pytz.UTC) + timedelta(days=1)
+ self.available_date = datetime.now(ZoneInfo("UTC")) + timedelta(days=1)
self.course = CourseOverviewFactory.create(
self_paced=True, # Any option to allow the certificate to be viewable for the course
certificate_available_date=self.available_date,
@@ -1023,7 +1023,7 @@ class UpdateCertificateAvailableDateOnCourseUpdateTestCase(CredentialsApiConfigM
def setUp(self):
super().setUp()
- self.end_date = datetime.now(pytz.UTC) + timedelta(days=90)
+ self.end_date = datetime.now(ZoneInfo("UTC")) + timedelta(days=90)
self.credentials_api_config = self.create_credentials_config(enabled=False)
def tearDown(self):
@@ -1135,7 +1135,7 @@ def test_update_certificate_available_date_instructor_paced_cdb_end_with_date(se
explicitly set as part of the course overview.
"""
self._update_credentials_api_config(True)
- certificate_available_date = datetime.now(pytz.UTC) + timedelta(days=120)
+ certificate_available_date = datetime.now(ZoneInfo("UTC")) + timedelta(days=120)
course_overview = self._create_course_overview(
False,
@@ -1168,7 +1168,7 @@ def test_update_certificate_available_date_self_paced(self, mock_update):
invalid data is set in a course overview, we don't pass it to Credentials.
"""
self._update_credentials_api_config(True)
- certificate_available_date = datetime.now(pytz.UTC) + timedelta(days=120)
+ certificate_available_date = datetime.now(ZoneInfo("UTC")) + timedelta(days=120)
course_overview = self._create_course_overview(
True,
diff --git a/openedx/core/djangoapps/programs/tests/test_utils.py b/openedx/core/djangoapps/programs/tests/test_utils.py
index 264f1a6aeebd..18058c827a52 100644
--- a/openedx/core/djangoapps/programs/tests/test_utils.py
+++ b/openedx/core/djangoapps/programs/tests/test_utils.py
@@ -15,7 +15,7 @@
from django.urls import reverse
from edx_toggles.toggles.testutils import override_waffle_switch
from opaque_keys.edx.keys import CourseKey # lint-amnesty, pylint: disable=wrong-import-order
-from pytz import utc
+from zoneinfo import ZoneInfo
from testfixtures import LogCapture
from common.djangoapps.course_modes.models import CourseMode
@@ -209,7 +209,7 @@ def test_single_program_multiple_entitlements(self, mock_get_programs):
CourseEntitlementFactory.create(
user=self.user,
course_uuid=course_uuid,
- expired_at=datetime.datetime.now(utc),
+ expired_at=datetime.datetime.now(ZoneInfo("UTC")),
mode=CourseMode.VERIFIED,
enrollment_course_run=enrollment
@@ -308,7 +308,7 @@ def test_in_progress_course_upgrade_deadline_check(self, offset, mock_get_progra
the right type for which the upgrade deadline has not passed.
"""
course_run_key = generate_course_run_key()
- now = datetime.datetime.now(utc)
+ now = datetime.datetime.now(ZoneInfo("UTC"))
upgrade_deadline = None if not offset else str(now + datetime.timedelta(days=offset))
required_seat = SeatFactory(type=CourseMode.VERIFIED, upgrade_deadline=upgrade_deadline)
enrolled_seat = SeatFactory(type=CourseMode.AUDIT)
@@ -488,7 +488,7 @@ def test_shared_entitlement_engagement(self, mock_get_programs):
def test_simulate_progress(self, mock_get_programs): # lint-amnesty, pylint: disable=too-many-statements
"""Simulate the entirety of a user's progress through a program."""
- today = datetime.datetime.now(utc)
+ today = datetime.datetime.now(ZoneInfo("UTC"))
two_days_ago = today - datetime.timedelta(days=2)
three_days_ago = today - datetime.timedelta(days=3)
yesterday = today - datetime.timedelta(days=1)
@@ -862,8 +862,8 @@ def _create_course(self, course_price, course_run_count=1, make_entitlement=Fals
course_runs = []
for x in range(course_run_count):
course = ModuleStoreCourseFactory.create(run='Run_' + str(x))
- course.start = datetime.datetime.now(utc) - datetime.timedelta(days=1)
- course.end = datetime.datetime.now(utc) + datetime.timedelta(days=1)
+ course.start = datetime.datetime.now(ZoneInfo("UTC")) - datetime.timedelta(days=1)
+ course.end = datetime.datetime.now(ZoneInfo("UTC")) + datetime.timedelta(days=1)
course.instructor_info = self.instructors
course = self.update_course(course, self.user.id)
@@ -899,8 +899,8 @@ def setUp(self):
super().setUp()
self.course = ModuleStoreCourseFactory()
- self.course.start = datetime.datetime.now(utc) - datetime.timedelta(days=1)
- self.course.end = datetime.datetime.now(utc) + datetime.timedelta(days=1)
+ self.course.start = datetime.datetime.now(ZoneInfo("UTC")) - datetime.timedelta(days=1)
+ self.course.end = datetime.datetime.now(ZoneInfo("UTC")) + datetime.timedelta(days=1)
self.course = self.update_course(self.course, self.user.id)
self.course_run = CourseRunFactory(key=str(self.course.id))
@@ -941,7 +941,7 @@ def test_is_enrollment_open(self, days_offset):
Verify that changes to the course run end date do not affect our
assessment of the course run being open for enrollment.
"""
- self.course.end = datetime.datetime.now(utc) + datetime.timedelta(days=days_offset)
+ self.course.end = datetime.datetime.now(ZoneInfo("UTC")) + datetime.timedelta(days=days_offset)
self.course = self.update_course(self.course, self.user.id)
data = ProgramDataExtender(self.program, self.user).extend()
@@ -1022,8 +1022,8 @@ def test_course_run_enrollment_status(self, start_offset, end_offset, is_enrollm
"""
Verify that course run enrollment status is reflected correctly.
"""
- self.course.enrollment_start = datetime.datetime.now(utc) - datetime.timedelta(days=start_offset)
- self.course.enrollment_end = datetime.datetime.now(utc) - datetime.timedelta(days=end_offset)
+ self.course.enrollment_start = datetime.datetime.now(ZoneInfo("UTC")) - datetime.timedelta(days=start_offset)
+ self.course.enrollment_end = datetime.datetime.now(ZoneInfo("UTC")) - datetime.timedelta(days=end_offset)
self.course = self.update_course(self.course, self.user.id)
@@ -1040,7 +1040,7 @@ def test_no_enrollment_start_date(self):
Verify that a closed course run with no explicit enrollment start date
doesn't cause an error. Regression test for ECOM-4973.
"""
- self.course.enrollment_end = datetime.datetime.now(utc) - datetime.timedelta(days=1)
+ self.course.enrollment_end = datetime.datetime.now(ZoneInfo("UTC")) - datetime.timedelta(days=1)
self.course = self.update_course(self.course, self.user.id)
data = ProgramDataExtender(self.program, self.user).extend()
diff --git a/openedx/core/djangoapps/programs/utils.py b/openedx/core/djangoapps/programs/utils.py
index 76263c4b405c..49fb054b9b52 100644
--- a/openedx/core/djangoapps/programs/utils.py
+++ b/openedx/core/djangoapps/programs/utils.py
@@ -14,7 +14,7 @@
from django.urls import reverse
from django.utils.functional import cached_property
from opaque_keys.edx.keys import CourseKey
-from pytz import utc
+from zoneinfo import ZoneInfo
from requests.exceptions import RequestException
from common.djangoapps.course_modes.api import get_paid_modes_for_course
@@ -43,7 +43,7 @@
from xmodule.modulestore.django import modulestore
# The datetime module's strftime() methods require a year >= 1900.
-DEFAULT_ENROLLMENT_START_DATE = datetime.datetime(1900, 1, 1, tzinfo=utc)
+DEFAULT_ENROLLMENT_START_DATE = datetime.datetime(1900, 1, 1, tzinfo=ZoneInfo("UTC"))
log = logging.getLogger(__name__)
@@ -286,7 +286,7 @@ def progress(self, programs: list[dict | None] | None = None, count_only: bool =
list of dict, each containing information about a user's progress
towards completing a program.
"""
- now = datetime.datetime.now(utc)
+ now = datetime.datetime.now(ZoneInfo("UTC"))
progress = []
programs = programs or self.engaged_programs
@@ -598,15 +598,17 @@ def _attach_course_run_enrollment_open_date(self, run_mode):
run_mode["enrollment_open_date"] = strftime_localized(self.enrollment_start, "SHORT_DATE")
def _attach_course_run_is_course_ended(self, run_mode):
- end_date = self.course_overview.end or datetime.datetime.max.replace(tzinfo=utc)
- run_mode["is_course_ended"] = end_date < datetime.datetime.now(utc)
+ end_date = self.course_overview.end or datetime.datetime.max.replace(tzinfo=ZoneInfo("UTC"))
+ run_mode["is_course_ended"] = end_date < datetime.datetime.now(ZoneInfo("UTC"))
def _attach_course_run_is_enrolled(self, run_mode):
run_mode["is_enrolled"] = CourseEnrollment.is_enrolled(self.user, self.course_run_key)
def _attach_course_run_is_enrollment_open(self, run_mode):
- enrollment_end = self.course_overview.enrollment_end or datetime.datetime.max.replace(tzinfo=utc)
- run_mode["is_enrollment_open"] = self.enrollment_start <= datetime.datetime.now(utc) < enrollment_end
+ enrollment_end = self.course_overview.enrollment_end or datetime.datetime.max.replace(tzinfo=ZoneInfo("UTC"))
+ run_mode["is_enrollment_open"] = (
+ self.enrollment_start <= datetime.datetime.now(ZoneInfo("UTC")) < enrollment_end
+ )
def _attach_course_run_advertised_start(self, run_mode):
"""
diff --git a/openedx/core/djangoapps/schedules/management/commands/__init__.py b/openedx/core/djangoapps/schedules/management/commands/__init__.py
index 0b7255976563..16484ef367ea 100644
--- a/openedx/core/djangoapps/schedules/management/commands/__init__.py
+++ b/openedx/core/djangoapps/schedules/management/commands/__init__.py
@@ -5,7 +5,7 @@
import datetime
-import pytz
+from zoneinfo import ZoneInfo
from django.contrib.sites.models import Site
from django.core.management.base import BaseCommand
@@ -62,7 +62,7 @@ def handle(self, *args, **options):
current_date = datetime.datetime(
*[int(x) for x in options['date'].split('-')],
- tzinfo=pytz.UTC
+ tzinfo=ZoneInfo("UTC")
)
self.log_debug('Current date = %s', current_date.isoformat())
override_recipient_email = options.get('override_recipient_email')
diff --git a/openedx/core/djangoapps/schedules/management/commands/send_course_next_section_update.py b/openedx/core/djangoapps/schedules/management/commands/send_course_next_section_update.py
index 53e2100649a1..6faa92308840 100644
--- a/openedx/core/djangoapps/schedules/management/commands/send_course_next_section_update.py
+++ b/openedx/core/djangoapps/schedules/management/commands/send_course_next_section_update.py
@@ -3,7 +3,7 @@
"""
import datetime
-import pytz
+from zoneinfo import ZoneInfo
from textwrap import dedent # lint-amnesty, pylint: disable=wrong-import-order
from django.contrib.sites.models import Site
@@ -23,7 +23,7 @@ class Command(SendEmailBaseCommand):
def handle(self, *args, ** options):
current_date = datetime.datetime(
*[int(x) for x in options['date'].split('-')],
- tzinfo=pytz.UTC
+ tzinfo=ZoneInfo("UTC")
)
site = Site.objects.get(domain__iexact=options['site_domain_name'])
diff --git a/openedx/core/djangoapps/schedules/management/commands/setup_models_to_send_test_emails.py b/openedx/core/djangoapps/schedules/management/commands/setup_models_to_send_test_emails.py
index 976f1aa16fd9..587271fc599c 100644
--- a/openedx/core/djangoapps/schedules/management/commands/setup_models_to_send_test_emails.py
+++ b/openedx/core/djangoapps/schedules/management/commands/setup_models_to_send_test_emails.py
@@ -7,7 +7,7 @@
from textwrap import dedent
import factory
-import pytz
+from zoneinfo import ZoneInfo
from django.contrib.sites.models import Site
from django.core.management.base import BaseCommand
@@ -26,29 +26,29 @@ class ThreeDayNudgeSchedule(ScheduleFactory):
"""
A ScheduleFactory that creates a Schedule set up for a 3-day nudge email.
"""
- start_date = factory.Faker('date_time_between', start_date='-3d', end_date='-3d', tzinfo=pytz.UTC)
+ start_date = factory.Faker('date_time_between', start_date='-3d', end_date='-3d', tzinfo=ZoneInfo("UTC"))
class TenDayNudgeSchedule(ScheduleFactory):
"""
A ScheduleFactory that creates a Schedule set up for a 10-day nudge email.
"""
- start_date = factory.Faker('date_time_between', start_date='-10d', end_date='-10d', tzinfo=pytz.UTC)
+ start_date = factory.Faker('date_time_between', start_date='-10d', end_date='-10d', tzinfo=ZoneInfo("UTC"))
class UpgradeReminderSchedule(ScheduleFactory):
"""
A ScheduleFactory that creates a Schedule set up for a 2-days-remaining upgrade reminder.
"""
- start_date = factory.Faker('past_datetime', tzinfo=pytz.UTC)
- upgrade_deadline = factory.Faker('date_time_between', start_date='+2d', end_date='+2d', tzinfo=pytz.UTC)
+ start_date = factory.Faker('past_datetime', tzinfo=ZoneInfo("UTC"))
+ upgrade_deadline = factory.Faker('date_time_between', start_date='+2d', end_date='+2d', tzinfo=ZoneInfo("UTC"))
class ContentHighlightSchedule(ScheduleFactory):
"""
A ScheduleFactory that creates a Schedule set up for a course highlights email.
"""
- start_date = factory.Faker('date_time_between', start_date='-7d', end_date='-7d', tzinfo=pytz.UTC)
+ start_date = factory.Faker('date_time_between', start_date='-7d', end_date='-7d', tzinfo=ZoneInfo("UTC"))
experience = factory.RelatedFactory(ScheduleExperienceFactory, 'schedule', experience_type=ScheduleExperience.EXPERIENCES.course_updates) # lint-amnesty, pylint: disable=line-too-long
diff --git a/openedx/core/djangoapps/schedules/management/commands/tests/send_email_base.py b/openedx/core/djangoapps/schedules/management/commands/tests/send_email_base.py
index 774f1f418124..c0a63954644d 100644
--- a/openedx/core/djangoapps/schedules/management/commands/tests/send_email_base.py
+++ b/openedx/core/djangoapps/schedules/management/commands/tests/send_email_base.py
@@ -11,7 +11,7 @@
import attr
import ddt
-import pytz
+from zoneinfo import ZoneInfo
from django.conf import settings
from django.contrib.auth.models import User # lint-amnesty, pylint: disable=imported-auth-user
from django.db.models import Max
@@ -119,7 +119,7 @@ def _next_user_id(self):
return max_user_id + num_bins - (max_user_id % num_bins)
def _get_dates(self, offset=None): # lint-amnesty, pylint: disable=missing-function-docstring
- current_day = _get_datetime_beginning_of_day(datetime.datetime.now(pytz.UTC))
+ current_day = _get_datetime_beginning_of_day(datetime.datetime.now(ZoneInfo("UTC")))
offset = offset or self.expected_offsets[0]
target_day = current_day + datetime.timedelta(days=offset)
if self.resolver.schedule_date_field == 'upgrade_deadline':
@@ -148,7 +148,7 @@ def _schedule_factory(self, offset=None, **factory_kwargs): # lint-amnesty, pyl
CourseModeFactory(
course_id=course_id,
mode_slug=CourseMode.VERIFIED,
- expiration_datetime=datetime.datetime.now(pytz.UTC) + datetime.timedelta(days=30),
+ expiration_datetime=datetime.datetime.now(ZoneInfo("UTC")) + datetime.timedelta(days=30),
)
self._courses_with_verified_modes.add(course_id)
return schedule
@@ -158,7 +158,7 @@ def _update_schedule_config(self, schedule_config_kwargs):
Updates the schedule config model by making sure the new entry
has a later timestamp.
"""
- later_time = datetime.datetime.now(pytz.UTC) + datetime.timedelta(minutes=1)
+ later_time = datetime.datetime.now(ZoneInfo("UTC")) + datetime.timedelta(minutes=1)
with freeze_time(later_time):
ScheduleConfigFactory.create(**schedule_config_kwargs)
@@ -167,7 +167,7 @@ def test_command_task_binding(self):
def test_handle(self):
with patch.object(self.command, 'async_send_task') as mock_send:
- test_day = datetime.datetime(2017, 8, 1, tzinfo=pytz.UTC)
+ test_day = datetime.datetime(2017, 8, 1, tzinfo=ZoneInfo("UTC"))
self.command().handle(date='2017-08-01', site_domain_name=self.site_config.site.domain)
for offset in self.expected_offsets:
@@ -287,7 +287,7 @@ def test_enqueue_config(self, is_enabled):
}
self._update_schedule_config(schedule_config_kwargs)
- current_datetime = datetime.datetime(2017, 8, 1, tzinfo=pytz.UTC)
+ current_datetime = datetime.datetime(2017, 8, 1, tzinfo=ZoneInfo("UTC"))
with patch.object(self.task, 'apply_async') as mock_apply_async:
self.task.enqueue(self.site_config.site, current_datetime, 3)
diff --git a/openedx/core/djangoapps/schedules/management/commands/tests/test_send_email_base_command.py b/openedx/core/djangoapps/schedules/management/commands/tests/test_send_email_base_command.py
index 11b33d585195..eb1f7fe40f71 100644
--- a/openedx/core/djangoapps/schedules/management/commands/tests/test_send_email_base_command.py
+++ b/openedx/core/djangoapps/schedules/management/commands/tests/test_send_email_base_command.py
@@ -8,7 +8,7 @@
from unittest.mock import DEFAULT, Mock, patch
import ddt
-import pytz
+from zoneinfo import ZoneInfo
from django.conf import settings
from django.contrib.sites.models import Site
@@ -33,7 +33,7 @@ def test_handle(self):
self.command.handle(site_domain_name=self.site.domain, date='2017-09-29')
send_emails.assert_called_once_with(
self.site,
- datetime.datetime(2017, 9, 29, tzinfo=pytz.UTC),
+ datetime.datetime(2017, 9, 29, tzinfo=ZoneInfo("UTC")),
None,
None
)
@@ -45,7 +45,7 @@ def test_handle_all_sites(self):
for expected_site in expected_sites:
send_emails.assert_any_call(
expected_site,
- datetime.datetime(2017, 9, 29, tzinfo=pytz.UTC),
+ datetime.datetime(2017, 9, 29, tzinfo=ZoneInfo("UTC")),
None,
None
)
diff --git a/openedx/core/djangoapps/schedules/tests/factories.py b/openedx/core/djangoapps/schedules/tests/factories.py
index 882b62fb8b78..c3ffcad246ed 100644
--- a/openedx/core/djangoapps/schedules/tests/factories.py
+++ b/openedx/core/djangoapps/schedules/tests/factories.py
@@ -4,7 +4,7 @@
import factory
-import pytz
+from zoneinfo import ZoneInfo
from openedx.core.djangoapps.schedules import models
from openedx.core.djangoapps.site_configuration.tests.factories import SiteFactory
@@ -22,8 +22,8 @@ class ScheduleFactory(factory.django.DjangoModelFactory): # lint-amnesty, pylin
class Meta:
model = models.Schedule
- start_date = factory.Faker('future_datetime', tzinfo=pytz.UTC)
- upgrade_deadline = factory.Faker('future_datetime', tzinfo=pytz.UTC)
+ start_date = factory.Faker('future_datetime', tzinfo=ZoneInfo("UTC"))
+ upgrade_deadline = factory.Faker('future_datetime', tzinfo=ZoneInfo("UTC"))
enrollment = factory.SubFactory(CourseEnrollmentFactory)
experience = factory.RelatedFactory(ScheduleExperienceFactory, 'schedule')
diff --git a/openedx/core/djangoapps/schedules/tests/test_resolvers.py b/openedx/core/djangoapps/schedules/tests/test_resolvers.py
index 2c37608e5cff..20536abcbd6f 100644
--- a/openedx/core/djangoapps/schedules/tests/test_resolvers.py
+++ b/openedx/core/djangoapps/schedules/tests/test_resolvers.py
@@ -8,7 +8,7 @@
import crum
import ddt
-import pytz
+from zoneinfo import ZoneInfo
from django.test import TestCase
from django.test.client import RequestFactory
from django.test.utils import override_settings
@@ -123,7 +123,7 @@ def test_external_course_updates(self, bucket):
# experiment. Note that the experiment waffle is currently inactive, but they should still be excluded because
# they were bucketed at enrollment time.
bin_num = BinnedSchedulesBaseResolver.bin_num_for_user_id(user.id)
- resolver = BinnedSchedulesBaseResolver(None, self.site, datetime.datetime.now(pytz.UTC), 0, bin_num)
+ resolver = BinnedSchedulesBaseResolver(None, self.site, datetime.datetime.now(ZoneInfo("UTC")), 0, bin_num)
resolver.schedule_date_field = 'created'
schedules = resolver.get_schedules_with_target_date_by_bin_and_orgs()
@@ -235,7 +235,7 @@ class TestCourseNextSectionUpdateResolver(SchedulesResolverTestMixin, ModuleStor
def setUp(self):
super().setUp()
- self.today = datetime.datetime.utcnow()
+ self.today = datetime.datetime.now(ZoneInfo("UTC"))
self.yesterday = self.today - datetime.timedelta(days=1)
self.course = CourseFactory.create(
highlights_enabled_for_messaging=True, self_paced=True,
diff --git a/openedx/core/djangoapps/schedules/tests/test_signals.py b/openedx/core/djangoapps/schedules/tests/test_signals.py
index 023fbbdbafcc..b862f8e00e26 100644
--- a/openedx/core/djangoapps/schedules/tests/test_signals.py
+++ b/openedx/core/djangoapps/schedules/tests/test_signals.py
@@ -8,7 +8,7 @@
import ddt
import pytest
-from pytz import utc
+from zoneinfo import ZoneInfo
from common.djangoapps.course_modes.models import CourseMode
from common.djangoapps.course_modes.tests.factories import CourseModeFactory
@@ -188,7 +188,7 @@ def _create_course_run(self_paced=True, start_day_offset=-1):
Both audit and verified `CourseMode` objects will be created for the course run.
"""
- now = datetime.datetime.now(utc)
+ now = datetime.datetime.now(ZoneInfo("UTC"))
start = now + datetime.timedelta(days=start_day_offset)
course = CourseFactory.create(start=start, self_paced=self_paced)
diff --git a/openedx/core/djangoapps/schedules/tests/test_utils.py b/openedx/core/djangoapps/schedules/tests/test_utils.py
index f1d0cd9fcba0..2b48550b421d 100644
--- a/openedx/core/djangoapps/schedules/tests/test_utils.py
+++ b/openedx/core/djangoapps/schedules/tests/test_utils.py
@@ -5,7 +5,7 @@
import datetime
import ddt
-from pytz import utc
+from zoneinfo import ZoneInfo
from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory
@@ -26,7 +26,7 @@ def create_schedule(self, enrollment_offset=0, course_start_offset=-100):
# pylint: disable=attribute-defined-outside-init
self.config = ScheduleConfigFactory()
- start = datetime.datetime.now(utc) + datetime.timedelta(days=course_start_offset)
+ start = datetime.datetime.now(ZoneInfo("UTC")) + datetime.timedelta(days=course_start_offset)
self.course = CourseFactory.create(start=start, self_paced=True)
self.enrollment = CourseEnrollmentFactory(
diff --git a/openedx/core/djangoapps/schedules/utils.py b/openedx/core/djangoapps/schedules/utils.py
index c2565a1c87a3..344e7566b1e3 100644
--- a/openedx/core/djangoapps/schedules/utils.py
+++ b/openedx/core/djangoapps/schedules/utils.py
@@ -3,7 +3,7 @@
import datetime
import logging
-import pytz
+from zoneinfo import ZoneInfo
from django.db import transaction
from openedx.core.djangoapps.schedules.models import Schedule
@@ -59,7 +59,7 @@ def reset_self_paced_schedule(user, course_key, use_enrollment_date=False):
if use_enrollment_date:
new_start_date = schedule.enrollment.created
else:
- new_start_date = datetime.datetime.now(pytz.utc)
+ new_start_date = datetime.datetime.now(ZoneInfo("UTC"))
# Make sure we don't start the clock on the learner's schedule before the course even starts
new_start_date = max(new_start_date, schedule.enrollment.course.start)
diff --git a/openedx/core/djangoapps/user_api/accounts/api.py b/openedx/core/djangoapps/user_api/accounts/api.py
index 6970ea6f852f..c3fd805a3166 100644
--- a/openedx/core/djangoapps/user_api/accounts/api.py
+++ b/openedx/core/djangoapps/user_api/accounts/api.py
@@ -12,7 +12,7 @@
from django.utils.translation import gettext as _
from django.utils.translation import override as override_language
from eventtracking import tracker
-from pytz import UTC
+from zoneinfo import ZoneInfo
from common.djangoapps.student import views as student_views
from common.djangoapps.student.models import (
@@ -375,7 +375,7 @@ def _store_old_name_if_needed(old_name, user_profile, requesting_user):
meta['old_names'].append([
old_name,
f"Name change requested through account API by {requesting_user.username}",
- datetime.datetime.now(UTC).isoformat()
+ datetime.datetime.now(ZoneInfo("UTC")).isoformat()
])
user_profile.set_meta(meta)
user_profile.save()
diff --git a/openedx/core/djangoapps/user_api/accounts/tests/retirement_helpers.py b/openedx/core/djangoapps/user_api/accounts/tests/retirement_helpers.py
index a3c40002f61c..d91ba37399d4 100644
--- a/openedx/core/djangoapps/user_api/accounts/tests/retirement_helpers.py
+++ b/openedx/core/djangoapps/user_api/accounts/tests/retirement_helpers.py
@@ -6,7 +6,7 @@
import datetime
import pytest
-import pytz
+from zoneinfo import ZoneInfo
from django.test import TestCase
from social_django.models import UserSocialAuth
@@ -67,7 +67,7 @@ def create_retirement_status(user, state=None, create_datetime=None):
Assumes that retirement states have been setup before calling.
"""
if create_datetime is None:
- create_datetime = datetime.datetime.now(pytz.UTC) - datetime.timedelta(days=8)
+ create_datetime = datetime.datetime.now(ZoneInfo("UTC")) - datetime.timedelta(days=8)
retirement = UserRetirementStatus.create_retirement(user)
if state:
diff --git a/openedx/core/djangoapps/user_api/accounts/tests/test_api.py b/openedx/core/djangoapps/user_api/accounts/tests/test_api.py
index f9071c06a5c2..8cdb7a634118 100644
--- a/openedx/core/djangoapps/user_api/accounts/tests/test_api.py
+++ b/openedx/core/djangoapps/user_api/accounts/tests/test_api.py
@@ -17,7 +17,7 @@
from django.test import TestCase
from django.test.client import RequestFactory
from django.urls import reverse
-from pytz import UTC
+from zoneinfo import ZoneInfo
from social_django.models import UserSocialAuth
from common.djangoapps.student.models import (
@@ -381,7 +381,7 @@ def test_validate_name_change_same_name(self):
meta['old_names'] = []
for num in range(3):
meta['old_names'].append(
- [f'old_name_{num}', 'test', datetime.datetime.now(UTC).isoformat()]
+ [f'old_name_{num}', 'test', datetime.datetime.now(ZoneInfo("UTC")).isoformat()]
)
user_profile.set_meta(meta)
user_profile.save()
diff --git a/openedx/core/djangoapps/user_api/accounts/tests/test_image_helpers.py b/openedx/core/djangoapps/user_api/accounts/tests/test_image_helpers.py
index 3608073a5217..c02069ab7b16 100644
--- a/openedx/core/djangoapps/user_api/accounts/tests/test_image_helpers.py
+++ b/openedx/core/djangoapps/user_api/accounts/tests/test_image_helpers.py
@@ -8,7 +8,7 @@
from unittest.mock import patch
from django.test import TestCase
-from pytz import UTC
+from zoneinfo import ZoneInfo
from openedx.core.djangolib.testing.utils import skip_unless_lms
from common.djangoapps.student.tests.factories import UserFactory
@@ -16,7 +16,7 @@
from ..image_helpers import get_profile_image_urls_for_user
TEST_SIZES = {'full': 50, 'small': 10}
-TEST_PROFILE_IMAGE_UPLOAD_DT = datetime.datetime(2002, 1, 9, 15, 43, 1, tzinfo=UTC)
+TEST_PROFILE_IMAGE_UPLOAD_DT = datetime.datetime(2002, 1, 9, 15, 43, 1, tzinfo=ZoneInfo("UTC"))
@patch.dict('django.conf.settings.PROFILE_IMAGE_SIZES_MAP', TEST_SIZES, clear=True)
diff --git a/openedx/core/djangoapps/user_api/accounts/tests/test_retirement_views.py b/openedx/core/djangoapps/user_api/accounts/tests/test_retirement_views.py
index b892eaa067e4..e93a118e649f 100644
--- a/openedx/core/djangoapps/user_api/accounts/tests/test_retirement_views.py
+++ b/openedx/core/djangoapps/user_api/accounts/tests/test_retirement_views.py
@@ -7,7 +7,7 @@
from unittest import mock
import ddt
-import pytz
+from zoneinfo import ZoneInfo
from consent.models import DataSharingConsent
from django.contrib.auth.models import User # lint-amnesty, pylint: disable=imported-auth-user
from django.contrib.sites.models import Site
@@ -524,7 +524,7 @@ def setUp(self):
self.headers = build_jwt_headers(self.test_superuser)
self.url = reverse('accounts_retirement_partner_report')
self.maxDiff = None
- self.test_created_datetime = datetime.datetime(2018, 1, 1, tzinfo=pytz.UTC)
+ self.test_created_datetime = datetime.datetime(2018, 1, 1, tzinfo=ZoneInfo("UTC"))
ExternalIdType.objects.get_or_create(name=ExternalIdType.CALIPER)
def get_user_dict(self, user, enrollments):
@@ -777,7 +777,7 @@ def test_date_filter(self):
# retirements = [2018-04-10..., 2018-04-09..., 2018-04-08...]
pending_state = RetirementState.objects.get(state_name='PENDING')
for days_back in range(1, days_back_to_test, -1):
- create_datetime = datetime.datetime.now(pytz.UTC) - datetime.timedelta(days=days_back)
+ create_datetime = datetime.datetime.now(ZoneInfo("UTC")) - datetime.timedelta(days=days_back)
retirements.append(create_retirement_status(
UserFactory(),
state=pending_state,
@@ -935,12 +935,12 @@ def test_date_filter(self):
# Create retirements for the last 10 days
for days_back in range(0, 10): # lint-amnesty, pylint: disable=simplifiable-range
- create_datetime = datetime.datetime.now(pytz.UTC) - datetime.timedelta(days=days_back)
+ create_datetime = datetime.datetime.now(ZoneInfo("UTC")) - datetime.timedelta(days=days_back)
ret = create_retirement_status(UserFactory(), state=complete_state, create_datetime=create_datetime)
retirements.append(self._retirement_to_dict(ret))
# Go back in time adding days to the query, assert the correct retirements are present
- end_date = datetime.datetime.now(pytz.UTC)
+ end_date = datetime.datetime.now(ZoneInfo("UTC"))
for days_back in range(1, 11):
retirement_dicts = retirements[:days_back]
start_date = end_date - datetime.timedelta(days=days_back - 1)
diff --git a/openedx/core/djangoapps/user_api/accounts/tests/test_views.py b/openedx/core/djangoapps/user_api/accounts/tests/test_views.py
index 46d6b5232b43..45b1773e4f02 100644
--- a/openedx/core/djangoapps/user_api/accounts/tests/test_views.py
+++ b/openedx/core/djangoapps/user_api/accounts/tests/test_views.py
@@ -10,7 +10,7 @@
from urllib.parse import quote
import ddt
-import pytz
+from zoneinfo import ZoneInfo
from django.conf import settings
from django.core.files.storage import FileSystemStorage
from django.test.testcases import TransactionTestCase
@@ -44,7 +44,7 @@
from .. import ALL_USERS_VISIBILITY, CUSTOM_VISIBILITY, PRIVATE_VISIBILITY
-TEST_PROFILE_IMAGE_UPLOADED_AT = datetime.datetime(2002, 1, 9, 15, 43, 1, tzinfo=pytz.UTC)
+TEST_PROFILE_IMAGE_UPLOADED_AT = datetime.datetime(2002, 1, 9, 15, 43, 1, tzinfo=ZoneInfo("UTC"))
# this is used in one test to check the behavior of profile image url
# generation with a relative url in the config.
@@ -304,7 +304,7 @@ def test_cancel_retirement_not_pending(self):
current_state=retirement_state,
last_state=retirement_state,
original_email=self.user.email,
- created=datetime.datetime.now(pytz.UTC)
+ created=datetime.datetime.now(ZoneInfo("UTC"))
)
url = reverse("cancel_account_retirement")
response = client.post(url, data={'retirement_id': user_retirement_status.id})
@@ -329,7 +329,7 @@ def test_cancel_retirement_successful(self):
current_state=retirement_state,
last_state=retirement_state,
original_email=self.user.email,
- created=datetime.datetime.now(pytz.UTC)
+ created=datetime.datetime.now(ZoneInfo("UTC"))
)
user_retirement_status.user.set_unusable_password()
assert UserRetirementStatus.objects.count() == 1
@@ -585,8 +585,8 @@ def test_get_account_by_user_id_non_integer(self, non_integer_id):
@mock.patch('openedx.core.djangoapps.user_api.accounts.views.is_email_retired')
@ddt.data(
- (datetime.datetime.now(pytz.UTC), True),
- (datetime.datetime.now(pytz.UTC) - datetime.timedelta(days=15), False)
+ (datetime.datetime.now(ZoneInfo("UTC")), True),
+ (datetime.datetime.now(ZoneInfo("UTC")) - datetime.timedelta(days=15), False)
)
@ddt.unpack
def test_search_emails_retired_before_cooloff_period(self, created_date, can_cancel, mock_is_email_retired):
diff --git a/openedx/core/djangoapps/user_api/accounts/views.py b/openedx/core/djangoapps/user_api/accounts/views.py
index c0809811aa72..b44f47cc8304 100644
--- a/openedx/core/djangoapps/user_api/accounts/views.py
+++ b/openedx/core/djangoapps/user_api/accounts/views.py
@@ -9,7 +9,7 @@
import logging
from functools import wraps
-import pytz
+from zoneinfo import ZoneInfo
from consent.models import DataSharingConsent
from django.apps import apps
from django.conf import settings
@@ -206,11 +206,11 @@ def list(self, request):
if is_email_retired(user_email):
can_cancel_retirement = True
retirement_id = None
- earliest_datetime = datetime.datetime.now(pytz.UTC) - datetime.timedelta(days=settings.COOL_OFF_DAYS)
+ earliest_datetime = datetime.datetime.now(ZoneInfo("UTC")) - datetime.timedelta(days=settings.COOL_OFF_DAYS)
try:
retirement_status = UserRetirementStatus.objects.get(
created__gt=earliest_datetime,
- created__lt=datetime.datetime.now(pytz.UTC),
+ created__lt=datetime.datetime.now(ZoneInfo("UTC")),
original_email=user_email,
)
retirement_id = retirement_status.id
@@ -899,7 +899,7 @@ def retirement_queue(self, request):
status=status.HTTP_400_BAD_REQUEST,
)
- earliest_datetime = datetime.datetime.now(pytz.UTC) - datetime.timedelta(days=cool_off_days)
+ earliest_datetime = datetime.datetime.now(ZoneInfo("UTC")) - datetime.timedelta(days=cool_off_days)
retirements = (
UserRetirementStatus.objects.select_related("user", "current_state", "last_state")
@@ -929,9 +929,12 @@ def retirements_by_status_and_date(self, request):
so to get one day you would set both dates to that day.
"""
try:
- start_date = datetime.datetime.strptime(request.GET["start_date"], "%Y-%m-%d").replace(tzinfo=pytz.UTC)
- end_date = datetime.datetime.strptime(request.GET["end_date"], "%Y-%m-%d").replace(tzinfo=pytz.UTC)
- now = datetime.datetime.now(pytz.UTC)
+ start_date = (
+ datetime.datetime.strptime(request.GET["start_date"], "%Y-%m-%d")
+ .replace(tzinfo=ZoneInfo("UTC"))
+ )
+ end_date = datetime.datetime.strptime(request.GET["end_date"], "%Y-%m-%d").replace(tzinfo=ZoneInfo("UTC"))
+ now = datetime.datetime.now(ZoneInfo("UTC"))
if start_date > now or end_date > now or start_date > end_date:
raise RetirementStateError("Dates must be today or earlier, and start must be earlier than end.")
diff --git a/openedx/core/djangoapps/user_api/management/commands/create_user_gdpr_testing.py b/openedx/core/djangoapps/user_api/management/commands/create_user_gdpr_testing.py
index e744baa6c7a3..c1f16c9c3fa1 100644
--- a/openedx/core/djangoapps/user_api/management/commands/create_user_gdpr_testing.py
+++ b/openedx/core/djangoapps/user_api/management/commands/create_user_gdpr_testing.py
@@ -20,7 +20,7 @@
PendingEnterpriseCustomerUser
)
from opaque_keys.edx.keys import CourseKey
-from pytz import UTC
+from zoneinfo import ZoneInfo
from common.djangoapps.entitlements.models import CourseEntitlement, CourseEntitlementSupportDetail
from lms.djangoapps.verify_student.models import SoftwareSecurePhotoVerification
@@ -88,7 +88,7 @@ def handle(self, *args, **options):
user.save()
# UserProfile
- profile_image_uploaded_date = datetime(2018, 5, 3, tzinfo=UTC)
+ profile_image_uploaded_date = datetime(2018, 5, 3, tzinfo=ZoneInfo("UTC"))
user_profile, __ = UserProfile.objects.get_or_create(
user=user
)
diff --git a/openedx/core/djangoapps/user_authn/tests/utils.py b/openedx/core/djangoapps/user_authn/tests/utils.py
index 09ca85145f35..31b4be7c400a 100644
--- a/openedx/core/djangoapps/user_authn/tests/utils.py
+++ b/openedx/core/djangoapps/user_authn/tests/utils.py
@@ -6,7 +6,7 @@
from unittest.mock import patch
import ddt
-import pytz
+from zoneinfo import ZoneInfo
from django.conf import settings
from oauth2_provider import models as dot_models
from rest_framework import status
@@ -42,7 +42,7 @@ def utcnow():
"""
Helper function to return the current UTC time localized to the UTC timezone.
"""
- return datetime.now(pytz.UTC)
+ return datetime.now(ZoneInfo("UTC"))
@ddt.ddt
diff --git a/openedx/core/djangoapps/user_authn/views/register.py b/openedx/core/djangoapps/user_authn/views/register.py
index aff210e7b26d..2fc0818a3ab9 100644
--- a/openedx/core/djangoapps/user_authn/views/register.py
+++ b/openedx/core/djangoapps/user_authn/views/register.py
@@ -26,7 +26,7 @@
from openedx_events.learning.data import UserData, UserPersonalData
from openedx_events.learning.signals import STUDENT_REGISTRATION_COMPLETED
from openedx_filters.learning.filters import StudentRegistrationRequested
-from pytz import UTC
+from zoneinfo import ZoneInfo
from django_ratelimit.decorators import ratelimit
from requests import HTTPError
from rest_framework.response import Response
@@ -371,7 +371,7 @@ def _track_user_registration(user, profile, params, third_party_provider, regist
'name': profile.name,
# Mailchimp requires the age & yearOfBirth to be integers, we send a sane integer default if falsey.
'age': profile.age or -1,
- 'yearOfBirth': profile.year_of_birth or datetime.datetime.now(UTC).year,
+ 'yearOfBirth': profile.year_of_birth or datetime.datetime.now(ZoneInfo("UTC")).year,
'education': profile.level_of_education_display,
'address': profile.mailing_address,
'gender': profile.gender_display,
@@ -530,7 +530,9 @@ def _record_utm_registration_attribution(request, user):
# We divide by 1000 here because the javascript timestamp generated is in milliseconds not seconds.
# PYTHON: time.time() => 1475590280.823698
# JS: new Date().getTime() => 1475590280823
- created_at_datetime = datetime.datetime.fromtimestamp(int(created_at_unixtime) / float(1000), tz=UTC)
+ created_at_datetime = datetime.datetime.fromtimestamp(
+ int(created_at_unixtime) / float(1000), tz=ZoneInfo("UTC")
+ )
UserAttribute.set_user_attribute(
user,
REGISTRATION_UTM_CREATED_AT,
diff --git a/openedx/core/djangoapps/user_authn/views/tests/test_auto_auth.py b/openedx/core/djangoapps/user_authn/views/tests/test_auto_auth.py
index 0346aee25ae6..b68774903092 100644
--- a/openedx/core/djangoapps/user_authn/views/tests/test_auto_auth.py
+++ b/openedx/core/djangoapps/user_authn/views/tests/test_auto_auth.py
@@ -21,6 +21,7 @@
)
from openedx.core.djangoapps.django_comment_common.utils import seed_permissions_roles
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase # lint-amnesty, pylint: disable=wrong-import-order
+from common.test.utils import assert_dict_contains_subset
class AutoAuthTestCase(UrlResetMixin, TestCase):
@@ -182,12 +183,13 @@ def test_json_response(self):
for key in ['created_status', 'username', 'email', 'password', 'user_id', 'anonymous_id']:
assert key in response_data
user = User.objects.get(username=response_data['username'])
- self.assertDictContainsSubset(
+ assert_dict_contains_subset(
+ self,
{
'created_status': 'Logged in',
'anonymous_id': anonymous_id_for_user(user, None),
},
- response_data
+ response_data,
)
@ddt.data(*COURSE_IDS_DDT)
diff --git a/openedx/core/djangoapps/user_authn/views/tests/test_events.py b/openedx/core/djangoapps/user_authn/views/tests/test_events.py
index acec4a935d60..7efd4e4cf5c0 100644
--- a/openedx/core/djangoapps/user_authn/views/tests/test_events.py
+++ b/openedx/core/djangoapps/user_authn/views/tests/test_events.py
@@ -18,6 +18,7 @@
from common.djangoapps.student.tests.factories import UserFactory, UserProfileFactory
from openedx.core.djangoapps.user_api.tests.test_views import UserAPITestCase
from openedx.core.djangolib.testing.utils import skip_unless_lms
+from common.test.utils import assert_dict_contains_subset
@skip_unless_lms
@@ -83,7 +84,8 @@ def test_send_registration_event(self):
user = User.objects.get(username=self.user_info.get("username"))
self.assertTrue(self.receiver_called)
- self.assertDictContainsSubset(
+ assert_dict_contains_subset(
+ self,
{
"signal": STUDENT_REGISTRATION_COMPLETED,
"sender": None,
@@ -97,7 +99,7 @@ def test_send_registration_event(self):
is_active=user.is_active,
),
},
- event_receiver.call_args.kwargs
+ event_receiver.call_args.kwargs,
)
@@ -165,7 +167,8 @@ def test_send_login_event(self):
user = User.objects.get(username=self.user.username)
self.assertTrue(self.receiver_called)
- self.assertDictContainsSubset(
+ assert_dict_contains_subset(
+ self,
{
"signal": SESSION_LOGIN_COMPLETED,
"sender": None,
@@ -179,5 +182,5 @@ def test_send_login_event(self):
is_active=user.is_active,
),
},
- event_receiver.call_args.kwargs
+ event_receiver.call_args.kwargs,
)
diff --git a/openedx/core/djangoapps/user_authn/views/tests/test_login.py b/openedx/core/djangoapps/user_authn/views/tests/test_login.py
index b00702ee25da..c8bfa082900b 100644
--- a/openedx/core/djangoapps/user_authn/views/tests/test_login.py
+++ b/openedx/core/djangoapps/user_authn/views/tests/test_login.py
@@ -44,6 +44,7 @@
from openedx.features.enterprise_support.tests.factories import EnterpriseCustomerUserFactory
from common.djangoapps.student.models import LoginFailures
from common.djangoapps.util.password_policy_validators import DEFAULT_MAX_PASSWORD_LENGTH
+from common.test.utils import assert_dict_contains_subset
@ddt.ddt
@@ -544,7 +545,7 @@ def test_unicode_mktg_cookie_names(self):
expected = {
'target': '/',
}
- self.assertDictContainsSubset(expected, response.context_data)
+ assert_dict_contains_subset(self, expected, response.context_data)
@patch.dict("django.conf.settings.FEATURES", {'SQUELCH_PII_IN_LOGS': True})
def test_logout_logging_no_pii(self):
diff --git a/openedx/core/djangoapps/user_authn/views/tests/test_logout.py b/openedx/core/djangoapps/user_authn/views/tests/test_logout.py
index 77c21c86e1b1..a81d11c42cf4 100644
--- a/openedx/core/djangoapps/user_authn/views/tests/test_logout.py
+++ b/openedx/core/djangoapps/user_authn/views/tests/test_logout.py
@@ -14,6 +14,7 @@
from openedx.core.djangoapps.oauth_dispatch.tests.factories import ApplicationFactory
from openedx.core.djangolib.testing.utils import skip_unless_lms
from common.djangoapps.student.tests.factories import UserFactory
+from common.test.utils import assert_dict_contains_subset
@skip_unless_lms
@@ -76,14 +77,14 @@ def test_logout_redirect_success(self, redirect_url, host):
expected = {
'target': urllib.parse.unquote(redirect_url),
}
- self.assertDictContainsSubset(expected, response.context_data)
+ assert_dict_contains_subset(self, expected, response.context_data)
def test_no_redirect_supplied(self):
response = self.client.get(reverse('logout'), HTTP_HOST='testserver')
expected = {
'target': '/',
}
- self.assertDictContainsSubset(expected, response.context_data)
+ assert_dict_contains_subset(self, expected, response.context_data)
@ddt.data(
('https://www.amazon.org', 'edx.org'),
@@ -100,7 +101,7 @@ def test_logout_redirect_failure(self, redirect_url, host):
expected = {
'target': '/',
}
- self.assertDictContainsSubset(expected, response.context_data)
+ assert_dict_contains_subset(self, expected, response.context_data)
def test_client_logout(self):
""" Verify the context includes a list of the logout URIs of the authenticated OpenID Connect clients.
@@ -113,7 +114,7 @@ def test_client_logout(self):
'logout_uris': [],
'target': '/',
}
- self.assertDictContainsSubset(expected, response.context_data)
+ assert_dict_contains_subset(self, expected, response.context_data)
@mock.patch(
'django.conf.settings.IDA_LOGOUT_URI_LIST',
@@ -138,7 +139,7 @@ def test_client_logout_with_dot_idas(self):
'logout_uris': expected_logout_uris,
'target': '/',
}
- self.assertDictContainsSubset(expected, response.context_data)
+ assert_dict_contains_subset(self, expected, response.context_data)
@mock.patch(
'django.conf.settings.IDA_LOGOUT_URI_LIST',
@@ -161,7 +162,7 @@ def test_client_logout_with_dot_idas_and_no_oidc_idas(self):
'logout_uris': expected_logout_uris,
'target': '/',
}
- self.assertDictContainsSubset(expected, response.context_data)
+ assert_dict_contains_subset(self, expected, response.context_data)
def test_filter_referring_service(self):
""" Verify that, if the user is directed to the logout page from a service, that service's logout URL
@@ -174,7 +175,7 @@ def test_filter_referring_service(self):
'target': '/',
'show_tpa_logout_link': False,
}
- self.assertDictContainsSubset(expected, response.context_data)
+ assert_dict_contains_subset(self, expected, response.context_data)
def test_learner_portal_logout_having_idp_logout_url(self):
"""
@@ -194,7 +195,7 @@ def test_learner_portal_logout_having_idp_logout_url(self):
'tpa_logout_url': idp_logout_url,
'show_tpa_logout_link': True,
}
- self.assertDictContainsSubset(expected, response.context_data)
+ assert_dict_contains_subset(self, expected, response.context_data)
@mock.patch('django.conf.settings.TPA_AUTOMATIC_LOGOUT_ENABLED', True)
def test_automatic_tpa_logout_url_redirect(self):
@@ -214,7 +215,7 @@ def test_automatic_tpa_logout_url_redirect(self):
expected = {
'target': idp_logout_url,
}
- self.assertDictContainsSubset(expected, response.context_data)
+ assert_dict_contains_subset(self, expected, response.context_data)
@mock.patch('django.conf.settings.TPA_AUTOMATIC_LOGOUT_ENABLED', True)
def test_no_automatic_tpa_logout_without_logout_url(self):
@@ -241,4 +242,4 @@ def test_logout_redirect_failure_with_xss_vulnerability(self, redirect_url, host
expected = {
'target': nh3.clean(urllib.parse.unquote(redirect_url)),
}
- self.assertDictContainsSubset(expected, response.context_data)
+ assert_dict_contains_subset(self, expected, response.context_data)
diff --git a/openedx/core/djangoapps/user_authn/views/tests/test_password.py b/openedx/core/djangoapps/user_authn/views/tests/test_password.py
index a403298a6e78..57a988c0b354 100644
--- a/openedx/core/djangoapps/user_authn/views/tests/test_password.py
+++ b/openedx/core/djangoapps/user_authn/views/tests/test_password.py
@@ -19,7 +19,7 @@
from freezegun import freeze_time
from oauth2_provider.models import AccessToken as dot_access_token
from oauth2_provider.models import RefreshToken as dot_refresh_token
-from pytz import UTC
+from zoneinfo import ZoneInfo
from testfixtures import LogCapture
from openedx.core.djangoapps.oauth_dispatch.tests import factories as dot_factories
@@ -319,7 +319,7 @@ def test_password_change_rate_limited(self):
# now reset the time to 1 min from now in future and change the email and
# verify that it will allow another request from same IP
- reset_time = datetime.now(UTC) + timedelta(seconds=61)
+ reset_time = datetime.now(ZoneInfo("UTC")) + timedelta(seconds=61)
with freeze_time(reset_time):
response = self._change_password(email=self.OLD_EMAIL)
assert response.status_code == 200
diff --git a/openedx/core/djangoapps/user_authn/views/tests/test_register.py b/openedx/core/djangoapps/user_authn/views/tests/test_register.py
index 54d42efa55c0..e79b8ae6daa2 100644
--- a/openedx/core/djangoapps/user_authn/views/tests/test_register.py
+++ b/openedx/core/djangoapps/user_authn/views/tests/test_register.py
@@ -16,7 +16,7 @@
from django.test.utils import override_settings
from django.urls import reverse
from openedx_events.tests.utils import OpenEdxEventsTestMixin
-from pytz import UTC
+from zoneinfo import ZoneInfo
from social_django.models import Partial, UserSocialAuth
from testfixtures import LogCapture
@@ -949,7 +949,7 @@ def test_register_form_gender_translations(self, fake_gettext):
)
def test_register_form_year_of_birth(self):
- this_year = datetime.now(UTC).year
+ this_year = datetime.now(ZoneInfo("UTC")).year
year_options = (
[
{
diff --git a/openedx/core/djangoapps/user_authn/views/tests/test_reset_password.py b/openedx/core/djangoapps/user_authn/views/tests/test_reset_password.py
index b89b458ed1ae..aed040a8a21c 100644
--- a/openedx/core/djangoapps/user_authn/views/tests/test_reset_password.py
+++ b/openedx/core/djangoapps/user_authn/views/tests/test_reset_password.py
@@ -24,7 +24,7 @@
from django.utils.http import int_to_base36
from freezegun import freeze_time
from oauth2_provider import models as dot_models
-from pytz import UTC
+from zoneinfo import ZoneInfo
from openedx.core.djangoapps.oauth_dispatch.tests import factories as dot_factories
from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers
@@ -267,7 +267,7 @@ def test_ratelimited_from_different_ips_with_same_email(self):
self.request_password_reset(200)
# now reset the time to 1 min from now in future and change the email and
# verify that it will allow another request from same IP
- reset_time = datetime.now(UTC) + timedelta(seconds=61)
+ reset_time = datetime.now(ZoneInfo("UTC")) + timedelta(seconds=61)
with freeze_time(reset_time):
for status in [200, 403]:
self.request_password_reset(status)
diff --git a/openedx/core/djangoapps/util/testing.py b/openedx/core/djangoapps/util/testing.py
index 040a2b5af180..4686bdc6e75d 100644
--- a/openedx/core/djangoapps/util/testing.py
+++ b/openedx/core/djangoapps/util/testing.py
@@ -3,7 +3,7 @@
from datetime import datetime
-from pytz import UTC
+from zoneinfo import ZoneInfo
from openedx.core.djangoapps.course_groups.models import CourseUserGroupPartitionGroup
from openedx.core.djangoapps.course_groups.tests.helpers import CohortFactory
@@ -32,7 +32,7 @@ def setUp(self):
# This test needs to use a course that has already started --
# discussion topics only show up if the course has already started,
# and the default start date for courses is Jan 1, 2030.
- start=datetime(2012, 2, 3, tzinfo=UTC),
+ start=datetime(2012, 2, 3, tzinfo=ZoneInfo("UTC")),
user_partitions=[
UserPartition(
0,
diff --git a/openedx/core/djangoapps/verified_track_content/tests/test_partition_scheme.py b/openedx/core/djangoapps/verified_track_content/tests/test_partition_scheme.py
index a2667bfa1c0b..f7de6042ef19 100644
--- a/openedx/core/djangoapps/verified_track_content/tests/test_partition_scheme.py
+++ b/openedx/core/djangoapps/verified_track_content/tests/test_partition_scheme.py
@@ -5,7 +5,7 @@
from datetime import datetime, timedelta
-import pytz
+from zoneinfo import ZoneInfo
import pytest
from common.djangoapps.course_modes.models import CourseMode
from common.djangoapps.student.models import CourseEnrollment
@@ -38,7 +38,7 @@ def test_multiple_groups(self):
# Note that the verified mode is expired-- this is intentional.
create_mode(
self.course, CourseMode.VERIFIED, "Verified Enrollment Track", min_price=1,
- expiration_datetime=datetime.now(pytz.UTC) + timedelta(days=-1)
+ expiration_datetime=datetime.now(ZoneInfo("UTC")) + timedelta(days=-1)
)
# Note that the credit mode is not selectable-- this is intentional so we
# can test that it is filtered out.
@@ -128,7 +128,7 @@ def test_enrolled_in_verified(self):
def test_enrolled_in_expired(self):
create_mode(
self.course, CourseMode.VERIFIED, "Verified Enrollment Track",
- min_price=1, expiration_datetime=datetime.now(pytz.UTC) + timedelta(days=-1)
+ min_price=1, expiration_datetime=datetime.now(ZoneInfo("UTC")) + timedelta(days=-1)
)
CourseEnrollment.enroll(self.student, self.course.id, mode=CourseMode.VERIFIED)
assert 'Verified Enrollment Track' == self._get_user_group().name
@@ -153,7 +153,7 @@ def test_credit_after_upgrade_deadline(self):
# the upgrade deadline has passed (see EDUCATOR-1511 for why this matters).
create_mode(
self.course, CourseMode.VERIFIED, "Verified Enrollment Track", min_price=1,
- expiration_datetime=datetime.now(pytz.UTC) + timedelta(days=-1)
+ expiration_datetime=datetime.now(ZoneInfo("UTC")) + timedelta(days=-1)
)
assert 'Verified Enrollment Track' == self._get_user_group().name
diff --git a/xmodule/video_block/sharing_sites.py b/openedx/core/djangoapps/video_config/sharing_sites.py
similarity index 100%
rename from xmodule/video_block/sharing_sites.py
rename to openedx/core/djangoapps/video_config/sharing_sites.py
diff --git a/openedx/core/djangoapps/xblock/runtime/shims.py b/openedx/core/djangoapps/xblock/runtime/shims.py
index c306b82bdc8b..578db22a5250 100644
--- a/openedx/core/djangoapps/xblock/runtime/shims.py
+++ b/openedx/core/djangoapps/xblock/runtime/shims.py
@@ -157,7 +157,7 @@ def process_xml(self, xml):
"""
# We can't parse XML in a vacuum - we need to know the parent block and/or the
# OLX file that holds this XML in order to generate useful definition keys etc.
- # The older ImportSystem runtime could do this because it stored the course_id
+ # The older XMLImportingModuleStoreRuntime runtime could do this because it stored the course_id
# as part of the runtime.
raise NotImplementedError("This newer runtime does not support process_xml()")
@@ -244,7 +244,7 @@ def xqueue(self):
def get_field_provenance(self, xblock, field):
"""
- A Studio-specific method that was implemented on DescriptorSystem.
+ A Studio-specific method that was implemented on ModuleStoreRuntime.
Used by the problem block.
For the given xblock, return a dict for the field's current state:
diff --git a/openedx/core/lib/tests/test_xblock_utils.py b/openedx/core/lib/tests/test_xblock_utils.py
index 8a10e0220dfb..7b817f3fe6fd 100644
--- a/openedx/core/lib/tests/test_xblock_utils.py
+++ b/openedx/core/lib/tests/test_xblock_utils.py
@@ -177,7 +177,10 @@ def test_is_not_xblock_aside(self):
"""test if xblock is not aside"""
assert is_xblock_aside(self.block.scope_ids.usage_id) is False
- @patch('xmodule.modulestore.xml.ImportSystem.applicable_aside_types', lambda self, block: ['test_aside'])
+ @patch(
+ 'xmodule.modulestore.xml.XMLImportingModuleStoreRuntime.applicable_aside_types',
+ lambda self, block: ['test_aside'],
+ )
@XBlockAside.register_temp_plugin(AsideTestType, 'test_aside')
def test_get_aside(self):
"""test get aside success"""
diff --git a/openedx/core/lib/xblock_utils/__init__.py b/openedx/core/lib/xblock_utils/__init__.py
index a8b76541b6e5..d398a159f998 100644
--- a/openedx/core/lib/xblock_utils/__init__.py
+++ b/openedx/core/lib/xblock_utils/__init__.py
@@ -19,7 +19,7 @@
from edx_django_utils.plugins import pluggable_override
from lxml import etree, html
from opaque_keys.edx.asides import AsideUsageKeyV1, AsideUsageKeyV2
-from pytz import UTC
+from zoneinfo import ZoneInfo
from web_fragments.fragment import Fragment
from xblock.core import XBlock
from xblock.exceptions import InvalidScopeError
@@ -310,7 +310,7 @@ def add_staff_markup(user, disable_staff_debug_info, block, view, frag, context)
# Useful to indicate to staff if problem has been released or not.
# TODO (ichuang): use _has_access_block.can_load in lms.courseware.access,
# instead of now>mstart comparison here.
- now = datetime.datetime.now(UTC)
+ now = datetime.datetime.now(ZoneInfo("UTC"))
is_released = "unknown"
mstart = block.start
diff --git a/openedx/core/release.py b/openedx/core/release.py
index ce30df8cc543..dda3add782a0 100644
--- a/openedx/core/release.py
+++ b/openedx/core/release.py
@@ -8,7 +8,7 @@
# The release line: an Open edX release name ("ficus"), or "master".
# This should always be "master" on the master branch, and will be changed
# manually when we start release-line branches, like open-release/ficus.master.
-RELEASE_LINE = "master"
+RELEASE_LINE = "ulmo"
def doc_version():
diff --git a/openedx/envs/common.py b/openedx/envs/common.py
index ada6fd3f0be2..c00a89cf4f69 100644
--- a/openedx/envs/common.py
+++ b/openedx/envs/common.py
@@ -37,6 +37,8 @@ def center_with_hashes(text: str, width: int = 76):
get_theme_base_dirs_from_settings
)
+# We have legacy components that reference these constants via the settings module.
+# New code should import them directly from `openedx.core.constants` instead.
from openedx.core.constants import ( # pylint: disable=unused-import
ASSET_KEY_PATTERN,
COURSE_KEY_REGEX,
diff --git a/openedx/features/announcements/__init__.py b/openedx/features/announcements/__init__.py
deleted file mode 100644
index e69de29bb2d1..000000000000
diff --git a/openedx/features/announcements/apps.py b/openedx/features/announcements/apps.py
deleted file mode 100644
index 4bf964cae51b..000000000000
--- a/openedx/features/announcements/apps.py
+++ /dev/null
@@ -1,32 +0,0 @@
-"""
-Announcements Application Configuration
-"""
-
-
-from django.apps import AppConfig
-from edx_django_utils.plugins import PluginURLs, PluginSettings
-
-from openedx.core.djangoapps.plugins.constants import ProjectType, SettingsType
-
-
-class AnnouncementsConfig(AppConfig):
- """
- Application Configuration for Announcements
- """
- name = 'openedx.features.announcements'
-
- plugin_app = {
- PluginURLs.CONFIG: {
- ProjectType.LMS: {
- PluginURLs.NAMESPACE: 'announcements',
- PluginURLs.REGEX: '^announcements/',
- PluginURLs.RELATIVE_PATH: 'urls',
- }
- },
- PluginSettings.CONFIG: {
- ProjectType.LMS: {
- SettingsType.COMMON: {PluginSettings.RELATIVE_PATH: 'settings.common'},
- SettingsType.TEST: {PluginSettings.RELATIVE_PATH: 'settings.test'},
- }
- }
- }
diff --git a/openedx/features/announcements/forms.py b/openedx/features/announcements/forms.py
deleted file mode 100644
index 879101ca37d0..000000000000
--- a/openedx/features/announcements/forms.py
+++ /dev/null
@@ -1,20 +0,0 @@
-"""
-Forms for the Announcement Editor
-"""
-
-
-from django import forms
-
-from .models import Announcement
-
-
-class AnnouncementForm(forms.ModelForm):
- """
- Form for editing Announcements
- """
- content = forms.CharField(widget=forms.Textarea, label='', required=False)
- active = forms.BooleanField(initial=True, required=False)
-
- class Meta:
- model = Announcement
- fields = ['content', 'active']
diff --git a/openedx/features/announcements/migrations/0001_initial.py b/openedx/features/announcements/migrations/0001_initial.py
deleted file mode 100644
index c959b634905e..000000000000
--- a/openedx/features/announcements/migrations/0001_initial.py
+++ /dev/null
@@ -1,18 +0,0 @@
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ]
-
- operations = [
- migrations.CreateModel(
- name='Announcement',
- fields=[
- ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
- ('content', models.CharField(default='lorem ipsum', max_length=1000)),
- ('active', models.BooleanField(default=True)),
- ],
- ),
- ]
diff --git a/openedx/features/announcements/migrations/__init__.py b/openedx/features/announcements/migrations/__init__.py
deleted file mode 100644
index e69de29bb2d1..000000000000
diff --git a/openedx/features/announcements/models.py b/openedx/features/announcements/models.py
deleted file mode 100644
index f58f61165db6..000000000000
--- a/openedx/features/announcements/models.py
+++ /dev/null
@@ -1,22 +0,0 @@
-"""
-Models for Announcements
-"""
-
-
-from django.db import models
-
-
-class Announcement(models.Model):
- """
- Site-wide announcements to be displayed on the dashboard
-
- .. no_pii:
- """
- class Meta:
- app_label = 'announcements'
-
- content = models.CharField(max_length=1000, null=False, default="lorem ipsum")
- active = models.BooleanField(default=True)
-
- def __str__(self):
- return self.content
diff --git a/openedx/features/announcements/settings/__init__.py b/openedx/features/announcements/settings/__init__.py
deleted file mode 100644
index e69de29bb2d1..000000000000
diff --git a/openedx/features/announcements/settings/common.py b/openedx/features/announcements/settings/common.py
deleted file mode 100644
index 4de3740d2d27..000000000000
--- a/openedx/features/announcements/settings/common.py
+++ /dev/null
@@ -1,21 +0,0 @@
-"""Common settings for Announcements"""
-
-
-def plugin_settings(settings):
- """
- Common settings for Announcements
- .. toggle_name: FEATURES['ENABLE_ANNOUNCEMENTS']
- .. toggle_implementation: SettingDictToggle
- .. toggle_default: False
- .. toggle_description: This feature can be enabled to show system wide announcements
- on the sidebar of the learner dashboard. Announcements can be created by Global Staff
- users on maintenance dashboard of studio. Maintenance dashboard can accessed at
- https://{studio.domain}/maintenance
- .. toggle_warning: TinyMCE is needed to show an editor in the studio.
- .. toggle_use_cases: open_edx
- .. toggle_creation_date: 2017-11-08
- .. toggle_tickets: https://github.com/openedx/edx-platform/pull/16496
- """
- settings.ENABLE_ANNOUNCEMENTS = False
- # Configure number of announcements to show per page
- settings.ANNOUNCEMENTS_PER_PAGE = 5
diff --git a/openedx/features/announcements/settings/test.py b/openedx/features/announcements/settings/test.py
deleted file mode 100644
index 8c8406d23f4b..000000000000
--- a/openedx/features/announcements/settings/test.py
+++ /dev/null
@@ -1,8 +0,0 @@
-"""Test settings for Announcements"""
-
-
-def plugin_settings(settings):
- """
- Test settings for Announcements
- """
- settings.ENABLE_ANNOUNCEMENTS = True
diff --git a/openedx/features/announcements/static/announcements/jsx/Announcements.jsx b/openedx/features/announcements/static/announcements/jsx/Announcements.jsx
deleted file mode 100644
index 9d370883352c..000000000000
--- a/openedx/features/announcements/static/announcements/jsx/Announcements.jsx
+++ /dev/null
@@ -1,141 +0,0 @@
-// eslint-disable-next-line max-classes-per-file
-import React from 'react';
-import ReactDOM from 'react-dom';
-import PropTypes from 'prop-types';
-import {Button} from '@edx/paragon';
-import $ from 'jquery';
-
-class AnnouncementSkipLink extends React.Component {
- constructor(props) {
- super(props);
- this.state = {
- count: 0
- };
- $.get('/announcements/page/1')
- .then(data => {
- this.setState({
- count: data.count
- });
- });
- }
-
- render() {
- return (
{'Skip to list of ' + this.state.count + ' announcements'}
);
- }
-}
-
-// eslint-disable-next-line react/prefer-stateless-function
-class Announcement extends React.Component {
- render() {
- return (
-
- );
- }
-}
-
-Announcement.propTypes = {
- content: PropTypes.string.isRequired,
-};
-
-class AnnouncementList extends React.Component {
- constructor(props) {
- super(props);
- this.state = {
- page: 1,
- announcements: [],
- // eslint-disable-next-line react/no-unused-state
- num_pages: 0,
- has_prev: false,
- has_next: false,
- start_index: 0,
- end_index: 0,
- };
- }
-
- retrievePage(page) {
- $.get('/announcements/page/' + page)
- .then(data => {
- this.setState({
- announcements: data.announcements,
- has_next: data.next,
- has_prev: data.prev,
- // eslint-disable-next-line react/no-unused-state
- num_pages: data.num_pages,
- count: data.count,
- start_index: data.start_index,
- end_index: data.end_index,
- page: page
- });
- });
- }
-
- renderPrevPage() {
- this.retrievePage(this.state.page - 1);
- }
-
- renderNextPage() {
- this.retrievePage(this.state.page + 1);
- }
-
- // eslint-disable-next-line react/no-deprecated, react/sort-comp
- componentWillMount() {
- this.retrievePage(this.state.page);
- }
-
- render() {
- var children = this.state.announcements.map(
- // eslint-disable-next-line react/no-array-index-key
- (announcement, index) =>
- );
- if (this.state.has_prev) {
- var prev_button = (
-
- this.renderPrevPage()}
- label="← previous"
- />
- {this.state.start_index + ' - ' + this.state.end_index + ') of ' + this.state.count}
-
- );
- }
- if (this.state.has_next) {
- var next_button = (
-
- this.renderNextPage()}
- label="next →"
- />
- {this.state.start_index + ' - ' + this.state.end_index + ') of ' + this.state.count}
-
- );
- }
- return (
-
- {children}
- {prev_button}
- {next_button}
-
- );
- }
-}
-
-export default class AnnouncementsView {
- constructor() {
- ReactDOM.render(
-
,
- document.getElementById('announcements'),
- );
- ReactDOM.render(
-
,
- document.getElementById('announcements-skip'),
- );
- }
-}
-
-export {AnnouncementsView, AnnouncementList, AnnouncementSkipLink};
diff --git a/openedx/features/announcements/static/announcements/jsx/Announcements.test.jsx b/openedx/features/announcements/static/announcements/jsx/Announcements.test.jsx
deleted file mode 100644
index 3ec55f392889..000000000000
--- a/openedx/features/announcements/static/announcements/jsx/Announcements.test.jsx
+++ /dev/null
@@ -1,25 +0,0 @@
-import React from 'react';
-import renderer from 'react-test-renderer';
-import testAnnouncements from './test-announcements.json';
-
-import {AnnouncementSkipLink, AnnouncementList} from './Announcements';
-
-describe('Announcements component', () => {
- test('render skip link', () => {
- const component = renderer.create(
-
,
- );
- component.root.instance.setState({count: 10});
- const tree = component.toJSON();
- expect(tree).toMatchSnapshot();
- });
-
- test('render test announcements', () => {
- const component = renderer.create(
-
,
- );
- component.root.instance.setState(testAnnouncements);
- const tree = component.toJSON();
- expect(tree).toMatchSnapshot();
- });
-});
diff --git a/openedx/features/announcements/static/announcements/jsx/__snapshots__/Announcements.test.jsx.snap b/openedx/features/announcements/static/announcements/jsx/__snapshots__/Announcements.test.jsx.snap
deleted file mode 100644
index bbf9bfaaaa69..000000000000
--- a/openedx/features/announcements/static/announcements/jsx/__snapshots__/Announcements.test.jsx.snap
+++ /dev/null
@@ -1,78 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`Announcements component render skip link 1`] = `
-
- Skip to list of 10 announcements
-
-`;
-
-exports[`Announcements component render test announcements 1`] = `
-
-
-
Announcement 2",
- }
- }
- />
-
-
-
-
-
-
- next →
-
-
- 1 - 5) of 6
-
-
-
-`;
diff --git a/openedx/features/announcements/static/announcements/jsx/test-announcements.json b/openedx/features/announcements/static/announcements/jsx/test-announcements.json
deleted file mode 100644
index d23d39303020..000000000000
--- a/openedx/features/announcements/static/announcements/jsx/test-announcements.json
+++ /dev/null
@@ -1,17 +0,0 @@
-{
- "announcements": [
- {"content": "Test Announcement 1"},
- {"content": "Bold
Announcement 2 "},
- {"content": "Test Announcement 3"},
- {"content": "Test Announcement 4"},
- {"content": "Test Announcement 5"},
- {"content": "Test Announcement 6"}
- ],
- "has_next": true,
- "has_prev": false,
- "num_pages": 2,
- "count": 6,
- "start_index": 1,
- "end_index": 5,
- "page": 1
-}
diff --git a/openedx/features/announcements/tests/__init__.py b/openedx/features/announcements/tests/__init__.py
deleted file mode 100644
index e69de29bb2d1..000000000000
diff --git a/openedx/features/announcements/tests/test_announcements.py b/openedx/features/announcements/tests/test_announcements.py
deleted file mode 100644
index 10c608b4a6cd..000000000000
--- a/openedx/features/announcements/tests/test_announcements.py
+++ /dev/null
@@ -1,95 +0,0 @@
-"""
-Unit tests for the announcements feature.
-"""
-
-import json
-from unittest.mock import patch
-
-from django.conf import settings
-from django.test import TestCase
-from django.test.client import Client
-from django.urls import reverse
-
-from common.djangoapps.student.tests.factories import AdminFactory
-from openedx.core.djangolib.testing.utils import skip_unless_lms
-from openedx.features.announcements.models import Announcement
-
-TEST_ANNOUNCEMENTS = [
- ("Active Announcement", True),
- ("Inactive Announcement", False),
- ("Another Test Announcement", True),
- ("
Formatted Announcement ", True),
- ("
Other Formatted Announcement ", True),
-]
-
-
-@skip_unless_lms
-class TestGlobalAnnouncements(TestCase):
- """
- Test Announcements in LMS
- """
-
- @classmethod
- def setUpTestData(cls):
- super().setUpTestData()
- Announcement.objects.bulk_create([
- Announcement(content=content, active=active)
- for content, active in TEST_ANNOUNCEMENTS
- ])
-
- def setUp(self):
- super().setUp()
- self.client = Client()
- self.admin = AdminFactory.create(
- email='staff@edx.org',
- username='admin',
- password='pass'
- )
- self.client.login(username=self.admin.username, password='pass')
-
- @patch.dict(settings.FEATURES, {'ENABLE_ANNOUNCEMENTS': False})
- def test_feature_flag_disabled(self):
- """Ensures that the default settings effectively disables the feature"""
- response = self.client.get('/dashboard')
- self.assertNotContains(response, 'AnnouncementsView')
- self.assertNotContains(response, '
Formatted Announcement")
diff --git a/openedx/features/announcements/urls.py b/openedx/features/announcements/urls.py
deleted file mode 100644
index 0f0ad3a33960..000000000000
--- a/openedx/features/announcements/urls.py
+++ /dev/null
@@ -1,13 +0,0 @@
-"""
-Defines URLs for announcements in the LMS.
-"""
-from django.contrib.auth.decorators import login_required
-from django.urls import path
-
-from .views import AnnouncementsJSONView
-
-urlpatterns = [
- path('page/
', login_required(AnnouncementsJSONView.as_view()),
- name='page',
- ),
-]
diff --git a/openedx/features/announcements/views.py b/openedx/features/announcements/views.py
deleted file mode 100644
index b6657c29cc12..000000000000
--- a/openedx/features/announcements/views.py
+++ /dev/null
@@ -1,37 +0,0 @@
-"""
-Views to show announcements.
-"""
-
-
-from django.conf import settings
-from django.http import JsonResponse
-from django.views.generic.list import ListView
-
-from .models import Announcement
-
-
-class AnnouncementsJSONView(ListView):
- """
- View returning a page of announcements for the dashboard
- """
- model = Announcement
- object_list = Announcement.objects.filter(active=True)
- paginate_by = settings.FEATURES.get('ANNOUNCEMENTS_PER_PAGE', 5)
-
- def get(self, request, *args, **kwargs):
- """
- Return active announcements as json
- """
- context = self.get_context_data()
-
- announcements = [{"content": announcement.content} for announcement in context['object_list']]
- result = {
- "announcements": announcements,
- "next": context['page_obj'].has_next(),
- "prev": context['page_obj'].has_previous(),
- "start_index": context['page_obj'].start_index(),
- "end_index": context['page_obj'].end_index(),
- "count": context['paginator'].count,
- "num_pages": context['paginator'].num_pages,
- }
- return JsonResponse(result)
diff --git a/openedx/features/calendar_sync/ics.py b/openedx/features/calendar_sync/ics.py
index fc465443e714..c6d3a03a1ccf 100644
--- a/openedx/features/calendar_sync/ics.py
+++ b/openedx/features/calendar_sync/ics.py
@@ -2,7 +2,7 @@
from datetime import datetime, timedelta
-import pytz
+from zoneinfo import ZoneInfo
from django.conf import settings
from django.utils.translation import gettext as _
from icalendar import Calendar, Event, vCalAddress, vText
@@ -59,7 +59,7 @@ def generate_ics_files_for_user_course(course, user, user_calendar_sync_config_i
assignments = get_course_assignments(course.id, user)
platform_name = get_value('platform_name', settings.PLATFORM_NAME)
platform_email = get_value('email_from_address', settings.DEFAULT_FROM_EMAIL)
- now = datetime.now(pytz.utc)
+ now = datetime.now(ZoneInfo("UTC"))
site_config = SiteConfiguration.get_configuration_for_org(course.org)
ics_files = {}
diff --git a/openedx/features/calendar_sync/tests/test_ics.py b/openedx/features/calendar_sync/tests/test_ics.py
index 02301079285d..73327b09a964 100644
--- a/openedx/features/calendar_sync/tests/test_ics.py
+++ b/openedx/features/calendar_sync/tests/test_ics.py
@@ -3,7 +3,7 @@
from datetime import datetime, timedelta
from unittest.mock import patch
-import pytz
+from zoneinfo import ZoneInfo
from django.test import RequestFactory, TestCase
from freezegun import freeze_time
@@ -21,7 +21,7 @@ class TestIcsGeneration(TestCase):
def setUp(self):
super().setUp()
- freezer = freeze_time(datetime(2013, 10, 3, 8, 24, 55, tzinfo=pytz.utc))
+ freezer = freeze_time(datetime(2013, 10, 3, 8, 24, 55, tzinfo=ZoneInfo("UTC")))
self.addCleanup(freezer.stop)
freezer.start()
@@ -103,7 +103,7 @@ def assert_ics(self, *assignments):
def test_generate_ics_for_user_course(self):
""" Tests that a simple sample set of course assignments is generated correctly """
- now = datetime.now(pytz.utc)
+ now = datetime.now(ZoneInfo("UTC"))
day1 = now + timedelta(1)
day2 = now + timedelta(1)
diff --git a/openedx/features/content_type_gating/partitions.py b/openedx/features/content_type_gating/partitions.py
index 61851ec43bbd..a5e833f60bc5 100644
--- a/openedx/features/content_type_gating/partitions.py
+++ b/openedx/features/content_type_gating/partitions.py
@@ -10,7 +10,7 @@
import logging
import crum
-import pytz
+from zoneinfo import ZoneInfo
from django.template.loader import render_to_string
from django.utils.translation import gettext_lazy as _
from web_fragments.fragment import Fragment
@@ -88,7 +88,7 @@ def access_denied_fragment(self, block, user, user_group, allowed_groups):
return None
expiration_datetime = verified_mode.expiration_datetime
- if expiration_datetime and expiration_datetime < datetime.datetime.now(pytz.UTC):
+ if expiration_datetime and expiration_datetime < datetime.datetime.now(ZoneInfo("UTC")):
ecommerce_checkout_link = None
else:
ecommerce_checkout_link = self._get_checkout_link(user, verified_mode.sku, str(course_key))
diff --git a/openedx/features/content_type_gating/tests/test_models.py b/openedx/features/content_type_gating/tests/test_models.py
index 673ce805a750..adb9bd6737df 100644
--- a/openedx/features/content_type_gating/tests/test_models.py
+++ b/openedx/features/content_type_gating/tests/test_models.py
@@ -6,7 +6,7 @@
from datetime import datetime, timedelta # lint-amnesty, pylint: disable=wrong-import-order
import ddt
-import pytz
+from zoneinfo import ZoneInfo
from django.utils import timezone
from edx_django_utils.cache import RequestCache
from unittest.mock import Mock # lint-amnesty, pylint: disable=wrong-import-order
@@ -217,17 +217,17 @@ def test_all_current_course_configs(self):
# Point-test some of the final configurations
assert all_configs[CourseLocator('7-True', 'test_course', 'run-None')] == {
'enabled': (True, Provenance.org),
- 'enabled_as_of': (datetime(2018, 1, 1, 0, tzinfo=pytz.UTC), Provenance.run),
+ 'enabled_as_of': (datetime(2018, 1, 1, 0, tzinfo=ZoneInfo("UTC")), Provenance.run),
'studio_override_enabled': (None, Provenance.default)
}
assert all_configs[CourseLocator('7-True', 'test_course', 'run-False')] == {
'enabled': (False, Provenance.run),
- 'enabled_as_of': (datetime(2018, 1, 1, 0, tzinfo=pytz.UTC), Provenance.run),
+ 'enabled_as_of': (datetime(2018, 1, 1, 0, tzinfo=ZoneInfo("UTC")), Provenance.run),
'studio_override_enabled': (None, Provenance.default)
}
assert all_configs[CourseLocator('7-None', 'test_course', 'run-None')] == {
'enabled': (True, Provenance.site),
- 'enabled_as_of': (datetime(2018, 1, 1, 0, tzinfo=pytz.UTC), Provenance.run),
+ 'enabled_as_of': (datetime(2018, 1, 1, 0, tzinfo=ZoneInfo("UTC")), Provenance.run),
'studio_override_enabled': (None, Provenance.default)
}
diff --git a/openedx/features/course_duration_limits/tests/test_access.py b/openedx/features/course_duration_limits/tests/test_access.py
index 558afec22ab3..f948324b48b3 100644
--- a/openedx/features/course_duration_limits/tests/test_access.py
+++ b/openedx/features/course_duration_limits/tests/test_access.py
@@ -8,7 +8,7 @@
from crum import set_current_request
from django.test import RequestFactory
from django.utils import timezone
-from pytz import UTC
+from zoneinfo import ZoneInfo
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from common.djangoapps.course_modes.models import CourseMode
@@ -34,9 +34,12 @@ class TestAccess(ModuleStoreTestCase):
def setUp(self):
super().setUp() # lint-amnesty, pylint: disable=super-with-arguments
- CourseDurationLimitConfig.objects.create(enabled=True, enabled_as_of=datetime(2018, 1, 1, tzinfo=UTC))
+ CourseDurationLimitConfig.objects.create(
+ enabled=True,
+ enabled_as_of=datetime(2018, 1, 1, tzinfo=ZoneInfo("UTC"))
+ )
DynamicUpgradeDeadlineConfiguration.objects.create(enabled=True)
- self.course = CourseOverviewFactory.create(start=datetime(2018, 1, 1, tzinfo=UTC), self_paced=True)
+ self.course = CourseOverviewFactory.create(start=datetime(2018, 1, 1, tzinfo=ZoneInfo("UTC")), self_paced=True)
def assertDateInMessage(self, date, message): # lint-amnesty, pylint: disable=missing-function-docstring
# First, check that the formatted version is in there
@@ -148,7 +151,7 @@ def test_schedule_start_date_in_past(self):
course_id=enrollment.course.id,
mode_slug=CourseMode.AUDIT,
)
- Schedule.objects.update(start_date=datetime(2017, 1, 1, tzinfo=UTC))
+ Schedule.objects.update(start_date=datetime(2017, 1, 1, tzinfo=ZoneInfo("UTC")))
content_availability_date = max(enrollment.created, enrollment.course.start)
access_duration = get_user_course_duration(enrollment.user, enrollment.course)
diff --git a/openedx/features/course_duration_limits/tests/test_models.py b/openedx/features/course_duration_limits/tests/test_models.py
index 0473faefd330..f596f3534489 100644
--- a/openedx/features/course_duration_limits/tests/test_models.py
+++ b/openedx/features/course_duration_limits/tests/test_models.py
@@ -8,7 +8,7 @@
import ddt
import pytest
-import pytz
+from zoneinfo import ZoneInfo
from django.utils import timezone
from edx_django_utils.cache import RequestCache
from opaque_keys.edx.locator import CourseLocator
@@ -178,13 +178,18 @@ def test_config_overrides(self, global_setting, site_setting, org_setting, cours
def test_all_current_course_configs(self):
# Set up test objects
for global_setting in (True, False, None):
- CourseDurationLimitConfig.objects.create(enabled=global_setting, enabled_as_of=datetime(2018, 1, 1, tzinfo=pytz.UTC)) # lint-amnesty, pylint: disable=line-too-long
+ CourseDurationLimitConfig.objects.create(
+ enabled=global_setting,
+ enabled_as_of=datetime(2018, 1, 1, tzinfo=ZoneInfo("UTC"))
+ )
for site_setting in (True, False, None):
test_site_cfg = SiteConfigurationFactory.create(
site_values={'course_org_filter': []}
)
CourseDurationLimitConfig.objects.create(
- site=test_site_cfg.site, enabled=site_setting, enabled_as_of=datetime(2018, 1, 1, tzinfo=pytz.UTC)
+ site=test_site_cfg.site,
+ enabled=site_setting,
+ enabled_as_of=datetime(2018, 1, 1, tzinfo=ZoneInfo("UTC"))
)
for org_setting in (True, False, None):
@@ -193,7 +198,7 @@ def test_all_current_course_configs(self):
test_site_cfg.save()
CourseDurationLimitConfig.objects.create(
- org=test_org, enabled=org_setting, enabled_as_of=datetime(2018, 1, 1, tzinfo=pytz.UTC)
+ org=test_org, enabled=org_setting, enabled_as_of=datetime(2018, 1, 1, tzinfo=ZoneInfo("UTC"))
)
for course_setting in (True, False, None):
@@ -202,7 +207,7 @@ def test_all_current_course_configs(self):
id=CourseLocator(test_org, 'test_course', f'run-{course_setting}')
)
CourseDurationLimitConfig.objects.create(
- course=test_course, enabled=course_setting, enabled_as_of=datetime(2018, 1, 1, tzinfo=pytz.UTC) # lint-amnesty, pylint: disable=line-too-long
+ course=test_course, enabled=course_setting, enabled_as_of=datetime(2018, 1, 1, tzinfo=ZoneInfo("UTC")) # lint-amnesty, pylint: disable=line-too-long
)
with self.assertNumQueries(4):
@@ -216,22 +221,25 @@ def test_all_current_course_configs(self):
# Point-test some of the final configurations
assert all_configs[CourseLocator('7-True', 'test_course', 'run-None')] == {
'enabled': (True, Provenance.org),
- 'enabled_as_of': (datetime(2018, 1, 1, 0, tzinfo=pytz.UTC),
+ 'enabled_as_of': (datetime(2018, 1, 1, 0, tzinfo=ZoneInfo("UTC")),
Provenance.run)
}
assert all_configs[CourseLocator('7-True', 'test_course', 'run-False')] == {
'enabled': (False, Provenance.run),
- 'enabled_as_of': (datetime(2018, 1, 1, 0, tzinfo=pytz.UTC),
+ 'enabled_as_of': (datetime(2018, 1, 1, 0, tzinfo=ZoneInfo("UTC")),
Provenance.run)
}
assert all_configs[CourseLocator('7-None', 'test_course', 'run-None')] == {
'enabled': (True, Provenance.site),
- 'enabled_as_of': (datetime(2018, 1, 1, 0, tzinfo=pytz.UTC),
+ 'enabled_as_of': (datetime(2018, 1, 1, 0, tzinfo=ZoneInfo("UTC")),
Provenance.run)
}
def test_caching_global(self):
- global_config = CourseDurationLimitConfig(enabled=True, enabled_as_of=datetime(2018, 1, 1, tzinfo=pytz.UTC))
+ global_config = CourseDurationLimitConfig(
+ enabled=True,
+ enabled_as_of=datetime(2018, 1, 1, tzinfo=ZoneInfo("UTC"))
+ )
global_config.save()
RequestCache.clear_all_namespaces()
@@ -257,7 +265,7 @@ def test_caching_global(self):
def test_caching_site(self):
site_cfg = SiteConfigurationFactory()
- site_config = CourseDurationLimitConfig(site=site_cfg.site, enabled=True, enabled_as_of=datetime(2018, 1, 1, tzinfo=pytz.UTC)) # lint-amnesty, pylint: disable=line-too-long
+ site_config = CourseDurationLimitConfig(site=site_cfg.site, enabled=True, enabled_as_of=datetime(2018, 1, 1, tzinfo=ZoneInfo("UTC"))) # lint-amnesty, pylint: disable=line-too-long
site_config.save()
RequestCache.clear_all_namespaces()
@@ -281,7 +289,10 @@ def test_caching_site(self):
with self.assertNumQueries(1):
assert not CourseDurationLimitConfig.current(site=site_cfg.site).enabled
- global_config = CourseDurationLimitConfig(enabled=True, enabled_as_of=datetime(2018, 1, 1, tzinfo=pytz.UTC))
+ global_config = CourseDurationLimitConfig(
+ enabled=True,
+ enabled_as_of=datetime(2018, 1, 1, tzinfo=ZoneInfo("UTC"))
+ )
global_config.save()
RequestCache.clear_all_namespaces()
@@ -295,7 +306,7 @@ def test_caching_org(self):
site_cfg = SiteConfigurationFactory.create(
site_values={'course_org_filter': course.org}
)
- org_config = CourseDurationLimitConfig(org=course.org, enabled=True, enabled_as_of=datetime(2018, 1, 1, tzinfo=pytz.UTC)) # lint-amnesty, pylint: disable=line-too-long
+ org_config = CourseDurationLimitConfig(org=course.org, enabled=True, enabled_as_of=datetime(2018, 1, 1, tzinfo=ZoneInfo("UTC"))) # lint-amnesty, pylint: disable=line-too-long
org_config.save()
RequestCache.clear_all_namespaces()
@@ -319,7 +330,10 @@ def test_caching_org(self):
with self.assertNumQueries(2):
assert not CourseDurationLimitConfig.current(org=course.org).enabled
- global_config = CourseDurationLimitConfig(enabled=True, enabled_as_of=datetime(2018, 1, 1, tzinfo=pytz.UTC))
+ global_config = CourseDurationLimitConfig(
+ enabled=True,
+ enabled_as_of=datetime(2018, 1, 1, tzinfo=ZoneInfo("UTC"))
+ )
global_config.save()
RequestCache.clear_all_namespaces()
@@ -328,7 +342,7 @@ def test_caching_org(self):
with self.assertNumQueries(0):
assert not CourseDurationLimitConfig.current(org=course.org).enabled
- site_config = CourseDurationLimitConfig(site=site_cfg.site, enabled=True, enabled_as_of=datetime(2018, 1, 1, tzinfo=pytz.UTC)) # lint-amnesty, pylint: disable=line-too-long
+ site_config = CourseDurationLimitConfig(site=site_cfg.site, enabled=True, enabled_as_of=datetime(2018, 1, 1, tzinfo=ZoneInfo("UTC"))) # lint-amnesty, pylint: disable=line-too-long
site_config.save()
RequestCache.clear_all_namespaces()
@@ -342,7 +356,7 @@ def test_caching_course(self):
site_cfg = SiteConfigurationFactory.create(
site_values={'course_org_filter': course.org}
)
- course_config = CourseDurationLimitConfig(course=course, enabled=True, enabled_as_of=datetime(2018, 1, 1, tzinfo=pytz.UTC)) # lint-amnesty, pylint: disable=line-too-long
+ course_config = CourseDurationLimitConfig(course=course, enabled=True, enabled_as_of=datetime(2018, 1, 1, tzinfo=ZoneInfo("UTC"))) # lint-amnesty, pylint: disable=line-too-long
course_config.save()
RequestCache.clear_all_namespaces()
@@ -366,7 +380,10 @@ def test_caching_course(self):
with self.assertNumQueries(2):
assert not CourseDurationLimitConfig.current(course_key=course.id).enabled
- global_config = CourseDurationLimitConfig(enabled=True, enabled_as_of=datetime(2018, 1, 1, tzinfo=pytz.UTC))
+ global_config = CourseDurationLimitConfig(
+ enabled=True,
+ enabled_as_of=datetime(2018, 1, 1, tzinfo=ZoneInfo("UTC"))
+ )
global_config.save()
RequestCache.clear_all_namespaces()
@@ -375,7 +392,7 @@ def test_caching_course(self):
with self.assertNumQueries(0):
assert not CourseDurationLimitConfig.current(course_key=course.id).enabled
- site_config = CourseDurationLimitConfig(site=site_cfg.site, enabled=True, enabled_as_of=datetime(2018, 1, 1, tzinfo=pytz.UTC)) # lint-amnesty, pylint: disable=line-too-long
+ site_config = CourseDurationLimitConfig(site=site_cfg.site, enabled=True, enabled_as_of=datetime(2018, 1, 1, tzinfo=ZoneInfo("UTC"))) # lint-amnesty, pylint: disable=line-too-long
site_config.save()
RequestCache.clear_all_namespaces()
@@ -384,7 +401,7 @@ def test_caching_course(self):
with self.assertNumQueries(0):
assert not CourseDurationLimitConfig.current(course_key=course.id).enabled
- org_config = CourseDurationLimitConfig(org=course.org, enabled=True, enabled_as_of=datetime(2018, 1, 1, tzinfo=pytz.UTC)) # lint-amnesty, pylint: disable=line-too-long
+ org_config = CourseDurationLimitConfig(org=course.org, enabled=True, enabled_as_of=datetime(2018, 1, 1, tzinfo=ZoneInfo("UTC"))) # lint-amnesty, pylint: disable=line-too-long
org_config.save()
RequestCache.clear_all_namespaces()
diff --git a/openedx/features/course_experience/api/v1/tests/test_utils.py b/openedx/features/course_experience/api/v1/tests/test_utils.py
new file mode 100644
index 000000000000..741a2d7658f1
--- /dev/null
+++ b/openedx/features/course_experience/api/v1/tests/test_utils.py
@@ -0,0 +1,117 @@
+"""
+Tests utils of course expirience feature.
+"""
+import datetime
+
+from django.urls import reverse
+from django.utils import timezone
+from rest_framework.test import APIRequestFactory
+
+from common.djangoapps.course_modes.models import CourseMode
+from common.djangoapps.student.models import CourseEnrollment
+from common.djangoapps.util.testing import EventTestMixin
+from lms.djangoapps.course_home_api.tests.utils import BaseCourseHomeTests
+from lms.djangoapps.courseware.tests.helpers import MasqueradeMixin
+from openedx.core.djangoapps.schedules.models import Schedule
+from openedx.features.course_experience.api.v1.utils import (
+ reset_deadlines_for_course,
+ reset_course_deadlines_for_user,
+ reset_bulk_course_deadlines
+)
+from xmodule.modulestore.tests.factories import CourseFactory
+
+
+class TestResetDeadlinesForCourse(EventTestMixin, BaseCourseHomeTests, MasqueradeMixin):
+ """
+ Tests for reset deadlines endpoint.
+ """
+ def setUp(self): # pylint: disable=arguments-differ
+ super().setUp("openedx.features.course_experience.api.v1.utils.tracker")
+ self.course = CourseFactory.create(self_paced=True, start=timezone.now() - datetime.timedelta(days=1000))
+
+ def test_reset_deadlines_for_course(self):
+ enrollment = CourseEnrollment.enroll(self.user, self.course.id, CourseMode.VERIFIED)
+ enrollment.schedule.start_date = timezone.now() - datetime.timedelta(days=100)
+ enrollment.schedule.save()
+
+ request = APIRequestFactory().post(
+ reverse("course-experience-reset-course-deadlines"), {"course_key": self.course.id}
+ )
+ request.user = self.user
+
+ reset_deadlines_for_course(request, self.course.id, {})
+
+ assert enrollment.schedule.start_date < Schedule.objects.get(id=enrollment.schedule.id).start_date
+ self.assert_event_emitted(
+ "edx.ui.lms.reset_deadlines.clicked",
+ courserun_key=str(self.course.id),
+ is_masquerading=False,
+ is_staff=False,
+ org_key=self.course.org,
+ user_id=self.user.id,
+ )
+
+ def test_reset_deadlines_with_masquerade(self):
+ """Staff users should be able to masquerade as a learner and reset the learner's schedule"""
+ student_username = self.user.username
+ student_user_id = self.user.id
+ student_enrollment = CourseEnrollment.enroll(self.user, self.course.id)
+ student_enrollment.schedule.start_date = timezone.now() - datetime.timedelta(days=100)
+ student_enrollment.schedule.save()
+
+ staff_enrollment = CourseEnrollment.enroll(self.staff_user, self.course.id)
+ staff_enrollment.schedule.start_date = timezone.now() - datetime.timedelta(days=30)
+ staff_enrollment.schedule.save()
+
+ self.switch_to_staff()
+ self.update_masquerade(course=self.course, username=student_username)
+
+ request = APIRequestFactory().post(
+ reverse("course-experience-reset-course-deadlines"), {"course_key": self.course.id}
+ )
+ request.user = self.staff_user
+ request.session = self.client.session
+
+ reset_deadlines_for_course(request, self.course.id, {})
+
+ updated_schedule = Schedule.objects.get(id=student_enrollment.schedule.id)
+ assert updated_schedule.start_date.date() == datetime.datetime.today().date()
+ updated_staff_schedule = Schedule.objects.get(id=staff_enrollment.schedule.id)
+ assert updated_staff_schedule.start_date == staff_enrollment.schedule.start_date
+ self.assert_event_emitted(
+ "edx.ui.lms.reset_deadlines.clicked",
+ courserun_key=str(self.course.id),
+ is_masquerading=True,
+ is_staff=False,
+ org_key=self.course.org,
+ user_id=student_user_id,
+ )
+
+ def test_reset_course_deadlines_for_user(self):
+ """Test the reset_course_deadlines_for_user utility function directly"""
+ enrollment = CourseEnrollment.enroll(self.user, self.course.id, CourseMode.VERIFIED)
+ enrollment.schedule.start_date = timezone.now() - datetime.timedelta(days=100)
+ enrollment.schedule.save()
+
+ result = reset_course_deadlines_for_user(self.user, self.course.id)
+
+ assert result is True
+ assert enrollment.schedule.start_date < Schedule.objects.get(id=enrollment.schedule.id).start_date
+
+ def test_reset_bulk_course_deadlines(self):
+ """Test the reset_bulk_course_deadlines utility function"""
+ enrollment = CourseEnrollment.enroll(self.user, self.course.id, CourseMode.VERIFIED)
+ enrollment.schedule.start_date = timezone.now() - datetime.timedelta(days=100)
+ enrollment.schedule.save()
+
+ request = APIRequestFactory().post(
+ reverse("course-experience-reset-all-course-deadlines"), {}
+ )
+ request.user = self.user
+
+ success_keys, failed_keys = reset_bulk_course_deadlines(request, [self.course.id], {})
+
+ assert len(success_keys) == 1
+ assert self.course.id in success_keys
+ assert len(failed_keys) == 0
+ assert enrollment.schedule.start_date < Schedule.objects.get(id=enrollment.schedule.id).start_date
diff --git a/openedx/features/course_experience/api/v1/tests/test_views.py b/openedx/features/course_experience/api/v1/tests/test_views.py
index 8cef39053b3e..097eb2c18c3b 100644
--- a/openedx/features/course_experience/api/v1/tests/test_views.py
+++ b/openedx/features/course_experience/api/v1/tests/test_views.py
@@ -1,7 +1,9 @@
"""
Tests for reset deadlines endpoint.
"""
+
import datetime
+from unittest import mock
import ddt
from django.urls import reverse
@@ -10,7 +12,6 @@
from common.djangoapps.course_modes.models import CourseMode
from common.djangoapps.student.models import CourseEnrollment
-from common.djangoapps.util.testing import EventTestMixin
from lms.djangoapps.course_home_api.tests.utils import BaseCourseHomeTests
from lms.djangoapps.courseware.tests.helpers import MasqueradeMixin
from openedx.core.djangoapps.schedules.models import Schedule
@@ -19,14 +20,12 @@
@ddt.ddt
-class ResetCourseDeadlinesViewTests(EventTestMixin, BaseCourseHomeTests, MasqueradeMixin):
+class ResetCourseDeadlinesViewTests(BaseCourseHomeTests, MasqueradeMixin):
"""
Tests for reset deadlines endpoint.
"""
def setUp(self): # pylint: disable=arguments-differ
- # Need to supply tracker name for the EventTestMixin. Also, EventTestMixin needs to come
- # first in class inheritance so the setUp call here appropriately works
- super().setUp('openedx.features.course_experience.api.v1.views.tracker')
+ super().setUp()
self.course = CourseFactory.create(self_paced=True, start=timezone.now() - datetime.timedelta(days=1000))
def test_reset_deadlines(self):
@@ -37,20 +36,11 @@ def test_reset_deadlines(self):
response = self.client.post(reverse('course-experience-reset-course-deadlines'), {'course': self.course.id})
assert response.status_code == 400
assert enrollment.schedule == Schedule.objects.get(id=enrollment.schedule.id)
- self.assert_no_events_were_emitted()
# Test correct post body
response = self.client.post(reverse('course-experience-reset-course-deadlines'), {'course_key': self.course.id})
assert response.status_code == 200
assert enrollment.schedule.start_date < Schedule.objects.get(id=enrollment.schedule.id).start_date
- self.assert_event_emitted(
- 'edx.ui.lms.reset_deadlines.clicked',
- courserun_key=str(self.course.id),
- is_masquerading=False,
- is_staff=False,
- org_key=self.course.org,
- user_id=self.user.id,
- )
@override_waffle_flag(RELATIVE_DATES_FLAG, active=True)
@override_waffle_flag(RELATIVE_DATES_DISABLE_RESET_FLAG, active=True)
@@ -62,36 +52,6 @@ def test_reset_deadlines_disabled(self):
response = self.client.post(reverse('course-experience-reset-course-deadlines'), {'course_key': self.course.id})
assert response.status_code == 200
assert enrollment.schedule == Schedule.objects.get(id=enrollment.schedule.id)
- self.assert_no_events_were_emitted()
-
- def test_reset_deadlines_with_masquerade(self):
- """ Staff users should be able to masquerade as a learner and reset the learner's schedule """
- student_username = self.user.username
- student_user_id = self.user.id
- student_enrollment = CourseEnrollment.enroll(self.user, self.course.id)
- student_enrollment.schedule.start_date = timezone.now() - datetime.timedelta(days=100)
- student_enrollment.schedule.save()
-
- staff_enrollment = CourseEnrollment.enroll(self.staff_user, self.course.id)
- staff_enrollment.schedule.start_date = timezone.now() - datetime.timedelta(days=30)
- staff_enrollment.schedule.save()
-
- self.switch_to_staff()
- self.update_masquerade(course=self.course, username=student_username)
-
- self.client.post(reverse('course-experience-reset-course-deadlines'), {'course_key': self.course.id})
- updated_schedule = Schedule.objects.get(id=student_enrollment.schedule.id)
- assert updated_schedule.start_date.date() == datetime.datetime.today().date()
- updated_staff_schedule = Schedule.objects.get(id=staff_enrollment.schedule.id)
- assert updated_staff_schedule.start_date == staff_enrollment.schedule.start_date
- self.assert_event_emitted(
- 'edx.ui.lms.reset_deadlines.clicked',
- courserun_key=str(self.course.id),
- is_masquerading=True,
- is_staff=False,
- org_key=self.course.org,
- user_id=student_user_id,
- )
def test_post_unauthenticated_user(self):
self.client.logout()
@@ -115,3 +75,52 @@ def test_mobile_get_unauthenticated_user(self):
self.client.logout()
response = self.client.get(reverse('course-experience-course-deadlines-mobile', args=[self.course.id]))
assert response.status_code == 401
+
+
+class ResetAllRelativeCourseDeadlinesViewTests(BaseCourseHomeTests, MasqueradeMixin):
+ """
+ Tests for reset all relative deadlines endpoint.
+ """
+
+ def setUp(self): # pylint: disable=arguments-differ
+ super().setUp()
+ self.course = CourseFactory.create(self_paced=True, start=timezone.now() - datetime.timedelta(days=1000))
+ self.enrollment = CourseEnrollment.enroll(self.user, self.course.id, CourseMode.VERIFIED)
+ self.enrollment.schedule.start_date = timezone.now() - datetime.timedelta(days=100)
+ self.enrollment.schedule.save()
+
+ def test_reset_all_course_deadlines(self):
+ """
+ Test reset all course deadlines endpoint
+ """
+ response = self.client.post(
+ reverse("course-experience-reset-all-course-deadlines"),
+ {},
+ )
+ assert response.status_code == 200
+ assert self.enrollment.schedule.start_date < Schedule.objects.get(id=self.enrollment.schedule.id).start_date
+ assert str(self.course.id) in response.data.get("success_course_keys")
+
+ def test_reset_all_course_deadlines_failure(self):
+ """
+ Raise exception on reset_bulk_course_deadlines and assert if failure course id is returned
+ """
+ with mock.patch(
+ "openedx.features.course_experience.api.v1.views.reset_bulk_course_deadlines",
+ return_value=([], [self.course.id]),
+ ):
+ response = self.client.post(reverse("course-experience-reset-all-course-deadlines"), {})
+
+ assert response.status_code == 200
+ assert str(self.course.id) in response.data.get("failed_course_keys")
+
+ def test_post_unauthenticated_user(self):
+ """
+ Test reset all relative course deadlines endpoint for unauthenticated user
+ """
+ self.client.logout()
+ response = self.client.post(
+ reverse("course-experience-reset-all-course-deadlines"),
+ {},
+ )
+ assert response.status_code == 401
diff --git a/openedx/features/course_experience/api/v1/urls.py b/openedx/features/course_experience/api/v1/urls.py
index 9a2c7106cd1b..2c84af437f70 100644
--- a/openedx/features/course_experience/api/v1/urls.py
+++ b/openedx/features/course_experience/api/v1/urls.py
@@ -4,9 +4,13 @@
from django.conf import settings
-from django.urls import re_path
+from django.urls import re_path, path
-from openedx.features.course_experience.api.v1.views import reset_course_deadlines, CourseDeadlinesMobileView
+from openedx.features.course_experience.api.v1.views import (
+ reset_course_deadlines,
+ reset_all_course_deadlines,
+ CourseDeadlinesMobileView,
+)
urlpatterns = []
@@ -17,6 +21,11 @@
reset_course_deadlines,
name='course-experience-reset-course-deadlines'
),
+ path(
+ 'v1/reset_all_course_deadlines/',
+ reset_all_course_deadlines,
+ name='course-experience-reset-all-course-deadlines',
+ )
]
# URL for retrieving course deadlines info
diff --git a/openedx/features/course_experience/api/v1/utils.py b/openedx/features/course_experience/api/v1/utils.py
new file mode 100644
index 000000000000..8f9205b0f113
--- /dev/null
+++ b/openedx/features/course_experience/api/v1/utils.py
@@ -0,0 +1,115 @@
+
+"""
+Course Experience API utilities.
+"""
+import logging
+from eventtracking import tracker
+
+from lms.djangoapps.courseware.access import has_access
+from lms.djangoapps.courseware.masquerade import is_masquerading, setup_masquerade
+from lms.djangoapps.course_api.api import course_detail
+from openedx.core.djangoapps.schedules.utils import reset_self_paced_schedule
+from openedx.features.course_experience.utils import dates_banner_should_display
+
+
+logger = logging.getLogger(__name__)
+
+
+def reset_course_deadlines_for_user(user, course_key):
+ """
+ Core function to reset deadlines for a single course and user.
+
+ Args:
+ user: The user object
+ course_key: The course key
+
+ Returns:
+ bool: True if deadlines were reset, False if gated content prevents reset
+ """
+ # We ignore the missed_deadlines because this util is used in endpoint from the Learning MFE for
+ # learners who have remaining attempts on a problem and reset their due dates in order to
+ # submit additional attempts. This can apply for 'completed' (submitted) content that would
+ # not be marked as past_due
+ _missed_deadlines, missed_gated_content = dates_banner_should_display(course_key, user)
+ if not missed_gated_content:
+ reset_self_paced_schedule(user, course_key)
+ return True
+ return False
+
+
+def reset_bulk_course_deadlines(request, course_keys, research_event_data={}): # lint-amnesty, pylint: disable=dangerous-default-value
+ """
+ Reset deadlines for multiple courses for the requesting user.
+
+ Args:
+ request (Request): The request object
+ course_keys (list): List of course keys
+ research_event_data (dict): Any data that should be included in the research tracking event
+
+ Returns:
+ tuple: (success_course_keys, failed_course_keys)
+ """
+ success_course_keys = []
+ failed_course_keys = []
+
+ for course_key in course_keys:
+ try:
+ course_masquerade, user = setup_masquerade(
+ request,
+ course_key,
+ has_access(request.user, 'staff', course_key)
+ )
+
+ if reset_course_deadlines_for_user(user, course_key):
+ success_course_keys.append(course_key)
+
+ course_overview = course_detail(request, user.username, course_key)
+
+ research_event_data.update({
+ 'courserun_key': str(course_key),
+ 'is_masquerading': is_masquerading(user, course_key, course_masquerade),
+ 'is_staff': has_access(user, 'staff', course_key).has_access,
+ 'org_key': course_overview.display_org_with_default,
+ 'user_id': user.id,
+ })
+ tracker.emit('edx.ui.lms.reset_deadlines.clicked', research_event_data)
+ else:
+ failed_course_keys.append(course_key)
+ except Exception: # pylint: disable=broad-exception-caught
+ logger.exception('Error occurred while trying to reset deadlines!')
+ failed_course_keys.append(course_key)
+
+ return success_course_keys, failed_course_keys
+
+
+def reset_deadlines_for_course(request, course_key, research_event_data={}): # lint-amnesty, pylint: disable=dangerous-default-value
+ """
+ Set the start_date of a schedule to today, which in turn will adjust due dates for
+ sequentials belonging to a self paced course
+
+ Args:
+ request (Request): The request object
+ course_key (str): The course key
+ research_event_data (dict): Any data that should be included in the research tracking event
+ Example: sending the location of where the reset deadlines banner (i.e. outline-tab)
+ """
+
+ course_masquerade, user = setup_masquerade(
+ request,
+ course_key,
+ has_access(request.user, 'staff', course_key)
+ )
+
+ if reset_course_deadlines_for_user(user, course_key):
+ course_overview = course_detail(request, user.username, course_key)
+ # For context here, research_event_data should already contain `location` indicating
+ # the page/location dates were reset from and could also contain `block_id` if reset
+ # within courseware.
+ research_event_data.update({
+ 'courserun_key': str(course_key),
+ 'is_masquerading': is_masquerading(user, course_key, course_masquerade),
+ 'is_staff': has_access(user, 'staff', course_key).has_access,
+ 'org_key': course_overview.display_org_with_default,
+ 'user_id': user.id,
+ })
+ tracker.emit('edx.ui.lms.reset_deadlines.clicked', research_event_data)
diff --git a/openedx/features/course_experience/api/v1/views.py b/openedx/features/course_experience/api/v1/views.py
index 16be4a4e0ed4..d822fdd65c67 100644
--- a/openedx/features/course_experience/api/v1/views.py
+++ b/openedx/features/course_experience/api/v1/views.py
@@ -5,7 +5,6 @@
from django.utils.html import format_html
from django.utils.translation import gettext as _
-from eventtracking import tracker
from rest_framework.decorators import api_view, authentication_classes, permission_classes
from rest_framework.exceptions import APIException, ParseError
@@ -17,17 +16,14 @@
from edx_rest_framework_extensions.auth.session.authentication import SessionAuthenticationAllowInactiveUser
from opaque_keys.edx.keys import CourseKey
-from lms.djangoapps.course_api.api import course_detail
+from common.djangoapps.student.models import CourseEnrollment
from lms.djangoapps.course_goals.models import UserActivity
-from lms.djangoapps.courseware.access import has_access
from lms.djangoapps.courseware.courses import get_course_with_access
-from lms.djangoapps.courseware.masquerade import is_masquerading, setup_masquerade
-from openedx.core.djangoapps.schedules.utils import reset_self_paced_schedule
from openedx.core.lib.api.authentication import BearerAuthenticationAllowInactiveUser
from openedx.features.course_experience.api.v1.serializers import CourseDeadlinesMobileSerializer
from openedx.features.course_experience.url_helpers import get_learning_mfe_home_url
-from openedx.features.course_experience.utils import dates_banner_should_display
+from openedx.features.course_experience.api.v1.utils import reset_deadlines_for_course, reset_bulk_course_deadlines
log = logging.getLogger(__name__)
@@ -65,32 +61,7 @@ def reset_course_deadlines(request):
try:
course_key = CourseKey.from_string(course_key)
- course_masquerade, user = setup_masquerade(
- request,
- course_key,
- has_access(request.user, 'staff', course_key)
- )
-
- # We ignore the missed_deadlines because this endpoint is used in the Learning MFE for
- # learners who have remaining attempts on a problem and reset their due dates in order to
- # submit additional attempts. This can apply for 'completed' (submitted) content that would
- # not be marked as past_due
- _missed_deadlines, missed_gated_content = dates_banner_should_display(course_key, user)
- if not missed_gated_content:
- reset_self_paced_schedule(user, course_key)
-
- course_overview = course_detail(request, user.username, course_key)
- # For context here, research_event_data should already contain `location` indicating
- # the page/location dates were reset from and could also contain `block_id` if reset
- # within courseware.
- research_event_data.update({
- 'courserun_key': str(course_key),
- 'is_masquerading': is_masquerading(user, course_key, course_masquerade),
- 'is_staff': has_access(user, 'staff', course_key).has_access,
- 'org_key': course_overview.display_org_with_default,
- 'user_id': user.id,
- })
- tracker.emit('edx.ui.lms.reset_deadlines.clicked', research_event_data)
+ reset_deadlines_for_course(request, course_key, research_event_data)
body_link = get_learning_mfe_home_url(course_key=course_key, url_fragment='dates')
@@ -106,6 +77,44 @@ def reset_course_deadlines(request):
raise UnableToResetDeadlines from reset_deadlines_exception
+@api_view(["POST"])
+@authentication_classes(
+ (
+ JwtAuthentication,
+ BearerAuthenticationAllowInactiveUser,
+ SessionAuthenticationAllowInactiveUser,
+ )
+)
+@permission_classes((IsAuthenticated,))
+def reset_all_course_deadlines(request):
+ """
+ Set the start_date of a schedule to today for all enrolled courses
+
+ Request Parameters:
+ research_event_data: any data that should be included in the research tracking event
+ Example: sending the location of where the reset deadlines banner (i.e. outline-tab)
+
+ Returns:
+ success_course_keys: list of course keys for which deadlines were successfully reset
+ failed_course_keys: list of course keys for which deadlines could not be reset
+ """
+ research_event_data = request.data.get("research_event_data", {})
+ course_keys = list(
+ CourseEnrollment.enrollments_for_user(request.user).select_related("course").values_list("course_id", flat=True)
+ )
+
+ success_course_keys, failed_course_keys = reset_bulk_course_deadlines(
+ request, course_keys, research_event_data
+ )
+
+ return Response(
+ {
+ "success_course_keys": [str(key) for key in success_course_keys],
+ "failed_course_keys": [str(key) for key in failed_course_keys],
+ }
+ )
+
+
class CourseDeadlinesMobileView(RetrieveAPIView):
"""
**Use Cases**
diff --git a/openedx/features/course_experience/tests/views/test_course_updates.py b/openedx/features/course_experience/tests/views/test_course_updates.py
index 379be52ed40f..6ca701f14677 100644
--- a/openedx/features/course_experience/tests/views/test_course_updates.py
+++ b/openedx/features/course_experience/tests/views/test_course_updates.py
@@ -5,7 +5,7 @@
from datetime import datetime
from django.urls import reverse
-from pytz import UTC
+from zoneinfo import ZoneInfo
from openedx.core.djangoapps.waffle_utils.testutils import WAFFLE_TABLES
from openedx.features.content_type_gating.models import ContentTypeGatingConfig
@@ -41,7 +41,7 @@ def test_view(self):
self.assertContains(response, 'Second Message')
def test_queries(self):
- ContentTypeGatingConfig.objects.create(enabled=True, enabled_as_of=datetime(2018, 1, 1, tzinfo=UTC))
+ ContentTypeGatingConfig.objects.create(enabled=True, enabled_as_of=datetime(2018, 1, 1, tzinfo=ZoneInfo("UTC")))
self.create_course_update('First Message')
# Pre-fetch the view to populate any caches
diff --git a/openedx/features/discounts/applicability.py b/openedx/features/discounts/applicability.py
index 97d6f74403bd..c930ae2da878 100644
--- a/openedx/features/discounts/applicability.py
+++ b/openedx/features/discounts/applicability.py
@@ -11,7 +11,7 @@
from datetime import datetime, timedelta
-import pytz
+from zoneinfo import ZoneInfo
from crum import get_current_request, impersonate
from django.conf import settings
from django.utils import timezone
@@ -197,7 +197,7 @@ def _is_in_holdback_and_bucket(user):
Return whether the specified user is in the first-purchase-discount holdback group.
This will also stable bucket the user.
"""
- if datetime(2020, 8, 1, tzinfo=pytz.UTC) <= datetime.now(tz=pytz.UTC):
+ if datetime(2020, 8, 1, tzinfo=ZoneInfo("UTC")) <= datetime.now(tz=ZoneInfo("UTC")):
return False
# Holdback is 10%
diff --git a/openedx/features/discounts/tests/test_applicability.py b/openedx/features/discounts/tests/test_applicability.py
index 60dbe7a67edf..472120b38705 100644
--- a/openedx/features/discounts/tests/test_applicability.py
+++ b/openedx/features/discounts/tests/test_applicability.py
@@ -6,7 +6,7 @@
import ddt
import pytest
-import pytz
+from zoneinfo import ZoneInfo
from django.contrib.sites.models import Site
from django.utils.timezone import now
from edx_toggles.toggles.testutils import override_waffle_flag
@@ -39,7 +39,7 @@ def setUp(self):
self.user = UserFactory.create()
self.course = CourseFactory.create(run='test', display_name='test')
CourseModeFactory.create(course_id=self.course.id, mode_slug='verified')
- now_time = datetime.now(tz=pytz.UTC).strftime("%Y-%m-%d %H:%M:%S%z")
+ now_time = datetime.now(tz=ZoneInfo("UTC")).strftime("%Y-%m-%d %H:%M:%S%z")
ExperimentData.objects.create(
user=self.user, experiment_id=REV1008_EXPERIMENT_ID, key=str(self.course.id), value=now_time
)
@@ -175,6 +175,6 @@ def test_holdback_expiry(self):
with patch('openedx.features.discounts.applicability.stable_bucketing_hash_group', return_value=0):
with patch(
'openedx.features.discounts.applicability.datetime',
- Mock(now=Mock(return_value=datetime(2020, 8, 1, 0, 1, tzinfo=pytz.UTC)), wraps=datetime),
+ Mock(now=Mock(return_value=datetime(2020, 8, 1, 0, 1, tzinfo=ZoneInfo("UTC"))), wraps=datetime),
):
assert not _is_in_holdback_and_bucket(self.user)
diff --git a/openedx/features/discounts/utils.py b/openedx/features/discounts/utils.py
index 92490f19bcb3..00b60f6cadfd 100644
--- a/openedx/features/discounts/utils.py
+++ b/openedx/features/discounts/utils.py
@@ -4,7 +4,7 @@
from datetime import datetime
-import pytz
+from zoneinfo import ZoneInfo
from django.conf import settings
from django.utils.translation import get_language
from django.utils.translation import gettext as _
@@ -89,7 +89,7 @@ def generate_offer_data(user, course):
ExperimentData.objects.get_or_create(
user=user, experiment_id=REV1008_EXPERIMENT_ID, key=str(course),
defaults={
- 'value': datetime.now(tz=pytz.UTC).strftime('%Y-%m-%d %H:%M:%S%z'),
+ 'value': datetime.now(tz=ZoneInfo("UTC")).strftime('%Y-%m-%d %H:%M:%S%z'),
},
)
diff --git a/openedx/features/enterprise_support/tests/test_logout.py b/openedx/features/enterprise_support/tests/test_logout.py
index c83b67c3a5e6..cbec23397795 100644
--- a/openedx/features/enterprise_support/tests/test_logout.py
+++ b/openedx/features/enterprise_support/tests/test_logout.py
@@ -19,6 +19,7 @@
factories
)
from openedx.features.enterprise_support.tests.mixins.enterprise import EnterpriseServiceMockMixin
+from common.test.utils import assert_dict_contains_subset
@ddt.ddt
@@ -60,7 +61,7 @@ def test_logout_enterprise_target(self, redirect_url, enterprise_target):
expected = {
'enterprise_target': enterprise_target,
}
- self.assertDictContainsSubset(expected, response.context_data)
+ assert_dict_contains_subset(self, expected, response.context_data)
if enterprise_target:
self.assertContains(response, 'We are signing you in.')
diff --git a/openedx/features/offline_content/docs/001-mobile-offline-content-support.rst b/openedx/features/offline_content/docs/001-mobile-offline-content-support.rst
new file mode 100644
index 000000000000..29ecf7d01efe
--- /dev/null
+++ b/openedx/features/offline_content/docs/001-mobile-offline-content-support.rst
@@ -0,0 +1,156 @@
+1. Offline content generation for mobile OeX app
+=============================================
+
+Status
+------
+
+Accepted
+
+Context
+-------
+
+The primary goal is to enable offline access to course content in the Open edX mobile application.
+This will allow users to download course materials when they have internet access and access them
+later without an internet connection, also it should support synchronization of the submitted results
+with backend service as connection become available again. This feature is crucial for learners
+in areas with unreliable internet connectivity or those who prefer to study on the go without using mobile data.
+It is possible to provide different kind of content using the Open edX platform, such as read-only materials,
+videos, and assessments. Therefore to provide the whole course experience in offline mode it's required to
+make all these types of content available offline. Of course it won't be feasible to recreate grading
+algorithms in mobile, so it's possible to save submission on the mobile app and execute synchronization
+of the user progres as not limited connectivity is back.
+
+From the product perspective the following Figma designs and product requirements should be considered:
+
+* `Download and Delete (Figma)`_
+* `Downloads (Figma)`_
+
+.. _Download and Delete (Figma): https://www.figma.com/design/iZ56YMjbRMShCCDxqrqRrR/Mobile-App-v2.4-%5BOpen-edX%5D?node-id=18472-187387&t=tMgymS6WIZZJbJHn-0
+.. _Downloads (Figma): https://www.figma.com/design/iZ56YMjbRMShCCDxqrqRrR/Mobile-App-v2.4-%5BOpen-edX%5D
+
+Decision
+--------
+
+The implementation of the offline content support require addition of the following features to the edx-platform:
+
+* It's necessary to generate an archive with all necessary HTML and assets for a student view of an xBlock, so it's possible to display an xBlock using mobile WebView.
+* Implement a new standard XBlock view called `offline_view` which would generate user-agnostic fragments suitable for offline use. This view will avoid any dependence on student-specific state, focusing solely on content and settings.
+* XBlock classes can opt into supporting `offline_view`. They can implement this view fully or partially. For example, a block that relies on user-specific randomization or interactive elements that require online connectivity would not be rendered offline.
+* The generated offline content should be provided to mobile device through mobile API.
+* To support CAPA problems and other kinds of assessments in offline mode it's necessary to create an additional
+ JavaScript layer that will allow communication with Mobile applications by sending JSON messages
+ using Android and IOS Bridge.
+
+
+Offline content generation
+~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Generating zip archive with xBlock data for HTML and CAPA problems
+When content is published in CMS and offline generation is enabled for the course or entire platform using waffle flags, the content generation task should be started for supported blocks.
+Every time block content republished ZIP archive with offline content should be regenerated.
+Supported XBlock class should implement `offline_view` method that will be used to generate the content.
+HTML should be processed, all related assets files, images and scripts should be included in the generated ZIP archive with offline content
+The Generation process should work with local media storage as well as s3.
+If error retrieving block happened, the generation task will be scheduled for retry 2 more times, with progressive delay.
+
+ .. image:: _images/mobile_offline_content_generation.svg
+ :alt: Mobile Offline Content Generation Process Diagram
+
+
+Offline content deletion
+~~~~~~~~~~~~~~~~~~~~~~~~
+
+When the course is published and some blocks are removed from the course, related ZIP archive should be deleted.
+When some blocks are removed from the course without publishing the course, the related ZIP archive shouldn't be deleted.
+
+
+Mobile API extension
+~~~~~~~~~~~~~~~~~~~~
+
+Extend the Course Home mobile API endpoint, and add a new version of the API (url /api/mobile/v4/course_info/blocks/)
+to return information about offline content available for download for supported blocks
+
+.. code-block:: json
+ {
+ ...
+ "offline_download": {
+ "file_url": "{file_url}" or null,
+ "last_modified": "{DT}" or null,
+ "file_size": ""
+ }
+ }
+
+
+JavaScript Bridge for interaction with mobile applications
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+Implement JS Bridge JS script to intercept and send results to mobile device for supported CAPA problems.
+
+The JS bridge will intercept AJAX requests in the mobile application and store the responses locally. If the user submits the response offline he will be shown the message "Your response is accepted" and the Submit button will be disabled as the submission will be sent twice.
+When the internet connection is back the mobile client will submit the cached responses one by one through the regular xBlock handler endpoints.
+Data from submission should be submitted through bridge on iOS and Android devices.
+This script should expose markCompleted JS function so mobile can change state of the offline problem after the data was saved into internal database or on initialization of the problem.
+
+* **Implement of a mechanism for generating and storing on a server or external storage**: The course content should be pre-generated and saved to the storage for later download.
+ * **Render block fragment**: Implement a new standard XBlock view called `offline_view` which would generate user-agnostic fragments suitable for offline use. This view will avoid any dependence on student-specific state, focusing solely on content and settings.
+ * **Replace static and media**: Save static and media assets files used in block to temporary directory and replace their static paths with local paths.
+ * **Archive and store content**: Archive the generated content and store it on the server or external storage.
+* **Mechanism for updating the generated data**: When updating course blocks (namely when publishing) the content that has been changed should be re-generated.
+ * **Track course publishing events on CMS side**: Add a new signal `course_cache_updated` to be called after the course structure cache update in `update_course_in_cache_v2`. Add a signal that listens to `course_cache_updated` and starts block generation.
+ * **Update archive**: Check generated archive creation date and update it if less than course publishing date.
+* **Implement a Mobile Local Storage Mechanism**: Use the device's local storage to save course content for offline access.
+ * **Extend blocks API**: Add links to download blocks content and where it is possible.
+* **Sync Mechanism**: Periodically synchronize local data with the server when the device is online.
+ * **Sync on app side**: On course outline screen, check if the course content is up to date and update it if necessary.
+ * **Sync user responses**: When the device is offline, save user responses locally and send them to the server when the device is online.
+* **Selective Download**: Allow users to choose specific content to download for offline use.
+* **Full Course Download**: Provide an option to download entire courses for offline access.
+
+Supported xBlocks in offline mode
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+It was decided to include a fraction of Open edX xBlocks to be supported.
+The following list of blocks is currently planned to be added to the support:
+
+* **Common problems**:
+ * **Checkboxes** - full support
+ * **Dropdown** - full support
+ * **Multiple Choice** - full support
+ * **Numerical Input** - full support
+ * **Text Input** - full support
+ * **Checkboxes with Hints and Feedback** - partial support without Hints and Feedback
+ * **Dropdown with Hints and Feedback** - partial support without Hints and Feedback
+ * **Multiple Choice with Hints and Feedback** - partial support without Hints and Feedback
+ * **Numerical Input with Hints and Feedback** - partially supported without Hints and Feedback
+ * **Text Input with Hints and Feedback** - partially supported without Hints and Feedback
+ * **Blank Advanced Problems** - partially supported, without loncapa/python problems or multi-part problems
+* **Text**:
+ * **Text** - full support
+ * **IFrame Tool** - full support
+ * **Raw HTML** - full support
+ * **Zooming Image Tool** - full support
+* **Video** - already supported
+
+
+Consequences
+------------
+
+* Enhanced learner experience with flexible access to course materials.
+* Increased accessibility for learners in regions with poor internet connectivity.
+* Improved engagement and completion rates due to uninterrupted access to content.
+* Simplified Maintenance by using a unified rendering view (`offline_view`), the complexity of maintaining separate renderers for online and offline content is significantly reduced.
+* The proposed approach not only caters to the current needs of mobile users but also sets a foundation for expanding offline access to other platforms and uses.
+* Potential increase in app size due to locally stored content.
+* Increased complexity in managing content synchronization and updates.
+* Need for continuous monitoring and updates to handle new content types and formats.
+
+Rejected Solutions
+------------------
+
+* **Store common .js and .css files of blocks in a separate folder:**
+ * This solution was rejected because it is unclear how to track potential changes to these files and re-generate the content of the blocks.
+
+* **Generate content on the fly when the user requests it:**
+ * This solution was rejected because it would require a significant amount of processing power and time to generate content for each block when requested.
+
+* **Separate Offline Renderer**:
+ * The initial proposal of creating a separate renderer for offline content was rejected due to the increased complexity and potential for inconsistent behavior between online and offline content.
diff --git a/openedx/features/offline_content/docs/002-mobile-offline-content-support.rst b/openedx/features/offline_content/docs/002-mobile-offline-content-support.rst
new file mode 100644
index 000000000000..7d1e91e7e882
--- /dev/null
+++ b/openedx/features/offline_content/docs/002-mobile-offline-content-support.rst
@@ -0,0 +1,55 @@
+2. Offline Mode enhancements
+=========================
+
+Status
+------
+
+Proposed
+
+Context
+-------
+
+`offline_view` generalized and can be used for Non-mobile offline mode, Anonymous access or Regular student access.
+Static files like JavaScript and CSS will be de-duplicated based on their content hash.
+
+Decisions
+--------
+
+1. Efficient resource management
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+ - Shared resources like JS and CSS files will be de-duplicated based on their content hash, to prevent duplication for every block.
+ - All shared content should be stored in the separate ZIP archive.
+ - This archive will be regenerated 1 time and contains all JS and CSS files related to default Xblocks.
+ - Xblock specific resources will still be stored in the block ZIP archive.
+ - This will ensure that the same resource is not duplicated across different blocks, reducing storage and bandwidth usage.
+
+
+2. Anonymous access
+~~~~~~~~~~~~~~~~~~~
+
+ - Re-implement `public_view` on top of `offline_view`. If it is possible to get pre-rendered block without knowing user state, then it is possible to serve that pre-renderable view as the public experience for logged-out users.
+ - This will allow broader access to educational content without the need for user authentication, potentially increasing user engagement and content reach.
+
+
+3. Non-mobile offline mode
+~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+ - The `offline_view` will be generalized to support non-mobile offline mode.
+ - This mode will enable users on desktop and other non-mobile platforms to download and access course content without an active internet connection, providing greater flexibility in how content is accessed.
+
+
+4. Regular student access
+~~~~~~~~~~~~~~~~~~~~~~~~~
+
+ - `student_view` will be implemented on top of `offline_view` wherever it is supported.
+ - For XBlocks compatible with this architecture, offline-ready content will be served by default, and dynamic online features will be engaged only when a user has a reliable connection.
+ - This setting is intended to improve the learning process by providing constant access to content when the Internet connection is unstable.
+
+
+Consequences
+------------
+
+* **Resource Efficiency**: The avoidance of duplicating static resources for each block enhances the efficient use of storage and bandwidth.
+* **Enhanced Flexibility**: The system can skip rendering blocks that require student-specific interactions, ensuring reliability and reducing the potential for behavior discrepancies between online and offline modes.
+* **Broader Accessibility**: The ability to serve pre-rendered views to anonymous users increases the reach of educational content, making it more accessible to a wider audience.
diff --git a/openedx/features/offline_content/docs/_images/mobile_offline_content_generation.svg b/openedx/features/offline_content/docs/_images/mobile_offline_content_generation.svg
new file mode 100644
index 000000000000..ca07617294a9
--- /dev/null
+++ b/openedx/features/offline_content/docs/_images/mobile_offline_content_generation.svg
@@ -0,0 +1,4 @@
+
+
+CMS External media storage Change/create course content Publish changes Generate course xblock files Offline content generation handler Run celery task to generate xblocks for course Checking whether the content of a specific xblock needs to be generated (re- generated) Creating a temporary dir to store xblock content Call offline_view for xblock Rendering xblock HTML file via CMS renderer Copy all related static files (js and css) for the rendered xblock Archive xblock temporary dir, and its deletion Courses offline content Other OeX media Course 1 folder Problem xblock archive HTML xblock archive .... Course 2 folder Problem xblock archive HTML xblock archive .... .... Yes Save xblock archive to the storage Сourse cache updates
\ No newline at end of file
diff --git a/openedx/features/survey_report/migrations/0006_mariadb_uuid_conversion.py b/openedx/features/survey_report/migrations/0006_mariadb_uuid_conversion.py
new file mode 100644
index 000000000000..13f1d284d792
--- /dev/null
+++ b/openedx/features/survey_report/migrations/0006_mariadb_uuid_conversion.py
@@ -0,0 +1,75 @@
+# Generated migration for MariaDB UUID field conversion (Django 5.2)
+"""
+Migration to convert UUIDField from char(32) to uuid type for MariaDB compatibility.
+
+This migration is necessary because Django 5 changed the behavior of UUIDField for MariaDB
+databases from using CharField(32) to using a proper UUID type. This change isn't managed
+automatically, so we need to generate migrations to safely convert the columns.
+
+This migration only executes for MariaDB databases and is a no-op for other backends.
+
+See: https://www.albertyw.com/note/django-5-mariadb-uuidfield
+"""
+
+from django.db import migrations
+
+
+def apply_mariadb_migration(apps, schema_editor):
+ """Apply the migration only for MariaDB databases."""
+ connection = schema_editor.connection
+
+ # Check if this is a MariaDB database
+ if connection.vendor != 'mysql':
+ return
+
+ # Additional check for MariaDB specifically (vs MySQL)
+ with connection.cursor() as cursor:
+ cursor.execute("SELECT VERSION()")
+ version = cursor.fetchone()[0]
+ if 'mariadb' not in version.lower():
+ return
+
+ # Apply the field changes for MariaDB
+ with connection.cursor() as cursor:
+ # The id field is a primary key
+ cursor.execute(
+ "ALTER TABLE survey_report_surveyreportanonymoussiteid "
+ "MODIFY id uuid NOT NULL"
+ )
+
+
+def reverse_mariadb_migration(apps, schema_editor):
+ """Reverse the migration only for MariaDB databases."""
+ connection = schema_editor.connection
+
+ # Check if this is a MariaDB database
+ if connection.vendor != 'mysql':
+ return
+
+ # Additional check for MariaDB specifically (vs MySQL)
+ with connection.cursor() as cursor:
+ cursor.execute("SELECT VERSION()")
+ version = cursor.fetchone()[0]
+ if 'mariadb' not in version.lower():
+ return
+
+ # Reverse the field changes for MariaDB
+ with connection.cursor() as cursor:
+ cursor.execute(
+ "ALTER TABLE survey_report_surveyreportanonymoussiteid "
+ "MODIFY id char(32) NOT NULL"
+ )
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('survey_report', '0005_surveyreportanonymoussiteid'),
+ ]
+
+ operations = [
+ migrations.RunPython(
+ code=apply_mariadb_migration,
+ reverse_code=reverse_mariadb_migration,
+ ),
+ ]
diff --git a/openedx/tests/completion_integration/test_handlers.py b/openedx/tests/completion_integration/test_handlers.py
index 677a7514628e..4522768739ad 100644
--- a/openedx/tests/completion_integration/test_handlers.py
+++ b/openedx/tests/completion_integration/test_handlers.py
@@ -11,7 +11,7 @@
from completion.models import BlockCompletion
from completion.test_utils import CompletionSetUpMixin
from django.test import TestCase
-from pytz import utc
+from zoneinfo import ZoneInfo
from xblock.completable import XBlockCompletionMode
from xblock.core import XBlock
@@ -66,7 +66,7 @@ def call_scorable_block_completion_handler(self, block_key, score_deleted=None):
usage_id=str(block_key),
weighted_earned=0.0,
weighted_possible=3.0,
- modified=datetime.utcnow().replace(tzinfo=utc),
+ modified=datetime.utcnow().replace(tzinfo=ZoneInfo("UTC")),
score_db_table='submissions',
**params
)
@@ -127,7 +127,7 @@ def test_signal_calls_handler(self):
usage_id=str(self.block_key),
weighted_earned=0.0,
weighted_possible=3.0,
- modified=datetime.utcnow().replace(tzinfo=utc),
+ modified=datetime.utcnow().replace(tzinfo=ZoneInfo("UTC")),
score_db_table='submissions',
)
mock_handler.assert_called()
@@ -153,7 +153,7 @@ def test_disabled_handler_does_not_submit_completion(self):
usage_id=str(self.block_key),
weighted_earned=0.0,
weighted_possible=3.0,
- modified=datetime.utcnow().replace(tzinfo=utc),
+ modified=datetime.utcnow().replace(tzinfo=ZoneInfo("UTC")),
score_db_table='submissions',
)
with pytest.raises(BlockCompletion.DoesNotExist):
diff --git a/openedx/tests/xblock_integration/xblock_testcase.py b/openedx/tests/xblock_integration/xblock_testcase.py
index 6f598a342cb1..af2a3410b75f 100644
--- a/openedx/tests/xblock_integration/xblock_testcase.py
+++ b/openedx/tests/xblock_integration/xblock_testcase.py
@@ -44,7 +44,7 @@
import html
from unittest import mock
-import pytz
+from zoneinfo import ZoneInfo
from bs4 import BeautifulSoup
from django.conf import settings
from django.urls import reverse
@@ -199,7 +199,7 @@ def capture_score(user_id, usage_key, score, max_score):
'score': score,
'max_score': max_score})
# Shim a return time, defaults to 1 hour before now
- return datetime.now().replace(tzinfo=pytz.UTC) - timedelta(hours=1)
+ return datetime.now().replace(tzinfo=ZoneInfo("UTC")) - timedelta(hours=1)
self.scores = []
patcher = mock.patch("lms.djangoapps.grades.signals.handlers.set_score", capture_score)
diff --git a/package-lock.json b/package-lock.json
index 6db7903427b4..68f63236fc65 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -19,7 +19,6 @@
"@edx/edx-proctoring": "^4.18.1",
"@edx/frontend-component-cookie-policy-banner": "2.2.0",
"@edx/paragon": "2.6.4",
- "@edx/studio-frontend": "^2.1.0",
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^12.1.5",
"@testing-library/user-event": "^12.8.3",
@@ -89,7 +88,6 @@
"karma-jasmine-html-reporter": "0.2.2",
"karma-junit-reporter": "2.0.1",
"karma-requirejs": "1.1.0",
- "karma-selenium-webdriver-launcher": "github:openedx/karma-selenium-webdriver-launcher#0.0.4-openedx.0",
"karma-sourcemap-loader": "0.4.0",
"karma-spec-reporter": "0.0.20",
"karma-webpack": "^5.0.1",
@@ -104,9 +102,9 @@
}
},
"node_modules/@adobe/css-tools": {
- "version": "4.4.2",
- "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.2.tgz",
- "integrity": "sha512-baYZExFpsdkBNuvGKTKWCwKH57HRZLVtycZS05WTQNVOiXVSeAki3nU35zlRbToeMW8aHlJfyS+1C4BOv27q0A==",
+ "version": "4.4.4",
+ "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.4.tgz",
+ "integrity": "sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg==",
"license": "MIT"
},
"node_modules/@ampproject/remapping": {
@@ -123,23 +121,23 @@
}
},
"node_modules/@babel/code-frame": {
- "version": "7.26.2",
- "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz",
- "integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==",
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz",
+ "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==",
"license": "MIT",
"dependencies": {
- "@babel/helper-validator-identifier": "^7.25.9",
+ "@babel/helper-validator-identifier": "^7.27.1",
"js-tokens": "^4.0.0",
- "picocolors": "^1.0.0"
+ "picocolors": "^1.1.1"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/compat-data": {
- "version": "7.26.8",
- "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.26.8.tgz",
- "integrity": "sha512-oH5UPLMWR3L2wEFLnFJ1TZXqHufiTKAiLfqw5zkhS4dKXLJ10yVztfil/twG8EDTA4F/tvVNw9nOl4ZMslB8rQ==",
+ "version": "7.28.4",
+ "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.4.tgz",
+ "integrity": "sha512-YsmSKC29MJwf0gF8Rjjrg5LQCmyh+j/nD8/eP7f+BeoQTKYqs9RoWbjGOdy0+1Ekr68RJZMUOPVQaQisnIo4Rw==",
"license": "MIT",
"engines": {
"node": ">=6.9.0"
@@ -150,6 +148,7 @@
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.26.0.tgz",
"integrity": "sha512-i1SLeK+DzNnQ3LL/CswPCa/E5u4lh1k6IAEphON8F+cXt0t9euTshDru0q7/IqMa1PMPz5RnHuHscF8/ZJsStg==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"@ampproject/remapping": "^2.2.0",
"@babel/code-frame": "^7.26.0",
@@ -176,15 +175,15 @@
}
},
"node_modules/@babel/generator": {
- "version": "7.27.0",
- "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.27.0.tgz",
- "integrity": "sha512-VybsKvpiN1gU1sdMZIp7FcqphVVKEwcuj02x73uvcHE0PTihx1nlBcowYWhDwjpoAXRv43+gDzyggGnn1XZhVw==",
+ "version": "7.28.3",
+ "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.3.tgz",
+ "integrity": "sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw==",
"license": "MIT",
"dependencies": {
- "@babel/parser": "^7.27.0",
- "@babel/types": "^7.27.0",
- "@jridgewell/gen-mapping": "^0.3.5",
- "@jridgewell/trace-mapping": "^0.3.25",
+ "@babel/parser": "^7.28.3",
+ "@babel/types": "^7.28.2",
+ "@jridgewell/gen-mapping": "^0.3.12",
+ "@jridgewell/trace-mapping": "^0.3.28",
"jsesc": "^3.0.2"
},
"engines": {
@@ -192,25 +191,25 @@
}
},
"node_modules/@babel/helper-annotate-as-pure": {
- "version": "7.25.9",
- "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.25.9.tgz",
- "integrity": "sha512-gv7320KBUFJz1RnylIg5WWYPRXKZ884AGkYpgpWW02TH66Dl+HaC1t1CKd0z3R4b6hdYEcmrNZHUmfCP+1u3/g==",
+ "version": "7.27.3",
+ "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.3.tgz",
+ "integrity": "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==",
"license": "MIT",
"dependencies": {
- "@babel/types": "^7.25.9"
+ "@babel/types": "^7.27.3"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-compilation-targets": {
- "version": "7.27.0",
- "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.0.tgz",
- "integrity": "sha512-LVk7fbXml0H2xH34dFzKQ7TDZ2G4/rVTOrq9V+icbbadjbVxxeFeDsNHv2SrZeWoA+6ZiTyWYWtScEIW07EAcA==",
+ "version": "7.27.2",
+ "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz",
+ "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==",
"license": "MIT",
"dependencies": {
- "@babel/compat-data": "^7.26.8",
- "@babel/helper-validator-option": "^7.25.9",
+ "@babel/compat-data": "^7.27.2",
+ "@babel/helper-validator-option": "^7.27.1",
"browserslist": "^4.24.0",
"lru-cache": "^5.1.1",
"semver": "^6.3.1"
@@ -220,17 +219,17 @@
}
},
"node_modules/@babel/helper-create-class-features-plugin": {
- "version": "7.27.0",
- "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.27.0.tgz",
- "integrity": "sha512-vSGCvMecvFCd/BdpGlhpXYNhhC4ccxyvQWpbGL4CWbvfEoLFWUZuSuf7s9Aw70flgQF+6vptvgK2IfOnKlRmBg==",
+ "version": "7.28.3",
+ "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.28.3.tgz",
+ "integrity": "sha512-V9f6ZFIYSLNEbuGA/92uOvYsGCJNsuA8ESZ4ldc09bWk/j8H8TKiPw8Mk1eG6olpnO0ALHJmYfZvF4MEE4gajg==",
"license": "MIT",
"dependencies": {
- "@babel/helper-annotate-as-pure": "^7.25.9",
- "@babel/helper-member-expression-to-functions": "^7.25.9",
- "@babel/helper-optimise-call-expression": "^7.25.9",
- "@babel/helper-replace-supers": "^7.26.5",
- "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9",
- "@babel/traverse": "^7.27.0",
+ "@babel/helper-annotate-as-pure": "^7.27.3",
+ "@babel/helper-member-expression-to-functions": "^7.27.1",
+ "@babel/helper-optimise-call-expression": "^7.27.1",
+ "@babel/helper-replace-supers": "^7.27.1",
+ "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1",
+ "@babel/traverse": "^7.28.3",
"semver": "^6.3.1"
},
"engines": {
@@ -241,12 +240,12 @@
}
},
"node_modules/@babel/helper-create-regexp-features-plugin": {
- "version": "7.27.0",
- "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.27.0.tgz",
- "integrity": "sha512-fO8l08T76v48BhpNRW/nQ0MxfnSdoSKUJBMjubOAYffsVuGG5qOfMq7N6Es7UJvi7Y8goXXo07EfcHZXDPuELQ==",
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.27.1.tgz",
+ "integrity": "sha512-uVDC72XVf8UbrH5qQTc18Agb8emwjTiZrQE11Nv3CuBEZmVvTwwE9CBUEvHku06gQCAyYf8Nv6ja1IN+6LMbxQ==",
"license": "MIT",
"dependencies": {
- "@babel/helper-annotate-as-pure": "^7.25.9",
+ "@babel/helper-annotate-as-pure": "^7.27.1",
"regexpu-core": "^6.2.0",
"semver": "^6.3.1"
},
@@ -258,56 +257,65 @@
}
},
"node_modules/@babel/helper-define-polyfill-provider": {
- "version": "0.6.4",
- "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.4.tgz",
- "integrity": "sha512-jljfR1rGnXXNWnmQg2K3+bvhkxB51Rl32QRaOTuwwjviGrHzIbSc8+x9CpraDtbT7mfyjXObULP4w/adunNwAw==",
+ "version": "0.6.5",
+ "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.5.tgz",
+ "integrity": "sha512-uJnGFcPsWQK8fvjgGP5LZUZZsYGIoPeRjSF5PGwrelYgq7Q15/Ft9NGFp1zglwgIv//W0uG4BevRuSJRyylZPg==",
"license": "MIT",
"dependencies": {
- "@babel/helper-compilation-targets": "^7.22.6",
- "@babel/helper-plugin-utils": "^7.22.5",
- "debug": "^4.1.1",
+ "@babel/helper-compilation-targets": "^7.27.2",
+ "@babel/helper-plugin-utils": "^7.27.1",
+ "debug": "^4.4.1",
"lodash.debounce": "^4.0.8",
- "resolve": "^1.14.2"
+ "resolve": "^1.22.10"
},
"peerDependencies": {
"@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0"
}
},
+ "node_modules/@babel/helper-globals": {
+ "version": "7.28.0",
+ "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz",
+ "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
"node_modules/@babel/helper-member-expression-to-functions": {
- "version": "7.25.9",
- "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.25.9.tgz",
- "integrity": "sha512-wbfdZ9w5vk0C0oyHqAJbc62+vet5prjj01jjJ8sKn3j9h3MQQlflEdXYvuqRWjHnM12coDEqiC1IRCi0U/EKwQ==",
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.27.1.tgz",
+ "integrity": "sha512-E5chM8eWjTp/aNoVpcbfM7mLxu9XGLWYise2eBKGQomAk/Mb4XoxyqXTZbuTohbsl8EKqdlMhnDI2CCLfcs9wA==",
"license": "MIT",
"dependencies": {
- "@babel/traverse": "^7.25.9",
- "@babel/types": "^7.25.9"
+ "@babel/traverse": "^7.27.1",
+ "@babel/types": "^7.27.1"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-module-imports": {
- "version": "7.25.9",
- "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.25.9.tgz",
- "integrity": "sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw==",
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz",
+ "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==",
"license": "MIT",
"dependencies": {
- "@babel/traverse": "^7.25.9",
- "@babel/types": "^7.25.9"
+ "@babel/traverse": "^7.27.1",
+ "@babel/types": "^7.27.1"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-module-transforms": {
- "version": "7.26.0",
- "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.26.0.tgz",
- "integrity": "sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw==",
+ "version": "7.28.3",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz",
+ "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==",
"license": "MIT",
"dependencies": {
- "@babel/helper-module-imports": "^7.25.9",
- "@babel/helper-validator-identifier": "^7.25.9",
- "@babel/traverse": "^7.25.9"
+ "@babel/helper-module-imports": "^7.27.1",
+ "@babel/helper-validator-identifier": "^7.27.1",
+ "@babel/traverse": "^7.28.3"
},
"engines": {
"node": ">=6.9.0"
@@ -317,35 +325,35 @@
}
},
"node_modules/@babel/helper-optimise-call-expression": {
- "version": "7.25.9",
- "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.25.9.tgz",
- "integrity": "sha512-FIpuNaz5ow8VyrYcnXQTDRGvV6tTjkNtCK/RYNDXGSLlUD6cBuQTSw43CShGxjvfBTfcUA/r6UhUCbtYqkhcuQ==",
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.27.1.tgz",
+ "integrity": "sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw==",
"license": "MIT",
"dependencies": {
- "@babel/types": "^7.25.9"
+ "@babel/types": "^7.27.1"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-plugin-utils": {
- "version": "7.26.5",
- "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.26.5.tgz",
- "integrity": "sha512-RS+jZcRdZdRFzMyr+wcsaqOmld1/EqTghfaBGQQd/WnRdzdlvSZ//kF7U8VQTxf1ynZ4cjUcYgjVGx13ewNPMg==",
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz",
+ "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==",
"license": "MIT",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-remap-async-to-generator": {
- "version": "7.25.9",
- "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.25.9.tgz",
- "integrity": "sha512-IZtukuUeBbhgOcaW2s06OXTzVNJR0ybm4W5xC1opWFFJMZbwRj5LCk+ByYH7WdZPZTt8KnFwA8pvjN2yqcPlgw==",
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.27.1.tgz",
+ "integrity": "sha512-7fiA521aVw8lSPeI4ZOD3vRFkoqkJcS+z4hFo82bFSH/2tNd6eJ5qCVMS5OzDmZh/kaHQeBaeyxK6wljcPtveA==",
"license": "MIT",
"dependencies": {
- "@babel/helper-annotate-as-pure": "^7.25.9",
- "@babel/helper-wrap-function": "^7.25.9",
- "@babel/traverse": "^7.25.9"
+ "@babel/helper-annotate-as-pure": "^7.27.1",
+ "@babel/helper-wrap-function": "^7.27.1",
+ "@babel/traverse": "^7.27.1"
},
"engines": {
"node": ">=6.9.0"
@@ -355,14 +363,14 @@
}
},
"node_modules/@babel/helper-replace-supers": {
- "version": "7.26.5",
- "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.26.5.tgz",
- "integrity": "sha512-bJ6iIVdYX1YooY2X7w1q6VITt+LnUILtNk7zT78ykuwStx8BauCzxvFqFaHjOpW1bVnSUM1PN1f0p5P21wHxvg==",
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.27.1.tgz",
+ "integrity": "sha512-7EHz6qDZc8RYS5ElPoShMheWvEgERonFCs7IAonWLLUTXW59DP14bCZt89/GKyreYn8g3S83m21FelHKbeDCKA==",
"license": "MIT",
"dependencies": {
- "@babel/helper-member-expression-to-functions": "^7.25.9",
- "@babel/helper-optimise-call-expression": "^7.25.9",
- "@babel/traverse": "^7.26.5"
+ "@babel/helper-member-expression-to-functions": "^7.27.1",
+ "@babel/helper-optimise-call-expression": "^7.27.1",
+ "@babel/traverse": "^7.27.1"
},
"engines": {
"node": ">=6.9.0"
@@ -372,79 +380,79 @@
}
},
"node_modules/@babel/helper-skip-transparent-expression-wrappers": {
- "version": "7.25.9",
- "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.25.9.tgz",
- "integrity": "sha512-K4Du3BFa3gvyhzgPcntrkDgZzQaq6uozzcpGbOO1OEJaI+EJdqWIMTLgFgQf6lrfiDFo5FU+BxKepI9RmZqahA==",
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.27.1.tgz",
+ "integrity": "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg==",
"license": "MIT",
"dependencies": {
- "@babel/traverse": "^7.25.9",
- "@babel/types": "^7.25.9"
+ "@babel/traverse": "^7.27.1",
+ "@babel/types": "^7.27.1"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-string-parser": {
- "version": "7.25.9",
- "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz",
- "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==",
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
+ "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
"license": "MIT",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-validator-identifier": {
- "version": "7.25.9",
- "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz",
- "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==",
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz",
+ "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==",
"license": "MIT",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-validator-option": {
- "version": "7.25.9",
- "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.25.9.tgz",
- "integrity": "sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw==",
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz",
+ "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==",
"license": "MIT",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-wrap-function": {
- "version": "7.25.9",
- "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.25.9.tgz",
- "integrity": "sha512-ETzz9UTjQSTmw39GboatdymDq4XIQbR8ySgVrylRhPOFpsd+JrKHIuF0de7GCWmem+T4uC5z7EZguod7Wj4A4g==",
+ "version": "7.28.3",
+ "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.28.3.tgz",
+ "integrity": "sha512-zdf983tNfLZFletc0RRXYrHrucBEg95NIFMkn6K9dbeMYnsgHaSBGcQqdsCSStG2PYwRre0Qc2NNSCXbG+xc6g==",
"license": "MIT",
"dependencies": {
- "@babel/template": "^7.25.9",
- "@babel/traverse": "^7.25.9",
- "@babel/types": "^7.25.9"
+ "@babel/template": "^7.27.2",
+ "@babel/traverse": "^7.28.3",
+ "@babel/types": "^7.28.2"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helpers": {
- "version": "7.27.0",
- "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.0.tgz",
- "integrity": "sha512-U5eyP/CTFPuNE3qk+WZMxFkp/4zUzdceQlfzf7DdGdhp+Fezd7HD+i8Y24ZuTMKX3wQBld449jijbGq6OdGNQg==",
+ "version": "7.28.4",
+ "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz",
+ "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==",
"license": "MIT",
"dependencies": {
- "@babel/template": "^7.27.0",
- "@babel/types": "^7.27.0"
+ "@babel/template": "^7.27.2",
+ "@babel/types": "^7.28.4"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/parser": {
- "version": "7.27.0",
- "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.0.tgz",
- "integrity": "sha512-iaepho73/2Pz7w2eMS0Q5f83+0RKI7i4xmiYeBmDzfRVbQtTOG7Ts0S4HzJVsTMGI9keU8rNfuZr8DKfSt7Yyg==",
+ "version": "7.28.4",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.4.tgz",
+ "integrity": "sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==",
"license": "MIT",
"dependencies": {
- "@babel/types": "^7.27.0"
+ "@babel/types": "^7.28.4"
},
"bin": {
"parser": "bin/babel-parser.js"
@@ -454,13 +462,13 @@
}
},
"node_modules/@babel/plugin-bugfix-firefox-class-in-computed-class-key": {
- "version": "7.25.9",
- "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.25.9.tgz",
- "integrity": "sha512-ZkRyVkThtxQ/J6nv3JFYv1RYY+JT5BvU0y3k5bWrmuG4woXypRa4PXmm9RhOwodRkYFWqC0C0cqcJ4OqR7kW+g==",
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.27.1.tgz",
+ "integrity": "sha512-QPG3C9cCVRQLxAVwmefEmwdTanECuUBMQZ/ym5kiw3XKCGA7qkuQLcjWWHcrD/GKbn/WmJwaezfuuAOcyKlRPA==",
"license": "MIT",
"dependencies": {
- "@babel/helper-plugin-utils": "^7.25.9",
- "@babel/traverse": "^7.25.9"
+ "@babel/helper-plugin-utils": "^7.27.1",
+ "@babel/traverse": "^7.27.1"
},
"engines": {
"node": ">=6.9.0"
@@ -470,12 +478,12 @@
}
},
"node_modules/@babel/plugin-bugfix-safari-class-field-initializer-scope": {
- "version": "7.25.9",
- "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-class-field-initializer-scope/-/plugin-bugfix-safari-class-field-initializer-scope-7.25.9.tgz",
- "integrity": "sha512-MrGRLZxLD/Zjj0gdU15dfs+HH/OXvnw/U4jJD8vpcP2CJQapPEv1IWwjc/qMg7ItBlPwSv1hRBbb7LeuANdcnw==",
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-class-field-initializer-scope/-/plugin-bugfix-safari-class-field-initializer-scope-7.27.1.tgz",
+ "integrity": "sha512-qNeq3bCKnGgLkEXUuFry6dPlGfCdQNZbn7yUAPCInwAJHMU7THJfrBSozkcWq5sNM6RcF3S8XyQL2A52KNR9IA==",
"license": "MIT",
"dependencies": {
- "@babel/helper-plugin-utils": "^7.25.9"
+ "@babel/helper-plugin-utils": "^7.27.1"
},
"engines": {
"node": ">=6.9.0"
@@ -485,12 +493,12 @@
}
},
"node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": {
- "version": "7.25.9",
- "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.25.9.tgz",
- "integrity": "sha512-2qUwwfAFpJLZqxd02YW9btUCZHl+RFvdDkNfZwaIJrvB8Tesjsk8pEQkTvGwZXLqXUx/2oyY3ySRhm6HOXuCug==",
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.27.1.tgz",
+ "integrity": "sha512-g4L7OYun04N1WyqMNjldFwlfPCLVkgB54A/YCXICZYBsvJJE3kByKv9c9+R/nAfmIfjl2rKYLNyMHboYbZaWaA==",
"license": "MIT",
"dependencies": {
- "@babel/helper-plugin-utils": "^7.25.9"
+ "@babel/helper-plugin-utils": "^7.27.1"
},
"engines": {
"node": ">=6.9.0"
@@ -500,14 +508,14 @@
}
},
"node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": {
- "version": "7.25.9",
- "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.25.9.tgz",
- "integrity": "sha512-6xWgLZTJXwilVjlnV7ospI3xi+sl8lN8rXXbBD6vYn3UYDlGsag8wrZkKcSI8G6KgqKP7vNFaDgeDnfAABq61g==",
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.27.1.tgz",
+ "integrity": "sha512-oO02gcONcD5O1iTLi/6frMJBIwWEHceWGSGqrpCmEL8nogiS6J9PBlE48CaK20/Jx1LuRml9aDftLgdjXT8+Cw==",
"license": "MIT",
"dependencies": {
- "@babel/helper-plugin-utils": "^7.25.9",
- "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9",
- "@babel/plugin-transform-optional-chaining": "^7.25.9"
+ "@babel/helper-plugin-utils": "^7.27.1",
+ "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1",
+ "@babel/plugin-transform-optional-chaining": "^7.27.1"
},
"engines": {
"node": ">=6.9.0"
@@ -517,13 +525,13 @@
}
},
"node_modules/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": {
- "version": "7.25.9",
- "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.25.9.tgz",
- "integrity": "sha512-aLnMXYPnzwwqhYSCyXfKkIkYgJ8zv9RK+roo9DkTXz38ynIhd9XCbN08s3MGvqL2MYGVUGdRQLL/JqBIeJhJBg==",
+ "version": "7.28.3",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.28.3.tgz",
+ "integrity": "sha512-b6YTX108evsvE4YgWyQ921ZAFFQm3Bn+CA3+ZXlNVnPhx+UfsVURoPjfGAPCjBgrqo30yX/C2nZGX96DxvR9Iw==",
"license": "MIT",
"dependencies": {
- "@babel/helper-plugin-utils": "^7.25.9",
- "@babel/traverse": "^7.25.9"
+ "@babel/helper-plugin-utils": "^7.27.1",
+ "@babel/traverse": "^7.28.3"
},
"engines": {
"node": ">=6.9.0"
@@ -620,12 +628,12 @@
}
},
"node_modules/@babel/plugin-syntax-import-assertions": {
- "version": "7.26.0",
- "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.26.0.tgz",
- "integrity": "sha512-QCWT5Hh830hK5EQa7XzuqIkQU9tT/whqbDz7kuaZMHFl1inRRg7JnuAEOQ0Ur0QUl0NufCk1msK2BeY79Aj/eg==",
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.27.1.tgz",
+ "integrity": "sha512-UT/Jrhw57xg4ILHLFnzFpPDlMbcdEicaAtjPQpbj9wa8T4r5KVWCimHcL/460g8Ht0DMxDyjsLgiWSkVjnwPFg==",
"license": "MIT",
"dependencies": {
- "@babel/helper-plugin-utils": "^7.25.9"
+ "@babel/helper-plugin-utils": "^7.27.1"
},
"engines": {
"node": ">=6.9.0"
@@ -635,12 +643,12 @@
}
},
"node_modules/@babel/plugin-syntax-import-attributes": {
- "version": "7.26.0",
- "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.26.0.tgz",
- "integrity": "sha512-e2dttdsJ1ZTpi3B9UYGLw41hifAubg19AtCu/2I/F1QNVclOBr1dYpTdmdyZ84Xiz43BS/tCUkMAZNLv12Pi+A==",
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.27.1.tgz",
+ "integrity": "sha512-oFT0FrKHgF53f4vOsZGi2Hh3I35PfSmVs4IBFLFj4dnafP+hIWDLg3VyKmUHfLoLHlyxY4C7DGtmHuJgn+IGww==",
"license": "MIT",
"dependencies": {
- "@babel/helper-plugin-utils": "^7.25.9"
+ "@babel/helper-plugin-utils": "^7.27.1"
},
"engines": {
"node": ">=6.9.0"
@@ -676,12 +684,12 @@
}
},
"node_modules/@babel/plugin-syntax-jsx": {
- "version": "7.25.9",
- "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.25.9.tgz",
- "integrity": "sha512-ld6oezHQMZsZfp6pWtbjaNDF2tiiCYYDqQszHt5VV437lewP9aSi2Of99CK0D0XB21k7FLgnLcmQKyKzynfeAA==",
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.27.1.tgz",
+ "integrity": "sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w==",
"license": "MIT",
"dependencies": {
- "@babel/helper-plugin-utils": "^7.25.9"
+ "@babel/helper-plugin-utils": "^7.27.1"
},
"engines": {
"node": ">=6.9.0"
@@ -800,13 +808,13 @@
}
},
"node_modules/@babel/plugin-syntax-typescript": {
- "version": "7.25.9",
- "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.25.9.tgz",
- "integrity": "sha512-hjMgRy5hb8uJJjUcdWunWVcoi9bGpJp8p5Ol1229PoN6aytsLwNMgmdftO23wnCLMfVmTwZDWMPNq/D1SY60JQ==",
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.27.1.tgz",
+ "integrity": "sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@babel/helper-plugin-utils": "^7.25.9"
+ "@babel/helper-plugin-utils": "^7.27.1"
},
"engines": {
"node": ">=6.9.0"
@@ -832,12 +840,12 @@
}
},
"node_modules/@babel/plugin-transform-arrow-functions": {
- "version": "7.25.9",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.25.9.tgz",
- "integrity": "sha512-6jmooXYIwn9ca5/RylZADJ+EnSxVUS5sjeJ9UPk6RWRzXCmOJCy6dqItPJFpw2cuCangPK4OYr5uhGKcmrm5Qg==",
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.27.1.tgz",
+ "integrity": "sha512-8Z4TGic6xW70FKThA5HYEKKyBpOOsucTOD1DjU3fZxDg+K3zBJcXMFnt/4yQiZnf5+MiOMSXQ9PaEK/Ilh1DeA==",
"license": "MIT",
"dependencies": {
- "@babel/helper-plugin-utils": "^7.25.9"
+ "@babel/helper-plugin-utils": "^7.27.1"
},
"engines": {
"node": ">=6.9.0"
@@ -847,14 +855,14 @@
}
},
"node_modules/@babel/plugin-transform-async-generator-functions": {
- "version": "7.26.8",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.26.8.tgz",
- "integrity": "sha512-He9Ej2X7tNf2zdKMAGOsmg2MrFc+hfoAhd3po4cWfo/NWjzEAKa0oQruj1ROVUdl0e6fb6/kE/G3SSxE0lRJOg==",
+ "version": "7.28.0",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.28.0.tgz",
+ "integrity": "sha512-BEOdvX4+M765icNPZeidyADIvQ1m1gmunXufXxvRESy/jNNyfovIqUyE7MVgGBjWktCoJlzvFA1To2O4ymIO3Q==",
"license": "MIT",
"dependencies": {
- "@babel/helper-plugin-utils": "^7.26.5",
- "@babel/helper-remap-async-to-generator": "^7.25.9",
- "@babel/traverse": "^7.26.8"
+ "@babel/helper-plugin-utils": "^7.27.1",
+ "@babel/helper-remap-async-to-generator": "^7.27.1",
+ "@babel/traverse": "^7.28.0"
},
"engines": {
"node": ">=6.9.0"
@@ -864,14 +872,14 @@
}
},
"node_modules/@babel/plugin-transform-async-to-generator": {
- "version": "7.25.9",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.25.9.tgz",
- "integrity": "sha512-NT7Ejn7Z/LjUH0Gv5KsBCxh7BH3fbLTV0ptHvpeMvrt3cPThHfJfst9Wrb7S8EvJ7vRTFI7z+VAvFVEQn/m5zQ==",
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.27.1.tgz",
+ "integrity": "sha512-NREkZsZVJS4xmTr8qzE5y8AfIPqsdQfRuUiLRTEzb7Qii8iFWCyDKaUV2c0rCuh4ljDZ98ALHP/PetiBV2nddA==",
"license": "MIT",
"dependencies": {
- "@babel/helper-module-imports": "^7.25.9",
- "@babel/helper-plugin-utils": "^7.25.9",
- "@babel/helper-remap-async-to-generator": "^7.25.9"
+ "@babel/helper-module-imports": "^7.27.1",
+ "@babel/helper-plugin-utils": "^7.27.1",
+ "@babel/helper-remap-async-to-generator": "^7.27.1"
},
"engines": {
"node": ">=6.9.0"
@@ -881,12 +889,12 @@
}
},
"node_modules/@babel/plugin-transform-block-scoped-functions": {
- "version": "7.26.5",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.26.5.tgz",
- "integrity": "sha512-chuTSY+hq09+/f5lMj8ZSYgCFpppV2CbYrhNFJ1BFoXpiWPnnAb7R0MqrafCpN8E1+YRrtM1MXZHJdIx8B6rMQ==",
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.27.1.tgz",
+ "integrity": "sha512-cnqkuOtZLapWYZUYM5rVIdv1nXYuFVIltZ6ZJ7nIj585QsjKM5dhL2Fu/lICXZ1OyIAFc7Qy+bvDAtTXqGrlhg==",
"license": "MIT",
"dependencies": {
- "@babel/helper-plugin-utils": "^7.26.5"
+ "@babel/helper-plugin-utils": "^7.27.1"
},
"engines": {
"node": ">=6.9.0"
@@ -896,12 +904,12 @@
}
},
"node_modules/@babel/plugin-transform-block-scoping": {
- "version": "7.27.0",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.27.0.tgz",
- "integrity": "sha512-u1jGphZ8uDI2Pj/HJj6YQ6XQLZCNjOlprjxB5SVz6rq2T6SwAR+CdrWK0CP7F+9rDVMXdB0+r6Am5G5aobOjAQ==",
+ "version": "7.28.4",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.28.4.tgz",
+ "integrity": "sha512-1yxmvN0MJHOhPVmAsmoW5liWwoILobu/d/ShymZmj867bAdxGbehIrew1DuLpw2Ukv+qDSSPQdYW1dLNE7t11A==",
"license": "MIT",
"dependencies": {
- "@babel/helper-plugin-utils": "^7.26.5"
+ "@babel/helper-plugin-utils": "^7.27.1"
},
"engines": {
"node": ">=6.9.0"
@@ -911,13 +919,13 @@
}
},
"node_modules/@babel/plugin-transform-class-properties": {
- "version": "7.25.9",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.25.9.tgz",
- "integrity": "sha512-bbMAII8GRSkcd0h0b4X+36GksxuheLFjP65ul9w6C3KgAamI3JqErNgSrosX6ZPj+Mpim5VvEbawXxJCyEUV3Q==",
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.27.1.tgz",
+ "integrity": "sha512-D0VcalChDMtuRvJIu3U/fwWjf8ZMykz5iZsg77Nuj821vCKI3zCyRLwRdWbsuJ/uRwZhZ002QtCqIkwC/ZkvbA==",
"license": "MIT",
"dependencies": {
- "@babel/helper-create-class-features-plugin": "^7.25.9",
- "@babel/helper-plugin-utils": "^7.25.9"
+ "@babel/helper-create-class-features-plugin": "^7.27.1",
+ "@babel/helper-plugin-utils": "^7.27.1"
},
"engines": {
"node": ">=6.9.0"
@@ -927,13 +935,13 @@
}
},
"node_modules/@babel/plugin-transform-class-static-block": {
- "version": "7.26.0",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.26.0.tgz",
- "integrity": "sha512-6J2APTs7BDDm+UMqP1useWqhcRAXo0WIoVj26N7kPFB6S73Lgvyka4KTZYIxtgYXiN5HTyRObA72N2iu628iTQ==",
+ "version": "7.28.3",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.28.3.tgz",
+ "integrity": "sha512-LtPXlBbRoc4Njl/oh1CeD/3jC+atytbnf/UqLoqTDcEYGUPj022+rvfkbDYieUrSj3CaV4yHDByPE+T2HwfsJg==",
"license": "MIT",
"dependencies": {
- "@babel/helper-create-class-features-plugin": "^7.25.9",
- "@babel/helper-plugin-utils": "^7.25.9"
+ "@babel/helper-create-class-features-plugin": "^7.28.3",
+ "@babel/helper-plugin-utils": "^7.27.1"
},
"engines": {
"node": ">=6.9.0"
@@ -943,17 +951,17 @@
}
},
"node_modules/@babel/plugin-transform-classes": {
- "version": "7.25.9",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.25.9.tgz",
- "integrity": "sha512-mD8APIXmseE7oZvZgGABDyM34GUmK45Um2TXiBUt7PnuAxrgoSVf123qUzPxEr/+/BHrRn5NMZCdE2m/1F8DGg==",
+ "version": "7.28.4",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.28.4.tgz",
+ "integrity": "sha512-cFOlhIYPBv/iBoc+KS3M6et2XPtbT2HiCRfBXWtfpc9OAyostldxIf9YAYB6ypURBBbx+Qv6nyrLzASfJe+hBA==",
"license": "MIT",
"dependencies": {
- "@babel/helper-annotate-as-pure": "^7.25.9",
- "@babel/helper-compilation-targets": "^7.25.9",
- "@babel/helper-plugin-utils": "^7.25.9",
- "@babel/helper-replace-supers": "^7.25.9",
- "@babel/traverse": "^7.25.9",
- "globals": "^11.1.0"
+ "@babel/helper-annotate-as-pure": "^7.27.3",
+ "@babel/helper-compilation-targets": "^7.27.2",
+ "@babel/helper-globals": "^7.28.0",
+ "@babel/helper-plugin-utils": "^7.27.1",
+ "@babel/helper-replace-supers": "^7.27.1",
+ "@babel/traverse": "^7.28.4"
},
"engines": {
"node": ">=6.9.0"
@@ -963,13 +971,13 @@
}
},
"node_modules/@babel/plugin-transform-computed-properties": {
- "version": "7.25.9",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.25.9.tgz",
- "integrity": "sha512-HnBegGqXZR12xbcTHlJ9HGxw1OniltT26J5YpfruGqtUHlz/xKf/G2ak9e+t0rVqrjXa9WOhvYPz1ERfMj23AA==",
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.27.1.tgz",
+ "integrity": "sha512-lj9PGWvMTVksbWiDT2tW68zGS/cyo4AkZ/QTp0sQT0mjPopCmrSkzxeXkznjqBxzDI6TclZhOJbBmbBLjuOZUw==",
"license": "MIT",
"dependencies": {
- "@babel/helper-plugin-utils": "^7.25.9",
- "@babel/template": "^7.25.9"
+ "@babel/helper-plugin-utils": "^7.27.1",
+ "@babel/template": "^7.27.1"
},
"engines": {
"node": ">=6.9.0"
@@ -979,12 +987,13 @@
}
},
"node_modules/@babel/plugin-transform-destructuring": {
- "version": "7.25.9",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.25.9.tgz",
- "integrity": "sha512-WkCGb/3ZxXepmMiX101nnGiU+1CAdut8oHyEOHxkKuS1qKpU2SMXE2uSvfz8PBuLd49V6LEsbtyPhWC7fnkgvQ==",
+ "version": "7.28.0",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.28.0.tgz",
+ "integrity": "sha512-v1nrSMBiKcodhsyJ4Gf+Z0U/yawmJDBOTpEB3mcQY52r9RIyPneGyAS/yM6seP/8I+mWI3elOMtT5dB8GJVs+A==",
"license": "MIT",
"dependencies": {
- "@babel/helper-plugin-utils": "^7.25.9"
+ "@babel/helper-plugin-utils": "^7.27.1",
+ "@babel/traverse": "^7.28.0"
},
"engines": {
"node": ">=6.9.0"
@@ -994,13 +1003,13 @@
}
},
"node_modules/@babel/plugin-transform-dotall-regex": {
- "version": "7.25.9",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.25.9.tgz",
- "integrity": "sha512-t7ZQ7g5trIgSRYhI9pIJtRl64KHotutUJsh4Eze5l7olJv+mRSg4/MmbZ0tv1eeqRbdvo/+trvJD/Oc5DmW2cA==",
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.27.1.tgz",
+ "integrity": "sha512-gEbkDVGRvjj7+T1ivxrfgygpT7GUd4vmODtYpbs0gZATdkX8/iSnOtZSxiZnsgm1YjTgjI6VKBGSJJevkrclzw==",
"license": "MIT",
"dependencies": {
- "@babel/helper-create-regexp-features-plugin": "^7.25.9",
- "@babel/helper-plugin-utils": "^7.25.9"
+ "@babel/helper-create-regexp-features-plugin": "^7.27.1",
+ "@babel/helper-plugin-utils": "^7.27.1"
},
"engines": {
"node": ">=6.9.0"
@@ -1010,12 +1019,12 @@
}
},
"node_modules/@babel/plugin-transform-duplicate-keys": {
- "version": "7.25.9",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.25.9.tgz",
- "integrity": "sha512-LZxhJ6dvBb/f3x8xwWIuyiAHy56nrRG3PeYTpBkkzkYRRQ6tJLu68lEF5VIqMUZiAV7a8+Tb78nEoMCMcqjXBw==",
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.27.1.tgz",
+ "integrity": "sha512-MTyJk98sHvSs+cvZ4nOauwTTG1JeonDjSGvGGUNHreGQns+Mpt6WX/dVzWBHgg+dYZhkC4X+zTDfkTU+Vy9y7Q==",
"license": "MIT",
"dependencies": {
- "@babel/helper-plugin-utils": "^7.25.9"
+ "@babel/helper-plugin-utils": "^7.27.1"
},
"engines": {
"node": ">=6.9.0"
@@ -1025,13 +1034,13 @@
}
},
"node_modules/@babel/plugin-transform-duplicate-named-capturing-groups-regex": {
- "version": "7.25.9",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-named-capturing-groups-regex/-/plugin-transform-duplicate-named-capturing-groups-regex-7.25.9.tgz",
- "integrity": "sha512-0UfuJS0EsXbRvKnwcLjFtJy/Sxc5J5jhLHnFhy7u4zih97Hz6tJkLU+O+FMMrNZrosUPxDi6sYxJ/EA8jDiAog==",
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-named-capturing-groups-regex/-/plugin-transform-duplicate-named-capturing-groups-regex-7.27.1.tgz",
+ "integrity": "sha512-hkGcueTEzuhB30B3eJCbCYeCaaEQOmQR0AdvzpD4LoN0GXMWzzGSuRrxR2xTnCrvNbVwK9N6/jQ92GSLfiZWoQ==",
"license": "MIT",
"dependencies": {
- "@babel/helper-create-regexp-features-plugin": "^7.25.9",
- "@babel/helper-plugin-utils": "^7.25.9"
+ "@babel/helper-create-regexp-features-plugin": "^7.27.1",
+ "@babel/helper-plugin-utils": "^7.27.1"
},
"engines": {
"node": ">=6.9.0"
@@ -1041,12 +1050,28 @@
}
},
"node_modules/@babel/plugin-transform-dynamic-import": {
- "version": "7.25.9",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.25.9.tgz",
- "integrity": "sha512-GCggjexbmSLaFhqsojeugBpeaRIgWNTcgKVq/0qIteFEqY2A+b9QidYadrWlnbWQUrW5fn+mCvf3tr7OeBFTyg==",
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.27.1.tgz",
+ "integrity": "sha512-MHzkWQcEmjzzVW9j2q8LGjwGWpG2mjwaaB0BNQwst3FIjqsg8Ct/mIZlvSPJvfi9y2AC8mi/ktxbFVL9pZ1I4A==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-explicit-resource-management": {
+ "version": "7.28.0",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-explicit-resource-management/-/plugin-transform-explicit-resource-management-7.28.0.tgz",
+ "integrity": "sha512-K8nhUcn3f6iB+P3gwCv/no7OdzOZQcKchW6N389V6PD8NUWKZHzndOd9sPDVbMoBsbmjMqlB4L9fm+fEFNVlwQ==",
"license": "MIT",
"dependencies": {
- "@babel/helper-plugin-utils": "^7.25.9"
+ "@babel/helper-plugin-utils": "^7.27.1",
+ "@babel/plugin-transform-destructuring": "^7.28.0"
},
"engines": {
"node": ">=6.9.0"
@@ -1056,12 +1081,12 @@
}
},
"node_modules/@babel/plugin-transform-exponentiation-operator": {
- "version": "7.26.3",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.26.3.tgz",
- "integrity": "sha512-7CAHcQ58z2chuXPWblnn1K6rLDnDWieghSOEmqQsrBenH0P9InCUtOJYD89pvngljmZlJcz3fcmgYsXFNGa1ZQ==",
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.27.1.tgz",
+ "integrity": "sha512-uspvXnhHvGKf2r4VVtBpeFnuDWsJLQ6MF6lGJLC89jBR1uoVeqM416AZtTuhTezOfgHicpJQmoD5YUakO/YmXQ==",
"license": "MIT",
"dependencies": {
- "@babel/helper-plugin-utils": "^7.25.9"
+ "@babel/helper-plugin-utils": "^7.27.1"
},
"engines": {
"node": ">=6.9.0"
@@ -1071,12 +1096,12 @@
}
},
"node_modules/@babel/plugin-transform-export-namespace-from": {
- "version": "7.25.9",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.25.9.tgz",
- "integrity": "sha512-2NsEz+CxzJIVOPx2o9UsW1rXLqtChtLoVnwYHHiB04wS5sgn7mrV45fWMBX0Kk+ub9uXytVYfNP2HjbVbCB3Ww==",
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.27.1.tgz",
+ "integrity": "sha512-tQvHWSZ3/jH2xuq/vZDy0jNn+ZdXJeM8gHvX4lnJmsc3+50yPlWdZXIc5ay+umX+2/tJIqHqiEqcJvxlmIvRvQ==",
"license": "MIT",
"dependencies": {
- "@babel/helper-plugin-utils": "^7.25.9"
+ "@babel/helper-plugin-utils": "^7.27.1"
},
"engines": {
"node": ">=6.9.0"
@@ -1086,13 +1111,13 @@
}
},
"node_modules/@babel/plugin-transform-for-of": {
- "version": "7.26.9",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.26.9.tgz",
- "integrity": "sha512-Hry8AusVm8LW5BVFgiyUReuoGzPUpdHQQqJY5bZnbbf+ngOHWuCuYFKw/BqaaWlvEUrF91HMhDtEaI1hZzNbLg==",
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.27.1.tgz",
+ "integrity": "sha512-BfbWFFEJFQzLCQ5N8VocnCtA8J1CLkNTe2Ms2wocj75dd6VpiqS5Z5quTYcUoo4Yq+DN0rtikODccuv7RU81sw==",
"license": "MIT",
"dependencies": {
- "@babel/helper-plugin-utils": "^7.26.5",
- "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9"
+ "@babel/helper-plugin-utils": "^7.27.1",
+ "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1"
},
"engines": {
"node": ">=6.9.0"
@@ -1102,14 +1127,14 @@
}
},
"node_modules/@babel/plugin-transform-function-name": {
- "version": "7.25.9",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.25.9.tgz",
- "integrity": "sha512-8lP+Yxjv14Vc5MuWBpJsoUCd3hD6V9DgBon2FVYL4jJgbnVQ9fTgYmonchzZJOVNgzEgbxp4OwAf6xz6M/14XA==",
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.27.1.tgz",
+ "integrity": "sha512-1bQeydJF9Nr1eBCMMbC+hdwmRlsv5XYOMu03YSWFwNs0HsAmtSxxF1fyuYPqemVldVyFmlCU7w8UE14LupUSZQ==",
"license": "MIT",
"dependencies": {
- "@babel/helper-compilation-targets": "^7.25.9",
- "@babel/helper-plugin-utils": "^7.25.9",
- "@babel/traverse": "^7.25.9"
+ "@babel/helper-compilation-targets": "^7.27.1",
+ "@babel/helper-plugin-utils": "^7.27.1",
+ "@babel/traverse": "^7.27.1"
},
"engines": {
"node": ">=6.9.0"
@@ -1119,12 +1144,12 @@
}
},
"node_modules/@babel/plugin-transform-json-strings": {
- "version": "7.25.9",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.25.9.tgz",
- "integrity": "sha512-xoTMk0WXceiiIvsaquQQUaLLXSW1KJ159KP87VilruQm0LNNGxWzahxSS6T6i4Zg3ezp4vA4zuwiNUR53qmQAw==",
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.27.1.tgz",
+ "integrity": "sha512-6WVLVJiTjqcQauBhn1LkICsR2H+zm62I3h9faTDKt1qP4jn2o72tSvqMwtGFKGTpojce0gJs+76eZ2uCHRZh0Q==",
"license": "MIT",
"dependencies": {
- "@babel/helper-plugin-utils": "^7.25.9"
+ "@babel/helper-plugin-utils": "^7.27.1"
},
"engines": {
"node": ">=6.9.0"
@@ -1134,12 +1159,12 @@
}
},
"node_modules/@babel/plugin-transform-literals": {
- "version": "7.25.9",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.25.9.tgz",
- "integrity": "sha512-9N7+2lFziW8W9pBl2TzaNht3+pgMIRP74zizeCSrtnSKVdUl8mAjjOP2OOVQAfZ881P2cNjDj1uAMEdeD50nuQ==",
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.27.1.tgz",
+ "integrity": "sha512-0HCFSepIpLTkLcsi86GG3mTUzxV5jpmbv97hTETW3yzrAij8aqlD36toB1D0daVFJM8NK6GvKO0gslVQmm+zZA==",
"license": "MIT",
"dependencies": {
- "@babel/helper-plugin-utils": "^7.25.9"
+ "@babel/helper-plugin-utils": "^7.27.1"
},
"engines": {
"node": ">=6.9.0"
@@ -1149,12 +1174,12 @@
}
},
"node_modules/@babel/plugin-transform-logical-assignment-operators": {
- "version": "7.25.9",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.25.9.tgz",
- "integrity": "sha512-wI4wRAzGko551Y8eVf6iOY9EouIDTtPb0ByZx+ktDGHwv6bHFimrgJM/2T021txPZ2s4c7bqvHbd+vXG6K948Q==",
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.27.1.tgz",
+ "integrity": "sha512-SJvDs5dXxiae4FbSL1aBJlG4wvl594N6YEVVn9e3JGulwioy6z3oPjx/sQBO3Y4NwUu5HNix6KJ3wBZoewcdbw==",
"license": "MIT",
"dependencies": {
- "@babel/helper-plugin-utils": "^7.25.9"
+ "@babel/helper-plugin-utils": "^7.27.1"
},
"engines": {
"node": ">=6.9.0"
@@ -1164,12 +1189,12 @@
}
},
"node_modules/@babel/plugin-transform-member-expression-literals": {
- "version": "7.25.9",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.25.9.tgz",
- "integrity": "sha512-PYazBVfofCQkkMzh2P6IdIUaCEWni3iYEerAsRWuVd8+jlM1S9S9cz1dF9hIzyoZ8IA3+OwVYIp9v9e+GbgZhA==",
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.27.1.tgz",
+ "integrity": "sha512-hqoBX4dcZ1I33jCSWcXrP+1Ku7kdqXf1oeah7ooKOIiAdKQ+uqftgCFNOSzA5AMS2XIHEYeGFg4cKRCdpxzVOQ==",
"license": "MIT",
"dependencies": {
- "@babel/helper-plugin-utils": "^7.25.9"
+ "@babel/helper-plugin-utils": "^7.27.1"
},
"engines": {
"node": ">=6.9.0"
@@ -1179,13 +1204,13 @@
}
},
"node_modules/@babel/plugin-transform-modules-amd": {
- "version": "7.25.9",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.25.9.tgz",
- "integrity": "sha512-g5T11tnI36jVClQlMlt4qKDLlWnG5pP9CSM4GhdRciTNMRgkfpo5cR6b4rGIOYPgRRuFAvwjPQ/Yk+ql4dyhbw==",
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.27.1.tgz",
+ "integrity": "sha512-iCsytMg/N9/oFq6n+gFTvUYDZQOMK5kEdeYxmxt91fcJGycfxVP9CnrxoliM0oumFERba2i8ZtwRUCMhvP1LnA==",
"license": "MIT",
"dependencies": {
- "@babel/helper-module-transforms": "^7.25.9",
- "@babel/helper-plugin-utils": "^7.25.9"
+ "@babel/helper-module-transforms": "^7.27.1",
+ "@babel/helper-plugin-utils": "^7.27.1"
},
"engines": {
"node": ">=6.9.0"
@@ -1195,13 +1220,13 @@
}
},
"node_modules/@babel/plugin-transform-modules-commonjs": {
- "version": "7.26.3",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.26.3.tgz",
- "integrity": "sha512-MgR55l4q9KddUDITEzEFYn5ZsGDXMSsU9E+kh7fjRXTIC3RHqfCo8RPRbyReYJh44HQ/yomFkqbOFohXvDCiIQ==",
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.27.1.tgz",
+ "integrity": "sha512-OJguuwlTYlN0gBZFRPqwOGNWssZjfIUdS7HMYtN8c1KmwpwHFBwTeFZrg9XZa+DFTitWOW5iTAG7tyCUPsCCyw==",
"license": "MIT",
"dependencies": {
- "@babel/helper-module-transforms": "^7.26.0",
- "@babel/helper-plugin-utils": "^7.25.9"
+ "@babel/helper-module-transforms": "^7.27.1",
+ "@babel/helper-plugin-utils": "^7.27.1"
},
"engines": {
"node": ">=6.9.0"
@@ -1211,15 +1236,15 @@
}
},
"node_modules/@babel/plugin-transform-modules-systemjs": {
- "version": "7.25.9",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.25.9.tgz",
- "integrity": "sha512-hyss7iIlH/zLHaehT+xwiymtPOpsiwIIRlCAOwBB04ta5Tt+lNItADdlXw3jAWZ96VJ2jlhl/c+PNIQPKNfvcA==",
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.27.1.tgz",
+ "integrity": "sha512-w5N1XzsRbc0PQStASMksmUeqECuzKuTJer7kFagK8AXgpCMkeDMO5S+aaFb7A51ZYDF7XI34qsTX+fkHiIm5yA==",
"license": "MIT",
"dependencies": {
- "@babel/helper-module-transforms": "^7.25.9",
- "@babel/helper-plugin-utils": "^7.25.9",
- "@babel/helper-validator-identifier": "^7.25.9",
- "@babel/traverse": "^7.25.9"
+ "@babel/helper-module-transforms": "^7.27.1",
+ "@babel/helper-plugin-utils": "^7.27.1",
+ "@babel/helper-validator-identifier": "^7.27.1",
+ "@babel/traverse": "^7.27.1"
},
"engines": {
"node": ">=6.9.0"
@@ -1229,13 +1254,13 @@
}
},
"node_modules/@babel/plugin-transform-modules-umd": {
- "version": "7.25.9",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.25.9.tgz",
- "integrity": "sha512-bS9MVObUgE7ww36HEfwe6g9WakQ0KF07mQF74uuXdkoziUPfKyu/nIm663kz//e5O1nPInPFx36z7WJmJ4yNEw==",
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.27.1.tgz",
+ "integrity": "sha512-iQBE/xC5BV1OxJbp6WG7jq9IWiD+xxlZhLrdwpPkTX3ydmXdvoCpyfJN7acaIBZaOqTfr76pgzqBJflNbeRK+w==",
"license": "MIT",
"dependencies": {
- "@babel/helper-module-transforms": "^7.25.9",
- "@babel/helper-plugin-utils": "^7.25.9"
+ "@babel/helper-module-transforms": "^7.27.1",
+ "@babel/helper-plugin-utils": "^7.27.1"
},
"engines": {
"node": ">=6.9.0"
@@ -1245,13 +1270,13 @@
}
},
"node_modules/@babel/plugin-transform-named-capturing-groups-regex": {
- "version": "7.25.9",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.25.9.tgz",
- "integrity": "sha512-oqB6WHdKTGl3q/ItQhpLSnWWOpjUJLsOCLVyeFgeTktkBSCiurvPOsyt93gibI9CmuKvTUEtWmG5VhZD+5T/KA==",
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.27.1.tgz",
+ "integrity": "sha512-SstR5JYy8ddZvD6MhV0tM/j16Qds4mIpJTOd1Yu9J9pJjH93bxHECF7pgtc28XvkzTD6Pxcm/0Z73Hvk7kb3Ng==",
"license": "MIT",
"dependencies": {
- "@babel/helper-create-regexp-features-plugin": "^7.25.9",
- "@babel/helper-plugin-utils": "^7.25.9"
+ "@babel/helper-create-regexp-features-plugin": "^7.27.1",
+ "@babel/helper-plugin-utils": "^7.27.1"
},
"engines": {
"node": ">=6.9.0"
@@ -1261,12 +1286,12 @@
}
},
"node_modules/@babel/plugin-transform-new-target": {
- "version": "7.25.9",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.25.9.tgz",
- "integrity": "sha512-U/3p8X1yCSoKyUj2eOBIx3FOn6pElFOKvAAGf8HTtItuPyB+ZeOqfn+mvTtg9ZlOAjsPdK3ayQEjqHjU/yLeVQ==",
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.27.1.tgz",
+ "integrity": "sha512-f6PiYeqXQ05lYq3TIfIDu/MtliKUbNwkGApPUvyo6+tc7uaR4cPjPe7DFPr15Uyycg2lZU6btZ575CuQoYh7MQ==",
"license": "MIT",
"dependencies": {
- "@babel/helper-plugin-utils": "^7.25.9"
+ "@babel/helper-plugin-utils": "^7.27.1"
},
"engines": {
"node": ">=6.9.0"
@@ -1276,12 +1301,12 @@
}
},
"node_modules/@babel/plugin-transform-nullish-coalescing-operator": {
- "version": "7.26.6",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.26.6.tgz",
- "integrity": "sha512-CKW8Vu+uUZneQCPtXmSBUC6NCAUdya26hWCElAWh5mVSlSRsmiCPUUDKb3Z0szng1hiAJa098Hkhg9o4SE35Qw==",
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.27.1.tgz",
+ "integrity": "sha512-aGZh6xMo6q9vq1JGcw58lZ1Z0+i0xB2x0XaauNIUXd6O1xXc3RwoWEBlsTQrY4KQ9Jf0s5rgD6SiNkaUdJegTA==",
"license": "MIT",
"dependencies": {
- "@babel/helper-plugin-utils": "^7.26.5"
+ "@babel/helper-plugin-utils": "^7.27.1"
},
"engines": {
"node": ">=6.9.0"
@@ -1291,12 +1316,12 @@
}
},
"node_modules/@babel/plugin-transform-numeric-separator": {
- "version": "7.25.9",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.25.9.tgz",
- "integrity": "sha512-TlprrJ1GBZ3r6s96Yq8gEQv82s8/5HnCVHtEJScUj90thHQbwe+E5MLhi2bbNHBEJuzrvltXSru+BUxHDoog7Q==",
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.27.1.tgz",
+ "integrity": "sha512-fdPKAcujuvEChxDBJ5c+0BTaS6revLV7CJL08e4m3de8qJfNIuCc2nc7XJYOjBoTMJeqSmwXJ0ypE14RCjLwaw==",
"license": "MIT",
"dependencies": {
- "@babel/helper-plugin-utils": "^7.25.9"
+ "@babel/helper-plugin-utils": "^7.27.1"
},
"engines": {
"node": ">=6.9.0"
@@ -1306,12 +1331,12 @@
}
},
"node_modules/@babel/plugin-transform-object-assign": {
- "version": "7.25.9",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-assign/-/plugin-transform-object-assign-7.25.9.tgz",
- "integrity": "sha512-I/Vl1aQnPsrrn837oLbo+VQtkNcjuuiATqwmuweg4fTauwHHQoxyjmjjOVKyO8OaTxgqYTKW3LuQsykXjDf5Ag==",
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-assign/-/plugin-transform-object-assign-7.27.1.tgz",
+ "integrity": "sha512-LP6tsnirA6iy13uBKiYgjJsfQrodmlSrpZModtlo1Vk8sOO68gfo7dfA9TGJyEgxTiO7czK4EGZm8FJEZtk4kQ==",
"license": "MIT",
"dependencies": {
- "@babel/helper-plugin-utils": "^7.25.9"
+ "@babel/helper-plugin-utils": "^7.27.1"
},
"engines": {
"node": ">=6.9.0"
@@ -1321,14 +1346,16 @@
}
},
"node_modules/@babel/plugin-transform-object-rest-spread": {
- "version": "7.25.9",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.25.9.tgz",
- "integrity": "sha512-fSaXafEE9CVHPweLYw4J0emp1t8zYTXyzN3UuG+lylqkvYd7RMrsOQ8TYx5RF231be0vqtFC6jnx3UmpJmKBYg==",
+ "version": "7.28.4",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.28.4.tgz",
+ "integrity": "sha512-373KA2HQzKhQCYiRVIRr+3MjpCObqzDlyrM6u4I201wL8Mp2wHf7uB8GhDwis03k2ti8Zr65Zyyqs1xOxUF/Ew==",
"license": "MIT",
"dependencies": {
- "@babel/helper-compilation-targets": "^7.25.9",
- "@babel/helper-plugin-utils": "^7.25.9",
- "@babel/plugin-transform-parameters": "^7.25.9"
+ "@babel/helper-compilation-targets": "^7.27.2",
+ "@babel/helper-plugin-utils": "^7.27.1",
+ "@babel/plugin-transform-destructuring": "^7.28.0",
+ "@babel/plugin-transform-parameters": "^7.27.7",
+ "@babel/traverse": "^7.28.4"
},
"engines": {
"node": ">=6.9.0"
@@ -1338,13 +1365,13 @@
}
},
"node_modules/@babel/plugin-transform-object-super": {
- "version": "7.25.9",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.25.9.tgz",
- "integrity": "sha512-Kj/Gh+Rw2RNLbCK1VAWj2U48yxxqL2x0k10nPtSdRa0O2xnHXalD0s+o1A6a0W43gJ00ANo38jxkQreckOzv5A==",
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.27.1.tgz",
+ "integrity": "sha512-SFy8S9plRPbIcxlJ8A6mT/CxFdJx/c04JEctz4jf8YZaVS2px34j7NXRrlGlHkN/M2gnpL37ZpGRGVFLd3l8Ng==",
"license": "MIT",
"dependencies": {
- "@babel/helper-plugin-utils": "^7.25.9",
- "@babel/helper-replace-supers": "^7.25.9"
+ "@babel/helper-plugin-utils": "^7.27.1",
+ "@babel/helper-replace-supers": "^7.27.1"
},
"engines": {
"node": ">=6.9.0"
@@ -1354,12 +1381,12 @@
}
},
"node_modules/@babel/plugin-transform-optional-catch-binding": {
- "version": "7.25.9",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.25.9.tgz",
- "integrity": "sha512-qM/6m6hQZzDcZF3onzIhZeDHDO43bkNNlOX0i8n3lR6zLbu0GN2d8qfM/IERJZYauhAHSLHy39NF0Ctdvcid7g==",
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.27.1.tgz",
+ "integrity": "sha512-txEAEKzYrHEX4xSZN4kJ+OfKXFVSWKB2ZxM9dpcE3wT7smwkNmXo5ORRlVzMVdJbD+Q8ILTgSD7959uj+3Dm3Q==",
"license": "MIT",
"dependencies": {
- "@babel/helper-plugin-utils": "^7.25.9"
+ "@babel/helper-plugin-utils": "^7.27.1"
},
"engines": {
"node": ">=6.9.0"
@@ -1369,13 +1396,13 @@
}
},
"node_modules/@babel/plugin-transform-optional-chaining": {
- "version": "7.25.9",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.25.9.tgz",
- "integrity": "sha512-6AvV0FsLULbpnXeBjrY4dmWF8F7gf8QnvTEoO/wX/5xm/xE1Xo8oPuD3MPS+KS9f9XBEAWN7X1aWr4z9HdOr7A==",
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.27.1.tgz",
+ "integrity": "sha512-BQmKPPIuc8EkZgNKsv0X4bPmOoayeu4F1YCwx2/CfmDSXDbp7GnzlUH+/ul5VGfRg1AoFPsrIThlEBj2xb4CAg==",
"license": "MIT",
"dependencies": {
- "@babel/helper-plugin-utils": "^7.25.9",
- "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9"
+ "@babel/helper-plugin-utils": "^7.27.1",
+ "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1"
},
"engines": {
"node": ">=6.9.0"
@@ -1385,12 +1412,12 @@
}
},
"node_modules/@babel/plugin-transform-parameters": {
- "version": "7.25.9",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.25.9.tgz",
- "integrity": "sha512-wzz6MKwpnshBAiRmn4jR8LYz/g8Ksg0o80XmwZDlordjwEk9SxBzTWC7F5ef1jhbrbOW2DJ5J6ayRukrJmnr0g==",
+ "version": "7.27.7",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.27.7.tgz",
+ "integrity": "sha512-qBkYTYCb76RRxUM6CcZA5KRu8K4SM8ajzVeUgVdMVO9NN9uI/GaVmBg/WKJJGnNokV9SY8FxNOVWGXzqzUidBg==",
"license": "MIT",
"dependencies": {
- "@babel/helper-plugin-utils": "^7.25.9"
+ "@babel/helper-plugin-utils": "^7.27.1"
},
"engines": {
"node": ">=6.9.0"
@@ -1400,13 +1427,13 @@
}
},
"node_modules/@babel/plugin-transform-private-methods": {
- "version": "7.25.9",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.25.9.tgz",
- "integrity": "sha512-D/JUozNpQLAPUVusvqMxyvjzllRaF8/nSrP1s2YGQT/W4LHK4xxsMcHjhOGTS01mp9Hda8nswb+FblLdJornQw==",
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.27.1.tgz",
+ "integrity": "sha512-10FVt+X55AjRAYI9BrdISN9/AQWHqldOeZDUoLyif1Kn05a56xVBXb8ZouL8pZ9jem8QpXaOt8TS7RHUIS+GPA==",
"license": "MIT",
"dependencies": {
- "@babel/helper-create-class-features-plugin": "^7.25.9",
- "@babel/helper-plugin-utils": "^7.25.9"
+ "@babel/helper-create-class-features-plugin": "^7.27.1",
+ "@babel/helper-plugin-utils": "^7.27.1"
},
"engines": {
"node": ">=6.9.0"
@@ -1416,14 +1443,14 @@
}
},
"node_modules/@babel/plugin-transform-private-property-in-object": {
- "version": "7.25.9",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.25.9.tgz",
- "integrity": "sha512-Evf3kcMqzXA3xfYJmZ9Pg1OvKdtqsDMSWBDzZOPLvHiTt36E75jLDQo5w1gtRU95Q4E5PDttrTf25Fw8d/uWLw==",
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.27.1.tgz",
+ "integrity": "sha512-5J+IhqTi1XPa0DXF83jYOaARrX+41gOewWbkPyjMNRDqgOCqdffGh8L3f/Ek5utaEBZExjSAzcyjmV9SSAWObQ==",
"license": "MIT",
"dependencies": {
- "@babel/helper-annotate-as-pure": "^7.25.9",
- "@babel/helper-create-class-features-plugin": "^7.25.9",
- "@babel/helper-plugin-utils": "^7.25.9"
+ "@babel/helper-annotate-as-pure": "^7.27.1",
+ "@babel/helper-create-class-features-plugin": "^7.27.1",
+ "@babel/helper-plugin-utils": "^7.27.1"
},
"engines": {
"node": ">=6.9.0"
@@ -1433,12 +1460,12 @@
}
},
"node_modules/@babel/plugin-transform-property-literals": {
- "version": "7.25.9",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.25.9.tgz",
- "integrity": "sha512-IvIUeV5KrS/VPavfSM/Iu+RE6llrHrYIKY1yfCzyO/lMXHQ+p7uGhonmGVisv6tSBSVgWzMBohTcvkC9vQcQFA==",
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.27.1.tgz",
+ "integrity": "sha512-oThy3BCuCha8kDZ8ZkgOg2exvPYUlprMukKQXI1r1pJ47NCvxfkEy8vK+r/hT9nF0Aa4H1WUPZZjHTFtAhGfmQ==",
"license": "MIT",
"dependencies": {
- "@babel/helper-plugin-utils": "^7.25.9"
+ "@babel/helper-plugin-utils": "^7.27.1"
},
"engines": {
"node": ">=6.9.0"
@@ -1448,12 +1475,12 @@
}
},
"node_modules/@babel/plugin-transform-react-display-name": {
- "version": "7.25.9",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.25.9.tgz",
- "integrity": "sha512-KJfMlYIUxQB1CJfO3e0+h0ZHWOTLCPP115Awhaz8U0Zpq36Gl/cXlpoyMRnUWlhNUBAzldnCiAZNvCDj7CrKxQ==",
+ "version": "7.28.0",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.28.0.tgz",
+ "integrity": "sha512-D6Eujc2zMxKjfa4Zxl4GHMsmhKKZ9VpcqIchJLvwTxad9zWIYulwYItBovpDOoNLISpcZSXoDJ5gaGbQUDqViA==",
"license": "MIT",
"dependencies": {
- "@babel/helper-plugin-utils": "^7.25.9"
+ "@babel/helper-plugin-utils": "^7.27.1"
},
"engines": {
"node": ">=6.9.0"
@@ -1463,16 +1490,16 @@
}
},
"node_modules/@babel/plugin-transform-react-jsx": {
- "version": "7.25.9",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.25.9.tgz",
- "integrity": "sha512-s5XwpQYCqGerXl+Pu6VDL3x0j2d82eiV77UJ8a2mDHAW7j9SWRqQ2y1fNo1Z74CdcYipl5Z41zvjj4Nfzq36rw==",
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.27.1.tgz",
+ "integrity": "sha512-2KH4LWGSrJIkVf5tSiBFYuXDAoWRq2MMwgivCf+93dd0GQi8RXLjKA/0EvRnVV5G0hrHczsquXuD01L8s6dmBw==",
"license": "MIT",
"dependencies": {
- "@babel/helper-annotate-as-pure": "^7.25.9",
- "@babel/helper-module-imports": "^7.25.9",
- "@babel/helper-plugin-utils": "^7.25.9",
- "@babel/plugin-syntax-jsx": "^7.25.9",
- "@babel/types": "^7.25.9"
+ "@babel/helper-annotate-as-pure": "^7.27.1",
+ "@babel/helper-module-imports": "^7.27.1",
+ "@babel/helper-plugin-utils": "^7.27.1",
+ "@babel/plugin-syntax-jsx": "^7.27.1",
+ "@babel/types": "^7.27.1"
},
"engines": {
"node": ">=6.9.0"
@@ -1482,12 +1509,12 @@
}
},
"node_modules/@babel/plugin-transform-react-jsx-development": {
- "version": "7.25.9",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.25.9.tgz",
- "integrity": "sha512-9mj6rm7XVYs4mdLIpbZnHOYdpW42uoiBCTVowg7sP1thUOiANgMb4UtpRivR0pp5iL+ocvUv7X4mZgFRpJEzGw==",
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.27.1.tgz",
+ "integrity": "sha512-ykDdF5yI4f1WrAolLqeF3hmYU12j9ntLQl/AOG1HAS21jxyg1Q0/J/tpREuYLfatGdGmXp/3yS0ZA76kOlVq9Q==",
"license": "MIT",
"dependencies": {
- "@babel/plugin-transform-react-jsx": "^7.25.9"
+ "@babel/plugin-transform-react-jsx": "^7.27.1"
},
"engines": {
"node": ">=6.9.0"
@@ -1497,13 +1524,13 @@
}
},
"node_modules/@babel/plugin-transform-react-pure-annotations": {
- "version": "7.25.9",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.25.9.tgz",
- "integrity": "sha512-KQ/Takk3T8Qzj5TppkS1be588lkbTp5uj7w6a0LeQaTMSckU/wK0oJ/pih+T690tkgI5jfmg2TqDJvd41Sj1Cg==",
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.27.1.tgz",
+ "integrity": "sha512-JfuinvDOsD9FVMTHpzA/pBLisxpv1aSf+OIV8lgH3MuWrks19R27e6a6DipIg4aX1Zm9Wpb04p8wljfKrVSnPA==",
"license": "MIT",
"dependencies": {
- "@babel/helper-annotate-as-pure": "^7.25.9",
- "@babel/helper-plugin-utils": "^7.25.9"
+ "@babel/helper-annotate-as-pure": "^7.27.1",
+ "@babel/helper-plugin-utils": "^7.27.1"
},
"engines": {
"node": ">=6.9.0"
@@ -1513,13 +1540,12 @@
}
},
"node_modules/@babel/plugin-transform-regenerator": {
- "version": "7.27.0",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.27.0.tgz",
- "integrity": "sha512-LX/vCajUJQDqE7Aum/ELUMZAY19+cDpghxrnyt5I1tV6X5PyC86AOoWXWFYFeIvauyeSA6/ktn4tQVn/3ZifsA==",
+ "version": "7.28.4",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.28.4.tgz",
+ "integrity": "sha512-+ZEdQlBoRg9m2NnzvEeLgtvBMO4tkFBw5SQIUgLICgTrumLoU7lr+Oghi6km2PFj+dbUt2u1oby2w3BDO9YQnA==",
"license": "MIT",
"dependencies": {
- "@babel/helper-plugin-utils": "^7.26.5",
- "regenerator-transform": "^0.15.2"
+ "@babel/helper-plugin-utils": "^7.27.1"
},
"engines": {
"node": ">=6.9.0"
@@ -1529,13 +1555,13 @@
}
},
"node_modules/@babel/plugin-transform-regexp-modifiers": {
- "version": "7.26.0",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regexp-modifiers/-/plugin-transform-regexp-modifiers-7.26.0.tgz",
- "integrity": "sha512-vN6saax7lrA2yA/Pak3sCxuD6F5InBjn9IcrIKQPjpsLvuHYLVroTxjdlVRHjjBWxKOqIwpTXDkOssYT4BFdRw==",
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regexp-modifiers/-/plugin-transform-regexp-modifiers-7.27.1.tgz",
+ "integrity": "sha512-TtEciroaiODtXvLZv4rmfMhkCv8jx3wgKpL68PuiPh2M4fvz5jhsA7697N1gMvkvr/JTF13DrFYyEbY9U7cVPA==",
"license": "MIT",
"dependencies": {
- "@babel/helper-create-regexp-features-plugin": "^7.25.9",
- "@babel/helper-plugin-utils": "^7.25.9"
+ "@babel/helper-create-regexp-features-plugin": "^7.27.1",
+ "@babel/helper-plugin-utils": "^7.27.1"
},
"engines": {
"node": ">=6.9.0"
@@ -1545,12 +1571,12 @@
}
},
"node_modules/@babel/plugin-transform-reserved-words": {
- "version": "7.25.9",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.25.9.tgz",
- "integrity": "sha512-7DL7DKYjn5Su++4RXu8puKZm2XBPHyjWLUidaPEkCUBbE7IPcsrkRHggAOOKydH1dASWdcUBxrkOGNxUv5P3Jg==",
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.27.1.tgz",
+ "integrity": "sha512-V2ABPHIJX4kC7HegLkYoDpfg9PVmuWy/i6vUM5eGK22bx4YVFD3M5F0QQnWQoDs6AGsUWTVOopBiMFQgHaSkVw==",
"license": "MIT",
"dependencies": {
- "@babel/helper-plugin-utils": "^7.25.9"
+ "@babel/helper-plugin-utils": "^7.27.1"
},
"engines": {
"node": ">=6.9.0"
@@ -1560,12 +1586,12 @@
}
},
"node_modules/@babel/plugin-transform-shorthand-properties": {
- "version": "7.25.9",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.25.9.tgz",
- "integrity": "sha512-MUv6t0FhO5qHnS/W8XCbHmiRWOphNufpE1IVxhK5kuN3Td9FT1x4rx4K42s3RYdMXCXpfWkGSbCSd0Z64xA7Ng==",
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.27.1.tgz",
+ "integrity": "sha512-N/wH1vcn4oYawbJ13Y/FxcQrWk63jhfNa7jef0ih7PHSIHX2LB7GWE1rkPrOnka9kwMxb6hMl19p7lidA+EHmQ==",
"license": "MIT",
"dependencies": {
- "@babel/helper-plugin-utils": "^7.25.9"
+ "@babel/helper-plugin-utils": "^7.27.1"
},
"engines": {
"node": ">=6.9.0"
@@ -1575,13 +1601,13 @@
}
},
"node_modules/@babel/plugin-transform-spread": {
- "version": "7.25.9",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.25.9.tgz",
- "integrity": "sha512-oNknIB0TbURU5pqJFVbOOFspVlrpVwo2H1+HUIsVDvp5VauGGDP1ZEvO8Nn5xyMEs3dakajOxlmkNW7kNgSm6A==",
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.27.1.tgz",
+ "integrity": "sha512-kpb3HUqaILBJcRFVhFUs6Trdd4mkrzcGXss+6/mxUd273PfbWqSDHRzMT2234gIg2QYfAjvXLSquP1xECSg09Q==",
"license": "MIT",
"dependencies": {
- "@babel/helper-plugin-utils": "^7.25.9",
- "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9"
+ "@babel/helper-plugin-utils": "^7.27.1",
+ "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1"
},
"engines": {
"node": ">=6.9.0"
@@ -1591,12 +1617,12 @@
}
},
"node_modules/@babel/plugin-transform-sticky-regex": {
- "version": "7.25.9",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.25.9.tgz",
- "integrity": "sha512-WqBUSgeVwucYDP9U/xNRQam7xV8W5Zf+6Eo7T2SRVUFlhRiMNFdFz58u0KZmCVVqs2i7SHgpRnAhzRNmKfi2uA==",
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.27.1.tgz",
+ "integrity": "sha512-lhInBO5bi/Kowe2/aLdBAawijx+q1pQzicSgnkB6dUPc1+RC8QmJHKf2OjvU+NZWitguJHEaEmbV6VWEouT58g==",
"license": "MIT",
"dependencies": {
- "@babel/helper-plugin-utils": "^7.25.9"
+ "@babel/helper-plugin-utils": "^7.27.1"
},
"engines": {
"node": ">=6.9.0"
@@ -1606,12 +1632,12 @@
}
},
"node_modules/@babel/plugin-transform-template-literals": {
- "version": "7.26.8",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.26.8.tgz",
- "integrity": "sha512-OmGDL5/J0CJPJZTHZbi2XpO0tyT2Ia7fzpW5GURwdtp2X3fMmN8au/ej6peC/T33/+CRiIpA8Krse8hFGVmT5Q==",
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.27.1.tgz",
+ "integrity": "sha512-fBJKiV7F2DxZUkg5EtHKXQdbsbURW3DZKQUWphDum0uRP6eHGGa/He9mc0mypL680pb+e/lDIthRohlv8NCHkg==",
"license": "MIT",
"dependencies": {
- "@babel/helper-plugin-utils": "^7.26.5"
+ "@babel/helper-plugin-utils": "^7.27.1"
},
"engines": {
"node": ">=6.9.0"
@@ -1621,12 +1647,12 @@
}
},
"node_modules/@babel/plugin-transform-typeof-symbol": {
- "version": "7.27.0",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.27.0.tgz",
- "integrity": "sha512-+LLkxA9rKJpNoGsbLnAgOCdESl73vwYn+V6b+5wHbrE7OGKVDPHIQvbFSzqE6rwqaCw2RE+zdJrlLkcf8YOA0w==",
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.27.1.tgz",
+ "integrity": "sha512-RiSILC+nRJM7FY5srIyc4/fGIwUhyDuuBSdWn4y6yT6gm652DpCHZjIipgn6B7MQ1ITOUnAKWixEUjQRIBIcLw==",
"license": "MIT",
"dependencies": {
- "@babel/helper-plugin-utils": "^7.26.5"
+ "@babel/helper-plugin-utils": "^7.27.1"
},
"engines": {
"node": ">=6.9.0"
@@ -1636,12 +1662,12 @@
}
},
"node_modules/@babel/plugin-transform-unicode-escapes": {
- "version": "7.25.9",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.25.9.tgz",
- "integrity": "sha512-s5EDrE6bW97LtxOcGj1Khcx5AaXwiMmi4toFWRDP9/y0Woo6pXC+iyPu/KuhKtfSrNFd7jJB+/fkOtZy6aIC6Q==",
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.27.1.tgz",
+ "integrity": "sha512-Ysg4v6AmF26k9vpfFuTZg8HRfVWzsh1kVfowA23y9j/Gu6dOuahdUVhkLqpObp3JIv27MLSii6noRnuKN8H0Mg==",
"license": "MIT",
"dependencies": {
- "@babel/helper-plugin-utils": "^7.25.9"
+ "@babel/helper-plugin-utils": "^7.27.1"
},
"engines": {
"node": ">=6.9.0"
@@ -1651,13 +1677,13 @@
}
},
"node_modules/@babel/plugin-transform-unicode-property-regex": {
- "version": "7.25.9",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.25.9.tgz",
- "integrity": "sha512-Jt2d8Ga+QwRluxRQ307Vlxa6dMrYEMZCgGxoPR8V52rxPyldHu3hdlHspxaqYmE7oID5+kB+UKUB/eWS+DkkWg==",
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.27.1.tgz",
+ "integrity": "sha512-uW20S39PnaTImxp39O5qFlHLS9LJEmANjMG7SxIhap8rCHqu0Ik+tLEPX5DKmHn6CsWQ7j3lix2tFOa5YtL12Q==",
"license": "MIT",
"dependencies": {
- "@babel/helper-create-regexp-features-plugin": "^7.25.9",
- "@babel/helper-plugin-utils": "^7.25.9"
+ "@babel/helper-create-regexp-features-plugin": "^7.27.1",
+ "@babel/helper-plugin-utils": "^7.27.1"
},
"engines": {
"node": ">=6.9.0"
@@ -1667,13 +1693,13 @@
}
},
"node_modules/@babel/plugin-transform-unicode-regex": {
- "version": "7.25.9",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.25.9.tgz",
- "integrity": "sha512-yoxstj7Rg9dlNn9UQxzk4fcNivwv4nUYz7fYXBaKxvw/lnmPuOm/ikoELygbYq68Bls3D/D+NBPHiLwZdZZ4HA==",
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.27.1.tgz",
+ "integrity": "sha512-xvINq24TRojDuyt6JGtHmkVkrfVV3FPT16uytxImLeBZqW3/H52yN+kM1MGuyPkIQxrzKwPHs5U/MP3qKyzkGw==",
"license": "MIT",
"dependencies": {
- "@babel/helper-create-regexp-features-plugin": "^7.25.9",
- "@babel/helper-plugin-utils": "^7.25.9"
+ "@babel/helper-create-regexp-features-plugin": "^7.27.1",
+ "@babel/helper-plugin-utils": "^7.27.1"
},
"engines": {
"node": ">=6.9.0"
@@ -1683,13 +1709,13 @@
}
},
"node_modules/@babel/plugin-transform-unicode-sets-regex": {
- "version": "7.25.9",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.25.9.tgz",
- "integrity": "sha512-8BYqO3GeVNHtx69fdPshN3fnzUNLrWdHhk/icSwigksJGczKSizZ+Z6SBCxTs723Fr5VSNorTIK7a+R2tISvwQ==",
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.27.1.tgz",
+ "integrity": "sha512-EtkOujbc4cgvb0mlpQefi4NTPBzhSIevblFevACNLUspmrALgmEBdL/XfnyyITfd8fKBZrZys92zOWcik7j9Tw==",
"license": "MIT",
"dependencies": {
- "@babel/helper-create-regexp-features-plugin": "^7.25.9",
- "@babel/helper-plugin-utils": "^7.25.9"
+ "@babel/helper-create-regexp-features-plugin": "^7.27.1",
+ "@babel/helper-plugin-utils": "^7.27.1"
},
"engines": {
"node": ">=6.9.0"
@@ -1698,91 +1724,81 @@
"@babel/core": "^7.0.0"
}
},
- "node_modules/@babel/polyfill": {
- "version": "7.12.1",
- "resolved": "https://registry.npmjs.org/@babel/polyfill/-/polyfill-7.12.1.tgz",
- "integrity": "sha512-X0pi0V6gxLi6lFZpGmeNa4zxtwEmCs42isWLNjZZDE0Y8yVfgu0T2OAHlzBbdYlqbW/YXVvoBHpATEM+goCj8g==",
- "deprecated": "🚨 This package has been deprecated in favor of separate inclusion of a polyfill and regenerator-runtime (when needed). See the @babel/polyfill docs (https://babeljs.io/docs/en/babel-polyfill) for more information.",
- "license": "MIT",
- "dependencies": {
- "core-js": "^2.6.5",
- "regenerator-runtime": "^0.13.4"
- }
- },
"node_modules/@babel/preset-env": {
- "version": "7.26.9",
- "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.26.9.tgz",
- "integrity": "sha512-vX3qPGE8sEKEAZCWk05k3cpTAE3/nOYca++JA+Rd0z2NCNzabmYvEiSShKzm10zdquOIAVXsy2Ei/DTW34KlKQ==",
- "license": "MIT",
- "dependencies": {
- "@babel/compat-data": "^7.26.8",
- "@babel/helper-compilation-targets": "^7.26.5",
- "@babel/helper-plugin-utils": "^7.26.5",
- "@babel/helper-validator-option": "^7.25.9",
- "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.25.9",
- "@babel/plugin-bugfix-safari-class-field-initializer-scope": "^7.25.9",
- "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.25.9",
- "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.25.9",
- "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.25.9",
+ "version": "7.28.3",
+ "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.28.3.tgz",
+ "integrity": "sha512-ROiDcM+GbYVPYBOeCR6uBXKkQpBExLl8k9HO1ygXEyds39j+vCCsjmj7S8GOniZQlEs81QlkdJZe76IpLSiqpg==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/compat-data": "^7.28.0",
+ "@babel/helper-compilation-targets": "^7.27.2",
+ "@babel/helper-plugin-utils": "^7.27.1",
+ "@babel/helper-validator-option": "^7.27.1",
+ "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.27.1",
+ "@babel/plugin-bugfix-safari-class-field-initializer-scope": "^7.27.1",
+ "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.27.1",
+ "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.27.1",
+ "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.28.3",
"@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2",
- "@babel/plugin-syntax-import-assertions": "^7.26.0",
- "@babel/plugin-syntax-import-attributes": "^7.26.0",
+ "@babel/plugin-syntax-import-assertions": "^7.27.1",
+ "@babel/plugin-syntax-import-attributes": "^7.27.1",
"@babel/plugin-syntax-unicode-sets-regex": "^7.18.6",
- "@babel/plugin-transform-arrow-functions": "^7.25.9",
- "@babel/plugin-transform-async-generator-functions": "^7.26.8",
- "@babel/plugin-transform-async-to-generator": "^7.25.9",
- "@babel/plugin-transform-block-scoped-functions": "^7.26.5",
- "@babel/plugin-transform-block-scoping": "^7.25.9",
- "@babel/plugin-transform-class-properties": "^7.25.9",
- "@babel/plugin-transform-class-static-block": "^7.26.0",
- "@babel/plugin-transform-classes": "^7.25.9",
- "@babel/plugin-transform-computed-properties": "^7.25.9",
- "@babel/plugin-transform-destructuring": "^7.25.9",
- "@babel/plugin-transform-dotall-regex": "^7.25.9",
- "@babel/plugin-transform-duplicate-keys": "^7.25.9",
- "@babel/plugin-transform-duplicate-named-capturing-groups-regex": "^7.25.9",
- "@babel/plugin-transform-dynamic-import": "^7.25.9",
- "@babel/plugin-transform-exponentiation-operator": "^7.26.3",
- "@babel/plugin-transform-export-namespace-from": "^7.25.9",
- "@babel/plugin-transform-for-of": "^7.26.9",
- "@babel/plugin-transform-function-name": "^7.25.9",
- "@babel/plugin-transform-json-strings": "^7.25.9",
- "@babel/plugin-transform-literals": "^7.25.9",
- "@babel/plugin-transform-logical-assignment-operators": "^7.25.9",
- "@babel/plugin-transform-member-expression-literals": "^7.25.9",
- "@babel/plugin-transform-modules-amd": "^7.25.9",
- "@babel/plugin-transform-modules-commonjs": "^7.26.3",
- "@babel/plugin-transform-modules-systemjs": "^7.25.9",
- "@babel/plugin-transform-modules-umd": "^7.25.9",
- "@babel/plugin-transform-named-capturing-groups-regex": "^7.25.9",
- "@babel/plugin-transform-new-target": "^7.25.9",
- "@babel/plugin-transform-nullish-coalescing-operator": "^7.26.6",
- "@babel/plugin-transform-numeric-separator": "^7.25.9",
- "@babel/plugin-transform-object-rest-spread": "^7.25.9",
- "@babel/plugin-transform-object-super": "^7.25.9",
- "@babel/plugin-transform-optional-catch-binding": "^7.25.9",
- "@babel/plugin-transform-optional-chaining": "^7.25.9",
- "@babel/plugin-transform-parameters": "^7.25.9",
- "@babel/plugin-transform-private-methods": "^7.25.9",
- "@babel/plugin-transform-private-property-in-object": "^7.25.9",
- "@babel/plugin-transform-property-literals": "^7.25.9",
- "@babel/plugin-transform-regenerator": "^7.25.9",
- "@babel/plugin-transform-regexp-modifiers": "^7.26.0",
- "@babel/plugin-transform-reserved-words": "^7.25.9",
- "@babel/plugin-transform-shorthand-properties": "^7.25.9",
- "@babel/plugin-transform-spread": "^7.25.9",
- "@babel/plugin-transform-sticky-regex": "^7.25.9",
- "@babel/plugin-transform-template-literals": "^7.26.8",
- "@babel/plugin-transform-typeof-symbol": "^7.26.7",
- "@babel/plugin-transform-unicode-escapes": "^7.25.9",
- "@babel/plugin-transform-unicode-property-regex": "^7.25.9",
- "@babel/plugin-transform-unicode-regex": "^7.25.9",
- "@babel/plugin-transform-unicode-sets-regex": "^7.25.9",
+ "@babel/plugin-transform-arrow-functions": "^7.27.1",
+ "@babel/plugin-transform-async-generator-functions": "^7.28.0",
+ "@babel/plugin-transform-async-to-generator": "^7.27.1",
+ "@babel/plugin-transform-block-scoped-functions": "^7.27.1",
+ "@babel/plugin-transform-block-scoping": "^7.28.0",
+ "@babel/plugin-transform-class-properties": "^7.27.1",
+ "@babel/plugin-transform-class-static-block": "^7.28.3",
+ "@babel/plugin-transform-classes": "^7.28.3",
+ "@babel/plugin-transform-computed-properties": "^7.27.1",
+ "@babel/plugin-transform-destructuring": "^7.28.0",
+ "@babel/plugin-transform-dotall-regex": "^7.27.1",
+ "@babel/plugin-transform-duplicate-keys": "^7.27.1",
+ "@babel/plugin-transform-duplicate-named-capturing-groups-regex": "^7.27.1",
+ "@babel/plugin-transform-dynamic-import": "^7.27.1",
+ "@babel/plugin-transform-explicit-resource-management": "^7.28.0",
+ "@babel/plugin-transform-exponentiation-operator": "^7.27.1",
+ "@babel/plugin-transform-export-namespace-from": "^7.27.1",
+ "@babel/plugin-transform-for-of": "^7.27.1",
+ "@babel/plugin-transform-function-name": "^7.27.1",
+ "@babel/plugin-transform-json-strings": "^7.27.1",
+ "@babel/plugin-transform-literals": "^7.27.1",
+ "@babel/plugin-transform-logical-assignment-operators": "^7.27.1",
+ "@babel/plugin-transform-member-expression-literals": "^7.27.1",
+ "@babel/plugin-transform-modules-amd": "^7.27.1",
+ "@babel/plugin-transform-modules-commonjs": "^7.27.1",
+ "@babel/plugin-transform-modules-systemjs": "^7.27.1",
+ "@babel/plugin-transform-modules-umd": "^7.27.1",
+ "@babel/plugin-transform-named-capturing-groups-regex": "^7.27.1",
+ "@babel/plugin-transform-new-target": "^7.27.1",
+ "@babel/plugin-transform-nullish-coalescing-operator": "^7.27.1",
+ "@babel/plugin-transform-numeric-separator": "^7.27.1",
+ "@babel/plugin-transform-object-rest-spread": "^7.28.0",
+ "@babel/plugin-transform-object-super": "^7.27.1",
+ "@babel/plugin-transform-optional-catch-binding": "^7.27.1",
+ "@babel/plugin-transform-optional-chaining": "^7.27.1",
+ "@babel/plugin-transform-parameters": "^7.27.7",
+ "@babel/plugin-transform-private-methods": "^7.27.1",
+ "@babel/plugin-transform-private-property-in-object": "^7.27.1",
+ "@babel/plugin-transform-property-literals": "^7.27.1",
+ "@babel/plugin-transform-regenerator": "^7.28.3",
+ "@babel/plugin-transform-regexp-modifiers": "^7.27.1",
+ "@babel/plugin-transform-reserved-words": "^7.27.1",
+ "@babel/plugin-transform-shorthand-properties": "^7.27.1",
+ "@babel/plugin-transform-spread": "^7.27.1",
+ "@babel/plugin-transform-sticky-regex": "^7.27.1",
+ "@babel/plugin-transform-template-literals": "^7.27.1",
+ "@babel/plugin-transform-typeof-symbol": "^7.27.1",
+ "@babel/plugin-transform-unicode-escapes": "^7.27.1",
+ "@babel/plugin-transform-unicode-property-regex": "^7.27.1",
+ "@babel/plugin-transform-unicode-regex": "^7.27.1",
+ "@babel/plugin-transform-unicode-sets-regex": "^7.27.1",
"@babel/preset-modules": "0.1.6-no-external-plugins",
- "babel-plugin-polyfill-corejs2": "^0.4.10",
- "babel-plugin-polyfill-corejs3": "^0.11.0",
- "babel-plugin-polyfill-regenerator": "^0.6.1",
- "core-js-compat": "^3.40.0",
+ "babel-plugin-polyfill-corejs2": "^0.4.14",
+ "babel-plugin-polyfill-corejs3": "^0.13.0",
+ "babel-plugin-polyfill-regenerator": "^0.6.5",
+ "core-js-compat": "^3.43.0",
"semver": "^6.3.1"
},
"engines": {
@@ -1827,72 +1843,63 @@
}
},
"node_modules/@babel/runtime": {
- "version": "7.27.0",
- "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.0.tgz",
- "integrity": "sha512-VtPOkrdPHZsKc/clNqyi9WUA8TINkZ4cGk63UUE3u4pmB2k+ZMQRDuIOagv8UVd6j7k0T3+RRIb7beKTebNbcw==",
+ "version": "7.28.4",
+ "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz",
+ "integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==",
"license": "MIT",
- "dependencies": {
- "regenerator-runtime": "^0.14.0"
- },
"engines": {
"node": ">=6.9.0"
}
},
- "node_modules/@babel/runtime/node_modules/regenerator-runtime": {
- "version": "0.14.1",
- "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz",
- "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==",
- "license": "MIT"
- },
"node_modules/@babel/template": {
- "version": "7.27.0",
- "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.0.tgz",
- "integrity": "sha512-2ncevenBqXI6qRMukPlXwHKHchC7RyMuu4xv5JBXRfOGVcTy1mXCD12qrp7Jsoxll1EV3+9sE4GugBVRjT2jFA==",
+ "version": "7.27.2",
+ "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz",
+ "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==",
"license": "MIT",
"dependencies": {
- "@babel/code-frame": "^7.26.2",
- "@babel/parser": "^7.27.0",
- "@babel/types": "^7.27.0"
+ "@babel/code-frame": "^7.27.1",
+ "@babel/parser": "^7.27.2",
+ "@babel/types": "^7.27.1"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/traverse": {
- "version": "7.27.0",
- "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.27.0.tgz",
- "integrity": "sha512-19lYZFzYVQkkHkl4Cy4WrAVcqBkgvV2YM2TU3xG6DIwO7O3ecbDPfW3yM3bjAGcqcQHi+CCtjMR3dIEHxsd6bA==",
+ "version": "7.28.4",
+ "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.4.tgz",
+ "integrity": "sha512-YEzuboP2qvQavAcjgQNVgsvHIDv6ZpwXvcvjmyySP2DIMuByS/6ioU5G9pYrWHM6T2YDfc7xga9iNzYOs12CFQ==",
"license": "MIT",
"dependencies": {
- "@babel/code-frame": "^7.26.2",
- "@babel/generator": "^7.27.0",
- "@babel/parser": "^7.27.0",
- "@babel/template": "^7.27.0",
- "@babel/types": "^7.27.0",
- "debug": "^4.3.1",
- "globals": "^11.1.0"
+ "@babel/code-frame": "^7.27.1",
+ "@babel/generator": "^7.28.3",
+ "@babel/helper-globals": "^7.28.0",
+ "@babel/parser": "^7.28.4",
+ "@babel/template": "^7.27.2",
+ "@babel/types": "^7.28.4",
+ "debug": "^4.3.1"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/types": {
- "version": "7.27.0",
- "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.0.tgz",
- "integrity": "sha512-H45s8fVLYjbhFH62dIJ3WtmJ6RSPt/3DRO0ZcT2SUiYiQyz3BLVb9ADEnLl91m74aQPS3AzzeajZHYOalWe3bg==",
+ "version": "7.28.4",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.4.tgz",
+ "integrity": "sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q==",
"license": "MIT",
"dependencies": {
- "@babel/helper-string-parser": "^7.25.9",
- "@babel/helper-validator-identifier": "^7.25.9"
+ "@babel/helper-string-parser": "^7.27.1",
+ "@babel/helper-validator-identifier": "^7.27.1"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@bazel/runfiles": {
- "version": "6.3.1",
- "resolved": "https://registry.npmjs.org/@bazel/runfiles/-/runfiles-6.3.1.tgz",
- "integrity": "sha512-1uLNT5NZsUVIGS4syuHwTzZ8HycMPyr6POA3FCE4GbMtc4rhoJk8aZKtNIRthJYfL+iioppi+rTfH3olMPr9nA==",
+ "version": "6.5.0",
+ "resolved": "https://registry.npmjs.org/@bazel/runfiles/-/runfiles-6.5.0.tgz",
+ "integrity": "sha512-RzahvqTkfpY2jsDxo8YItPX+/iZ6hbiikw1YhE0bA9EKBR5Og8Pa6FHn9PO9M0zaXRVsr0GFQLKbB/0rzy9SzA==",
"dev": true,
"license": "Apache-2.0"
},
@@ -1903,10 +1910,64 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/@cacheable/memoize": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/@cacheable/memoize/-/memoize-2.0.3.tgz",
+ "integrity": "sha512-hl9wfQgpiydhQEIv7fkjEzTGE+tcosCXLKFDO707wYJ/78FVOlowb36djex5GdbSyeHnG62pomYLMuV/OT8Pbw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@cacheable/utils": "^2.0.3"
+ }
+ },
+ "node_modules/@cacheable/memory": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/@cacheable/memory/-/memory-2.0.3.tgz",
+ "integrity": "sha512-R3UKy/CKOyb1LZG/VRCTMcpiMDyLH7SH3JrraRdK6kf3GweWCOU3sgvE13W3TiDRbxnDKylzKJvhUAvWl9LQOA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@cacheable/memoize": "^2.0.3",
+ "@cacheable/utils": "^2.0.3",
+ "@keyv/bigmap": "^1.0.2",
+ "hookified": "^1.12.1",
+ "keyv": "^5.5.3"
+ }
+ },
+ "node_modules/@cacheable/memory/node_modules/keyv": {
+ "version": "5.5.3",
+ "resolved": "https://registry.npmjs.org/keyv/-/keyv-5.5.3.tgz",
+ "integrity": "sha512-h0Un1ieD+HUrzBH6dJXhod3ifSghk5Hw/2Y4/KHBziPlZecrFyE9YOTPU6eOs0V9pYl8gOs86fkr/KN8lUX39A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@keyv/serialize": "^1.1.1"
+ }
+ },
+ "node_modules/@cacheable/utils": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/@cacheable/utils/-/utils-2.1.0.tgz",
+ "integrity": "sha512-ZdxfOiaarMqMj+H7qwlt5EBKWaeGihSYVHdQv5lUsbn8MJJOTW82OIwirQ39U5tMZkNvy3bQE+ryzC+xTAb9/g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "keyv": "^5.5.3"
+ }
+ },
+ "node_modules/@cacheable/utils/node_modules/keyv": {
+ "version": "5.5.3",
+ "resolved": "https://registry.npmjs.org/keyv/-/keyv-5.5.3.tgz",
+ "integrity": "sha512-h0Un1ieD+HUrzBH6dJXhod3ifSghk5Hw/2Y4/KHBziPlZecrFyE9YOTPU6eOs0V9pYl8gOs86fkr/KN8lUX39A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@keyv/serialize": "^1.1.1"
+ }
+ },
"node_modules/@csstools/css-parser-algorithms": {
- "version": "3.0.4",
- "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.4.tgz",
- "integrity": "sha512-Up7rBoV77rv29d3uKHUIVubz1BTcgyUK72IvCQAbfbMv584xHcGKCKbWh7i8hPrRJ7qU4Y8IO3IY9m+iTB7P3A==",
+ "version": "3.0.5",
+ "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz",
+ "integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==",
"dev": true,
"funding": [
{
@@ -1924,13 +1985,13 @@
"node": ">=18"
},
"peerDependencies": {
- "@csstools/css-tokenizer": "^3.0.3"
+ "@csstools/css-tokenizer": "^3.0.4"
}
},
"node_modules/@csstools/css-tokenizer": {
- "version": "3.0.3",
- "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.3.tgz",
- "integrity": "sha512-UJnjoFsmxfKUdNYdWgOB0mWUypuLvAfQPH1+pyvRJs6euowbFkFC6P13w1l8mJyi3vxYMxc9kld5jZEGRQs6bw==",
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz",
+ "integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==",
"dev": true,
"funding": [
{
@@ -1949,9 +2010,9 @@
}
},
"node_modules/@csstools/media-query-list-parser": {
- "version": "4.0.2",
- "resolved": "https://registry.npmjs.org/@csstools/media-query-list-parser/-/media-query-list-parser-4.0.2.tgz",
- "integrity": "sha512-EUos465uvVvMJehckATTlNqGj4UJWkTmdWuDMjqvSUkjGpmOyFZBVwb4knxCm/k2GMTXY+c/5RkdndzFYWeX5A==",
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/@csstools/media-query-list-parser/-/media-query-list-parser-4.0.3.tgz",
+ "integrity": "sha512-HAYH7d3TLRHDOUQK4mZKf9k9Ph/m8Akstg66ywKR4SFAigjs3yBiUeZtFxywiTm5moZMAp/5W/ZuFnNXXYLuuQ==",
"dev": true,
"funding": [
{
@@ -1964,13 +2025,12 @@
}
],
"license": "MIT",
- "peer": true,
"engines": {
"node": ">=18"
},
"peerDependencies": {
- "@csstools/css-parser-algorithms": "^3.0.4",
- "@csstools/css-tokenizer": "^3.0.3"
+ "@csstools/css-parser-algorithms": "^3.0.5",
+ "@csstools/css-tokenizer": "^3.0.4"
}
},
"node_modules/@csstools/selector-specificity": {
@@ -1989,7 +2049,6 @@
}
],
"license": "MIT-0",
- "peer": true,
"engines": {
"node": ">=18"
},
@@ -2008,22 +2067,28 @@
}
},
"node_modules/@dual-bundle/import-meta-resolve": {
- "version": "4.1.0",
- "resolved": "https://registry.npmjs.org/@dual-bundle/import-meta-resolve/-/import-meta-resolve-4.1.0.tgz",
- "integrity": "sha512-+nxncfwHM5SgAtrVzgpzJOI1ol0PkumhVo469KCf9lUi21IGcY90G98VuHm9VRrUypmAzawAHO9bs6hqeADaVg==",
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/@dual-bundle/import-meta-resolve/-/import-meta-resolve-4.2.1.tgz",
+ "integrity": "sha512-id+7YRUgoUX6CgV0DtuhirQWodeeA7Lf4i2x71JS/vtA5pRb/hIGWlw+G6MeXvsM+MXrz0VAydTGElX1rAfgPg==",
"dev": true,
"license": "MIT",
- "peer": true,
"funding": {
"type": "github",
- "url": "https://github.com/sponsors/wooorm"
+ "url": "https://github.com/sponsors/JounQin"
}
},
"node_modules/@edx/brand": {
"name": "@openedx/brand-openedx",
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/@openedx/brand-openedx/-/brand-openedx-1.2.3.tgz",
- "integrity": "sha512-Dn9CtpC8fovh++Xi4NF5NJoeR9yU2yXZnV9IujxIyGd/dn0Phq5t6dzJVfupwq09mpDnzJv7egA8Znz/3ljO+w=="
+ "integrity": "sha512-Dn9CtpC8fovh++Xi4NF5NJoeR9yU2yXZnV9IujxIyGd/dn0Phq5t6dzJVfupwq09mpDnzJv7egA8Znz/3ljO+w==",
+ "license": "GPL-3.0-or-later"
+ },
+ "node_modules/@edx/brand-edx.org": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/@edx/brand-edx.org/-/brand-edx.org-2.0.3.tgz",
+ "integrity": "sha512-QRmq2su1Xy+9GhY3NRZ+WdjtYWHmgfuKbVCW2skxgfgW9Q6kea8L6LrgigfrZtW+kQq/KdX2tVJcYBkB9xALtQ==",
+ "license": "UNLICENSED"
},
"node_modules/@edx/edx-bootstrap": {
"version": "1.0.4",
@@ -2060,7 +2125,8 @@
"version": "3.7.1",
"resolved": "https://registry.npmjs.org/jquery/-/jquery-3.7.1.tgz",
"integrity": "sha512-m4avr8yL8kmFN8psrbFFFmB/If14iN5o9nw/NgnnM+kybDJpRsAynV2BsfpTYrTRysYUdADVD7CkUUizgkpLfg==",
- "license": "MIT"
+ "license": "MIT",
+ "peer": true
},
"node_modules/@edx/edx-proctoring": {
"version": "4.18.4",
@@ -2087,12 +2153,6 @@
"react-dom": "^16.1.0 || ^17.0.0 || ^18.0.0"
}
},
- "node_modules/@edx/frontend-component-cookie-policy-banner/node_modules/@edx/brand-edx.org": {
- "version": "2.0.3",
- "resolved": "https://registry.npmjs.org/@edx/brand-edx.org/-/brand-edx.org-2.0.3.tgz",
- "integrity": "sha512-QRmq2su1Xy+9GhY3NRZ+WdjtYWHmgfuKbVCW2skxgfgW9Q6kea8L6LrgigfrZtW+kQq/KdX2tVJcYBkB9xALtQ==",
- "license": "UNLICENSED"
- },
"node_modules/@edx/frontend-component-cookie-policy-banner/node_modules/@edx/paragon": {
"version": "12.8.0",
"resolved": "https://registry.npmjs.org/@edx/paragon/-/paragon-12.8.0.tgz",
@@ -2124,47 +2184,6 @@
"react-dom": "^16.8.6"
}
},
- "node_modules/@edx/frontend-component-cookie-policy-banner/node_modules/@edx/paragon/node_modules/airbnb-prop-types": {
- "version": "2.16.0",
- "resolved": "https://registry.npmjs.org/airbnb-prop-types/-/airbnb-prop-types-2.16.0.tgz",
- "integrity": "sha512-7WHOFolP/6cS96PhKNrslCLMYAI8yB1Pp6u6XmxozQOiZbsI5ycglZr5cHhBFfuRcQQjzCMith5ZPZdYiJCxUg==",
- "deprecated": "This package has been renamed to 'prop-types-tools'",
- "license": "MIT",
- "dependencies": {
- "array.prototype.find": "^2.1.1",
- "function.prototype.name": "^1.1.2",
- "is-regex": "^1.1.0",
- "object-is": "^1.1.2",
- "object.assign": "^4.1.0",
- "object.entries": "^1.1.2",
- "prop-types": "^15.7.2",
- "prop-types-exact": "^1.2.0",
- "react-is": "^16.13.1"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- },
- "peerDependencies": {
- "react": "^0.14 || ^15.0.0 || ^16.0.0-alpha"
- }
- },
- "node_modules/@edx/frontend-component-cookie-policy-banner/node_modules/@edx/paragon/node_modules/react-responsive": {
- "version": "6.1.2",
- "resolved": "https://registry.npmjs.org/react-responsive/-/react-responsive-6.1.2.tgz",
- "integrity": "sha512-AXentVC/kN3KED9zhzJv2pu4vZ0i6cSHdTtbCScVV1MT6F5KXaG2qs5D7WLmhdaOvmiMX8UfmS4ZSO+WPwDt4g==",
- "license": "MIT",
- "dependencies": {
- "hyphenate-style-name": "^1.0.0",
- "matchmediaquery": "^0.3.0",
- "prop-types": "^15.6.1"
- },
- "engines": {
- "node": ">= 0.10"
- },
- "peerDependencies": {
- "react": "^16.3.0"
- }
- },
"node_modules/@edx/frontend-component-cookie-policy-banner/node_modules/bootstrap": {
"version": "4.6.2",
"resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-4.6.2.tgz",
@@ -2185,16 +2204,6 @@
"popper.js": "^1.16.1"
}
},
- "node_modules/@edx/frontend-component-cookie-policy-banner/node_modules/dom-helpers": {
- "version": "5.2.1",
- "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz",
- "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==",
- "license": "MIT",
- "dependencies": {
- "@babel/runtime": "^7.8.7",
- "csstype": "^3.0.2"
- }
- },
"node_modules/@edx/frontend-component-cookie-policy-banner/node_modules/email-prop-type": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/email-prop-type/-/email-prop-type-3.0.1.tgz",
@@ -2204,28 +2213,6 @@
"email-validator": "^2.0.4"
}
},
- "node_modules/@edx/frontend-component-cookie-policy-banner/node_modules/react-is": {
- "version": "16.13.1",
- "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
- "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
- "license": "MIT"
- },
- "node_modules/@edx/frontend-component-cookie-policy-banner/node_modules/react-transition-group": {
- "version": "4.4.5",
- "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz",
- "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==",
- "license": "BSD-3-Clause",
- "dependencies": {
- "@babel/runtime": "^7.5.5",
- "dom-helpers": "^5.0.1",
- "loose-envify": "^1.4.0",
- "prop-types": "^15.6.2"
- },
- "peerDependencies": {
- "react": ">=16.6.0",
- "react-dom": ">=16.6.0"
- }
- },
"node_modules/@edx/paragon": {
"version": "2.6.4",
"resolved": "https://registry.npmjs.org/@edx/paragon/-/paragon-2.6.4.tgz",
@@ -2245,138 +2232,6 @@
"react-proptype-conditional-require": "^1.0.4"
}
},
- "node_modules/@edx/studio-frontend": {
- "version": "2.4.0",
- "resolved": "https://registry.npmjs.org/@edx/studio-frontend/-/studio-frontend-2.4.0.tgz",
- "integrity": "sha512-wdbUG1zUqpo5uW91TuV49XWToDeUKBBcClVR2BSjjPzYTm1vk8jeDRlALOTcIEwfAIM5Qio7utsQBwe51S7SNw==",
- "license": "AGPL-3.0",
- "dependencies": {
- "@babel/polyfill": "^7.0.0",
- "@edx/edx-bootstrap": "^1.0.0",
- "@edx/paragon": "3.4.8",
- "airbnb-prop-types": "^2.10.0",
- "classnames": "^2.2.5",
- "copy-to-clipboard": "^3.0.8",
- "custom-event-polyfill": "^0.3.0",
- "font-awesome": "^4.7.0",
- "js-cookie": "^2.1.4",
- "popper.js": "^1.12.5",
- "prop-types": "^15.5.10",
- "react": "^16.2.0",
- "react-dom": "^16.1.0",
- "react-dropzone": "^4.2.3",
- "react-intl": "^2.4.0",
- "react-intl-translations-manager": "^5.0.1",
- "react-redux": "^5.0.6",
- "react-transition-group": "^2.2.1",
- "redux": "^4.0.0",
- "redux-devtools-extension": "^2.13.3",
- "redux-thunk": "^2.2.0",
- "reselect": "^3.0.1",
- "whatwg-fetch": "^2.0.3"
- }
- },
- "node_modules/@edx/studio-frontend/node_modules/@edx/paragon": {
- "version": "3.4.8",
- "resolved": "https://registry.npmjs.org/@edx/paragon/-/paragon-3.4.8.tgz",
- "integrity": "sha512-Aba1/s7IEvHhyBvtILL3MIpghu4gJ04lvKXpuvl3AqdGluSVEp1u4dfCvsvBF4ZDP2CPUwkGXWolIA9yHxj7Nw==",
- "license": "Apache-2.0",
- "dependencies": {
- "@edx/edx-bootstrap": "^1.0.0",
- "airbnb-prop-types": "^2.10.0",
- "babel-polyfill": "^6.26.0",
- "classnames": "^2.2.5",
- "email-prop-type": "^1.1.5",
- "font-awesome": "^4.7.0",
- "mailto-link": "^1.0.0",
- "prop-types": "^15.5.8",
- "react": "^16.4.2",
- "react-dom": "^16.1.0",
- "react-element-proptypes": "^1.0.0",
- "react-proptype-conditional-require": "^1.0.4",
- "react-responsive": "^5.0.0",
- "sanitize-html": "^1.18.2"
- }
- },
- "node_modules/@edx/studio-frontend/node_modules/airbnb-prop-types": {
- "version": "2.16.0",
- "resolved": "https://registry.npmjs.org/airbnb-prop-types/-/airbnb-prop-types-2.16.0.tgz",
- "integrity": "sha512-7WHOFolP/6cS96PhKNrslCLMYAI8yB1Pp6u6XmxozQOiZbsI5ycglZr5cHhBFfuRcQQjzCMith5ZPZdYiJCxUg==",
- "deprecated": "This package has been renamed to 'prop-types-tools'",
- "license": "MIT",
- "dependencies": {
- "array.prototype.find": "^2.1.1",
- "function.prototype.name": "^1.1.2",
- "is-regex": "^1.1.0",
- "object-is": "^1.1.2",
- "object.assign": "^4.1.0",
- "object.entries": "^1.1.2",
- "prop-types": "^15.7.2",
- "prop-types-exact": "^1.2.0",
- "react-is": "^16.13.1"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- },
- "peerDependencies": {
- "react": "^0.14 || ^15.0.0 || ^16.0.0-alpha"
- }
- },
- "node_modules/@edx/studio-frontend/node_modules/js-cookie": {
- "version": "2.2.1",
- "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-2.2.1.tgz",
- "integrity": "sha512-HvdH2LzI/EAZcUwA8+0nKNtWHqS+ZmijLA30RwZA0bo7ToCckjK5MkGhjED9KoRcXO6BaGI3I9UIzSA1FKFPOQ==",
- "license": "MIT"
- },
- "node_modules/@edx/studio-frontend/node_modules/react-intl": {
- "version": "2.9.0",
- "resolved": "https://registry.npmjs.org/react-intl/-/react-intl-2.9.0.tgz",
- "integrity": "sha512-27jnDlb/d2A7mSJwrbOBnUgD+rPep+abmoJE511Tf8BnoONIAUehy/U1zZCHGO17mnOwMWxqN4qC0nW11cD6rA==",
- "license": "BSD-3-Clause",
- "dependencies": {
- "hoist-non-react-statics": "^3.3.0",
- "intl-format-cache": "^2.0.5",
- "intl-messageformat": "^2.1.0",
- "intl-relativeformat": "^2.1.0",
- "invariant": "^2.1.1"
- },
- "peerDependencies": {
- "prop-types": "^15.5.4",
- "react": "^0.14.9 || ^15.0.0 || ^16.0.0"
- }
- },
- "node_modules/@edx/studio-frontend/node_modules/react-is": {
- "version": "16.13.1",
- "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
- "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
- "license": "MIT"
- },
- "node_modules/@edx/studio-frontend/node_modules/react-responsive": {
- "version": "5.0.0",
- "resolved": "https://registry.npmjs.org/react-responsive/-/react-responsive-5.0.0.tgz",
- "integrity": "sha512-oEimZ0FTCC3/pjGDEBHOz06nWbBNDIbMGOdRYp6K9SBUmrqgNAX77hTiqvmRQeLyI97zz4F4kiaFRxFspDxE+w==",
- "license": "MIT",
- "dependencies": {
- "hyphenate-style-name": "^1.0.0",
- "matchmediaquery": "^0.3.0",
- "prop-types": "^15.6.1"
- },
- "engines": {
- "node": ">= 0.10"
- },
- "peerDependencies": {
- "react": "^16.0.0"
- }
- },
- "node_modules/@edx/studio-frontend/node_modules/redux": {
- "version": "4.2.1",
- "resolved": "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz",
- "integrity": "sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==",
- "license": "MIT",
- "dependencies": {
- "@babel/runtime": "^7.9.2"
- }
- },
"node_modules/@edx/stylelint-config-edx": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/@edx/stylelint-config-edx/-/stylelint-config-edx-2.3.3.tgz",
@@ -2406,6 +2261,7 @@
}
],
"license": "MIT",
+ "peer": true,
"engines": {
"node": "^14 || ^16 || >=18"
},
@@ -2429,6 +2285,7 @@
}
],
"license": "MIT",
+ "peer": true,
"engines": {
"node": "^14 || ^16 || >=18"
}
@@ -2683,6 +2540,7 @@
"integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"cssesc": "^3.0.0",
"util-deprecate": "^1.0.2"
@@ -2772,14 +2630,11 @@
}
},
"node_modules/@edx/stylelint-config-edx/node_modules/strip-indent": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-4.0.0.tgz",
- "integrity": "sha512-mnVSV2l+Zv6BLpSD/8V87CW/y9EmmbYzGCIavsnsI6/nwn26DwffM/yztm30Z/I2DY9wdS3vXVCMnHDgZaVNoA==",
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-4.1.1.tgz",
+ "integrity": "sha512-SlyRoSkdh1dYP0PzclLE7r0M9sgbFKKMFXpFRUMNuKhQSbC6VQIGzq3E0qsfvGJaUFJPGv6Ws1NZ/haTAjfbMA==",
"dev": true,
"license": "MIT",
- "dependencies": {
- "min-indent": "^1.0.1"
- },
"engines": {
"node": ">=12"
},
@@ -2793,6 +2648,7 @@
"integrity": "sha512-78O4c6IswZ9TzpcIiQJIN49K3qNoXTM8zEJzhaTE/xRTCZswaovSEVIa/uwbOltZrk16X4jAxjaOhzz/hTm1Kw==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"@csstools/css-parser-algorithms": "^2.3.1",
"@csstools/css-tokenizer": "^2.2.0",
@@ -2976,6 +2832,7 @@
"integrity": "sha512-YUcsLQKYb6DmaJjIHdDWpBIGCcyE/W+p/LMGvjQem55Mm2XWVAP5kWTMKWLv9lwpCVjpLxPyOMOyUocP1GxrtA==",
"hasInstallScript": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"@fortawesome/fontawesome-common-types": "^0.2.36"
},
@@ -3027,9 +2884,9 @@
}
},
"node_modules/@isaacs/cliui/node_modules/ansi-regex": {
- "version": "6.1.0",
- "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz",
- "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==",
+ "version": "6.2.2",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz",
+ "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==",
"license": "MIT",
"engines": {
"node": ">=12"
@@ -3039,9 +2896,9 @@
}
},
"node_modules/@isaacs/cliui/node_modules/ansi-styles": {
- "version": "6.2.1",
- "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz",
- "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==",
+ "version": "6.2.3",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz",
+ "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==",
"license": "MIT",
"engines": {
"node": ">=12"
@@ -3074,9 +2931,9 @@
}
},
"node_modules/@isaacs/cliui/node_modules/strip-ansi": {
- "version": "7.1.0",
- "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz",
- "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==",
+ "version": "7.1.2",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz",
+ "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==",
"license": "MIT",
"dependencies": {
"ansi-regex": "^6.0.1"
@@ -3382,9 +3239,9 @@
}
},
"node_modules/@jest/reporters/node_modules/semver": {
- "version": "7.7.1",
- "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz",
- "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==",
+ "version": "7.7.3",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
+ "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
"dev": true,
"license": "ISC",
"bin": {
@@ -3498,17 +3355,13 @@
}
},
"node_modules/@jridgewell/gen-mapping": {
- "version": "0.3.8",
- "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz",
- "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==",
+ "version": "0.3.13",
+ "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
+ "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
"license": "MIT",
"dependencies": {
- "@jridgewell/set-array": "^1.2.1",
- "@jridgewell/sourcemap-codec": "^1.4.10",
+ "@jridgewell/sourcemap-codec": "^1.5.0",
"@jridgewell/trace-mapping": "^0.3.24"
- },
- "engines": {
- "node": ">=6.0.0"
}
},
"node_modules/@jridgewell/resolve-uri": {
@@ -3520,19 +3373,10 @@
"node": ">=6.0.0"
}
},
- "node_modules/@jridgewell/set-array": {
- "version": "1.2.1",
- "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz",
- "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==",
- "license": "MIT",
- "engines": {
- "node": ">=6.0.0"
- }
- },
"node_modules/@jridgewell/source-map": {
- "version": "0.3.6",
- "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.6.tgz",
- "integrity": "sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==",
+ "version": "0.3.11",
+ "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz",
+ "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==",
"license": "MIT",
"dependencies": {
"@jridgewell/gen-mapping": "^0.3.5",
@@ -3540,15 +3384,15 @@
}
},
"node_modules/@jridgewell/sourcemap-codec": {
- "version": "1.5.0",
- "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz",
- "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==",
+ "version": "1.5.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
+ "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
"license": "MIT"
},
"node_modules/@jridgewell/trace-mapping": {
- "version": "0.3.25",
- "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz",
- "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==",
+ "version": "0.3.31",
+ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
+ "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
"license": "MIT",
"dependencies": {
"@jridgewell/resolve-uri": "^3.1.0",
@@ -3563,6 +3407,26 @@
"license": "MIT",
"peer": true
},
+ "node_modules/@keyv/bigmap": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/@keyv/bigmap/-/bigmap-1.0.3.tgz",
+ "integrity": "sha512-jUEkNlnE9tYzX2AIBeoSe1gVUvSOfIOQ5EFPL5Un8cFHGvjD9L/fxpxlS1tEivRLHgapO2RZJ3D93HYAa049pg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "hookified": "^1.12.1"
+ },
+ "engines": {
+ "node": ">= 18"
+ }
+ },
+ "node_modules/@keyv/serialize": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@keyv/serialize/-/serialize-1.1.1.tgz",
+ "integrity": "sha512-dXn3FZhPv0US+7dtJsIi2R+c7qWYiReoEh5zUntWCf4oSpMNib8FDhSoed6m3QyZdx5hK7iLFkYk3rNxwt8vTA==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/@nodelib/fs.scandir": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
@@ -3617,105 +3481,265 @@
"node": "^18.17.0 || >=20.5.0"
}
},
- "node_modules/@npmcli/agent/node_modules/agent-base": {
- "version": "7.1.3",
- "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz",
- "integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==",
+ "node_modules/@npmcli/agent/node_modules/agent-base": {
+ "version": "7.1.4",
+ "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz",
+ "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/@npmcli/agent/node_modules/http-proxy-agent": {
+ "version": "7.0.2",
+ "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz",
+ "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==",
+ "license": "MIT",
+ "dependencies": {
+ "agent-base": "^7.1.0",
+ "debug": "^4.3.4"
+ },
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/@npmcli/agent/node_modules/https-proxy-agent": {
+ "version": "7.0.6",
+ "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
+ "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
+ "license": "MIT",
+ "dependencies": {
+ "agent-base": "^7.1.2",
+ "debug": "4"
+ },
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/@npmcli/agent/node_modules/lru-cache": {
+ "version": "10.4.3",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
+ "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
+ "license": "ISC"
+ },
+ "node_modules/@npmcli/fs": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-4.0.0.tgz",
+ "integrity": "sha512-/xGlezI6xfGO9NwuJlnwz/K14qD1kCSAGtacBHnGzeAIuJGazcp45KP5NuyARXoKb7cwulAGWVsbeSxdG/cb0Q==",
+ "license": "ISC",
+ "dependencies": {
+ "semver": "^7.3.5"
+ },
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/@npmcli/fs/node_modules/semver": {
+ "version": "7.7.3",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
+ "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@parcel/watcher": {
+ "version": "2.5.1",
+ "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.1.tgz",
+ "integrity": "sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==",
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "detect-libc": "^1.0.3",
+ "is-glob": "^4.0.3",
+ "micromatch": "^4.0.5",
+ "node-addon-api": "^7.0.0"
+ },
+ "engines": {
+ "node": ">= 10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ },
+ "optionalDependencies": {
+ "@parcel/watcher-android-arm64": "2.5.1",
+ "@parcel/watcher-darwin-arm64": "2.5.1",
+ "@parcel/watcher-darwin-x64": "2.5.1",
+ "@parcel/watcher-freebsd-x64": "2.5.1",
+ "@parcel/watcher-linux-arm-glibc": "2.5.1",
+ "@parcel/watcher-linux-arm-musl": "2.5.1",
+ "@parcel/watcher-linux-arm64-glibc": "2.5.1",
+ "@parcel/watcher-linux-arm64-musl": "2.5.1",
+ "@parcel/watcher-linux-x64-glibc": "2.5.1",
+ "@parcel/watcher-linux-x64-musl": "2.5.1",
+ "@parcel/watcher-win32-arm64": "2.5.1",
+ "@parcel/watcher-win32-ia32": "2.5.1",
+ "@parcel/watcher-win32-x64": "2.5.1"
+ }
+ },
+ "node_modules/@parcel/watcher-android-arm64": {
+ "version": "2.5.1",
+ "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.1.tgz",
+ "integrity": "sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">= 10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@parcel/watcher-darwin-arm64": {
+ "version": "2.5.1",
+ "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.1.tgz",
+ "integrity": "sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@parcel/watcher-darwin-x64": {
+ "version": "2.5.1",
+ "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.1.tgz",
+ "integrity": "sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@parcel/watcher-freebsd-x64": {
+ "version": "2.5.1",
+ "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.1.tgz",
+ "integrity": "sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ==",
+ "cpu": [
+ "x64"
+ ],
"license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
"engines": {
- "node": ">= 14"
+ "node": ">= 10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
}
},
- "node_modules/@npmcli/agent/node_modules/http-proxy-agent": {
- "version": "7.0.2",
- "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz",
- "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==",
+ "node_modules/@parcel/watcher-linux-arm-glibc": {
+ "version": "2.5.1",
+ "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.1.tgz",
+ "integrity": "sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA==",
+ "cpu": [
+ "arm"
+ ],
"license": "MIT",
- "dependencies": {
- "agent-base": "^7.1.0",
- "debug": "^4.3.4"
- },
+ "optional": true,
+ "os": [
+ "linux"
+ ],
"engines": {
- "node": ">= 14"
+ "node": ">= 10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
}
},
- "node_modules/@npmcli/agent/node_modules/https-proxy-agent": {
- "version": "7.0.6",
- "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
- "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
+ "node_modules/@parcel/watcher-linux-arm-musl": {
+ "version": "2.5.1",
+ "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.1.tgz",
+ "integrity": "sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==",
+ "cpu": [
+ "arm"
+ ],
"license": "MIT",
- "dependencies": {
- "agent-base": "^7.1.2",
- "debug": "4"
- },
+ "optional": true,
+ "os": [
+ "linux"
+ ],
"engines": {
- "node": ">= 14"
- }
- },
- "node_modules/@npmcli/agent/node_modules/lru-cache": {
- "version": "10.4.3",
- "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
- "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
- "license": "ISC"
- },
- "node_modules/@npmcli/fs": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-4.0.0.tgz",
- "integrity": "sha512-/xGlezI6xfGO9NwuJlnwz/K14qD1kCSAGtacBHnGzeAIuJGazcp45KP5NuyARXoKb7cwulAGWVsbeSxdG/cb0Q==",
- "license": "ISC",
- "dependencies": {
- "semver": "^7.3.5"
+ "node": ">= 10.0.0"
},
- "engines": {
- "node": "^18.17.0 || >=20.5.0"
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
}
},
- "node_modules/@npmcli/fs/node_modules/semver": {
- "version": "7.7.1",
- "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz",
- "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==",
- "license": "ISC",
- "bin": {
- "semver": "bin/semver.js"
- },
+ "node_modules/@parcel/watcher-linux-arm64-glibc": {
+ "version": "2.5.1",
+ "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.1.tgz",
+ "integrity": "sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
"engines": {
- "node": ">=10"
+ "node": ">= 10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
}
},
- "node_modules/@parcel/watcher": {
+ "node_modules/@parcel/watcher-linux-arm64-musl": {
"version": "2.5.1",
- "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.1.tgz",
- "integrity": "sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==",
- "hasInstallScript": true,
+ "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.1.tgz",
+ "integrity": "sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==",
+ "cpu": [
+ "arm64"
+ ],
"license": "MIT",
"optional": true,
- "dependencies": {
- "detect-libc": "^1.0.3",
- "is-glob": "^4.0.3",
- "micromatch": "^4.0.5",
- "node-addon-api": "^7.0.0"
- },
+ "os": [
+ "linux"
+ ],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
- },
- "optionalDependencies": {
- "@parcel/watcher-android-arm64": "2.5.1",
- "@parcel/watcher-darwin-arm64": "2.5.1",
- "@parcel/watcher-darwin-x64": "2.5.1",
- "@parcel/watcher-freebsd-x64": "2.5.1",
- "@parcel/watcher-linux-arm-glibc": "2.5.1",
- "@parcel/watcher-linux-arm-musl": "2.5.1",
- "@parcel/watcher-linux-arm64-glibc": "2.5.1",
- "@parcel/watcher-linux-arm64-musl": "2.5.1",
- "@parcel/watcher-linux-x64-glibc": "2.5.1",
- "@parcel/watcher-linux-x64-musl": "2.5.1",
- "@parcel/watcher-win32-arm64": "2.5.1",
- "@parcel/watcher-win32-ia32": "2.5.1",
- "@parcel/watcher-win32-x64": "2.5.1"
}
},
"node_modules/@parcel/watcher-linux-x64-glibc": {
@@ -3758,6 +3782,66 @@
"url": "https://opencollective.com/parcel"
}
},
+ "node_modules/@parcel/watcher-win32-arm64": {
+ "version": "2.5.1",
+ "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.1.tgz",
+ "integrity": "sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@parcel/watcher-win32-ia32": {
+ "version": "2.5.1",
+ "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.1.tgz",
+ "integrity": "sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ==",
+ "cpu": [
+ "ia32"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@parcel/watcher-win32-x64": {
+ "version": "2.5.1",
+ "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.1.tgz",
+ "integrity": "sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
"node_modules/@parcel/watcher/node_modules/is-extglob": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
@@ -3847,14 +3931,13 @@
}
},
"node_modules/@sinonjs/samsam": {
- "version": "8.0.2",
- "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-8.0.2.tgz",
- "integrity": "sha512-v46t/fwnhejRSFTGqbpn9u+LQ9xJDse10gNnPgAcxgdoCDMXj/G2asWAC/8Qs+BAZDicX+MNZouXT1A7c83kVw==",
+ "version": "8.0.3",
+ "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-8.0.3.tgz",
+ "integrity": "sha512-hw6HbX+GyVZzmaYNh82Ecj1vdGZrqVIn/keDTg63IgAwiQPO+xCz99uG6Woqgb4tM0mUiFENKZ4cqd7IX94AXQ==",
"dev": true,
"license": "BSD-3-Clause",
"dependencies": {
"@sinonjs/commons": "^3.0.1",
- "lodash.get": "^4.4.2",
"type-detect": "^4.1.0"
}
},
@@ -3876,9 +3959,9 @@
"license": "(Unlicense OR Apache-2.0)"
},
"node_modules/@testing-library/dom": {
- "version": "10.4.0",
- "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.0.tgz",
- "integrity": "sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ==",
+ "version": "10.4.1",
+ "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz",
+ "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==",
"license": "MIT",
"peer": true,
"dependencies": {
@@ -3886,9 +3969,9 @@
"@babel/runtime": "^7.12.5",
"@types/aria-query": "^5.0.1",
"aria-query": "5.3.0",
- "chalk": "^4.1.0",
"dom-accessibility-api": "^0.5.9",
"lz-string": "^1.5.0",
+ "picocolors": "1.1.1",
"pretty-format": "^27.0.2"
},
"engines": {
@@ -3896,17 +3979,16 @@
}
},
"node_modules/@testing-library/jest-dom": {
- "version": "6.6.3",
- "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.6.3.tgz",
- "integrity": "sha512-IteBhl4XqYNkM54f4ejhLRJiZNqcSCoXUOG2CPK7qbD322KjQozM4kHQOfkG2oln9b9HTYqs+Sae8vBATubxxA==",
+ "version": "6.9.1",
+ "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.9.1.tgz",
+ "integrity": "sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==",
"license": "MIT",
"dependencies": {
"@adobe/css-tools": "^4.4.0",
"aria-query": "^5.0.0",
- "chalk": "^3.0.0",
"css.escape": "^1.5.1",
"dom-accessibility-api": "^0.6.3",
- "lodash": "^4.17.21",
+ "picocolors": "^1.1.1",
"redent": "^3.0.0"
},
"engines": {
@@ -3915,19 +3997,6 @@
"yarn": ">=1"
}
},
- "node_modules/@testing-library/jest-dom/node_modules/chalk": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz",
- "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==",
- "license": "MIT",
- "dependencies": {
- "ansi-styles": "^4.1.0",
- "supports-color": "^7.1.0"
- },
- "engines": {
- "node": ">=8"
- }
- },
"node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz",
@@ -4047,13 +4116,13 @@
}
},
"node_modules/@types/babel__traverse": {
- "version": "7.20.7",
- "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.7.tgz",
- "integrity": "sha512-dkO5fhS7+/oos4ciWxyEyjWe48zmG6wbCheo/G2ZnHx4fs3EU6YC6UM8rk56gAjNJ9P3MTH2jo5jb92/K6wbng==",
+ "version": "7.28.0",
+ "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz",
+ "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@babel/types": "^7.20.7"
+ "@babel/types": "^7.28.2"
}
},
"node_modules/@types/cookie": {
@@ -4083,9 +4152,9 @@
}
},
"node_modules/@types/estree": {
- "version": "1.0.7",
- "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz",
- "integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==",
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
+ "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
"license": "MIT"
},
"node_modules/@types/graceful-fs": {
@@ -4153,12 +4222,12 @@
"license": "MIT"
},
"node_modules/@types/node": {
- "version": "22.15.3",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.3.tgz",
- "integrity": "sha512-lX7HFZeHf4QG/J7tBZqrCAXwz9J5RD56Y6MpP0eJkka8p+K0RY/yBTW7CYFJ4VGCclxqOLKmiGP5juQc6MKgcw==",
+ "version": "24.7.2",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-24.7.2.tgz",
+ "integrity": "sha512-/NbVmcGTP+lj5oa4yiYxxeBjRivKQ5Ns1eSZeB99ExsEQ6rX5XYU1Zy/gGxY/ilqtD4Etx9mKyrPxZRetiahhA==",
"license": "MIT",
"dependencies": {
- "undici-types": "~6.21.0"
+ "undici-types": "~7.14.0"
}
},
"node_modules/@types/normalize-package-data": {
@@ -4169,16 +4238,17 @@
"license": "MIT"
},
"node_modules/@types/prop-types": {
- "version": "15.7.14",
- "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.14.tgz",
- "integrity": "sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ==",
+ "version": "15.7.15",
+ "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz",
+ "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==",
"license": "MIT"
},
"node_modules/@types/react": {
- "version": "17.0.87",
- "resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.87.tgz",
- "integrity": "sha512-wpg9AbtJ6agjA+BKYmhG6dRWEU/2DHYwMzCaBzsz137ft6IyuqZ5fI4ic1DWL4DrI03Zy78IyVE6ucrXl0mu4g==",
+ "version": "17.0.89",
+ "resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.89.tgz",
+ "integrity": "sha512-I98SaDCar5lvEYl80ClRIUztH/hyWHR+I2f+5yTVp/MQ205HgYkA2b5mVdry/+nsEIrf8I65KA5V/PASx68MsQ==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"@types/prop-types": "*",
"@types/scheduler": "^0.16",
@@ -4488,10 +4558,11 @@
}
},
"node_modules/acorn": {
- "version": "8.14.1",
- "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz",
- "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==",
+ "version": "8.15.0",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
+ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"license": "MIT",
+ "peer": true,
"bin": {
"acorn": "bin/acorn"
},
@@ -4509,6 +4580,18 @@
"acorn-walk": "^8.0.2"
}
},
+ "node_modules/acorn-import-phases": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz",
+ "integrity": "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10.13.0"
+ },
+ "peerDependencies": {
+ "acorn": "^8.14.0"
+ }
+ },
"node_modules/acorn-jsx": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-3.0.1.tgz",
@@ -4563,11 +4646,42 @@
"node": ">= 6.0.0"
}
},
+ "node_modules/airbnb-prop-types": {
+ "version": "2.16.0",
+ "resolved": "https://registry.npmjs.org/airbnb-prop-types/-/airbnb-prop-types-2.16.0.tgz",
+ "integrity": "sha512-7WHOFolP/6cS96PhKNrslCLMYAI8yB1Pp6u6XmxozQOiZbsI5ycglZr5cHhBFfuRcQQjzCMith5ZPZdYiJCxUg==",
+ "deprecated": "This package has been renamed to 'prop-types-tools'",
+ "license": "MIT",
+ "dependencies": {
+ "array.prototype.find": "^2.1.1",
+ "function.prototype.name": "^1.1.2",
+ "is-regex": "^1.1.0",
+ "object-is": "^1.1.2",
+ "object.assign": "^4.1.0",
+ "object.entries": "^1.1.2",
+ "prop-types": "^15.7.2",
+ "prop-types-exact": "^1.2.0",
+ "react-is": "^16.13.1"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ },
+ "peerDependencies": {
+ "react": "^0.14 || ^15.0.0 || ^16.0.0-alpha"
+ }
+ },
+ "node_modules/airbnb-prop-types/node_modules/react-is": {
+ "version": "16.13.1",
+ "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
+ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
+ "license": "MIT"
+ },
"node_modules/ajv": {
"version": "8.17.1",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz",
"integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"fast-deep-equal": "^3.1.3",
"fast-uri": "^3.0.1",
@@ -4719,9 +4833,9 @@
}
},
"node_modules/aria-hidden": {
- "version": "1.2.4",
- "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.4.tgz",
- "integrity": "sha512-y+CcFFwelSXpLZk/7fMB2mUbGtX9lKycf1MWJ7CaTIERyitVlyQx6C+sxcROU2BAJ24OiZyK+8wj2i8AlBoS3A==",
+ "version": "1.2.6",
+ "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.6.tgz",
+ "integrity": "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==",
"license": "MIT",
"dependencies": {
"tslib": "^2.0.0"
@@ -4941,18 +5055,6 @@
"node": ">= 4.5.0"
}
},
- "node_modules/attr-accept": {
- "version": "1.1.3",
- "resolved": "https://registry.npmjs.org/attr-accept/-/attr-accept-1.1.3.tgz",
- "integrity": "sha512-iT40nudw8zmCweivz6j58g+RT33I4KbaIvRUhjNmDwO2WmsQUxFEZZYZ5w3vXe5x5MX9D7mfvA/XaLOZYFR9EQ==",
- "license": "MIT",
- "dependencies": {
- "core-js": "^2.5.0"
- },
- "engines": {
- "node": ">=4"
- }
- },
"node_modules/available-typed-arrays": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz",
@@ -5272,13 +5374,13 @@
}
},
"node_modules/babel-plugin-polyfill-corejs2": {
- "version": "0.4.13",
- "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.13.tgz",
- "integrity": "sha512-3sX/eOms8kd3q2KZ6DAhKPc0dgm525Gqq5NtWKZ7QYYZEv57OQ54KtblzJzH1lQF/eQxO8KjWGIK9IPUJNus5g==",
+ "version": "0.4.14",
+ "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.14.tgz",
+ "integrity": "sha512-Co2Y9wX854ts6U8gAAPXfn0GmAyctHuK8n0Yhfjd6t30g7yvKjspvvOo9yG+z52PZRgFErt7Ka2pYnXCjLKEpg==",
"license": "MIT",
"dependencies": {
- "@babel/compat-data": "^7.22.6",
- "@babel/helper-define-polyfill-provider": "^0.6.4",
+ "@babel/compat-data": "^7.27.7",
+ "@babel/helper-define-polyfill-provider": "^0.6.5",
"semver": "^6.3.1"
},
"peerDependencies": {
@@ -5286,25 +5388,25 @@
}
},
"node_modules/babel-plugin-polyfill-corejs3": {
- "version": "0.11.1",
- "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.11.1.tgz",
- "integrity": "sha512-yGCqvBT4rwMczo28xkH/noxJ6MZ4nJfkVYdoDaC/utLtWrXxv27HVrzAeSbqR8SxDsp46n0YF47EbHoixy6rXQ==",
+ "version": "0.13.0",
+ "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.13.0.tgz",
+ "integrity": "sha512-U+GNwMdSFgzVmfhNm8GJUX88AadB3uo9KpJqS3FaqNIPKgySuvMb+bHPsOmmuWyIcuqZj/pzt1RUIUZns4y2+A==",
"license": "MIT",
"dependencies": {
- "@babel/helper-define-polyfill-provider": "^0.6.3",
- "core-js-compat": "^3.40.0"
+ "@babel/helper-define-polyfill-provider": "^0.6.5",
+ "core-js-compat": "^3.43.0"
},
"peerDependencies": {
"@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0"
}
},
"node_modules/babel-plugin-polyfill-regenerator": {
- "version": "0.6.4",
- "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.4.tgz",
- "integrity": "sha512-7gD3pRadPrbjhjLyxebmx/WrFYcuSjZ0XbdUujQMZ/fcE9oeewk2U/7PCvez84UeuK3oSjmPZ0Ch0dlupQvGzw==",
+ "version": "0.6.5",
+ "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.5.tgz",
+ "integrity": "sha512-ISqQ2frbiNU9vIJkzg7dlPpznPZ4jOiUQ1uSmB0fEHeowtN3COYRsXr/xexn64NpU13P06jc/L5TgiJXOgrbEg==",
"license": "MIT",
"dependencies": {
- "@babel/helper-define-polyfill-provider": "^0.6.4"
+ "@babel/helper-define-polyfill-provider": "^0.6.5"
},
"peerDependencies": {
"@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0"
@@ -5411,16 +5513,10 @@
"regenerator-runtime": "^0.10.5"
}
},
- "node_modules/babel-polyfill/node_modules/regenerator-runtime": {
- "version": "0.10.5",
- "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.10.5.tgz",
- "integrity": "sha512-02YopEIhAgiBHWeoTiA8aitHDt8z6w+rQqNuIftlM+ZtvSl/brTouaU7DW6GO/cHtvxJvS4Hwv2ibKdxIRi24w==",
- "license": "MIT"
- },
"node_modules/babel-preset-current-node-syntax": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.1.0.tgz",
- "integrity": "sha512-ldYss8SbBlWva1bs28q78Ju5Zq1F+8BrqBZZ0VFhLBvhh6lCpC2o3gDJi/5DRLs9FgYZCnmPYIVFU4lRXCkyUw==",
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.2.0.tgz",
+ "integrity": "sha512-E/VlAEzRrsLEb2+dv8yp3bo4scof3l9nR4lrld+Iy5NyVqgVYUJnDAmunkhPMisRI32Qc4iRiz425d8vM++2fg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -5441,7 +5537,7 @@
"@babel/plugin-syntax-top-level-await": "^7.14.5"
},
"peerDependencies": {
- "@babel/core": "^7.0.0"
+ "@babel/core": "^7.0.0 || ^8.0.0-0"
}
},
"node_modules/babel-preset-jest": {
@@ -5543,17 +5639,8 @@
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
"license": "MIT",
- "dependencies": {
- "ms": "2.0.0"
- }
- },
- "node_modules/babel-traverse/node_modules/globals": {
- "version": "9.18.0",
- "resolved": "https://registry.npmjs.org/globals/-/globals-9.18.0.tgz",
- "integrity": "sha512-S0nG3CLEQiY/ILxqtztTWH/3iRRdyBLw6KMDxnKMchrtbj2OFmehVh0WUCfW3DUrIgx/qFrJPICrq4Z4sTR9UQ==",
- "license": "MIT",
- "engines": {
- "node": ">=0.10.0"
+ "dependencies": {
+ "ms": "2.0.0"
}
},
"node_modules/babel-traverse/node_modules/ms": {
@@ -5675,6 +5762,15 @@
"node": ">= 0.4.0"
}
},
+ "node_modules/baseline-browser-mapping": {
+ "version": "2.8.16",
+ "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.16.tgz",
+ "integrity": "sha512-OMu3BGQ4E7P1ErFsIPpbJh0qvDudM/UuJeHgkAvfWe+0HFJCXh+t/l8L6fVLR55RI/UbKrVLnAXZSVwd9ysWYw==",
+ "license": "Apache-2.0",
+ "bin": {
+ "baseline-browser-mapping": "dist/cli.js"
+ }
+ },
"node_modules/batch": {
"version": "0.5.3",
"resolved": "https://registry.npmjs.org/batch/-/batch-0.5.3.tgz",
@@ -5712,6 +5808,17 @@
"node": ">=0.10.0"
}
},
+ "node_modules/bindings": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz",
+ "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "file-uri-to-path": "1.0.0"
+ }
+ },
"node_modules/blob": {
"version": "0.0.4",
"resolved": "https://registry.npmjs.org/blob/-/blob-0.0.4.tgz",
@@ -5781,9 +5888,10 @@
}
},
"node_modules/brace-expansion": {
- "version": "1.1.11",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
- "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+ "version": "1.1.12",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
+ "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
+ "dev": true,
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0",
@@ -5803,9 +5911,9 @@
}
},
"node_modules/browserslist": {
- "version": "4.24.4",
- "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.4.tgz",
- "integrity": "sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==",
+ "version": "4.26.3",
+ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.26.3.tgz",
+ "integrity": "sha512-lAUU+02RFBuCKQPj/P6NgjlbCnLBMp4UtgTx7vNHd3XSIJF87s9a5rA3aH2yw3GS9DqZAUbOtZdCCiZeVRqt0w==",
"funding": [
{
"type": "opencollective",
@@ -5821,11 +5929,13 @@
}
],
"license": "MIT",
+ "peer": true,
"dependencies": {
- "caniuse-lite": "^1.0.30001688",
- "electron-to-chromium": "^1.5.73",
- "node-releases": "^2.0.19",
- "update-browserslist-db": "^1.1.1"
+ "baseline-browser-mapping": "^2.8.9",
+ "caniuse-lite": "^1.0.30001746",
+ "electron-to-chromium": "^1.5.227",
+ "node-releases": "^2.0.21",
+ "update-browserslist-db": "^1.1.3"
},
"bin": {
"browserslist": "cli.js"
@@ -5909,9 +6019,9 @@
}
},
"node_modules/cacache/node_modules/brace-expansion": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
- "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
+ "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0"
@@ -5980,26 +6090,28 @@
}
},
"node_modules/cacheable": {
- "version": "1.8.10",
- "resolved": "https://registry.npmjs.org/cacheable/-/cacheable-1.8.10.tgz",
- "integrity": "sha512-0ZnbicB/N2R6uziva8l6O6BieBklArWyiGx4GkwAhLKhSHyQtRfM9T1nx7HHuHDKkYB/efJQhz3QJ6x/YqoZzA==",
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/cacheable/-/cacheable-2.1.0.tgz",
+ "integrity": "sha512-zzL1BxdnqwD69JRT0dihnawAcLkBMwAH+hZSKjUzeBbPedVhk3qYPjRw9VOMYWwt5xRih5xd8S+3kEdGohZm/g==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
- "hookified": "^1.8.1",
- "keyv": "^5.3.2"
+ "@cacheable/memoize": "^2.0.3",
+ "@cacheable/memory": "^2.0.3",
+ "@cacheable/utils": "^2.1.0",
+ "hookified": "^1.12.1",
+ "keyv": "^5.5.3",
+ "qified": "^0.5.0"
}
},
"node_modules/cacheable/node_modules/keyv": {
- "version": "5.3.3",
- "resolved": "https://registry.npmjs.org/keyv/-/keyv-5.3.3.tgz",
- "integrity": "sha512-Rwu4+nXI9fqcxiEHtbkvoes2X+QfkTRo1TMkPfwzipGsJlJO/z69vqB4FNl9xJ3xCpAcbkvmEabZfPzrwN3+gQ==",
+ "version": "5.5.3",
+ "resolved": "https://registry.npmjs.org/keyv/-/keyv-5.5.3.tgz",
+ "integrity": "sha512-h0Un1ieD+HUrzBH6dJXhod3ifSghk5Hw/2Y4/KHBziPlZecrFyE9YOTPU6eOs0V9pYl8gOs86fkr/KN8lUX39A==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
- "@keyv/serialize": "^1.0.3"
+ "@keyv/serialize": "^1.1.1"
}
},
"node_modules/call-bind": {
@@ -6156,9 +6268,9 @@
}
},
"node_modules/caniuse-lite": {
- "version": "1.0.30001715",
- "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001715.tgz",
- "integrity": "sha512-7ptkFGMm2OAOgvZpwgA4yjQ5SQbrNVGdRjzH0pBdy1Fasvcr+KAeECmbCAECzTuDuoX0FCY8KzUxjf9+9kfZEw==",
+ "version": "1.0.30001750",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001750.tgz",
+ "integrity": "sha512-cuom0g5sdX6rw00qOoLNSFCJ9/mYIsuSOA+yzpDw8eopiFqcVwQvZHqov0vmEighRxX++cfC0Vg1G+1Iy/mSpQ==",
"funding": [
{
"type": "opencollective",
@@ -6282,6 +6394,26 @@
"node": ">=0.10.0"
}
},
+ "node_modules/chokidar/node_modules/fsevents": {
+ "version": "1.2.13",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.13.tgz",
+ "integrity": "sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw==",
+ "deprecated": "Upgrade to fsevents v2 to mitigate potential security issues",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "dependencies": {
+ "bindings": "^1.5.0",
+ "nan": "^2.12.1"
+ },
+ "engines": {
+ "node": ">= 4.0"
+ }
+ },
"node_modules/chokidar/node_modules/kind-of": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
@@ -6658,6 +6790,7 @@
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
+ "dev": true,
"license": "MIT"
},
"node_modules/concat-stream": {
@@ -6794,15 +6927,6 @@
"node": ">=0.10.0"
}
},
- "node_modules/copy-to-clipboard": {
- "version": "3.3.3",
- "resolved": "https://registry.npmjs.org/copy-to-clipboard/-/copy-to-clipboard-3.3.3.tgz",
- "integrity": "sha512-2KV8NhB5JqC3ky0r9PMCAZKbUHSwtEo4CwCs0KXgruG43gX5PMqDEBbVU4OUzw2MuAWUfsuFmWvEKG5QRfSnJA==",
- "license": "MIT",
- "dependencies": {
- "toggle-selection": "^1.0.6"
- }
- },
"node_modules/core-js": {
"version": "2.6.12",
"resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.12.tgz",
@@ -6812,12 +6936,12 @@
"license": "MIT"
},
"node_modules/core-js-compat": {
- "version": "3.41.0",
- "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.41.0.tgz",
- "integrity": "sha512-RFsU9LySVue9RTwdDVX/T0e2Y6jRYWXERKElIjpuEOEnxaXffI0X7RUwVzfYLfzuLXSNJDYoRYUAmRUcyln20A==",
+ "version": "3.46.0",
+ "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.46.0.tgz",
+ "integrity": "sha512-p9hObIIEENxSV8xIu+V68JjSeARg6UVMG5mR+JEUguG3sI6MsiS1njz2jHmyJDvA+8jX/sytkBHup6kxhM9law==",
"license": "MIT",
"dependencies": {
- "browserslist": "^4.24.4"
+ "browserslist": "^4.26.3"
},
"funding": {
"type": "opencollective",
@@ -6837,7 +6961,6 @@
"integrity": "sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"env-paths": "^2.2.1",
"import-fresh": "^3.3.0",
@@ -6864,8 +6987,7 @@
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
"dev": true,
- "license": "Python-2.0",
- "peer": true
+ "license": "Python-2.0"
},
"node_modules/cosmiconfig/node_modules/js-yaml": {
"version": "4.1.0",
@@ -6873,7 +6995,6 @@
"integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"argparse": "^2.0.1"
},
@@ -6963,9 +7084,9 @@
}
},
"node_modules/css-loader/node_modules/semver": {
- "version": "7.7.1",
- "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz",
- "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==",
+ "version": "7.7.3",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
+ "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
@@ -6986,7 +7107,6 @@
"integrity": "sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"mdn-data": "2.12.2",
"source-map-js": "^1.0.1"
@@ -7050,12 +7170,6 @@
"dev": true,
"license": "MIT"
},
- "node_modules/custom-event-polyfill": {
- "version": "0.3.0",
- "resolved": "https://registry.npmjs.org/custom-event-polyfill/-/custom-event-polyfill-0.3.0.tgz",
- "integrity": "sha512-dgGyHwa3sJVUVgnF1IQmPnco4SdAJKllCDXL2W7wyw70vchJTSubthvyjlrxUagc4QZrHq31Dz7p6tzukJ0OgA==",
- "license": "MIT"
- },
"node_modules/d": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/d/-/d-1.0.2.tgz",
@@ -7145,9 +7259,9 @@
}
},
"node_modules/datatables.net": {
- "version": "2.2.2",
- "resolved": "https://registry.npmjs.org/datatables.net/-/datatables.net-2.2.2.tgz",
- "integrity": "sha512-gfODIKE3gpgbVeZy2QGj2Dq9roO6hy00S+k1knklrqlMyAMrh1wt0Q6ryBUM7gU96U77ysbq8dYhxFdmcC/oPQ==",
+ "version": "2.3.4",
+ "resolved": "https://registry.npmjs.org/datatables.net/-/datatables.net-2.3.4.tgz",
+ "integrity": "sha512-fKuRlrBIdpAl2uIFgl9enKecHB41QmFd/2nN9LBbOvItV/JalAxLcyqdZXex7wX4ZXjnJQEnv6xeS9veOpKzSw==",
"license": "MIT",
"dependencies": {
"jquery": ">=1.7"
@@ -7170,9 +7284,9 @@
"dev": true
},
"node_modules/debug": {
- "version": "4.4.0",
- "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
- "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==",
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
"license": "MIT",
"dependencies": {
"ms": "^2.1.3"
@@ -7237,9 +7351,9 @@
}
},
"node_modules/decimal.js": {
- "version": "10.5.0",
- "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.5.0.tgz",
- "integrity": "sha512-8vDa8Qxvr/+d94hSh5P3IJwI5t8/c0KsMp+g8bNw9cY2icONa5aPfvKeieW1WlG0WQYwwhJ7mjui2xtiePQSXw==",
+ "version": "10.6.0",
+ "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz",
+ "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==",
"license": "MIT"
},
"node_modules/decode-uri-component": {
@@ -7253,9 +7367,9 @@
}
},
"node_modules/dedent": {
- "version": "1.5.3",
- "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.5.3.tgz",
- "integrity": "sha512-NHQtfOOW68WD8lgypbLA5oT+Bt0xXJhiYvoR6SmmNXZfpzOGXwdKWmcwG8N7PwVVWV3eF/68nmD9BaJSsTBhyQ==",
+ "version": "1.7.0",
+ "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.0.tgz",
+ "integrity": "sha512-HGFtf8yhuhGhqO07SV79tRp+br4MnbdjeVxotpn1QBl30pcLLCQjX5b2295ll0fv8RKDKsmWYrl05usHM9CewQ==",
"dev": true,
"license": "MIT",
"peerDependencies": {
@@ -7505,12 +7619,13 @@
"license": "MIT"
},
"node_modules/dom-helpers": {
- "version": "3.4.0",
- "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-3.4.0.tgz",
- "integrity": "sha512-LnuPJ+dwqKDIyotW1VzmOZ5TONUN7CwkCR5hrgawTUbkBGYdeoNLZo6nNfGkCrjtE1nXXaj7iMMpDa8/d9WoIA==",
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz",
+ "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==",
"license": "MIT",
"dependencies": {
- "@babel/runtime": "^7.1.2"
+ "@babel/runtime": "^7.8.7",
+ "csstype": "^3.0.2"
}
},
"node_modules/dom-serialize": {
@@ -7699,9 +7814,9 @@
"license": "MIT"
},
"node_modules/electron-to-chromium": {
- "version": "1.5.144",
- "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.144.tgz",
- "integrity": "sha512-eJIaMRKeAzxfBSxtjYnoIAw/tdD6VIH6tHBZepZnAbE3Gyqqs5mGN87DvcldPUbVkIljTK8pY0CMcUljP64lfQ==",
+ "version": "1.5.235",
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.235.tgz",
+ "integrity": "sha512-i/7ntLFwOdoHY7sgjlTIDo4Sl8EdoTjWIaKinYOVfC6bOp71bmwenyZthWHcasxgHDNWbWxvG9M3Ia116zIaYQ==",
"license": "ISC"
},
"node_modules/email-prop-type": {
@@ -7890,9 +8005,9 @@
}
},
"node_modules/enhanced-resolve": {
- "version": "5.18.1",
- "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.1.tgz",
- "integrity": "sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg==",
+ "version": "5.18.3",
+ "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz",
+ "integrity": "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==",
"license": "MIT",
"dependencies": {
"graceful-fs": "^4.2.4",
@@ -7925,9 +8040,9 @@
}
},
"node_modules/entities": {
- "version": "6.0.0",
- "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.0.tgz",
- "integrity": "sha512-aKstq2TDOndCn4diEyp9Uq/Flu2i1GlLkc6XIDQSDMuaFE3OPW5OphLCyQ5SpSJZTb4reN+kTcYru5yIfXoRPw==",
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz",
+ "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==",
"license": "BSD-2-Clause",
"engines": {
"node": ">=0.12"
@@ -7946,9 +8061,9 @@
}
},
"node_modules/envinfo": {
- "version": "7.14.0",
- "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.14.0.tgz",
- "integrity": "sha512-CO40UI41xDQzhLB1hWyqUKgFhs250pNcGbyGKe1l/e4FSaI/+YE4IMG76GDt0In67WLPACIITC+sOi08x4wIvg==",
+ "version": "7.18.0",
+ "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.18.0.tgz",
+ "integrity": "sha512-02QGCLRW+Jb8PC270ic02lat+N57iBaWsvHjcJViqp6UVupRB+Vsg7brYPTqEFXvsdTql3KnSczv5ModZFpl8Q==",
"dev": true,
"license": "MIT",
"bin": {
@@ -7978,9 +8093,9 @@
"license": "MIT"
},
"node_modules/error-ex": {
- "version": "1.3.2",
- "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz",
- "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==",
+ "version": "1.3.4",
+ "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz",
+ "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -7988,27 +8103,27 @@
}
},
"node_modules/es-abstract": {
- "version": "1.23.9",
- "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.9.tgz",
- "integrity": "sha512-py07lI0wjxAC/DcfK1S6G7iANonniZwTISvdPzk9hzeH0IZIshbuuFxLIU96OyF89Yb9hiqWn8M/bY83KY5vzA==",
+ "version": "1.24.0",
+ "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.0.tgz",
+ "integrity": "sha512-WSzPgsdLtTcQwm4CROfS5ju2Wa1QQcVeT37jFjYzdFz1r9ahadC8B8/a4qxJxM+09F18iumCdRmlr96ZYkQvEg==",
"license": "MIT",
"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.3",
+ "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.0.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.2.7",
- "get-proto": "^1.0.0",
+ "get-intrinsic": "^1.3.0",
+ "get-proto": "^1.0.1",
"get-symbol-description": "^1.1.0",
"globalthis": "^1.0.4",
"gopd": "^1.2.0",
@@ -8020,21 +8135,24 @@
"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.0",
+ "is-weakref": "^1.1.1",
"math-intrinsics": "^1.1.0",
- "object-inspect": "^1.13.3",
+ "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.3",
+ "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",
@@ -8043,7 +8161,7 @@
"typed-array-byte-offset": "^1.0.4",
"typed-array-length": "^1.0.7",
"unbox-primitive": "^1.1.0",
- "which-typed-array": "^1.1.18"
+ "which-typed-array": "^1.1.19"
},
"engines": {
"node": ">= 0.4"
@@ -8488,16 +8606,6 @@
"node": ">=4.0"
}
},
- "node_modules/eslint/node_modules/globals": {
- "version": "9.18.0",
- "resolved": "https://registry.npmjs.org/globals/-/globals-9.18.0.tgz",
- "integrity": "sha512-S0nG3CLEQiY/ILxqtztTWH/3iRRdyBLw6KMDxnKMchrtbj2OFmehVh0WUCfW3DUrIgx/qFrJPICrq4Z4sTR9UQ==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=0.10.0"
- }
- },
"node_modules/eslint/node_modules/ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
@@ -8766,9 +8874,9 @@
}
},
"node_modules/exponential-backoff": {
- "version": "3.1.2",
- "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.2.tgz",
- "integrity": "sha512-8QxYTVXUkuy7fIIoitQkPwGonB8F3Zj8eEO8Sqg9Zv/bkI7RJAzowee4gr81Hak/dUTpA2Z7VfQgoijjPNlUZA==",
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.3.tgz",
+ "integrity": "sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA==",
"license": "Apache-2.0"
},
"node_modules/exports-loader": {
@@ -8911,9 +9019,9 @@
"license": "MIT"
},
"node_modules/fast-uri": {
- "version": "3.0.6",
- "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.6.tgz",
- "integrity": "sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw==",
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz",
+ "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==",
"funding": [
{
"type": "github",
@@ -9009,6 +9117,7 @@
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"fast-deep-equal": "^3.1.1",
"fast-json-stable-stringify": "^2.0.0",
@@ -9067,6 +9176,14 @@
"url": "https://opencollective.com/webpack"
}
},
+ "node_modules/file-uri-to-path": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
+ "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true
+ },
"node_modules/filename-regex": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/filename-regex/-/filename-regex-2.0.1.tgz",
@@ -9282,9 +9399,9 @@
"license": "MIT"
},
"node_modules/follow-redirects": {
- "version": "1.15.9",
- "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz",
- "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==",
+ "version": "1.15.11",
+ "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
+ "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
"dev": true,
"funding": [
{
@@ -9378,14 +9495,15 @@
}
},
"node_modules/form-data": {
- "version": "4.0.2",
- "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz",
- "integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==",
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz",
+ "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==",
"license": "MIT",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"es-set-tostringtag": "^2.1.0",
+ "hasown": "^2.0.2",
"mime-types": "^2.1.12"
},
"engines": {
@@ -9444,8 +9562,24 @@
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
"integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
+ "dev": true,
"license": "ISC"
},
+ "node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
"node_modules/function-bind": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
@@ -9504,6 +9638,15 @@
"is-property": "^1.0.0"
}
},
+ "node_modules/generator-function": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz",
+ "integrity": "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
"node_modules/gensync": {
"version": "1.0.0-beta.2",
"resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
@@ -9524,9 +9667,9 @@
}
},
"node_modules/get-east-asian-width": {
- "version": "1.3.0",
- "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.3.0.tgz",
- "integrity": "sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ==",
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.4.0.tgz",
+ "integrity": "sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q==",
"dev": true,
"license": "MIT",
"engines": {
@@ -9637,6 +9780,7 @@
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
"integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
"deprecated": "Glob versions prior to v9 are no longer supported",
+ "dev": true,
"license": "ISC",
"dependencies": {
"fs.realpath": "^1.0.0",
@@ -9725,12 +9869,12 @@
}
},
"node_modules/globals": {
- "version": "11.12.0",
- "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz",
- "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==",
+ "version": "9.18.0",
+ "resolved": "https://registry.npmjs.org/globals/-/globals-9.18.0.tgz",
+ "integrity": "sha512-S0nG3CLEQiY/ILxqtztTWH/3iRRdyBLw6KMDxnKMchrtbj2OFmehVh0WUCfW3DUrIgx/qFrJPICrq4Z4sTR9UQ==",
"license": "MIT",
"engines": {
- "node": ">=4"
+ "node": ">=0.10.0"
}
},
"node_modules/globalthis": {
@@ -10065,12 +10209,11 @@
"license": "MIT"
},
"node_modules/hookified": {
- "version": "1.8.2",
- "resolved": "https://registry.npmjs.org/hookified/-/hookified-1.8.2.tgz",
- "integrity": "sha512-5nZbBNP44sFCDjSoB//0N7m508APCgbQ4mGGo1KJGBYyCKNHfry1Pvd0JVHZIxjdnqn8nFRBAN/eFB6Rk/4w5w==",
+ "version": "1.12.1",
+ "resolved": "https://registry.npmjs.org/hookified/-/hookified-1.12.1.tgz",
+ "integrity": "sha512-xnKGl+iMIlhrZmGHB729MqlmPoWBznctSQTYCpFKqNsCgimJQmithcW0xSQMMFzYnV2iKUh25alswn6epgxS0Q==",
"dev": true,
- "license": "MIT",
- "peer": true
+ "license": "MIT"
},
"node_modules/hosted-git-info": {
"version": "4.1.0",
@@ -10172,9 +10315,9 @@
}
},
"node_modules/http-cache-semantics": {
- "version": "4.1.1",
- "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz",
- "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==",
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz",
+ "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==",
"license": "BSD-2-Clause"
},
"node_modules/http-errors": {
@@ -10302,9 +10445,9 @@
"license": "MIT"
},
"node_modules/immutable": {
- "version": "5.1.1",
- "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.1.tgz",
- "integrity": "sha512-3jatXi9ObIsPGr3N5hGw/vWWcTkq6hUYhpQz4k0wLC+owqWi/LiugIw9x0EdNZ2yGedKN/HzePiBvaJRXa0Ujg==",
+ "version": "5.1.4",
+ "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.4.tgz",
+ "integrity": "sha512-p6u1bG3YSnINT5RQmx/yRZBpenIl30kVxkTLDyHLIMk0gict704Q9n+thfDI7lTRm9vXdDYutVzXhzcThxTnXA==",
"license": "MIT"
},
"node_modules/import-fresh": {
@@ -10428,6 +10571,7 @@
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
"integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
"deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.",
+ "dev": true,
"license": "ISC",
"dependencies": {
"once": "^1.3.0",
@@ -10553,38 +10697,6 @@
"node": ">=10.13.0"
}
},
- "node_modules/intl-format-cache": {
- "version": "2.2.9",
- "resolved": "https://registry.npmjs.org/intl-format-cache/-/intl-format-cache-2.2.9.tgz",
- "integrity": "sha512-Zv/u8wRpekckv0cLkwpVdABYST4hZNTDaX7reFetrYTJwxExR2VyTqQm+l0WmL0Qo8Mjb9Tf33qnfj0T7pjxdQ==",
- "license": "BSD-3-Clause"
- },
- "node_modules/intl-messageformat": {
- "version": "2.2.0",
- "resolved": "https://registry.npmjs.org/intl-messageformat/-/intl-messageformat-2.2.0.tgz",
- "integrity": "sha512-I+tSvHnXqJYjDfNmY95tpFMj30yoakC6OXAo+wu/wTMy6tA/4Fd4mvV7Uzs4cqK/Ap29sHhwjcY+78a8eifcXw==",
- "license": "BSD-3-Clause",
- "dependencies": {
- "intl-messageformat-parser": "1.4.0"
- }
- },
- "node_modules/intl-messageformat-parser": {
- "version": "1.4.0",
- "resolved": "https://registry.npmjs.org/intl-messageformat-parser/-/intl-messageformat-parser-1.4.0.tgz",
- "integrity": "sha512-/XkqFHKezO6UcF4Av2/Lzfrez18R0jyw7kRFhSeB/YRakdrgSc9QfFZUwNJI9swMwMoNPygK1ArC5wdFSjPw+A==",
- "deprecated": "We've written a new parser that's 6x faster and is backwards compatible. Please use @formatjs/icu-messageformat-parser",
- "license": "BSD-3-Clause"
- },
- "node_modules/intl-relativeformat": {
- "version": "2.2.0",
- "resolved": "https://registry.npmjs.org/intl-relativeformat/-/intl-relativeformat-2.2.0.tgz",
- "integrity": "sha512-4bV/7kSKaPEmu6ArxXf9xjv1ny74Zkwuey8Pm01NH4zggPP7JHwg2STk8Y3JdspCKRDriwIyLRfEXnj2ZLr4Bw==",
- "deprecated": "This package has been deprecated, please see migration guide at 'https://github.com/formatjs/formatjs/tree/master/packages/intl-relativeformat#migration-guide'",
- "license": "BSD-3-Clause",
- "dependencies": {
- "intl-messageformat": "^2.0.0"
- }
- },
"node_modules/invariant": {
"version": "2.2.4",
"resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz",
@@ -10595,24 +10707,14 @@
}
},
"node_modules/ip-address": {
- "version": "9.0.5",
- "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz",
- "integrity": "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==",
+ "version": "10.0.1",
+ "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.0.1.tgz",
+ "integrity": "sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA==",
"license": "MIT",
- "dependencies": {
- "jsbn": "1.1.0",
- "sprintf-js": "^1.1.3"
- },
"engines": {
"node": ">= 12"
}
},
- "node_modules/ip-address/node_modules/sprintf-js": {
- "version": "1.1.3",
- "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz",
- "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==",
- "license": "BSD-3-Clause"
- },
"node_modules/irregular-plurals": {
"version": "3.5.0",
"resolved": "https://registry.npmjs.org/irregular-plurals/-/irregular-plurals-3.5.0.tgz",
@@ -10933,13 +11035,14 @@
}
},
"node_modules/is-generator-function": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.0.tgz",
- "integrity": "sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==",
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz",
+ "integrity": "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==",
"license": "MIT",
"dependencies": {
- "call-bound": "^1.0.3",
- "get-proto": "^1.0.0",
+ "call-bound": "^1.0.4",
+ "generator-function": "^2.0.0",
+ "get-proto": "^1.0.1",
"has-tostringtag": "^1.0.2",
"safe-regex-test": "^1.1.0"
},
@@ -10996,6 +11099,18 @@
"xtend": "^4.0.0"
}
},
+ "node_modules/is-negative-zero": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz",
+ "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/is-number": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/is-number/-/is-number-0.1.1.tgz",
@@ -11370,9 +11485,9 @@
}
},
"node_modules/istanbul-reports": {
- "version": "3.1.7",
- "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz",
- "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==",
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz",
+ "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==",
"dev": true,
"license": "BSD-3-Clause",
"dependencies": {
@@ -11403,12 +11518,13 @@
"resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-2.6.4.tgz",
"integrity": "sha512-HUYBYi/hlSnCIr8QH9xuDBJUAzSHS0El3HxTomovIQcNxtbNhoOtKwpEZaB/jq3sfW/qyhqwW/VDUtoB2RZ4Tg==",
"dev": true,
- "license": "MIT"
+ "license": "MIT",
+ "peer": true
},
"node_modules/jasmine-jquery": {
"version": "2.1.1",
"resolved": "git+https://git@github.com/velesin/jasmine-jquery.git#ebad463d592d3fea00c69f26ea18a930e09c7b58",
- "integrity": "sha512-P9aZDwDEAVgAbdHG/ViapRzAUJ6zBSq/4I1lJFluIbrld6Sv6LI+HT2J4dgWqtfaCgIyDnHBHSHiJ/anter7wQ==",
+ "integrity": "sha512-sWMb40chzlUOKrHZCGpZoUrVnGm6khfL/fAMKO8vLtUR8yOmWIVVN7MRmep3/DSFhy1Hilon6qAH+UbLZgGG0w==",
"dev": true,
"license": "MIT"
},
@@ -12197,9 +12313,9 @@
"license": "MIT"
},
"node_modules/jest-snapshot/node_modules/semver": {
- "version": "7.7.1",
- "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz",
- "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==",
+ "version": "7.7.3",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
+ "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
"dev": true,
"license": "ISC",
"bin": {
@@ -12349,7 +12465,8 @@
"resolved": "https://registry.npmjs.org/jquery/-/jquery-2.2.4.tgz",
"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.",
- "license": "MIT"
+ "license": "MIT",
+ "peer": true
},
"node_modules/jquery-migrate": {
"version": "1.4.1",
@@ -12395,12 +12512,6 @@
"js-yaml": "bin/js-yaml.js"
}
},
- "node_modules/jsbn": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz",
- "integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==",
- "license": "MIT"
- },
"node_modules/jsdom": {
"version": "20.0.3",
"resolved": "https://registry.npmjs.org/jsdom/-/jsdom-20.0.3.tgz",
@@ -12540,6 +12651,7 @@
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.3.0.tgz",
"integrity": "sha512-qtYiSSFlwot9XHtF9bD9c7rwKjr+RecWT//ZnPvSmEjpV5mmPOCN4j8UjY5hbjNkOwZ/jQv3J6R1/pL7RwgMsg==",
+ "dev": true,
"license": "MIT",
"dependencies": {
"call-bind": "^1.0.8",
@@ -12559,6 +12671,7 @@
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz",
"integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==",
+ "dev": true,
"license": "MIT"
},
"node_modules/json2mq": {
@@ -12603,6 +12716,7 @@
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.1.tgz",
"integrity": "sha512-2/Ki0GcmuqSrgFyelQq9M05y7PS0mEwuIzrf3f1fPqkVDVRvZrPZtVSMHxdgo8Aq0sxAOb/cr2aqqA3LeWHVPg==",
+ "dev": true,
"license": "Public Domain",
"funding": {
"url": "https://github.com/sponsors/ljharb"
@@ -12684,6 +12798,7 @@
"integrity": "sha512-A9/7e/IzHUkTcfjnTy5Wzo2P5wPuf7+QZh1JzNdTpYA0AN/vSrxfFjPKtKC3jRYJFZMJ7S1I9L2LItaJS1XMSg==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"batch": "^0.5.3",
"bluebird": "^2.9.27",
@@ -12849,19 +12964,6 @@
"requirejs": "^2.1.0"
}
},
- "node_modules/karma-selenium-webdriver-launcher": {
- "version": "0.0.4-openedx.0",
- "resolved": "git+ssh://git@github.com/openedx/karma-selenium-webdriver-launcher.git#79cfdc5037eb8585dd3e584875e4343febb6d61f",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "q": "~0.9.6"
- },
- "peerDependencies": {
- "karma": ">=0.9",
- "selenium-webdriver": ">=2.44.0"
- }
- },
"node_modules/karma-sourcemap-loader": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/karma-sourcemap-loader/-/karma-sourcemap-loader-0.4.0.tgz",
@@ -12913,9 +13015,9 @@
}
},
"node_modules/karma-webpack/node_modules/brace-expansion": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
- "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
+ "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -12995,12 +13097,11 @@
}
},
"node_modules/known-css-properties": {
- "version": "0.36.0",
- "resolved": "https://registry.npmjs.org/known-css-properties/-/known-css-properties-0.36.0.tgz",
- "integrity": "sha512-A+9jP+IUmuQsNdsLdcg6Yt7voiMF/D4K83ew0OpJtpu+l34ef7LaohWV0Rc6KNvzw6ZDizkqfyB5JznZnzuKQA==",
+ "version": "0.37.0",
+ "resolved": "https://registry.npmjs.org/known-css-properties/-/known-css-properties-0.37.0.tgz",
+ "integrity": "sha512-JCDrsP4Z1Sb9JwG0aJ8Eo2r7k4Ou5MwmThS/6lcIe1ICyb7UBJKGRIUUdqc2ASdE/42lgz6zFUnzAIhtXnBVrQ==",
"dev": true,
- "license": "MIT",
- "peer": true
+ "license": "MIT"
},
"node_modules/leven": {
"version": "3.1.0",
@@ -13044,12 +13145,16 @@
"license": "MIT"
},
"node_modules/loader-runner": {
- "version": "4.3.0",
- "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz",
- "integrity": "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==",
+ "version": "4.3.1",
+ "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.1.tgz",
+ "integrity": "sha512-IWqP2SCPhyVFTBtRcgMHdzlf9ul25NwaFx4wCEH/KjAXuuHY4yNjvPXsBokp8jCB936PyWRaPKUNh8NvylLp2Q==",
"license": "MIT",
"engines": {
"node": ">=6.11.5"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/webpack"
}
},
"node_modules/loader-utils": {
@@ -13124,14 +13229,6 @@
"integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==",
"license": "MIT"
},
- "node_modules/lodash.get": {
- "version": "4.4.2",
- "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz",
- "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==",
- "deprecated": "This package is deprecated. Use the optional chaining (?.) operator instead.",
- "dev": true,
- "license": "MIT"
- },
"node_modules/lodash.template": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/lodash.template/-/lodash.template-4.5.0.tgz",
@@ -13160,9 +13257,9 @@
"license": "MIT"
},
"node_modules/log-symbols": {
- "version": "7.0.0",
- "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-7.0.0.tgz",
- "integrity": "sha512-zrc91EDk2M+2AXo/9BTvK91pqb7qrPg2nX/Hy+u8a5qQlbaOflCKO+6SqgZ+M+xUFxGdKTgwnGiL96b1W3ikRA==",
+ "version": "7.0.1",
+ "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-7.0.1.tgz",
+ "integrity": "sha512-ja1E3yCr9i/0hmBVaM0bfwDjnGy8I/s6PP4DFp+yP+a+mrHO4Rm7DtmnqROTUkHIkqffC84YY7AeqX6oFk0WFg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -13266,9 +13363,9 @@
}
},
"node_modules/make-dir/node_modules/semver": {
- "version": "7.7.1",
- "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz",
- "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==",
+ "version": "7.7.3",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
+ "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
"dev": true,
"license": "ISC",
"bin": {
@@ -13387,8 +13484,7 @@
"resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.12.2.tgz",
"integrity": "sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==",
"dev": true,
- "license": "CC0-1.0",
- "peer": true
+ "license": "CC0-1.0"
},
"node_modules/media-typer": {
"version": "0.3.0",
@@ -13406,7 +13502,6 @@
"integrity": "sha512-pxQJQzB6djGPXh08dacEloMFopsOqGVRKFPYvPOt9XDZ1HasbgDZA74CJGreSU4G3Ak7EFJGoiH2auq+yXISgA==",
"dev": true,
"license": "MIT",
- "peer": true,
"engines": {
"node": ">=18"
},
@@ -13521,9 +13616,9 @@
}
},
"node_modules/mini-css-extract-plugin": {
- "version": "2.9.2",
- "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-2.9.2.tgz",
- "integrity": "sha512-GJuACcS//jtq4kCtd5ii/M0SZf7OZRH+BxdqXZHaJfb8TJiVl+NgQRPwiYt2EuqeSkNydn/7vP+bcE27C5mb9w==",
+ "version": "2.9.4",
+ "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-2.9.4.tgz",
+ "integrity": "sha512-ZWYT7ln73Hptxqxk2DxPU9MmapXRhxkJD6tkSR04dnQxm8BGu2hzgKLugK5yySD97u/8yy7Ma7E76k9ZdvtjkQ==",
"license": "MIT",
"dependencies": {
"schema-utils": "^4.0.0",
@@ -13544,6 +13639,7 @@
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+ "dev": true,
"license": "ISC",
"dependencies": {
"brace-expansion": "^1.1.7"
@@ -13703,9 +13799,9 @@
"license": "ISC"
},
"node_modules/minizlib": {
- "version": "3.0.2",
- "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.0.2.tgz",
- "integrity": "sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA==",
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz",
+ "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==",
"license": "MIT",
"dependencies": {
"minipass": "^7.1.2"
@@ -13732,6 +13828,7 @@
"version": "0.5.6",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz",
"integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==",
+ "dev": true,
"license": "MIT",
"dependencies": {
"minimist": "^1.2.6"
@@ -13744,6 +13841,7 @@
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
"integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
+ "dev": true,
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/ljharb"
@@ -13783,6 +13881,14 @@
"dev": true,
"license": "ISC"
},
+ "node_modules/nan": {
+ "version": "2.23.0",
+ "resolved": "https://registry.npmjs.org/nan/-/nan-2.23.0.tgz",
+ "integrity": "sha512-1UxuyYGdoQHcGg87Lkqm3FzefucTa0NAiOcuRsDmysep3c1LVCRK2krrUDafMWtjSG04htvAmvg96+SDknOmgQ==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true
+ },
"node_modules/nanoid": {
"version": "3.3.11",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
@@ -13888,13 +13994,14 @@
}
},
"node_modules/nise/node_modules/path-to-regexp": {
- "version": "8.2.0",
- "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.2.0.tgz",
- "integrity": "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==",
+ "version": "8.3.0",
+ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz",
+ "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==",
"dev": true,
"license": "MIT",
- "engines": {
- "node": ">=16"
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
}
},
"node_modules/node-addon-api": {
@@ -13929,9 +14036,9 @@
}
},
"node_modules/node-gyp/node_modules/brace-expansion": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
- "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
+ "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0"
@@ -13982,9 +14089,9 @@
}
},
"node_modules/node-gyp/node_modules/semver": {
- "version": "7.7.1",
- "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz",
- "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==",
+ "version": "7.7.3",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
+ "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
@@ -14016,9 +14123,9 @@
"license": "MIT"
},
"node_modules/node-releases": {
- "version": "2.0.19",
- "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz",
- "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==",
+ "version": "2.0.23",
+ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.23.tgz",
+ "integrity": "sha512-cCmFDMSm26S6tQSDpBCg/NR8NENrVPhAJSf+XbxBG4rPFaaonlEoE9wHQmun+cls499TQGSb7ZyPBRlzgKfpeg==",
"license": "MIT"
},
"node_modules/nopt": {
@@ -14053,9 +14160,9 @@
}
},
"node_modules/normalize-package-data/node_modules/semver": {
- "version": "7.7.1",
- "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz",
- "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==",
+ "version": "7.7.3",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
+ "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
"dev": true,
"license": "ISC",
"bin": {
@@ -14099,9 +14206,9 @@
}
},
"node_modules/nwsapi": {
- "version": "2.2.20",
- "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.20.tgz",
- "integrity": "sha512-/ieB+mDe4MrrKMT8z+mQL8klXydZWGR5Dowt4RAGKbJ3kIGEx3X4ljUo+6V73IXtUPWgfOlU5B9MlGxFO5T+cA==",
+ "version": "2.2.22",
+ "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.22.tgz",
+ "integrity": "sha512-ujSMe1OWVn55euT1ihwCI1ZcAaAU3nxUiDwfDQldc51ZXaB9m2AyOn6/jh1BLe2t/G8xd6uKG1UBF2aZJeg2SQ==",
"license": "MIT"
},
"node_modules/object-assign": {
@@ -14319,6 +14426,7 @@
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
+ "dev": true,
"license": "ISC",
"dependencies": {
"wrappy": "1"
@@ -14634,6 +14742,7 @@
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
"integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
+ "dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
@@ -14928,6 +15037,7 @@
"integrity": "sha512-Wb4p1J4zyFTbM+u6WuO4XstYx4Ky9Cewe4DWrel7B0w6VVICvPwdOpotjzcf6eD8TsckVnIMNONQyPIUFOUbCQ==",
"deprecated": "You can find the new Popper v2 at @popperjs/core, this package is dedicated to the legacy v1",
"license": "MIT",
+ "peer": true,
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/popperjs"
@@ -14963,9 +15073,9 @@
}
},
"node_modules/postcss": {
- "version": "8.5.3",
- "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz",
- "integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==",
+ "version": "8.5.6",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
+ "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
"funding": [
{
"type": "opencollective",
@@ -14981,8 +15091,9 @@
}
],
"license": "MIT",
+ "peer": true,
"dependencies": {
- "nanoid": "^3.3.8",
+ "nanoid": "^3.3.11",
"picocolors": "^1.1.1",
"source-map-js": "^1.2.1"
},
@@ -15083,7 +15194,6 @@
}
],
"license": "MIT",
- "peer": true,
"engines": {
"node": ">=18.0"
},
@@ -15123,6 +15233,7 @@
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz",
"integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"cssesc": "^3.0.0",
"util-deprecate": "^1.0.2"
@@ -15368,6 +15479,19 @@
"teleport": ">=0.2.0"
}
},
+ "node_modules/qified": {
+ "version": "0.5.0",
+ "resolved": "https://registry.npmjs.org/qified/-/qified-0.5.0.tgz",
+ "integrity": "sha512-Zj6Q/Vc/SQ+Fzc87N90jJUzBzxD7MVQ2ZvGyMmYtnl2u1a07CejAhvtk4ZwASos+SiHKCAIylyGHJKIek75QBw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "hookified": "^1.12.1"
+ },
+ "engines": {
+ "node": ">=20"
+ }
+ },
"node_modules/qs": {
"version": "6.13.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz",
@@ -15502,6 +15626,7 @@
"resolved": "https://registry.npmjs.org/react/-/react-16.14.0.tgz",
"integrity": "sha512-0X2CImDkJGApiAlcf0ODKIneSwBPhqJawOa5wCtKbu7ZECrmS26NvtSILynQ66cgkT/RJ4LidJOc3bUESwmU8g==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"loose-envify": "^1.1.0",
"object-assign": "^4.1.1",
@@ -15536,40 +15661,14 @@
"warning": "^4.0.3"
},
"peerDependencies": {
- "react": ">=16.8.0",
- "react-dom": ">=16.8.0"
- }
- },
- "node_modules/react-bootstrap/node_modules/dom-helpers": {
- "version": "5.2.1",
- "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz",
- "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==",
- "license": "MIT",
- "dependencies": {
- "@babel/runtime": "^7.8.7",
- "csstype": "^3.0.2"
- }
- },
- "node_modules/react-bootstrap/node_modules/react-transition-group": {
- "version": "4.4.5",
- "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz",
- "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==",
- "license": "BSD-3-Clause",
- "dependencies": {
- "@babel/runtime": "^7.5.5",
- "dom-helpers": "^5.0.1",
- "loose-envify": "^1.4.0",
- "prop-types": "^15.6.2"
- },
- "peerDependencies": {
- "react": ">=16.6.0",
- "react-dom": ">=16.6.0"
+ "react": ">=16.8.0",
+ "react-dom": ">=16.8.0"
}
},
"node_modules/react-clientside-effect": {
- "version": "1.2.7",
- "resolved": "https://registry.npmjs.org/react-clientside-effect/-/react-clientside-effect-1.2.7.tgz",
- "integrity": "sha512-gce9m0Pk/xYYMEojRI9bgvqQAkl6hm7ozQvqWPyQx+kULiatdHgkNM1QG4DQRx5N9BAzWSCJmt9mMV8/KsdgVg==",
+ "version": "1.2.8",
+ "resolved": "https://registry.npmjs.org/react-clientside-effect/-/react-clientside-effect-1.2.8.tgz",
+ "integrity": "sha512-ma2FePH0z3px2+WOu6h+YycZcEvFmmxIlAb62cF52bG86eMySciO/EQZeQMXd07kPCYB0a1dWDT5J+KE9mCDUw==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.12.13"
@@ -15583,6 +15682,7 @@
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.14.0.tgz",
"integrity": "sha512-1gCeQXDLoIqMgqD3IO2Ah9bnf0w9kzhwN5q4FGnHZ67hBm9yePzB5JJAIQCc8x3pFnNlwFq4RidZggNAAkzWWw==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"loose-envify": "^1.1.0",
"object-assign": "^4.1.1",
@@ -15593,19 +15693,6 @@
"react": "^16.14.0"
}
},
- "node_modules/react-dropzone": {
- "version": "4.3.0",
- "resolved": "https://registry.npmjs.org/react-dropzone/-/react-dropzone-4.3.0.tgz",
- "integrity": "sha512-ULfrLaTSsd8BDa9KVAGCueuq1AN3L14dtMsGGqtP0UwYyjG4Vhf158f/ITSHuSPYkZXbvfcIiOlZsH+e3QWm+Q==",
- "license": "MIT",
- "dependencies": {
- "attr-accept": "^1.1.3",
- "prop-types": "^15.5.7"
- },
- "peerDependencies": {
- "react": ">=0.14.0"
- }
- },
"node_modules/react-element-proptypes": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/react-element-proptypes/-/react-element-proptypes-1.0.0.tgz",
@@ -15628,24 +15715,24 @@
}
},
"node_modules/react-focus-on": {
- "version": "3.9.4",
- "resolved": "https://registry.npmjs.org/react-focus-on/-/react-focus-on-3.9.4.tgz",
- "integrity": "sha512-NFKmeH6++wu8e7LJcbwV8TTd4L5w/U5LMXTMOdUcXhCcZ7F5VOvgeTHd4XN1PD7TNmdvldDu/ENROOykUQ4yQg==",
+ "version": "3.10.0",
+ "resolved": "https://registry.npmjs.org/react-focus-on/-/react-focus-on-3.10.0.tgz",
+ "integrity": "sha512-r2yQchO6QfV5zB3J4Gj6cTYBoxD369vkt0oKj1NJLA5ChQzxjko6V/dqQ7nvmaUBm5pHC+pa8tzHT9jtsVRFMQ==",
"license": "MIT",
"dependencies": {
- "aria-hidden": "^1.2.2",
- "react-focus-lock": "^2.11.3",
- "react-remove-scroll": "^2.6.0",
- "react-style-singleton": "^2.2.1",
+ "aria-hidden": "^1.2.5",
+ "react-focus-lock": "^2.13.6",
+ "react-remove-scroll": "^2.6.3",
+ "react-style-singleton": "^2.2.3",
"tslib": "^2.3.1",
- "use-sidecar": "^1.1.2"
+ "use-sidecar": "^1.1.3"
},
"engines": {
"node": ">=8.5.0"
},
"peerDependencies": {
- "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0",
- "react": "^16.8.0 || ^17.0.0 || ^18.0.0"
+ "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"@types/react": {
@@ -15694,80 +15781,6 @@
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"license": "0BSD"
},
- "node_modules/react-intl-translations-manager": {
- "version": "5.0.3",
- "resolved": "https://registry.npmjs.org/react-intl-translations-manager/-/react-intl-translations-manager-5.0.3.tgz",
- "integrity": "sha512-EfBeugnOGFcdUbQyY9TqBMbuauQ8wm73ZqFr0UqCljhbXl7YDHQcVzclWFRkVmlUffzxitLQFhAZEVVeRNQSwA==",
- "license": "MIT",
- "dependencies": {
- "chalk": "^2.3.2",
- "glob": "^7.1.2",
- "json-stable-stringify": "^1.0.1",
- "mkdirp": "^0.5.1"
- }
- },
- "node_modules/react-intl-translations-manager/node_modules/ansi-styles": {
- "version": "3.2.1",
- "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
- "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
- "license": "MIT",
- "dependencies": {
- "color-convert": "^1.9.0"
- },
- "engines": {
- "node": ">=4"
- }
- },
- "node_modules/react-intl-translations-manager/node_modules/chalk": {
- "version": "2.4.2",
- "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
- "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
- "license": "MIT",
- "dependencies": {
- "ansi-styles": "^3.2.1",
- "escape-string-regexp": "^1.0.5",
- "supports-color": "^5.3.0"
- },
- "engines": {
- "node": ">=4"
- }
- },
- "node_modules/react-intl-translations-manager/node_modules/color-convert": {
- "version": "1.9.3",
- "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
- "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
- "license": "MIT",
- "dependencies": {
- "color-name": "1.1.3"
- }
- },
- "node_modules/react-intl-translations-manager/node_modules/color-name": {
- "version": "1.1.3",
- "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
- "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==",
- "license": "MIT"
- },
- "node_modules/react-intl-translations-manager/node_modules/has-flag": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
- "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==",
- "license": "MIT",
- "engines": {
- "node": ">=4"
- }
- },
- "node_modules/react-intl-translations-manager/node_modules/supports-color": {
- "version": "5.5.0",
- "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
- "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
- "license": "MIT",
- "dependencies": {
- "has-flag": "^3.0.0"
- },
- "engines": {
- "node": ">=4"
- }
- },
"node_modules/react-is": {
"version": "17.0.2",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
@@ -15800,16 +15813,6 @@
"react-dom": ">=16.3.0"
}
},
- "node_modules/react-overlays/node_modules/dom-helpers": {
- "version": "5.2.1",
- "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz",
- "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==",
- "license": "MIT",
- "dependencies": {
- "@babel/runtime": "^7.8.7",
- "csstype": "^3.0.2"
- }
- },
"node_modules/react-proptype-conditional-require": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/react-proptype-conditional-require/-/react-proptype-conditional-require-1.0.4.tgz",
@@ -15842,9 +15845,9 @@
"license": "MIT"
},
"node_modules/react-remove-scroll": {
- "version": "2.6.3",
- "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.6.3.tgz",
- "integrity": "sha512-pnAi91oOk8g8ABQKGF5/M9qxmmOPxaAnopyTHYfqYEwJhyFrbbBtHuSgtKEoH0jpcxx5o3hXqH1mNd9/Oi+8iQ==",
+ "version": "2.7.1",
+ "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.1.tgz",
+ "integrity": "sha512-HpMh8+oahmIdOuS5aFKKY6Pyog+FNaZV/XyJOq7b4YFwsFHe5yYfdbIalI4k3vU2nSDql7YskmUseHsRrJqIPA==",
"license": "MIT",
"dependencies": {
"react-remove-scroll-bar": "^2.3.7",
@@ -15900,6 +15903,23 @@
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"license": "0BSD"
},
+ "node_modules/react-responsive": {
+ "version": "6.1.2",
+ "resolved": "https://registry.npmjs.org/react-responsive/-/react-responsive-6.1.2.tgz",
+ "integrity": "sha512-AXentVC/kN3KED9zhzJv2pu4vZ0i6cSHdTtbCScVV1MT6F5KXaG2qs5D7WLmhdaOvmiMX8UfmS4ZSO+WPwDt4g==",
+ "license": "MIT",
+ "dependencies": {
+ "hyphenate-style-name": "^1.0.0",
+ "matchmediaquery": "^0.3.0",
+ "prop-types": "^15.6.1"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ },
+ "peerDependencies": {
+ "react": "^16.3.0"
+ }
+ },
"node_modules/react-router": {
"version": "5.3.4",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-5.3.4.tgz",
@@ -16026,19 +16046,19 @@
"license": "MIT"
},
"node_modules/react-transition-group": {
- "version": "2.9.0",
- "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-2.9.0.tgz",
- "integrity": "sha512-+HzNTCHpeQyl4MJ/bdE0u6XRMe9+XG/+aL4mCxVN4DnPBQ0/5bfHWPDuOZUzYdMj94daZaZdCCc1Dzt9R/xSSg==",
+ "version": "4.4.5",
+ "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz",
+ "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==",
"license": "BSD-3-Clause",
"dependencies": {
- "dom-helpers": "^3.4.0",
+ "@babel/runtime": "^7.5.5",
+ "dom-helpers": "^5.0.1",
"loose-envify": "^1.4.0",
- "prop-types": "^15.6.2",
- "react-lifecycles-compat": "^3.0.4"
+ "prop-types": "^15.6.2"
},
"peerDependencies": {
- "react": ">=15.0.0",
- "react-dom": ">=15.0.0"
+ "react": ">=16.6.0",
+ "react-dom": ">=16.6.0"
}
},
"node_modules/read-pkg": {
@@ -16545,6 +16565,7 @@
"resolved": "https://registry.npmjs.org/redux/-/redux-3.7.2.tgz",
"integrity": "sha512-pNqnf9q1hI5HHZRBkj3bAngGZW/JMCmexDlOxw4XagXY2o1327nHH54LoTjiPJ0gizoqPDRqWyX/00g0hD6w+A==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"lodash": "^4.2.1",
"lodash-es": "^4.2.1",
@@ -16552,16 +16573,6 @@
"symbol-observable": "^1.0.3"
}
},
- "node_modules/redux-devtools-extension": {
- "version": "2.13.9",
- "resolved": "https://registry.npmjs.org/redux-devtools-extension/-/redux-devtools-extension-2.13.9.tgz",
- "integrity": "sha512-cNJ8Q/EtjhQaZ71c8I9+BPySIBVEKssbPpskBfsXqb8HJ002A3KRVHfeRzwRo6mGPqsm7XuHTqNSNeS1Khig0A==",
- "deprecated": "Package moved to @redux-devtools/extension.",
- "license": "MIT",
- "peerDependencies": {
- "redux": "^3.1.0 || ^4.0.0"
- }
- },
"node_modules/redux-thunk": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-2.2.0.tgz",
@@ -16597,9 +16608,9 @@
"license": "MIT"
},
"node_modules/regenerate-unicode-properties": {
- "version": "10.2.0",
- "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.2.0.tgz",
- "integrity": "sha512-DqHn3DwbmmPVzeKj9woBadqmXxLvQoQIwu7nopMc72ztvxVmVk2SBhSnx67zuye5TP+lJsb/TBQsjLKhnDf3MA==",
+ "version": "10.2.2",
+ "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.2.2.tgz",
+ "integrity": "sha512-m03P+zhBeQd1RGnYxrGyDAPpWX/epKirLrp8e3qevZdVkKtnCrjjWczIbYc8+xd6vcTStVlqfycTx1KR4LOr0g==",
"license": "MIT",
"dependencies": {
"regenerate": "^1.4.2"
@@ -16609,20 +16620,11 @@
}
},
"node_modules/regenerator-runtime": {
- "version": "0.13.11",
- "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz",
- "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==",
+ "version": "0.10.5",
+ "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.10.5.tgz",
+ "integrity": "sha512-02YopEIhAgiBHWeoTiA8aitHDt8z6w+rQqNuIftlM+ZtvSl/brTouaU7DW6GO/cHtvxJvS4Hwv2ibKdxIRi24w==",
"license": "MIT"
},
- "node_modules/regenerator-transform": {
- "version": "0.15.2",
- "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.15.2.tgz",
- "integrity": "sha512-hfMp2BoF0qOk3uc5V20ALGDS2ddjQaLrdl7xrGXvAIow7qeWRM2VA2HuCHkUKk9slq3VwEwLNK3DFBqDfPGYtg==",
- "license": "MIT",
- "dependencies": {
- "@babel/runtime": "^7.8.4"
- }
- },
"node_modules/regex-cache": {
"version": "0.4.4",
"resolved": "https://registry.npmjs.org/regex-cache/-/regex-cache-0.4.4.tgz",
@@ -16671,17 +16673,17 @@
}
},
"node_modules/regexpu-core": {
- "version": "6.2.0",
- "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-6.2.0.tgz",
- "integrity": "sha512-H66BPQMrv+V16t8xtmq+UC0CBpiTBA60V8ibS1QVReIp8T1z8hwFxqcGzm9K6lgsN7sB5edVH8a+ze6Fqm4weA==",
+ "version": "6.4.0",
+ "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-6.4.0.tgz",
+ "integrity": "sha512-0ghuzq67LI9bLXpOX/ISfve/Mq33a4aFRzoQYhnnok1JOFpmE/A2TBGkNVenOGEeSBCjIiWcc6MVOG5HEQv0sA==",
"license": "MIT",
"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.1.0"
+ "unicode-match-property-value-ecmascript": "^2.2.1"
},
"engines": {
"node": ">=4"
@@ -16694,29 +16696,17 @@
"license": "MIT"
},
"node_modules/regjsparser": {
- "version": "0.12.0",
- "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.12.0.tgz",
- "integrity": "sha512-cnE+y8bz4NhMjISKbgeVJtqNbtf5QpjZP+Bslo+UqkIt9QPnX9q095eiRRASJG1/tz6dlNr6Z5NsBiWYokp6EQ==",
+ "version": "0.13.0",
+ "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.13.0.tgz",
+ "integrity": "sha512-NZQZdC5wOE/H3UT28fVGL+ikOZcEzfMGk/c3iN9UGxzWHMa1op7274oyiUVrAG4B2EuFhus8SvkaYnhvW92p9Q==",
"license": "BSD-2-Clause",
"dependencies": {
- "jsesc": "~3.0.2"
+ "jsesc": "~3.1.0"
},
"bin": {
"regjsparser": "bin/parser"
}
},
- "node_modules/regjsparser/node_modules/jsesc": {
- "version": "3.0.2",
- "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.0.2.tgz",
- "integrity": "sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==",
- "license": "MIT",
- "bin": {
- "jsesc": "bin/jsesc"
- },
- "engines": {
- "node": ">=6"
- }
- },
"node_modules/remove-trailing-separator": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz",
@@ -16791,6 +16781,7 @@
"resolved": "https://registry.npmjs.org/requirejs/-/requirejs-2.3.7.tgz",
"integrity": "sha512-DouTG8T1WanGok6Qjg2SXuCMzszOo0eHeH9hDZ5Y4x8Je+9JB38HdTLT4/VA8OaUhBa0JPVHJ0pyBkM1z+pDsw==",
"license": "MIT",
+ "peer": true,
"bin": {
"r_js": "bin/r.js",
"r.js": "bin/r.js"
@@ -16811,12 +16802,6 @@
"integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==",
"license": "MIT"
},
- "node_modules/reselect": {
- "version": "3.0.1",
- "resolved": "https://registry.npmjs.org/reselect/-/reselect-3.0.1.tgz",
- "integrity": "sha512-b/6tFZCmRhtBMa4xGqiiRp9jh9Aqi2A687Lo265cN0/QohJQEBPiQ52f4QB6i0eF3yp3hmLL21LSGBcML2dlxA==",
- "license": "MIT"
- },
"node_modules/resize-observer-polyfill": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz",
@@ -17273,9 +17258,9 @@
}
},
"node_modules/sass": {
- "version": "1.87.0",
- "resolved": "https://registry.npmjs.org/sass/-/sass-1.87.0.tgz",
- "integrity": "sha512-d0NoFH4v6SjEK7BoX810Jsrhj7IQSYHAHLi/iSpgqKc7LaIDshFRlSg5LOymf9FqQhxEHs2W5ZQXlvy0KD45Uw==",
+ "version": "1.93.2",
+ "resolved": "https://registry.npmjs.org/sass/-/sass-1.93.2.tgz",
+ "integrity": "sha512-t+YPtOQHpGW1QWsh1CHQ5cPIr9lbbGZLZnbihP/D/qZj/yuV68m8qarcV17nvkOX81BCrvzAlq2klCQFZghyTg==",
"license": "MIT",
"dependencies": {
"chokidar": "^4.0.0",
@@ -17383,9 +17368,9 @@
}
},
"node_modules/schema-utils": {
- "version": "4.3.2",
- "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.2.tgz",
- "integrity": "sha512-Gn/JaSk/Mt9gYubxTtSn/QCV4em9mpAPiR1rqy/Ocu19u/G9J5WWdNoUT4SiV6mFC3y6cxyFcFwdzPM3FgxGAQ==",
+ "version": "4.3.3",
+ "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz",
+ "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==",
"license": "MIT",
"dependencies": {
"@types/json-schema": "^7.0.9",
@@ -17423,6 +17408,7 @@
}
],
"license": "Apache-2.0",
+ "peer": true,
"dependencies": {
"@bazel/runfiles": "^6.3.1",
"jszip": "^3.10.1",
@@ -18031,12 +18017,12 @@
}
},
"node_modules/socks": {
- "version": "2.8.4",
- "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.4.tgz",
- "integrity": "sha512-D3YaD0aRxR3mEcqnidIs7ReYJFVzWdd6fXJYUM8ixcQcJRGTka/b3saV0KflYhyVJXKhb947GndU35SxYNResQ==",
+ "version": "2.8.7",
+ "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.7.tgz",
+ "integrity": "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==",
"license": "MIT",
"dependencies": {
- "ip-address": "^9.0.5",
+ "ip-address": "^10.0.1",
"smart-buffer": "^4.2.0"
},
"engines": {
@@ -18059,9 +18045,9 @@
}
},
"node_modules/socks-proxy-agent/node_modules/agent-base": {
- "version": "7.1.3",
- "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz",
- "integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==",
+ "version": "7.1.4",
+ "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz",
+ "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==",
"license": "MIT",
"engines": {
"node": ">= 14"
@@ -18159,9 +18145,9 @@
}
},
"node_modules/spdx-license-ids": {
- "version": "3.0.21",
- "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.21.tgz",
- "integrity": "sha512-Bvg/8F5XephndSK3JffaRqdT+gyhfqIPwDHpX80tJrF8QQRYMo8sNMeaZ2Dp5+jhwKnUmIOyFFQfHRkjJm5nXg==",
+ "version": "3.0.22",
+ "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.22.tgz",
+ "integrity": "sha512-4PRT4nh1EImPbt2jASOKHX7PB7I+e4IWNLvkKFDxNhJlfjbYlleYQh285Z/3mPTHSAK/AvdMmw5BNNuYH8ShgQ==",
"dev": true,
"license": "CC0-1.0"
},
@@ -18325,85 +18311,19 @@
}
},
"node_modules/string-replace-loader": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/string-replace-loader/-/string-replace-loader-3.1.0.tgz",
- "integrity": "sha512-5AOMUZeX5HE/ylKDnEa/KKBqvlnFmRZudSOjVJHxhoJg9QYTwl1rECx7SLR8BBH7tfxb4Rp7EM2XVfQFxIhsbQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "loader-utils": "^2.0.0",
- "schema-utils": "^3.0.0"
- },
- "peerDependencies": {
- "webpack": "^5"
- }
- },
- "node_modules/string-replace-loader/node_modules/ajv": {
- "version": "6.12.6",
- "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
- "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "fast-deep-equal": "^3.1.1",
- "fast-json-stable-stringify": "^2.0.0",
- "json-schema-traverse": "^0.4.1",
- "uri-js": "^4.2.2"
- },
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/epoberezkin"
- }
- },
- "node_modules/string-replace-loader/node_modules/ajv-keywords": {
- "version": "3.5.2",
- "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz",
- "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==",
- "dev": true,
- "license": "MIT",
- "peerDependencies": {
- "ajv": "^6.9.1"
- }
- },
- "node_modules/string-replace-loader/node_modules/json-schema-traverse": {
- "version": "0.4.1",
- "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
- "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/string-replace-loader/node_modules/loader-utils": {
- "version": "2.0.4",
- "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz",
- "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "big.js": "^5.2.2",
- "emojis-list": "^3.0.0",
- "json5": "^2.1.2"
- },
- "engines": {
- "node": ">=8.9.0"
- }
- },
- "node_modules/string-replace-loader/node_modules/schema-utils": {
- "version": "3.3.0",
- "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz",
- "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==",
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/string-replace-loader/-/string-replace-loader-3.2.0.tgz",
+ "integrity": "sha512-q7+F4DC6MAKkszF3ZQEuZ3dDH25wXPxFA0maTLk3TOTAYPLDgwqCeCKIvOd8xJhYYYl+EXusYRCyKIJliT/olg==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@types/json-schema": "^7.0.8",
- "ajv": "^6.12.5",
- "ajv-keywords": "^3.5.2"
+ "schema-utils": "^4"
},
"engines": {
- "node": ">= 10.13.0"
+ "node": ">=4"
},
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/webpack"
+ "peerDependencies": {
+ "webpack": "^5"
}
},
"node_modules/string-width": {
@@ -18625,9 +18545,9 @@
"license": "ISC"
},
"node_modules/stylelint": {
- "version": "16.19.1",
- "resolved": "https://registry.npmjs.org/stylelint/-/stylelint-16.19.1.tgz",
- "integrity": "sha512-C1SlPZNMKl+d/C867ZdCRthrS+6KuZ3AoGW113RZCOL0M8xOGpgx7G70wq7lFvqvm4dcfdGFVLB/mNaLFChRKw==",
+ "version": "16.25.0",
+ "resolved": "https://registry.npmjs.org/stylelint/-/stylelint-16.25.0.tgz",
+ "integrity": "sha512-Li0avYWV4nfv1zPbdnxLYBGq4z8DVZxbRgx4Kn6V+Uftz1rMoF1qiEI3oL4kgWqyYgCgs7gT5maHNZ82Gk03vQ==",
"dev": true,
"funding": [
{
@@ -18640,36 +18560,35 @@
}
],
"license": "MIT",
- "peer": true,
"dependencies": {
- "@csstools/css-parser-algorithms": "^3.0.4",
- "@csstools/css-tokenizer": "^3.0.3",
- "@csstools/media-query-list-parser": "^4.0.2",
+ "@csstools/css-parser-algorithms": "^3.0.5",
+ "@csstools/css-tokenizer": "^3.0.4",
+ "@csstools/media-query-list-parser": "^4.0.3",
"@csstools/selector-specificity": "^5.0.0",
- "@dual-bundle/import-meta-resolve": "^4.1.0",
+ "@dual-bundle/import-meta-resolve": "^4.2.1",
"balanced-match": "^2.0.0",
"colord": "^2.9.3",
"cosmiconfig": "^9.0.0",
"css-functions-list": "^3.2.3",
"css-tree": "^3.1.0",
- "debug": "^4.3.7",
+ "debug": "^4.4.3",
"fast-glob": "^3.3.3",
"fastest-levenshtein": "^1.0.16",
- "file-entry-cache": "^10.0.8",
+ "file-entry-cache": "^10.1.4",
"global-modules": "^2.0.0",
"globby": "^11.1.0",
"globjoin": "^0.1.4",
"html-tags": "^3.3.1",
- "ignore": "^7.0.3",
+ "ignore": "^7.0.5",
"imurmurhash": "^0.1.4",
"is-plain-object": "^5.0.0",
- "known-css-properties": "^0.36.0",
+ "known-css-properties": "^0.37.0",
"mathml-tag-names": "^2.1.3",
"meow": "^13.2.0",
"micromatch": "^4.0.8",
"normalize-path": "^3.0.0",
"picocolors": "^1.1.1",
- "postcss": "^8.5.3",
+ "postcss": "^8.5.6",
"postcss-resolve-nested-selector": "^0.1.6",
"postcss-safe-parser": "^7.0.1",
"postcss-selector-parser": "^7.1.0",
@@ -18720,9 +18639,9 @@
}
},
"node_modules/stylelint-formatter-pretty/node_modules/ansi-escapes": {
- "version": "7.0.0",
- "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.0.0.tgz",
- "integrity": "sha512-GdYO7a61mR0fOlAsvC9/rIHf7L96sBc6dEWzeOu+KAea5bZyQRPIpojrVoI4AXGJS/ycu/fBTdLrUkA4ODrvjw==",
+ "version": "7.1.1",
+ "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.1.1.tgz",
+ "integrity": "sha512-Zhl0ErHcSRUaVfGUeUdDuLgpkEo8KIFjB4Y9uAc46ScOpdDiU1Dbyplh7qWJeJ/ZHpbyMSM26+X3BySgnIz40Q==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -18736,9 +18655,9 @@
}
},
"node_modules/stylelint-formatter-pretty/node_modules/ansi-regex": {
- "version": "6.1.0",
- "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz",
- "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==",
+ "version": "6.2.2",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz",
+ "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==",
"dev": true,
"license": "MIT",
"engines": {
@@ -18749,9 +18668,9 @@
}
},
"node_modules/stylelint-formatter-pretty/node_modules/emoji-regex": {
- "version": "10.4.0",
- "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz",
- "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==",
+ "version": "10.6.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz",
+ "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==",
"dev": true,
"license": "MIT"
},
@@ -18774,9 +18693,9 @@
}
},
"node_modules/stylelint-formatter-pretty/node_modules/strip-ansi": {
- "version": "7.1.0",
- "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz",
- "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==",
+ "version": "7.1.2",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz",
+ "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -18794,40 +18713,36 @@
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-2.0.0.tgz",
"integrity": "sha512-1ugUSr8BHXRnK23KfuYS+gVMC3LB8QGH9W1iGtDPsNWoQbgtXSExkBu2aDR4epiGWZOjZsj6lDl/N/AqqTC3UA==",
"dev": true,
- "license": "MIT",
- "peer": true
+ "license": "MIT"
},
"node_modules/stylelint/node_modules/file-entry-cache": {
- "version": "10.0.8",
- "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-10.0.8.tgz",
- "integrity": "sha512-FGXHpfmI4XyzbLd3HQ8cbUcsFGohJpZtmQRHr8z8FxxtCe2PcpgIlVLwIgunqjvRmXypBETvwhV4ptJizA+Y1Q==",
+ "version": "10.1.4",
+ "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-10.1.4.tgz",
+ "integrity": "sha512-5XRUFc0WTtUbjfGzEwXc42tiGxQHBmtbUG1h9L2apu4SulCGN3Hqm//9D6FAolf8MYNL7f/YlJl9vy08pj5JuA==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
- "flat-cache": "^6.1.8"
+ "flat-cache": "^6.1.13"
}
},
"node_modules/stylelint/node_modules/flat-cache": {
- "version": "6.1.8",
- "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-6.1.8.tgz",
- "integrity": "sha512-R6MaD3nrJAtO7C3QOuS79ficm2pEAy++TgEUD8ii1LVlbcgZ9DtASLkt9B+RZSFCzm7QHDMlXPsqqB6W2Pfr1Q==",
+ "version": "6.1.18",
+ "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-6.1.18.tgz",
+ "integrity": "sha512-JUPnFgHMuAVmLmoH9/zoZ6RHOt5n9NlUw/sDXsTbROJ2SFoS2DS4s+swAV6UTeTbGH/CAsZIE6M8TaG/3jVxgQ==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
- "cacheable": "^1.8.9",
+ "cacheable": "^2.1.0",
"flatted": "^3.3.3",
- "hookified": "^1.8.1"
+ "hookified": "^1.12.0"
}
},
"node_modules/stylelint/node_modules/ignore": {
- "version": "7.0.4",
- "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.4.tgz",
- "integrity": "sha512-gJzzk+PQNznz8ysRrC0aOkBNVRBDtE1n53IqyqEf3PXrYwomFs5q4pGMizBMJF+ykh03insJ27hB8gSrD2Hn8A==",
+ "version": "7.0.5",
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz",
+ "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==",
"dev": true,
"license": "MIT",
- "peer": true,
"engines": {
"node": ">= 4"
}
@@ -18838,7 +18753,6 @@
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
"dev": true,
"license": "MIT",
- "peer": true,
"engines": {
"node": ">=8"
}
@@ -18849,7 +18763,6 @@
"integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==",
"dev": true,
"license": "MIT",
- "peer": true,
"engines": {
"node": ">=0.10.0"
}
@@ -18860,7 +18773,6 @@
"integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
"dev": true,
"license": "ISC",
- "peer": true,
"engines": {
"node": ">=14"
},
@@ -18874,7 +18786,6 @@
"integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"ansi-styles": "^4.0.0",
"astral-regex": "^2.0.0",
@@ -18893,7 +18804,6 @@
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"emoji-regex": "^8.0.0",
"is-fullwidth-code-point": "^3.0.0",
@@ -18909,7 +18819,6 @@
"integrity": "sha512-9kY+CygyYM6j02t5YFHbNz2FN5QmYGv9zAjVp4lCDjlCw7amdckXlEt/bjMhUIfj4ThGRE4gCUH5+yGnNuPo5A==",
"dev": true,
"license": "BSD-3-Clause",
- "peer": true,
"dependencies": {
"ajv": "^8.0.1",
"lodash.truncate": "^4.4.2",
@@ -18927,7 +18836,6 @@
"integrity": "sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==",
"dev": true,
"license": "ISC",
- "peer": true,
"dependencies": {
"imurmurhash": "^0.1.4",
"signal-exit": "^4.0.1"
@@ -19036,6 +18944,7 @@
"integrity": "sha512-I/bSHSNEcFFqXLf91nchoNB9D1Kie3QKcWdchYUaoIg1+1bdWDkdfdlvdIOJbi9U8xR0y+MWc5D+won9v95WlQ==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"co": "^4.6.0",
"json-stable-stringify": "^1.0.1"
@@ -19149,46 +19058,34 @@
}
},
"node_modules/tapable": {
- "version": "2.2.1",
- "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz",
- "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==",
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz",
+ "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==",
"license": "MIT",
"engines": {
"node": ">=6"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/webpack"
}
},
"node_modules/tar": {
- "version": "7.4.3",
- "resolved": "https://registry.npmjs.org/tar/-/tar-7.4.3.tgz",
- "integrity": "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==",
+ "version": "7.5.1",
+ "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.1.tgz",
+ "integrity": "sha512-nlGpxf+hv0v7GkWBK2V9spgactGOp0qvfWRxUMjqHyzrt3SgwE48DIv/FhqPHJYLHpgW1opq3nERbz5Anq7n1g==",
"license": "ISC",
"dependencies": {
"@isaacs/fs-minipass": "^4.0.0",
"chownr": "^3.0.0",
"minipass": "^7.1.2",
- "minizlib": "^3.0.1",
- "mkdirp": "^3.0.1",
+ "minizlib": "^3.1.0",
"yallist": "^5.0.0"
},
"engines": {
"node": ">=18"
}
},
- "node_modules/tar/node_modules/mkdirp": {
- "version": "3.0.1",
- "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz",
- "integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==",
- "license": "MIT",
- "bin": {
- "mkdirp": "dist/cjs/src/bin.js"
- },
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/sponsors/isaacs"
- }
- },
"node_modules/tar/node_modules/yallist": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz",
@@ -19199,13 +19096,13 @@
}
},
"node_modules/terser": {
- "version": "5.39.0",
- "resolved": "https://registry.npmjs.org/terser/-/terser-5.39.0.tgz",
- "integrity": "sha512-LBAhFyLho16harJoWMg/nZsQYgTrg5jXOn2nCYjRUcZZEdE3qa2zb8QEDRUGVZBW4rlazf2fxkg8tztybTaqWw==",
+ "version": "5.44.0",
+ "resolved": "https://registry.npmjs.org/terser/-/terser-5.44.0.tgz",
+ "integrity": "sha512-nIVck8DK+GM/0Frwd+nIhZ84pR/BX7rmXMfYwyg+Sri5oGVE99/E3KvXqpC2xHFxyqXyGHTKBSioxxplrO4I4w==",
"license": "BSD-2-Clause",
"dependencies": {
"@jridgewell/source-map": "^0.3.3",
- "acorn": "^8.8.2",
+ "acorn": "^8.15.0",
"commander": "^2.20.0",
"source-map-support": "~0.5.20"
},
@@ -19382,9 +19279,9 @@
"license": "MIT"
},
"node_modules/tmp": {
- "version": "0.2.3",
- "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.3.tgz",
- "integrity": "sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w==",
+ "version": "0.2.5",
+ "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz",
+ "integrity": "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==",
"dev": true,
"license": "MIT",
"engines": {
@@ -19476,12 +19373,6 @@
"node": ">=0.12.0"
}
},
- "node_modules/toggle-selection": {
- "version": "1.0.6",
- "resolved": "https://registry.npmjs.org/toggle-selection/-/toggle-selection-1.0.6.tgz",
- "integrity": "sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ==",
- "license": "MIT"
- },
"node_modules/toidentifier": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
@@ -19831,9 +19722,9 @@
"license": "BSD-3-Clause"
},
"node_modules/undici-types": {
- "version": "6.21.0",
- "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
- "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
+ "version": "7.14.0",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.14.0.tgz",
+ "integrity": "sha512-QQiYxHuyZ9gQUIrmPo3IA+hUl4KYk8uSA7cHrcKd/l3p1OTpZcM0Tbp9x7FAtXdAYhlasd60ncPpgu6ihG6TOA==",
"license": "MIT"
},
"node_modules/unicode-canonical-property-names-ecmascript": {
@@ -19859,18 +19750,18 @@
}
},
"node_modules/unicode-match-property-value-ecmascript": {
- "version": "2.2.0",
- "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.2.0.tgz",
- "integrity": "sha512-4IehN3V/+kkr5YeSSDDQG8QLqO26XpL2XP3GQtqwlT/QYSECAwFztxVHjlbh0+gjJ3XmNLS0zDsbgs9jWKExLg==",
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.2.1.tgz",
+ "integrity": "sha512-JQ84qTuMg4nVkx8ga4A16a1epI9H6uTXAknqxkGF/aFfRLw1xC/Bp24HNLaZhHSkWd3+84t8iXnp1J0kYcZHhg==",
"license": "MIT",
"engines": {
"node": ">=4"
}
},
"node_modules/unicode-property-aliases-ecmascript": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz",
- "integrity": "sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==",
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.2.0.tgz",
+ "integrity": "sha512-hpbDzxUY9BFwX+UeBnxv3Sh1q7HFxj48DTmXchNgRa46lO8uj3/1iEn3MiNUYTg1g9ctIqXCCERn8gYZhHC5lQ==",
"license": "MIT",
"engines": {
"node": ">=4"
@@ -20318,9 +20209,9 @@
}
},
"node_modules/watchpack": {
- "version": "2.4.2",
- "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.2.tgz",
- "integrity": "sha512-TnbFSbcOCcDgjZ4piURLCbJ3nJhznVh9kw6F6iokjiFPl8ONxe9A6nMDVXDiNbrSfLILs6vB07F7wLBrwPYzJw==",
+ "version": "2.4.4",
+ "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.4.tgz",
+ "integrity": "sha512-c5EGNOiyxxV5qmTtAB7rbiXxi1ooX1pQKMLX/MIabJjRA0SJBQOjKF+KSVfHkr9U1cADPon0mRiVe/riyaiDUA==",
"license": "MIT",
"dependencies": {
"glob-to-regexp": "^0.4.1",
@@ -20340,21 +20231,23 @@
}
},
"node_modules/webpack": {
- "version": "5.99.7",
- "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.99.7.tgz",
- "integrity": "sha512-CNqKBRMQjwcmKR0idID5va1qlhrqVUKpovi+Ec79ksW8ux7iS1+A6VqzfZXgVYCFRKl7XL5ap3ZoMpwBJxcg0w==",
+ "version": "5.102.1",
+ "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.102.1.tgz",
+ "integrity": "sha512-7h/weGm9d/ywQ6qzJ+Xy+r9n/3qgp/thalBbpOi5i223dPXKi04IBtqPN9nTd+jBc7QKfvDbaBnFipYp4sJAUQ==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"@types/eslint-scope": "^3.7.7",
- "@types/estree": "^1.0.6",
+ "@types/estree": "^1.0.8",
"@types/json-schema": "^7.0.15",
"@webassemblyjs/ast": "^1.14.1",
"@webassemblyjs/wasm-edit": "^1.14.1",
"@webassemblyjs/wasm-parser": "^1.14.1",
- "acorn": "^8.14.0",
- "browserslist": "^4.24.0",
+ "acorn": "^8.15.0",
+ "acorn-import-phases": "^1.0.3",
+ "browserslist": "^4.26.3",
"chrome-trace-event": "^1.0.2",
- "enhanced-resolve": "^5.17.1",
+ "enhanced-resolve": "^5.17.3",
"es-module-lexer": "^1.2.1",
"eslint-scope": "5.1.1",
"events": "^3.2.0",
@@ -20364,11 +20257,11 @@
"loader-runner": "^4.2.0",
"mime-types": "^2.1.27",
"neo-async": "^2.6.2",
- "schema-utils": "^4.3.2",
- "tapable": "^2.1.1",
+ "schema-utils": "^4.3.3",
+ "tapable": "^2.3.0",
"terser-webpack-plugin": "^5.3.11",
- "watchpack": "^2.4.1",
- "webpack-sources": "^3.2.3"
+ "watchpack": "^2.4.4",
+ "webpack-sources": "^3.3.3"
},
"bin": {
"webpack": "bin/webpack.js"
@@ -20398,6 +20291,7 @@
"integrity": "sha512-pIDJHIEI9LR0yxHXQ+Qh95k2EvXpWzZ5l+d+jIo+RdSm9MiHfzazIxwwni/p7+x4eJZuvG1AJwgC4TNQ7NRgsg==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"@discoveryjs/json-ext": "^0.5.0",
"@webpack-cli/configtest": "^2.1.1",
@@ -20478,9 +20372,9 @@
}
},
"node_modules/webpack-sources": {
- "version": "3.2.3",
- "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz",
- "integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==",
+ "version": "3.3.3",
+ "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.3.3.tgz",
+ "integrity": "sha512-yd1RBzSGanHkitROoPFd6qsrxt+oFhg/129YzheDGqeustzX0vTZJZsSsQjVQC4yzBQ56K55XU8gaNCtIzOnTg==",
"license": "MIT",
"engines": {
"node": ">=10.13.0"
@@ -20510,12 +20404,6 @@
"node": ">=0.10.0"
}
},
- "node_modules/whatwg-fetch": {
- "version": "2.0.4",
- "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-2.0.4.tgz",
- "integrity": "sha512-dcQ1GWpOD/eEQ97k66aiEVpNnapVj90/+R+SXTPYGHpYBBypfKJEQjLrvMZ7YXbKm21gXd4NcuxUTjiv1YtLng==",
- "license": "MIT"
- },
"node_modules/whatwg-mimetype": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz",
@@ -20768,6 +20656,7 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
+ "dev": true,
"license": "ISC"
},
"node_modules/write": {
@@ -20798,9 +20687,9 @@
}
},
"node_modules/ws": {
- "version": "8.18.1",
- "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.1.tgz",
- "integrity": "sha512-RKW2aJZMXeMxVpnZ6bck+RswznaxmzdULiBr6KY7XkTnW8uvt0iT9H5DkHUChXrc+uurzwa0rVI16n/Xzjdz1w==",
+ "version": "8.18.3",
+ "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz",
+ "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==",
"license": "MIT",
"engines": {
"node": ">=10.0.0"
@@ -20960,9 +20849,9 @@
}
},
"node_modules/yoctocolors": {
- "version": "2.1.1",
- "resolved": "https://registry.npmjs.org/yoctocolors/-/yoctocolors-2.1.1.tgz",
- "integrity": "sha512-GQHQqAopRhwU8Kt1DDM8NjibDXHC8eoh1erhGAJPEyveY9qqVeXvVikNKrDz69sHowPMorbPUrH/mx8c50eiBQ==",
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/yoctocolors/-/yoctocolors-2.1.2.tgz",
+ "integrity": "sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==",
"dev": true,
"license": "MIT",
"engines": {
diff --git a/package.json b/package.json
index a3dfdd45feff..969e4a017b1a 100644
--- a/package.json
+++ b/package.json
@@ -15,7 +15,7 @@
"watch-sass": "scripts/watch_sass.sh",
"test": "npm run test-jest && npm run test-karma",
"test-jest": "jest",
- "test-karma": "npm run test-karma-vanilla && npm run test-karma-require && echo 'WARNING: Skipped broken webpack tests. For details, see: https://github.com/openedx/edx-platform/issues/35956'",
+ "test-karma": "npm run test-karma-vanilla && npm run test-karma-require && npm run test-xmodule-webpack && echo 'WARNING: Skipped broken lms-webpack and cms-webpack tests. For details, see: https://github.com/openedx/edx-platform/issues/35956'",
"test-karma-vanilla": "npm run test-cms-vanilla && npm run test-xmodule-vanilla && npm run test-common-vanilla",
"test-karma-require": "npm run test-cms-require && npm run test-common-require",
"test-karma-webpack": "npm run test-cms-webpack && npm run test-lms-webpack && npm run test-xmodule-webpack",
@@ -44,7 +44,6 @@
"@edx/edx-proctoring": "^4.18.1",
"@edx/frontend-component-cookie-policy-banner": "2.2.0",
"@edx/paragon": "2.6.4",
- "@edx/studio-frontend": "^2.1.0",
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^12.1.5",
"@testing-library/user-event": "^12.8.3",
@@ -114,7 +113,6 @@
"karma-jasmine-html-reporter": "0.2.2",
"karma-junit-reporter": "2.0.1",
"karma-requirejs": "1.1.0",
- "karma-selenium-webdriver-launcher": "github:openedx/karma-selenium-webdriver-launcher#0.0.4-openedx.0",
"karma-sourcemap-loader": "0.4.0",
"karma-spec-reporter": "0.0.20",
"karma-webpack": "^5.0.1",
diff --git a/requirements/common_constraints.txt b/requirements/common_constraints.txt
index 748858b7015a..1f3e81f50334 100644
--- a/requirements/common_constraints.txt
+++ b/requirements/common_constraints.txt
@@ -22,3 +22,10 @@ Django<6.0
# elastic search changelog: https://www.elastic.co/guide/en/enterprise-search/master/release-notes-7.14.0.html
# See https://github.com/openedx/edx-platform/issues/35126 for more info
elasticsearch<7.14.0
+
+# pip 25.3 is incompatible with pip-tools hence causing failures during the build process
+# Make upgrade command and all requirements upgrade jobs are broken due to this.
+# See issue https://github.com/openedx/public-engineering/issues/440 for details regarding the ongoing fix.
+# The constraint can be removed once a release (pip-tools > 7.5.1) is available with support for pip 25.3
+# Issue to track this dependency and unpin later on: https://github.com/openedx/edx-lint/issues/503
+pip<25.3
diff --git a/requirements/constraints.txt b/requirements/constraints.txt
index 30dad548fed8..b18a0fb0f381 100644
--- a/requirements/constraints.txt
+++ b/requirements/constraints.txt
@@ -13,16 +13,16 @@
# This file contains all common constraints for edx-repos
-c common_constraints.txt
+# Date: 2025-10-07
+# Stay on LTS version, remove once this is added to common constraint
+Django<6.0
+
# Date: 2020-02-26
# As it is not clarified what exact breaking changes will be introduced as per
# the next major release, ensure the installed version is within boundaries.
# Issue for unpinning: https://github.com/openedx/edx-platform/issues/35280
celery>=5.2.2,<6.0.0
-# Date: 2024-02-02
-# Stay on LTS version, remove once this is added to common constraint
-Django<5.0
-
# Date: 2020-02-10
# django-oauth-toolkit version >=2.0.0 has breaking changes. More details
# mentioned on this issue https://github.com/openedx/edx-platform/issues/32884
@@ -61,7 +61,7 @@ numpy<2.0.0
# Date: 2023-09-18
# pinning this version to avoid updates while the library is being developed
# Issue for unpinning: https://github.com/openedx/edx-platform/issues/35269
-openedx-learning==0.27.1
+openedx-learning==0.30.2
# Date: 2023-11-29
# Open AI version 1.0.0 dropped support for openai.ChatCompletion which is currently in use in enterprise.
diff --git a/requirements/edx-sandbox/README.rst b/requirements/edx-sandbox/README.rst
index 4d628f3e2add..d4b1ab8199a1 100644
--- a/requirements/edx-sandbox/README.rst
+++ b/requirements/edx-sandbox/README.rst
@@ -74,3 +74,21 @@ releases/sumac.txt
.. _Python changelog: https://docs.python.org/3.11/whatsnew/changelog.html
.. _SciPy changelog: https://docs.scipy.org/doc/scipy/release.html
.. _NumPy changelog: https://numpy.org/doc/stable/release.html
+
+releases/teak.txt
+------------------
+
+* Frozen at the time of the Teak release
+* Supports Python 3.11 and Python 3.12
+* SciPy is upgraded from 1.14.1 to 1.15.2
+
+.. _SciPy changelog: https://docs.scipy.org/doc/scipy/release.html
+
+releases/ulmo.txt
+------------------
+
+* Frozen at the time of the Ulmo release
+* Supports Python 3.11 and Python 3.12
+* SciPy is upgraded from 1.15.2 to 1.16.3
+
+.. _SciPy changelog: https://docs.scipy.org/doc/scipy/release.html
diff --git a/requirements/edx-sandbox/base.txt b/requirements/edx-sandbox/base.txt
index a2013ea7485f..887f5cc1beaf 100644
--- a/requirements/edx-sandbox/base.txt
+++ b/requirements/edx-sandbox/base.txt
@@ -38,7 +38,7 @@ markupsafe==3.0.3
# via
# chem
# openedx-calc
-matplotlib==3.10.6
+matplotlib==3.10.7
# via -r requirements/edx-sandbox/base.in
mpmath==1.3.0
# via sympy
@@ -60,7 +60,7 @@ openedx-calc==4.0.2
# via -r requirements/edx-sandbox/base.in
packaging==25.0
# via matplotlib
-pillow==11.3.0
+pillow==12.0.0
# via matplotlib
pycparser==2.23
# via cffi
@@ -74,9 +74,9 @@ python-dateutil==2.9.0.post0
# via matplotlib
random2==1.0.2
# via -r requirements/edx-sandbox/base.in
-regex==2025.9.18
+regex==2025.10.23
# via nltk
-scipy==1.16.2
+scipy==1.16.3
# via
# -r requirements/edx-sandbox/base.in
# chem
diff --git a/requirements/edx-sandbox/releases/ulmo.txt b/requirements/edx-sandbox/releases/ulmo.txt
new file mode 100644
index 000000000000..887f5cc1beaf
--- /dev/null
+++ b/requirements/edx-sandbox/releases/ulmo.txt
@@ -0,0 +1,90 @@
+#
+# This file is autogenerated by pip-compile with Python 3.11
+# by the following command:
+#
+# make upgrade
+#
+cffi==2.0.0
+ # via cryptography
+chem==2.0.0
+ # via -r requirements/edx-sandbox/base.in
+click==8.3.0
+ # via nltk
+codejail-includes==2.0.0
+ # via -r requirements/edx-sandbox/base.in
+contourpy==1.3.3
+ # via matplotlib
+cryptography==45.0.7
+ # via
+ # -c requirements/constraints.txt
+ # -r requirements/edx-sandbox/base.in
+cycler==0.12.1
+ # via matplotlib
+fonttools==4.60.1
+ # via matplotlib
+joblib==1.5.2
+ # via nltk
+kiwisolver==1.4.9
+ # via matplotlib
+lxml[html-clean]==5.3.2
+ # via
+ # -c requirements/constraints.txt
+ # -r requirements/edx-sandbox/base.in
+ # lxml-html-clean
+ # openedx-calc
+lxml-html-clean==0.4.3
+ # via lxml
+markupsafe==3.0.3
+ # via
+ # chem
+ # openedx-calc
+matplotlib==3.10.7
+ # via -r requirements/edx-sandbox/base.in
+mpmath==1.3.0
+ # via sympy
+networkx==3.5
+ # via -r requirements/edx-sandbox/base.in
+nltk==3.9.2
+ # via
+ # -r requirements/edx-sandbox/base.in
+ # chem
+numpy==1.26.4
+ # via
+ # -c requirements/constraints.txt
+ # chem
+ # contourpy
+ # matplotlib
+ # openedx-calc
+ # scipy
+openedx-calc==4.0.2
+ # via -r requirements/edx-sandbox/base.in
+packaging==25.0
+ # via matplotlib
+pillow==12.0.0
+ # via matplotlib
+pycparser==2.23
+ # via cffi
+pyparsing==3.2.5
+ # via
+ # -r requirements/edx-sandbox/base.in
+ # chem
+ # matplotlib
+ # openedx-calc
+python-dateutil==2.9.0.post0
+ # via matplotlib
+random2==1.0.2
+ # via -r requirements/edx-sandbox/base.in
+regex==2025.10.23
+ # via nltk
+scipy==1.16.3
+ # via
+ # -r requirements/edx-sandbox/base.in
+ # chem
+six==1.17.0
+ # via python-dateutil
+sympy==1.14.0
+ # via
+ # -r requirements/edx-sandbox/base.in
+ # openedx-calc
+tqdm==4.67.1
+ # via nltk
diff --git a/requirements/edx/base.txt b/requirements/edx/base.txt
index 2b875e8e75ed..70dce4614e74 100644
--- a/requirements/edx/base.txt
+++ b/requirements/edx/base.txt
@@ -40,6 +40,7 @@ attrs==25.4.0
# edx-ace
# jsonschema
# lti-consumer-xblock
+ # openedx-authz
# openedx-events
# openedx-learning
# referencing
@@ -58,7 +59,7 @@ beautifulsoup4==4.14.2
# pynliner
billiard==4.2.2
# via celery
-bleach[css]==6.2.0
+bleach[css]==6.3.0
# via
# edx-enterprise
# lti-consumer-xblock
@@ -68,29 +69,33 @@ bleach[css]==6.2.0
# xblock-poll
boto==2.49.0
# via -r requirements/edx/kernel.in
-boto3==1.40.46
+boto3==1.40.62
# via
# -r requirements/edx/kernel.in
# django-ses
# fs-s3fs
# ora2
# snowflake-connector-python
-botocore==1.40.46
+botocore==1.40.62
# via
# -r requirements/edx/kernel.in
# boto3
# s3transfer
# snowflake-connector-python
+bracex==2.6
+ # via wcmatch
bridgekeeper==0.9
# via -r requirements/edx/kernel.in
cachecontrol==0.14.3
# via firebase-admin
-cachetools==6.2.0
+cachetools==6.2.1
# via
# edxval
# google-auth
-camel-converter[pydantic]==4.0.1
+camel-converter[pydantic]==5.0.0
# via meilisearch
+casbin-django-orm-adapter==1.7.0
+ # via openedx-authz
celery==5.5.3
# via
# -c requirements/constraints.txt
@@ -109,13 +114,13 @@ certifi==2025.10.5
# httpx
# requests
# snowflake-connector-python
-cffi==1.17.1
+cffi==2.0.0
# via
# cryptography
# pynacl
chardet==5.2.0
# via pysrt
-charset-normalizer==3.4.3
+charset-normalizer==3.4.4
# via
# requests
# snowflake-connector-python
@@ -156,7 +161,6 @@ cryptography==45.0.7
# pyjwt
# pyopenssl
# snowflake-connector-python
- # social-auth-core
cssutils==2.11.1
# via pynliner
defusedxml==0.7.1
@@ -166,11 +170,12 @@ defusedxml==0.7.1
# ora2
# python3-openid
# social-auth-core
-django==4.2.28
+django==5.2.7
# via
# -c requirements/common_constraints.txt
# -c requirements/constraints.txt
# -r requirements/edx/kernel.in
+ # casbin-django-orm-adapter
# django-appconf
# django-autocomplete-light
# django-celery-results
@@ -230,6 +235,7 @@ django==4.2.28
# help-tokens
# jsonfield
# lti-consumer-xblock
+ # openedx-authz
# openedx-django-pyfs
# openedx-django-wiki
# openedx-events
@@ -277,7 +283,7 @@ django-fernet-fields-v2==0.9
# via
# edx-enterprise
# enterprise-integrated-channels
-django-filter==25.1
+django-filter==25.2
# via
# -r requirements/edx/kernel.in
# edx-enterprise
@@ -360,7 +366,7 @@ django-storages==1.14.6
# via
# -r requirements/edx/kernel.in
# edxval
-django-user-tasks==3.4.3
+django-user-tasks==3.4.4
# via -r requirements/edx/kernel.in
django-waffle==5.0.0
# via
@@ -390,6 +396,7 @@ djangorestframework==3.16.1
# edx-organizations
# edx-proctoring
# edx-submissions
+ # openedx-authz
# openedx-forum
# openedx-learning
# ora2
@@ -414,7 +421,8 @@ edx-api-doc-tools==2.1.0
# via
# -r requirements/edx/kernel.in
# edx-name-affirmation
-edx-auth-backends==4.6.1
+ # openedx-authz
+edx-auth-backends==4.6.2
# via -r requirements/edx/kernel.in
edx-bulk-grades==1.2.0
# via
@@ -457,6 +465,7 @@ edx-django-utils==8.0.1
# edx-when
# enterprise-integrated-channels
# event-tracking
+ # openedx-authz
# openedx-events
# ora2
# super-csv
@@ -472,6 +481,7 @@ edx-drf-extensions==10.6.0
# edx-when
# edxval
# enterprise-integrated-channels
+ # openedx-authz
# openedx-learning
edx-enterprise==6.6.5
# via
@@ -504,6 +514,7 @@ edx-opaque-keys[django]==3.0.0
# edx-when
# enterprise-integrated-channels
# lti-consumer-xblock
+ # openedx-authz
# openedx-events
# openedx-filters
# ora2
@@ -528,7 +539,7 @@ edx-search==4.3.0
# openedx-forum
edx-sga==0.26.0
# via -r requirements/edx/bundled.in
-edx-submissions==3.12.0
+edx-submissions==3.12.1
# via
# -r requirements/edx/kernel.in
# ora2
@@ -571,9 +582,9 @@ event-tracking==3.3.0
# edx-completion
# edx-proctoring
# edx-search
-fastavro==1.12.0
+fastavro==1.12.1
# via openedx-events
-filelock==3.19.1
+filelock==3.20.0
# via snowflake-connector-python
firebase-admin==7.1.0
# via edx-ace
@@ -595,25 +606,25 @@ geoip2==5.1.0
# via -r requirements/edx/kernel.in
glob2==0.7
# via -r requirements/edx/kernel.in
-google-api-core[grpc]==2.25.2
+google-api-core[grpc]==2.28.1
# via
# firebase-admin
# google-cloud-core
# google-cloud-firestore
# google-cloud-storage
-google-auth==2.41.1
+google-auth==2.42.0
# via
# google-api-core
# google-cloud-core
# google-cloud-firestore
# google-cloud-storage
-google-cloud-core==2.4.3
+google-cloud-core==2.5.0
# via
# google-cloud-firestore
# google-cloud-storage
google-cloud-firestore==2.21.0
# via firebase-admin
-google-cloud-storage==3.4.0
+google-cloud-storage==3.4.1
# via firebase-admin
google-crc32c==1.7.1
# via
@@ -621,15 +632,15 @@ google-crc32c==1.7.1
# google-resumable-media
google-resumable-media==2.7.2
# via google-cloud-storage
-googleapis-common-protos==1.70.0
+googleapis-common-protos==1.71.0
# via
# google-api-core
# grpcio-status
-grpcio==1.75.1
+grpcio==1.76.0
# via
# google-api-core
# grpcio-status
-grpcio-status==1.75.1
+grpcio-status==1.76.0
# via google-api-core
gunicorn==23.0.0
# via -r requirements/edx/kernel.in
@@ -653,7 +664,7 @@ hyperframe==6.1.0
# via h2
icalendar==6.3.1
# via -r requirements/edx/kernel.in
-idna==3.10
+idna==3.11
# via
# anyio
# httpx
@@ -667,7 +678,7 @@ inflection==0.5.1
# via
# drf-spectacular
# drf-yasg
-invoke==2.2.0
+invoke==2.2.1
# via paramiko
ipaddress==1.0.23
# via -r requirements/edx/kernel.in
@@ -715,7 +726,7 @@ lazy==1.6
# xblock
loremipsum==1.0.5
# via ora2
-lti-consumer-xblock==9.14.2
+lti-consumer-xblock==9.14.3
# via -r requirements/edx/kernel.in
lxml[html-clean]==5.3.2
# via
@@ -757,7 +768,7 @@ markupsafe==3.0.3
# xblock
maxminddb==2.8.2
# via geoip2
-meilisearch==0.37.0
+meilisearch==0.37.1
# via
# -r requirements/edx/kernel.in
# edx-search
@@ -769,7 +780,7 @@ more-itertools==10.8.0
# via cssutils
mpmath==1.3.0
# via sympy
-msgpack==1.1.1
+msgpack==1.1.2
# via cachecontrol
multidict==6.7.0
# via
@@ -779,7 +790,7 @@ mysqlclient==2.2.7
# via
# -r requirements/edx/kernel.in
# openedx-forum
-nh3==0.3.0
+nh3==0.3.1
# via
# -r requirements/edx/kernel.in
# xblocks-contrib
@@ -809,7 +820,10 @@ openedx-atlas==0.7.0
# -r requirements/edx/kernel.in
# edx-enterprise
# enterprise-integrated-channels
+ # openedx-authz
# openedx-forum
+openedx-authz==0.20.0
+ # via -r requirements/edx/kernel.in
openedx-calc==4.0.2
# via -r requirements/edx/kernel.in
openedx-django-pyfs==3.8.0
@@ -835,15 +849,15 @@ openedx-filters==2.1.0
# -r requirements/edx/kernel.in
# lti-consumer-xblock
# ora2
-openedx-forum==0.3.6
+openedx-forum==0.3.8
# via -r requirements/edx/kernel.in
-openedx-learning==0.27.1
+openedx-learning==0.30.2
# via
# -c requirements/constraints.txt
# -r requirements/edx/kernel.in
optimizely-sdk==5.2.0
# via -r requirements/edx/bundled.in
-ora2==6.16.4
+ora2==6.17.1
# via -r requirements/edx/bundled.in
packaging==25.0
# via
@@ -868,19 +882,19 @@ pgpy==0.6.0
# via edx-enterprise
piexif==1.1.3
# via -r requirements/edx/kernel.in
-pillow==11.3.0
+pillow==12.0.0
# via
# -r requirements/edx/kernel.in
# edx-enterprise
# edx-organizations
# edxval
-platformdirs==4.4.0
+platformdirs==4.5.0
# via snowflake-connector-python
polib==1.2.0
# via edx-i18n-tools
prompt-toolkit==3.0.52
# via click-repl
-propcache==0.4.0
+propcache==0.4.1
# via
# aiohttp
# yarl
@@ -888,14 +902,14 @@ proto-plus==1.26.1
# via
# google-api-core
# google-cloud-firestore
-protobuf==6.32.1
+protobuf==6.33.0
# via
# google-api-core
# google-cloud-firestore
# googleapis-common-protos
# grpcio-status
# proto-plus
-psutil==7.1.0
+psutil==7.1.2
# via
# -r requirements/edx/kernel.in
# edx-django-utils
@@ -906,6 +920,10 @@ pyasn1==0.6.1
# rsa
pyasn1-modules==0.4.2
# via google-auth
+pycasbin==2.4.0
+ # via
+ # casbin-django-orm-adapter
+ # openedx-authz
pycountry==24.6.1
# via -r requirements/edx/kernel.in
pycparser==2.23
@@ -915,9 +933,9 @@ pycryptodomex==3.23.0
# -r requirements/edx/kernel.in
# edx-proctoring
# lti-consumer-xblock
-pydantic==2.11.10
+pydantic==2.12.3
# via camel-converter
-pydantic-core==2.33.2
+pydantic-core==2.41.4
# via pydantic
pyjwt[crypto]==2.10.1
# via
@@ -1021,15 +1039,15 @@ random2==1.0.2
# via -r requirements/edx/kernel.in
recommender-xblock==3.1.0
# via -r requirements/edx/bundled.in
-redis==6.4.0
+redis==7.0.1
# via
# -r requirements/edx/kernel.in
# walrus
-referencing==0.36.2
+referencing==0.37.0
# via
# jsonschema
# jsonschema-specifications
-regex==2025.9.18
+regex==2025.10.23
# via nltk
requests==2.32.5
# via
@@ -1060,7 +1078,7 @@ requests-oauthlib==2.0.0
# via
# -r requirements/edx/kernel.in
# social-auth-core
-rpds-py==0.27.1
+rpds-py==0.28.0
# via
# jsonschema
# referencing
@@ -1076,12 +1094,14 @@ s3transfer==0.14.0
# via boto3
sailthru-client==2.2.3
# via edx-ace
-scipy==1.16.2
+scipy==1.16.3
# via chem
semantic-version==2.10.0
# via edx-drf-extensions
shapely==2.1.2
# via -r requirements/edx/kernel.in
+simpleeval==1.0.3
+ # via pycasbin
simplejson==3.20.2
# via
# -r requirements/edx/kernel.in
@@ -1123,7 +1143,7 @@ social-auth-app-django==5.4.1
# -c requirements/constraints.txt
# -r requirements/edx/kernel.in
# edx-auth-backends
-social-auth-core==4.7.0
+social-auth-core==4.8.1
# via
# -r requirements/edx/kernel.in
# edx-auth-backends
@@ -1154,7 +1174,7 @@ super-csv==4.1.0
# via edx-bulk-grades
sympy==1.14.0
# via openedx-calc
-testfixtures==9.1.0
+testfixtures==10.0.0
# via edx-enterprise
text-unidecode==1.3
# via python-slugify
@@ -1215,6 +1235,8 @@ voluptuous==0.15.2
# via ora2
walrus==0.9.5
# via edx-event-bus-redis
+wcmatch==10.1
+ # via pycasbin
wcwidth==0.2.14
# via prompt-toolkit
web-fragments==3.1.0
@@ -1236,7 +1258,7 @@ webob==1.8.9
# xblock
wheel==0.45.1
# via django-pipeline
-wrapt==1.17.3
+wrapt==2.0.0
# via -r requirements/edx/kernel.in
xblock[django]==5.2.0
# via
diff --git a/requirements/edx/coverage.txt b/requirements/edx/coverage.txt
index 010306d68c45..92b0c43b73e3 100644
--- a/requirements/edx/coverage.txt
+++ b/requirements/edx/coverage.txt
@@ -6,7 +6,7 @@
#
chardet==5.2.0
# via diff-cover
-coverage==7.10.7
+coverage==7.11.0
# via -r requirements/edx/coverage.in
diff-cover==9.7.1
# via -r requirements/edx/coverage.in
diff --git a/requirements/edx/development.txt b/requirements/edx/development.txt
index 24054476b2aa..3a2a3fa77635 100644
--- a/requirements/edx/development.txt
+++ b/requirements/edx/development.txt
@@ -46,6 +46,10 @@ aniso8601==10.0.1
# -r requirements/edx/testing.txt
# edx-tincan-py35
# tincan
+annotated-doc==0.0.4
+ # via
+ # -r requirements/edx/testing.txt
+ # fastapi
annotated-types==0.7.0
# via
# -r requirements/edx/doc.txt
@@ -89,6 +93,7 @@ attrs==25.4.0
# edx-ace
# jsonschema
# lti-consumer-xblock
+ # openedx-authz
# openedx-events
# openedx-learning
# referencing
@@ -122,7 +127,7 @@ billiard==4.2.2
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
# celery
-bleach[css]==6.2.0
+bleach[css]==6.3.0
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
@@ -136,7 +141,7 @@ boto==2.49.0
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
-boto3==1.40.46
+boto3==1.40.62
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
@@ -144,13 +149,18 @@ boto3==1.40.46
# fs-s3fs
# ora2
# snowflake-connector-python
-botocore==1.40.46
+botocore==1.40.62
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
# boto3
# s3transfer
# snowflake-connector-python
+bracex==2.6
+ # via
+ # -r requirements/edx/doc.txt
+ # -r requirements/edx/testing.txt
+ # wcmatch
bridgekeeper==0.9
# via
# -r requirements/edx/doc.txt
@@ -164,18 +174,23 @@ cachecontrol==0.14.3
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
# firebase-admin
-cachetools==6.2.0
+cachetools==6.2.1
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
# edxval
# google-auth
# tox
-camel-converter[pydantic]==4.0.1
+camel-converter[pydantic]==5.0.0
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
# meilisearch
+casbin-django-orm-adapter==1.7.0
+ # via
+ # -r requirements/edx/doc.txt
+ # -r requirements/edx/testing.txt
+ # openedx-authz
celery==5.5.3
# via
# -c requirements/constraints.txt
@@ -197,12 +212,11 @@ certifi==2025.10.5
# httpx
# requests
# snowflake-connector-python
-cffi==1.17.1
+cffi==2.0.0
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
# cryptography
- # pact-python
# pynacl
chardet==5.2.0
# via
@@ -211,7 +225,7 @@ chardet==5.2.0
# diff-cover
# pysrt
# tox
-charset-normalizer==3.4.3
+charset-normalizer==3.4.4
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
@@ -275,7 +289,7 @@ colorama==0.4.6
# via
# -r requirements/edx/testing.txt
# tox
-coverage[toml]==7.10.7
+coverage[toml]==7.11.0
# via
# -r requirements/edx/testing.txt
# pytest-cov
@@ -296,7 +310,6 @@ cryptography==45.0.7
# pyjwt
# pyopenssl
# snowflake-connector-python
- # social-auth-core
cssselect==1.3.0
# via
# -r requirements/edx/testing.txt
@@ -330,12 +343,13 @@ distlib==0.4.0
# via
# -r requirements/edx/testing.txt
# virtualenv
-django==4.2.28
+django==5.2.7
# via
# -c requirements/common_constraints.txt
# -c requirements/constraints.txt
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
+ # casbin-django-orm-adapter
# django-appconf
# django-autocomplete-light
# django-celery-results
@@ -398,6 +412,7 @@ django==4.2.28
# help-tokens
# jsonfield
# lti-consumer-xblock
+ # openedx-authz
# openedx-django-pyfs
# openedx-django-wiki
# openedx-events
@@ -469,7 +484,7 @@ django-fernet-fields-v2==0.9
# -r requirements/edx/testing.txt
# edx-enterprise
# enterprise-integrated-channels
-django-filter==25.1
+django-filter==25.2
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
@@ -582,14 +597,14 @@ django-storages==1.14.6
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
# edxval
-django-stubs[compatible-mypy]==5.2.6
+django-stubs[compatible-mypy]==5.2.7
# via
# -c requirements/constraints.txt
# -r requirements/edx/development.in
# djangorestframework-stubs
-django-stubs-ext==5.2.6
+django-stubs-ext==5.2.7
# via django-stubs
-django-user-tasks==3.4.3
+django-user-tasks==3.4.4
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
@@ -624,11 +639,12 @@ djangorestframework==3.16.1
# edx-organizations
# edx-proctoring
# edx-submissions
+ # openedx-authz
# openedx-forum
# openedx-learning
# ora2
# super-csv
-djangorestframework-stubs==3.16.4
+djangorestframework-stubs==3.16.5
# via -r requirements/edx/development.in
djangorestframework-xml==2.0.0
# via
@@ -674,7 +690,8 @@ edx-api-doc-tools==2.1.0
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
# edx-name-affirmation
-edx-auth-backends==4.6.1
+ # openedx-authz
+edx-auth-backends==4.6.2
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
@@ -730,6 +747,7 @@ edx-django-utils==8.0.1
# edx-when
# enterprise-integrated-channels
# event-tracking
+ # openedx-authz
# openedx-events
# ora2
# super-csv
@@ -746,6 +764,7 @@ edx-drf-extensions==10.6.0
# edx-when
# edxval
# enterprise-integrated-channels
+ # openedx-authz
# openedx-learning
edx-enterprise==6.6.5
# via
@@ -791,6 +810,7 @@ edx-opaque-keys[django]==3.0.0
# edx-when
# enterprise-integrated-channels
# lti-consumer-xblock
+ # openedx-authz
# openedx-events
# openedx-filters
# ora2
@@ -825,7 +845,7 @@ edx-sga==0.26.0
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
-edx-submissions==3.12.0
+edx-submissions==3.12.1
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
@@ -892,20 +912,20 @@ execnet==2.1.1
# pytest-xdist
factory-boy==3.3.3
# via -r requirements/edx/testing.txt
-faker==37.8.0
+faker==37.12.0
# via
# -r requirements/edx/testing.txt
# factory-boy
-fastapi==0.118.0
+fastapi==0.120.2
# via
# -r requirements/edx/testing.txt
# pact-python
-fastavro==1.12.0
+fastavro==1.12.1
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
# openedx-events
-filelock==3.19.1
+filelock==3.20.0
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
@@ -951,7 +971,7 @@ glob2==0.7
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
-google-api-core[grpc]==2.25.2
+google-api-core[grpc]==2.28.1
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
@@ -959,7 +979,7 @@ google-api-core[grpc]==2.25.2
# google-cloud-core
# google-cloud-firestore
# google-cloud-storage
-google-auth==2.41.1
+google-auth==2.42.0
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
@@ -967,7 +987,7 @@ google-auth==2.41.1
# google-cloud-core
# google-cloud-firestore
# google-cloud-storage
-google-cloud-core==2.4.3
+google-cloud-core==2.5.0
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
@@ -978,7 +998,7 @@ google-cloud-firestore==2.21.0
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
# firebase-admin
-google-cloud-storage==3.4.0
+google-cloud-storage==3.4.1
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
@@ -994,23 +1014,23 @@ google-resumable-media==2.7.2
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
# google-cloud-storage
-googleapis-common-protos==1.70.0
+googleapis-common-protos==1.71.0
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
# google-api-core
# grpcio-status
-grimp==3.11
+grimp==3.13
# via
# -r requirements/edx/testing.txt
# import-linter
-grpcio==1.75.1
+grpcio==1.76.0
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
# google-api-core
# grpcio-status
-grpcio-status==1.75.1
+grpcio-status==1.76.0
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
@@ -1065,7 +1085,7 @@ icalendar==6.3.1
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
-idna==3.10
+idna==3.11
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
@@ -1079,7 +1099,7 @@ imagesize==1.4.1
# via
# -r requirements/edx/doc.txt
# sphinx
-import-linter==2.5
+import-linter==2.5.2
# via -r requirements/edx/testing.txt
importlib-metadata==8.7.0
# via
@@ -1091,11 +1111,11 @@ inflection==0.5.1
# -r requirements/edx/testing.txt
# drf-spectacular
# drf-yasg
-iniconfig==2.1.0
+iniconfig==2.3.0
# via
# -r requirements/edx/testing.txt
# pytest
-invoke==2.2.0
+invoke==2.2.1
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
@@ -1192,7 +1212,7 @@ loremipsum==1.0.5
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
# ora2
-lti-consumer-xblock==9.14.2
+lti-consumer-xblock==9.14.3
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
@@ -1254,7 +1274,7 @@ mccabe==0.7.0
# via
# -r requirements/edx/testing.txt
# pylint
-meilisearch==0.37.0
+meilisearch==0.37.1
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
@@ -1284,7 +1304,7 @@ mpmath==1.3.0
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
# sympy
-msgpack==1.1.1
+msgpack==1.1.2
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
@@ -1306,7 +1326,7 @@ mysqlclient==2.2.7
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
# openedx-forum
-nh3==0.3.0
+nh3==0.3.1
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
@@ -1349,7 +1369,12 @@ openedx-atlas==0.7.0
# -r requirements/edx/testing.txt
# edx-enterprise
# enterprise-integrated-channels
+ # openedx-authz
# openedx-forum
+openedx-authz==0.20.0
+ # via
+ # -r requirements/edx/doc.txt
+ # -r requirements/edx/testing.txt
openedx-calc==4.0.2
# via
# -r requirements/edx/doc.txt
@@ -1385,11 +1410,11 @@ openedx-filters==2.1.0
# -r requirements/edx/testing.txt
# lti-consumer-xblock
# ora2
-openedx-forum==0.3.6
+openedx-forum==0.3.8
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
-openedx-learning==0.27.1
+openedx-learning==0.30.2
# via
# -c requirements/constraints.txt
# -r requirements/edx/doc.txt
@@ -1398,7 +1423,7 @@ optimizely-sdk==5.2.0
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
-ora2==6.16.4
+ora2==6.17.1
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
@@ -1417,7 +1442,7 @@ packaging==25.0
# snowflake-connector-python
# sphinx
# tox
-pact-python==2.3.3
+pact-python==1.6.0
# via
# -c requirements/constraints.txt
# -r requirements/edx/testing.txt
@@ -1455,7 +1480,7 @@ piexif==1.1.3
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
-pillow==11.3.0
+pillow==12.0.0
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
@@ -1464,7 +1489,7 @@ pillow==11.3.0
# edxval
pip-tools==7.5.1
# via -r requirements/pip-tools.txt
-platformdirs==4.4.0
+platformdirs==4.5.0
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
@@ -1489,7 +1514,7 @@ prompt-toolkit==3.0.52
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
# click-repl
-propcache==0.4.0
+propcache==0.4.1
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
@@ -1501,7 +1526,7 @@ proto-plus==1.26.1
# -r requirements/edx/testing.txt
# google-api-core
# google-cloud-firestore
-protobuf==6.32.1
+protobuf==6.33.0
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
@@ -1510,7 +1535,7 @@ protobuf==6.32.1
# googleapis-common-protos
# grpcio-status
# proto-plus
-psutil==7.1.0
+psutil==7.1.2
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
@@ -1531,6 +1556,12 @@ pyasn1-modules==0.4.2
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
# google-auth
+pycasbin==2.4.0
+ # via
+ # -r requirements/edx/doc.txt
+ # -r requirements/edx/testing.txt
+ # casbin-django-orm-adapter
+ # openedx-authz
pycodestyle==2.8.0
# via
# -c requirements/constraints.txt
@@ -1550,13 +1581,13 @@ pycryptodomex==3.23.0
# -r requirements/edx/testing.txt
# edx-proctoring
# lti-consumer-xblock
-pydantic==2.11.10
+pydantic==2.12.3
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
# camel-converter
# fastapi
-pydantic-core==2.33.2
+pydantic-core==2.41.4
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
@@ -1654,7 +1685,7 @@ pyparsing==3.2.5
# -r requirements/edx/testing.txt
# chem
# openedx-calc
-pyproject-api==1.9.1
+pyproject-api==1.10.0
# via
# -r requirements/edx/testing.txt
# tox
@@ -1785,18 +1816,18 @@ recommender-xblock==3.1.0
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
-redis==6.4.0
+redis==7.0.1
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
# walrus
-referencing==0.36.2
+referencing==0.37.0
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
# jsonschema
# jsonschema-specifications
-regex==2025.9.18
+regex==2025.10.23
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
@@ -1840,7 +1871,7 @@ roman-numerals-py==3.1.0
# via
# -r requirements/edx/doc.txt
# sphinx
-rpds-py==0.27.1
+rpds-py==0.28.0
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
@@ -1868,7 +1899,7 @@ sailthru-client==2.2.3
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
# edx-ace
-scipy==1.16.2
+scipy==1.16.3
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
@@ -1882,6 +1913,11 @@ shapely==2.1.2
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
+simpleeval==1.0.3
+ # via
+ # -r requirements/edx/doc.txt
+ # -r requirements/edx/testing.txt
+ # pycasbin
simplejson==3.20.2
# via
# -r requirements/edx/doc.txt
@@ -1947,7 +1983,7 @@ social-auth-app-django==5.4.1
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
# edx-auth-backends
-social-auth-core==4.7.0
+social-auth-core==4.8.1
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
@@ -2036,7 +2072,7 @@ staff-graded-xblock==3.1.0
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
-starlette==0.48.0
+starlette==0.49.1
# via
# -r requirements/edx/testing.txt
# fastapi
@@ -2059,7 +2095,7 @@ sympy==1.14.0
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
# openedx-calc
-testfixtures==9.1.0
+testfixtures==10.0.0
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
@@ -2086,7 +2122,7 @@ tomlkit==0.13.3
# openedx-learning
# pylint
# snowflake-connector-python
-tox==4.30.3
+tox==4.32.0
# via -r requirements/edx/testing.txt
tqdm==4.67.1
# via
@@ -2162,9 +2198,10 @@ urllib3==2.5.0
# -r requirements/edx/testing.txt
# botocore
# elasticsearch
+ # pact-python
# requests
# types-requests
-uvicorn==0.37.0
+uvicorn==0.38.0
# via
# -r requirements/edx/testing.txt
# pact-python
@@ -2175,7 +2212,7 @@ vine==5.1.0
# amqp
# celery
# kombu
-virtualenv==20.34.0
+virtualenv==20.35.4
# via
# -r requirements/edx/testing.txt
# tox
@@ -2193,6 +2230,11 @@ walrus==0.9.5
# edx-event-bus-redis
watchdog==6.0.0
# via -r requirements/edx/development.in
+wcmatch==10.1
+ # via
+ # -r requirements/edx/doc.txt
+ # -r requirements/edx/testing.txt
+ # pycasbin
wcwidth==0.2.14
# via
# -r requirements/edx/doc.txt
@@ -2226,7 +2268,7 @@ wheel==0.45.1
# -r requirements/pip-tools.txt
# django-pipeline
# pip-tools
-wrapt==1.17.3
+wrapt==2.0.0
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
@@ -2284,7 +2326,6 @@ yarl==1.22.0
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
# aiohttp
- # pact-python
zipp==3.23.0
# via
# -r requirements/edx/doc.txt
diff --git a/requirements/edx/doc.txt b/requirements/edx/doc.txt
index 8d7576b1dfbd..06b1965fbf28 100644
--- a/requirements/edx/doc.txt
+++ b/requirements/edx/doc.txt
@@ -64,6 +64,7 @@ attrs==25.4.0
# edx-ace
# jsonschema
# lti-consumer-xblock
+ # openedx-authz
# openedx-events
# openedx-learning
# referencing
@@ -92,7 +93,7 @@ billiard==4.2.2
# via
# -r requirements/edx/base.txt
# celery
-bleach[css]==6.2.0
+bleach[css]==6.3.0
# via
# -r requirements/edx/base.txt
# edx-enterprise
@@ -103,34 +104,42 @@ bleach[css]==6.2.0
# xblock-poll
boto==2.49.0
# via -r requirements/edx/base.txt
-boto3==1.40.46
+boto3==1.40.62
# via
# -r requirements/edx/base.txt
# django-ses
# fs-s3fs
# ora2
# snowflake-connector-python
-botocore==1.40.46
+botocore==1.40.62
# via
# -r requirements/edx/base.txt
# boto3
# s3transfer
# snowflake-connector-python
+bracex==2.6
+ # via
+ # -r requirements/edx/base.txt
+ # wcmatch
bridgekeeper==0.9
# via -r requirements/edx/base.txt
cachecontrol==0.14.3
# via
# -r requirements/edx/base.txt
# firebase-admin
-cachetools==6.2.0
+cachetools==6.2.1
# via
# -r requirements/edx/base.txt
# edxval
# google-auth
-camel-converter[pydantic]==4.0.1
+camel-converter[pydantic]==5.0.0
# via
# -r requirements/edx/base.txt
# meilisearch
+casbin-django-orm-adapter==1.7.0
+ # via
+ # -r requirements/edx/base.txt
+ # openedx-authz
celery==5.5.3
# via
# -c requirements/constraints.txt
@@ -150,7 +159,7 @@ certifi==2025.10.5
# httpx
# requests
# snowflake-connector-python
-cffi==1.17.1
+cffi==2.0.0
# via
# -r requirements/edx/base.txt
# cryptography
@@ -159,7 +168,7 @@ chardet==5.2.0
# via
# -r requirements/edx/base.txt
# pysrt
-charset-normalizer==3.4.3
+charset-normalizer==3.4.4
# via
# -r requirements/edx/base.txt
# requests
@@ -210,7 +219,6 @@ cryptography==45.0.7
# pyjwt
# pyopenssl
# snowflake-connector-python
- # social-auth-core
cssutils==2.11.1
# via
# -r requirements/edx/base.txt
@@ -224,11 +232,12 @@ defusedxml==0.7.1
# ora2
# python3-openid
# social-auth-core
-django==4.2.28
+django==5.2.7
# via
# -c requirements/common_constraints.txt
# -c requirements/constraints.txt
# -r requirements/edx/base.txt
+ # casbin-django-orm-adapter
# django-appconf
# django-autocomplete-light
# django-celery-results
@@ -288,6 +297,7 @@ django==4.2.28
# help-tokens
# jsonfield
# lti-consumer-xblock
+ # openedx-authz
# openedx-django-pyfs
# openedx-django-wiki
# openedx-events
@@ -342,7 +352,7 @@ django-fernet-fields-v2==0.9
# -r requirements/edx/base.txt
# edx-enterprise
# enterprise-integrated-channels
-django-filter==25.1
+django-filter==25.2
# via
# -r requirements/edx/base.txt
# edx-enterprise
@@ -432,7 +442,7 @@ django-storages==1.14.6
# via
# -r requirements/edx/base.txt
# edxval
-django-user-tasks==3.4.3
+django-user-tasks==3.4.4
# via -r requirements/edx/base.txt
django-waffle==5.0.0
# via
@@ -462,6 +472,7 @@ djangorestframework==3.16.1
# edx-organizations
# edx-proctoring
# edx-submissions
+ # openedx-authz
# openedx-forum
# openedx-learning
# ora2
@@ -498,7 +509,8 @@ edx-api-doc-tools==2.1.0
# via
# -r requirements/edx/base.txt
# edx-name-affirmation
-edx-auth-backends==4.6.1
+ # openedx-authz
+edx-auth-backends==4.6.2
# via -r requirements/edx/base.txt
edx-bulk-grades==1.2.0
# via
@@ -541,6 +553,7 @@ edx-django-utils==8.0.1
# edx-when
# enterprise-integrated-channels
# event-tracking
+ # openedx-authz
# openedx-events
# ora2
# super-csv
@@ -556,6 +569,7 @@ edx-drf-extensions==10.6.0
# edx-when
# edxval
# enterprise-integrated-channels
+ # openedx-authz
# openedx-learning
edx-enterprise==6.6.5
# via
@@ -588,6 +602,7 @@ edx-opaque-keys[django]==3.0.0
# edx-when
# enterprise-integrated-channels
# lti-consumer-xblock
+ # openedx-authz
# openedx-events
# openedx-filters
# ora2
@@ -613,7 +628,7 @@ edx-search==4.3.0
# openedx-forum
edx-sga==0.26.0
# via -r requirements/edx/base.txt
-edx-submissions==3.12.0
+edx-submissions==3.12.1
# via
# -r requirements/edx/base.txt
# ora2
@@ -661,11 +676,11 @@ event-tracking==3.3.0
# edx-completion
# edx-proctoring
# edx-search
-fastavro==1.12.0
+fastavro==1.12.1
# via
# -r requirements/edx/base.txt
# openedx-events
-filelock==3.19.1
+filelock==3.20.0
# via
# -r requirements/edx/base.txt
# snowflake-connector-python
@@ -696,21 +711,21 @@ gitpython==3.1.45
# via -r requirements/edx/doc.in
glob2==0.7
# via -r requirements/edx/base.txt
-google-api-core[grpc]==2.25.2
+google-api-core[grpc]==2.28.1
# via
# -r requirements/edx/base.txt
# firebase-admin
# google-cloud-core
# google-cloud-firestore
# google-cloud-storage
-google-auth==2.41.1
+google-auth==2.42.0
# via
# -r requirements/edx/base.txt
# google-api-core
# google-cloud-core
# google-cloud-firestore
# google-cloud-storage
-google-cloud-core==2.4.3
+google-cloud-core==2.5.0
# via
# -r requirements/edx/base.txt
# google-cloud-firestore
@@ -719,7 +734,7 @@ google-cloud-firestore==2.21.0
# via
# -r requirements/edx/base.txt
# firebase-admin
-google-cloud-storage==3.4.0
+google-cloud-storage==3.4.1
# via
# -r requirements/edx/base.txt
# firebase-admin
@@ -732,17 +747,17 @@ google-resumable-media==2.7.2
# via
# -r requirements/edx/base.txt
# google-cloud-storage
-googleapis-common-protos==1.70.0
+googleapis-common-protos==1.71.0
# via
# -r requirements/edx/base.txt
# google-api-core
# grpcio-status
-grpcio==1.75.1
+grpcio==1.76.0
# via
# -r requirements/edx/base.txt
# google-api-core
# grpcio-status
-grpcio-status==1.75.1
+grpcio-status==1.76.0
# via
# -r requirements/edx/base.txt
# google-api-core
@@ -780,7 +795,7 @@ hyperframe==6.1.0
# h2
icalendar==6.3.1
# via -r requirements/edx/base.txt
-idna==3.10
+idna==3.11
# via
# -r requirements/edx/base.txt
# anyio
@@ -798,7 +813,7 @@ inflection==0.5.1
# -r requirements/edx/base.txt
# drf-spectacular
# drf-yasg
-invoke==2.2.0
+invoke==2.2.1
# via
# -r requirements/edx/base.txt
# paramiko
@@ -869,7 +884,7 @@ loremipsum==1.0.5
# via
# -r requirements/edx/base.txt
# ora2
-lti-consumer-xblock==9.14.2
+lti-consumer-xblock==9.14.3
# via -r requirements/edx/base.txt
lxml[html-clean]==5.3.2
# via
@@ -916,7 +931,7 @@ maxminddb==2.8.2
# via
# -r requirements/edx/base.txt
# geoip2
-meilisearch==0.37.0
+meilisearch==0.37.1
# via
# -r requirements/edx/base.txt
# edx-search
@@ -936,7 +951,7 @@ mpmath==1.3.0
# via
# -r requirements/edx/base.txt
# sympy
-msgpack==1.1.1
+msgpack==1.1.2
# via
# -r requirements/edx/base.txt
# cachecontrol
@@ -949,7 +964,7 @@ mysqlclient==2.2.7
# via
# -r requirements/edx/base.txt
# openedx-forum
-nh3==0.3.0
+nh3==0.3.1
# via
# -r requirements/edx/base.txt
# xblocks-contrib
@@ -982,7 +997,10 @@ openedx-atlas==0.7.0
# -r requirements/edx/base.txt
# edx-enterprise
# enterprise-integrated-channels
+ # openedx-authz
# openedx-forum
+openedx-authz==0.20.0
+ # via -r requirements/edx/base.txt
openedx-calc==4.0.2
# via -r requirements/edx/base.txt
openedx-django-pyfs==3.8.0
@@ -1009,15 +1027,15 @@ openedx-filters==2.1.0
# -r requirements/edx/base.txt
# lti-consumer-xblock
# ora2
-openedx-forum==0.3.6
+openedx-forum==0.3.8
# via -r requirements/edx/base.txt
-openedx-learning==0.27.1
+openedx-learning==0.30.2
# via
# -c requirements/constraints.txt
# -r requirements/edx/base.txt
optimizely-sdk==5.2.0
# via -r requirements/edx/base.txt
-ora2==6.16.4
+ora2==6.17.1
# via -r requirements/edx/base.txt
packaging==25.0
# via
@@ -1052,13 +1070,13 @@ picobox==4.0.0
# via sphinxcontrib-openapi
piexif==1.1.3
# via -r requirements/edx/base.txt
-pillow==11.3.0
+pillow==12.0.0
# via
# -r requirements/edx/base.txt
# edx-enterprise
# edx-organizations
# edxval
-platformdirs==4.4.0
+platformdirs==4.5.0
# via
# -r requirements/edx/base.txt
# snowflake-connector-python
@@ -1070,7 +1088,7 @@ prompt-toolkit==3.0.52
# via
# -r requirements/edx/base.txt
# click-repl
-propcache==0.4.0
+propcache==0.4.1
# via
# -r requirements/edx/base.txt
# aiohttp
@@ -1080,7 +1098,7 @@ proto-plus==1.26.1
# -r requirements/edx/base.txt
# google-api-core
# google-cloud-firestore
-protobuf==6.32.1
+protobuf==6.33.0
# via
# -r requirements/edx/base.txt
# google-api-core
@@ -1088,7 +1106,7 @@ protobuf==6.32.1
# googleapis-common-protos
# grpcio-status
# proto-plus
-psutil==7.1.0
+psutil==7.1.2
# via
# -r requirements/edx/base.txt
# edx-django-utils
@@ -1102,6 +1120,11 @@ pyasn1-modules==0.4.2
# via
# -r requirements/edx/base.txt
# google-auth
+pycasbin==2.4.0
+ # via
+ # -r requirements/edx/base.txt
+ # casbin-django-orm-adapter
+ # openedx-authz
pycountry==24.6.1
# via -r requirements/edx/base.txt
pycparser==2.23
@@ -1113,11 +1136,11 @@ pycryptodomex==3.23.0
# -r requirements/edx/base.txt
# edx-proctoring
# lti-consumer-xblock
-pydantic==2.11.10
+pydantic==2.12.3
# via
# -r requirements/edx/base.txt
# camel-converter
-pydantic-core==2.33.2
+pydantic-core==2.41.4
# via
# -r requirements/edx/base.txt
# pydantic
@@ -1247,16 +1270,16 @@ random2==1.0.2
# via -r requirements/edx/base.txt
recommender-xblock==3.1.0
# via -r requirements/edx/base.txt
-redis==6.4.0
+redis==7.0.1
# via
# -r requirements/edx/base.txt
# walrus
-referencing==0.36.2
+referencing==0.37.0
# via
# -r requirements/edx/base.txt
# jsonschema
# jsonschema-specifications
-regex==2025.9.18
+regex==2025.10.23
# via
# -r requirements/edx/base.txt
# nltk
@@ -1293,7 +1316,7 @@ requests-oauthlib==2.0.0
# social-auth-core
roman-numerals-py==3.1.0
# via sphinx
-rpds-py==0.27.1
+rpds-py==0.28.0
# via
# -r requirements/edx/base.txt
# jsonschema
@@ -1316,7 +1339,7 @@ sailthru-client==2.2.3
# via
# -r requirements/edx/base.txt
# edx-ace
-scipy==1.16.2
+scipy==1.16.3
# via
# -r requirements/edx/base.txt
# chem
@@ -1326,6 +1349,10 @@ semantic-version==2.10.0
# edx-drf-extensions
shapely==2.1.2
# via -r requirements/edx/base.txt
+simpleeval==1.0.3
+ # via
+ # -r requirements/edx/base.txt
+ # pycasbin
simplejson==3.20.2
# via
# -r requirements/edx/base.txt
@@ -1375,7 +1402,7 @@ social-auth-app-django==5.4.1
# -c requirements/constraints.txt
# -r requirements/edx/base.txt
# edx-auth-backends
-social-auth-core==4.7.0
+social-auth-core==4.8.1
# via
# -r requirements/edx/base.txt
# edx-auth-backends
@@ -1456,7 +1483,7 @@ sympy==1.14.0
# via
# -r requirements/edx/base.txt
# openedx-calc
-testfixtures==9.1.0
+testfixtures==10.0.0
# via
# -r requirements/edx/base.txt
# edx-enterprise
@@ -1540,6 +1567,10 @@ walrus==0.9.5
# via
# -r requirements/edx/base.txt
# edx-event-bus-redis
+wcmatch==10.1
+ # via
+ # -r requirements/edx/base.txt
+ # pycasbin
wcwidth==0.2.14
# via
# -r requirements/edx/base.txt
@@ -1566,7 +1597,7 @@ wheel==0.45.1
# via
# -r requirements/edx/base.txt
# django-pipeline
-wrapt==1.17.3
+wrapt==2.0.0
# via -r requirements/edx/base.txt
xblock[django]==5.2.0
# via
diff --git a/requirements/edx/kernel.in b/requirements/edx/kernel.in
index 2b043f71dd79..de2667f28438 100644
--- a/requirements/edx/kernel.in
+++ b/requirements/edx/kernel.in
@@ -161,3 +161,4 @@ wrapt # Better functools.wrapped. TODO: functools
XBlock[django] # Courseware component architecture
xss-utils # https://github.com/openedx/edx-platform/pull/20633 Fix XSS via Translations
unicodeit # Converts mathjax equation to plain text by using unicode symbols
+openedx-authz # Authorization Framework for the Open edX Ecosystem
diff --git a/requirements/edx/semgrep.txt b/requirements/edx/semgrep.txt
index 6adeb975ef01..2a779f3cfab0 100644
--- a/requirements/edx/semgrep.txt
+++ b/requirements/edx/semgrep.txt
@@ -30,26 +30,24 @@ certifi==2025.10.5
# httpcore
# httpx
# requests
-charset-normalizer==3.4.3
+charset-normalizer==3.4.4
# via requests
click==8.1.8
# via
# click-option-group
# semgrep
# uvicorn
-click-option-group==0.5.8
+click-option-group==0.5.9
# via semgrep
colorama==0.4.6
# via semgrep
-defusedxml==0.7.1
- # via semgrep
exceptiongroup==1.2.2
# via semgrep
face==24.0.0
# via glom
glom==22.1.0
# via semgrep
-googleapis-common-protos==1.70.0
+googleapis-common-protos==1.71.0
# via opentelemetry-exporter-otlp-proto-http
h11==0.16.0
# via
@@ -59,16 +57,16 @@ httpcore==1.0.9
# via httpx
httpx==0.28.1
# via mcp
-httpx-sse==0.4.1
+httpx-sse==0.4.3
# via mcp
-idna==3.10
+idna==3.11
# via
# anyio
# httpx
# requests
importlib-metadata==8.7.0
# via opentelemetry-api
-jsonschema==4.20.0
+jsonschema==4.25.1
# via
# mcp
# semgrep
@@ -76,7 +74,7 @@ jsonschema-specifications==2025.9.1
# via jsonschema
markdown-it-py==4.0.0
# via rich
-mcp==1.12.2
+mcp==1.16.0
# via semgrep
mdurl==0.1.2
# via markdown-it-py
@@ -117,25 +115,25 @@ packaging==25.0
# semgrep
peewee==3.18.2
# via semgrep
-protobuf==6.32.1
+protobuf==6.33.0
# via
# googleapis-common-protos
# opentelemetry-proto
-pydantic==2.11.10
+pydantic==2.12.3
# via
# mcp
# pydantic-settings
-pydantic-core==2.33.2
+pydantic-core==2.41.4
# via pydantic
pydantic-settings==2.11.0
# via mcp
pygments==2.19.2
# via rich
-python-dotenv==1.1.1
+python-dotenv==1.2.1
# via pydantic-settings
python-multipart==0.0.20
# via mcp
-referencing==0.36.2
+referencing==0.37.0
# via
# jsonschema
# jsonschema-specifications
@@ -145,23 +143,23 @@ requests==2.32.5
# semgrep
rich==13.5.3
# via semgrep
-rpds-py==0.27.1
+rpds-py==0.28.0
# via
# jsonschema
# referencing
-ruamel-yaml==0.18.15
+ruamel-yaml==0.18.16
# via semgrep
-ruamel-yaml-clib==0.2.12
+ruamel-yaml-clib==0.2.14
# via
# ruamel-yaml
# semgrep
-semgrep==1.139.0
+semgrep==1.141.1
# via -r requirements/edx/semgrep.in
sniffio==1.3.1
# via anyio
sse-starlette==3.0.2
# via mcp
-starlette==0.48.0
+starlette==0.49.1
# via mcp
tomli==2.0.2
# via semgrep
@@ -186,7 +184,7 @@ urllib3==2.5.0
# via
# requests
# semgrep
-uvicorn==0.37.0
+uvicorn==0.38.0
# via mcp
wcmatch==8.5.2
# via semgrep
diff --git a/requirements/edx/testing.txt b/requirements/edx/testing.txt
index 238fde4e137a..9d51cbffa80a 100644
--- a/requirements/edx/testing.txt
+++ b/requirements/edx/testing.txt
@@ -29,6 +29,8 @@ aniso8601==10.0.1
# -r requirements/edx/base.txt
# edx-tincan-py35
# tincan
+annotated-doc==0.0.4
+ # via fastapi
annotated-types==0.7.0
# via
# -r requirements/edx/base.txt
@@ -63,6 +65,7 @@ attrs==25.4.0
# edx-ace
# jsonschema
# lti-consumer-xblock
+ # openedx-authz
# openedx-events
# openedx-learning
# referencing
@@ -89,7 +92,7 @@ billiard==4.2.2
# via
# -r requirements/edx/base.txt
# celery
-bleach[css]==6.2.0
+bleach[css]==6.3.0
# via
# -r requirements/edx/base.txt
# edx-enterprise
@@ -100,35 +103,43 @@ bleach[css]==6.2.0
# xblock-poll
boto==2.49.0
# via -r requirements/edx/base.txt
-boto3==1.40.46
+boto3==1.40.62
# via
# -r requirements/edx/base.txt
# django-ses
# fs-s3fs
# ora2
# snowflake-connector-python
-botocore==1.40.46
+botocore==1.40.62
# via
# -r requirements/edx/base.txt
# boto3
# s3transfer
# snowflake-connector-python
+bracex==2.6
+ # via
+ # -r requirements/edx/base.txt
+ # wcmatch
bridgekeeper==0.9
# via -r requirements/edx/base.txt
cachecontrol==0.14.3
# via
# -r requirements/edx/base.txt
# firebase-admin
-cachetools==6.2.0
+cachetools==6.2.1
# via
# -r requirements/edx/base.txt
# edxval
# google-auth
# tox
-camel-converter[pydantic]==4.0.1
+camel-converter[pydantic]==5.0.0
# via
# -r requirements/edx/base.txt
# meilisearch
+casbin-django-orm-adapter==1.7.0
+ # via
+ # -r requirements/edx/base.txt
+ # openedx-authz
celery==5.5.3
# via
# -c requirements/constraints.txt
@@ -148,11 +159,10 @@ certifi==2025.10.5
# httpx
# requests
# snowflake-connector-python
-cffi==1.17.1
+cffi==2.0.0
# via
# -r requirements/edx/base.txt
# cryptography
- # pact-python
# pynacl
chardet==5.2.0
# via
@@ -161,7 +171,7 @@ chardet==5.2.0
# diff-cover
# pysrt
# tox
-charset-normalizer==3.4.3
+charset-normalizer==3.4.4
# via
# -r requirements/edx/base.txt
# requests
@@ -208,7 +218,7 @@ codejail-includes==2.0.0
# via -r requirements/edx/base.txt
colorama==0.4.6
# via tox
-coverage[toml]==7.10.7
+coverage[toml]==7.11.0
# via
# -r requirements/edx/coverage.txt
# pytest-cov
@@ -226,7 +236,6 @@ cryptography==45.0.7
# pyjwt
# pyopenssl
# snowflake-connector-python
- # social-auth-core
cssselect==1.3.0
# via
# -r requirements/edx/testing.in
@@ -250,11 +259,12 @@ dill==0.4.0
# via pylint
distlib==0.4.0
# via virtualenv
-django==4.2.28
+django==5.2.7
# via
# -c requirements/common_constraints.txt
# -c requirements/constraints.txt
# -r requirements/edx/base.txt
+ # casbin-django-orm-adapter
# django-appconf
# django-autocomplete-light
# django-celery-results
@@ -314,6 +324,7 @@ django==4.2.28
# help-tokens
# jsonfield
# lti-consumer-xblock
+ # openedx-authz
# openedx-django-pyfs
# openedx-django-wiki
# openedx-events
@@ -368,7 +379,7 @@ django-fernet-fields-v2==0.9
# -r requirements/edx/base.txt
# edx-enterprise
# enterprise-integrated-channels
-django-filter==25.1
+django-filter==25.2
# via
# -r requirements/edx/base.txt
# edx-enterprise
@@ -458,7 +469,7 @@ django-storages==1.14.6
# via
# -r requirements/edx/base.txt
# edxval
-django-user-tasks==3.4.3
+django-user-tasks==3.4.4
# via -r requirements/edx/base.txt
django-waffle==5.0.0
# via
@@ -488,6 +499,7 @@ djangorestframework==3.16.1
# edx-organizations
# edx-proctoring
# edx-submissions
+ # openedx-authz
# openedx-forum
# openedx-learning
# ora2
@@ -519,7 +531,8 @@ edx-api-doc-tools==2.1.0
# via
# -r requirements/edx/base.txt
# edx-name-affirmation
-edx-auth-backends==4.6.1
+ # openedx-authz
+edx-auth-backends==4.6.2
# via -r requirements/edx/base.txt
edx-bulk-grades==1.2.0
# via
@@ -562,6 +575,7 @@ edx-django-utils==8.0.1
# edx-when
# enterprise-integrated-channels
# event-tracking
+ # openedx-authz
# openedx-events
# ora2
# super-csv
@@ -577,6 +591,7 @@ edx-drf-extensions==10.6.0
# edx-when
# edxval
# enterprise-integrated-channels
+ # openedx-authz
# openedx-learning
edx-enterprise==6.6.5
# via
@@ -611,6 +626,7 @@ edx-opaque-keys[django]==3.0.0
# edx-when
# enterprise-integrated-channels
# lti-consumer-xblock
+ # openedx-authz
# openedx-events
# openedx-filters
# ora2
@@ -636,7 +652,7 @@ edx-search==4.3.0
# openedx-forum
edx-sga==0.26.0
# via -r requirements/edx/base.txt
-edx-submissions==3.12.0
+edx-submissions==3.12.1
# via
# -r requirements/edx/base.txt
# ora2
@@ -688,15 +704,15 @@ execnet==2.1.1
# via pytest-xdist
factory-boy==3.3.3
# via -r requirements/edx/testing.in
-faker==37.8.0
+faker==37.12.0
# via factory-boy
-fastapi==0.118.0
+fastapi==0.120.2
# via pact-python
-fastavro==1.12.0
+fastavro==1.12.1
# via
# -r requirements/edx/base.txt
# openedx-events
-filelock==3.19.1
+filelock==3.20.0
# via
# -r requirements/edx/base.txt
# snowflake-connector-python
@@ -727,21 +743,21 @@ geoip2==5.1.0
# via -r requirements/edx/base.txt
glob2==0.7
# via -r requirements/edx/base.txt
-google-api-core[grpc]==2.25.2
+google-api-core[grpc]==2.28.1
# via
# -r requirements/edx/base.txt
# firebase-admin
# google-cloud-core
# google-cloud-firestore
# google-cloud-storage
-google-auth==2.41.1
+google-auth==2.42.0
# via
# -r requirements/edx/base.txt
# google-api-core
# google-cloud-core
# google-cloud-firestore
# google-cloud-storage
-google-cloud-core==2.4.3
+google-cloud-core==2.5.0
# via
# -r requirements/edx/base.txt
# google-cloud-firestore
@@ -750,7 +766,7 @@ google-cloud-firestore==2.21.0
# via
# -r requirements/edx/base.txt
# firebase-admin
-google-cloud-storage==3.4.0
+google-cloud-storage==3.4.1
# via
# -r requirements/edx/base.txt
# firebase-admin
@@ -763,19 +779,19 @@ google-resumable-media==2.7.2
# via
# -r requirements/edx/base.txt
# google-cloud-storage
-googleapis-common-protos==1.70.0
+googleapis-common-protos==1.71.0
# via
# -r requirements/edx/base.txt
# google-api-core
# grpcio-status
-grimp==3.11
+grimp==3.13
# via import-linter
-grpcio==1.75.1
+grpcio==1.76.0
# via
# -r requirements/edx/base.txt
# google-api-core
# grpcio-status
-grpcio-status==1.75.1
+grpcio-status==1.76.0
# via
# -r requirements/edx/base.txt
# google-api-core
@@ -816,7 +832,7 @@ hyperframe==6.1.0
# h2
icalendar==6.3.1
# via -r requirements/edx/base.txt
-idna==3.10
+idna==3.11
# via
# -r requirements/edx/base.txt
# anyio
@@ -825,7 +841,7 @@ idna==3.10
# requests
# snowflake-connector-python
# yarl
-import-linter==2.5
+import-linter==2.5.2
# via -r requirements/edx/testing.in
importlib-metadata==8.7.0
# via -r requirements/edx/base.txt
@@ -834,9 +850,9 @@ inflection==0.5.1
# -r requirements/edx/base.txt
# drf-spectacular
# drf-yasg
-iniconfig==2.1.0
+iniconfig==2.3.0
# via pytest
-invoke==2.2.0
+invoke==2.2.1
# via
# -r requirements/edx/base.txt
# paramiko
@@ -910,7 +926,7 @@ loremipsum==1.0.5
# via
# -r requirements/edx/base.txt
# ora2
-lti-consumer-xblock==9.14.2
+lti-consumer-xblock==9.14.3
# via -r requirements/edx/base.txt
lxml[html-clean]==5.3.2
# via
@@ -961,7 +977,7 @@ maxminddb==2.8.2
# geoip2
mccabe==0.7.0
# via pylint
-meilisearch==0.37.0
+meilisearch==0.37.1
# via
# -r requirements/edx/base.txt
# edx-search
@@ -981,7 +997,7 @@ mpmath==1.3.0
# via
# -r requirements/edx/base.txt
# sympy
-msgpack==1.1.1
+msgpack==1.1.2
# via
# -r requirements/edx/base.txt
# cachecontrol
@@ -994,7 +1010,7 @@ mysqlclient==2.2.7
# via
# -r requirements/edx/base.txt
# openedx-forum
-nh3==0.3.0
+nh3==0.3.1
# via
# -r requirements/edx/base.txt
# xblocks-contrib
@@ -1027,7 +1043,10 @@ openedx-atlas==0.7.0
# -r requirements/edx/base.txt
# edx-enterprise
# enterprise-integrated-channels
+ # openedx-authz
# openedx-forum
+openedx-authz==0.20.0
+ # via -r requirements/edx/base.txt
openedx-calc==4.0.2
# via -r requirements/edx/base.txt
openedx-django-pyfs==3.8.0
@@ -1054,15 +1073,15 @@ openedx-filters==2.1.0
# -r requirements/edx/base.txt
# lti-consumer-xblock
# ora2
-openedx-forum==0.3.6
+openedx-forum==0.3.8
# via -r requirements/edx/base.txt
-openedx-learning==0.27.1
+openedx-learning==0.30.2
# via
# -c requirements/constraints.txt
# -r requirements/edx/base.txt
optimizely-sdk==5.2.0
# via -r requirements/edx/base.txt
-ora2==6.16.4
+ora2==6.17.1
# via -r requirements/edx/base.txt
packaging==25.0
# via
@@ -1074,7 +1093,7 @@ packaging==25.0
# pytest
# snowflake-connector-python
# tox
-pact-python==2.3.3
+pact-python==1.6.0
# via
# -c requirements/constraints.txt
# -r requirements/edx/testing.in
@@ -1100,13 +1119,13 @@ pgpy==0.6.0
# edx-enterprise
piexif==1.1.3
# via -r requirements/edx/base.txt
-pillow==11.3.0
+pillow==12.0.0
# via
# -r requirements/edx/base.txt
# edx-enterprise
# edx-organizations
# edxval
-platformdirs==4.4.0
+platformdirs==4.5.0
# via
# -r requirements/edx/base.txt
# pylint
@@ -1129,7 +1148,7 @@ prompt-toolkit==3.0.52
# via
# -r requirements/edx/base.txt
# click-repl
-propcache==0.4.0
+propcache==0.4.1
# via
# -r requirements/edx/base.txt
# aiohttp
@@ -1139,7 +1158,7 @@ proto-plus==1.26.1
# -r requirements/edx/base.txt
# google-api-core
# google-cloud-firestore
-protobuf==6.32.1
+protobuf==6.33.0
# via
# -r requirements/edx/base.txt
# google-api-core
@@ -1147,7 +1166,7 @@ protobuf==6.32.1
# googleapis-common-protos
# grpcio-status
# proto-plus
-psutil==7.1.0
+psutil==7.1.2
# via
# -r requirements/edx/base.txt
# edx-django-utils
@@ -1165,6 +1184,11 @@ pyasn1-modules==0.4.2
# via
# -r requirements/edx/base.txt
# google-auth
+pycasbin==2.4.0
+ # via
+ # -r requirements/edx/base.txt
+ # casbin-django-orm-adapter
+ # openedx-authz
pycodestyle==2.8.0
# via
# -c requirements/constraints.txt
@@ -1180,12 +1204,12 @@ pycryptodomex==3.23.0
# -r requirements/edx/base.txt
# edx-proctoring
# lti-consumer-xblock
-pydantic==2.11.10
+pydantic==2.12.3
# via
# -r requirements/edx/base.txt
# camel-converter
# fastapi
-pydantic-core==2.33.2
+pydantic-core==2.41.4
# via
# -r requirements/edx/base.txt
# pydantic
@@ -1255,7 +1279,7 @@ pyparsing==3.2.5
# -r requirements/edx/base.txt
# chem
# openedx-calc
-pyproject-api==1.9.1
+pyproject-api==1.10.0
# via tox
pyquery==2.0.1
# via -r requirements/edx/testing.in
@@ -1358,16 +1382,16 @@ random2==1.0.2
# via -r requirements/edx/base.txt
recommender-xblock==3.1.0
# via -r requirements/edx/base.txt
-redis==6.4.0
+redis==7.0.1
# via
# -r requirements/edx/base.txt
# walrus
-referencing==0.36.2
+referencing==0.37.0
# via
# -r requirements/edx/base.txt
# jsonschema
# jsonschema-specifications
-regex==2025.9.18
+regex==2025.10.23
# via
# -r requirements/edx/base.txt
# nltk
@@ -1402,7 +1426,7 @@ requests-oauthlib==2.0.0
# via
# -r requirements/edx/base.txt
# social-auth-core
-rpds-py==0.27.1
+rpds-py==0.28.0
# via
# -r requirements/edx/base.txt
# jsonschema
@@ -1425,7 +1449,7 @@ sailthru-client==2.2.3
# via
# -r requirements/edx/base.txt
# edx-ace
-scipy==1.16.2
+scipy==1.16.3
# via
# -r requirements/edx/base.txt
# chem
@@ -1435,6 +1459,10 @@ semantic-version==2.10.0
# edx-drf-extensions
shapely==2.1.2
# via -r requirements/edx/base.txt
+simpleeval==1.0.3
+ # via
+ # -r requirements/edx/base.txt
+ # pycasbin
simplejson==3.20.2
# via
# -r requirements/edx/base.txt
@@ -1483,7 +1511,7 @@ social-auth-app-django==5.4.1
# -c requirements/constraints.txt
# -r requirements/edx/base.txt
# edx-auth-backends
-social-auth-core==4.7.0
+social-auth-core==4.8.1
# via
# -r requirements/edx/base.txt
# edx-auth-backends
@@ -1506,7 +1534,7 @@ sqlparse==0.5.3
# django
staff-graded-xblock==3.1.0
# via -r requirements/edx/base.txt
-starlette==0.48.0
+starlette==0.49.1
# via fastapi
stevedore==5.5.0
# via
@@ -1524,7 +1552,7 @@ sympy==1.14.0
# via
# -r requirements/edx/base.txt
# openedx-calc
-testfixtures==9.1.0
+testfixtures==10.0.0
# via
# -r requirements/edx/base.txt
# -r requirements/edx/testing.in
@@ -1547,7 +1575,7 @@ tomlkit==0.13.3
# openedx-learning
# pylint
# snowflake-connector-python
-tox==4.30.3
+tox==4.32.0
# via -r requirements/edx/testing.in
tqdm==4.67.1
# via
@@ -1603,8 +1631,9 @@ urllib3==2.5.0
# -r requirements/edx/base.txt
# botocore
# elasticsearch
+ # pact-python
# requests
-uvicorn==0.37.0
+uvicorn==0.38.0
# via pact-python
vine==5.1.0
# via
@@ -1612,7 +1641,7 @@ vine==5.1.0
# amqp
# celery
# kombu
-virtualenv==20.34.0
+virtualenv==20.35.4
# via tox
voluptuous==0.15.2
# via
@@ -1622,6 +1651,10 @@ walrus==0.9.5
# via
# -r requirements/edx/base.txt
# edx-event-bus-redis
+wcmatch==10.1
+ # via
+ # -r requirements/edx/base.txt
+ # pycasbin
wcwidth==0.2.14
# via
# -r requirements/edx/base.txt
@@ -1648,7 +1681,7 @@ wheel==0.45.1
# via
# -r requirements/edx/base.txt
# django-pipeline
-wrapt==1.17.3
+wrapt==2.0.0
# via -r requirements/edx/base.txt
xblock[django]==5.2.0
# via
@@ -1690,7 +1723,6 @@ yarl==1.22.0
# via
# -r requirements/edx/base.txt
# aiohttp
- # pact-python
zipp==3.23.0
# via
# -r requirements/edx/base.txt
diff --git a/requirements/pip.txt b/requirements/pip.txt
index dec15874f740..c6158d38e981 100644
--- a/requirements/pip.txt
+++ b/requirements/pip.txt
@@ -9,6 +9,8 @@ wheel==0.45.1
# The following packages are considered to be unsafe in a requirements file:
pip==25.2
- # via -r requirements/pip.in
+ # via
+ # -c requirements/common_constraints.txt
+ # -r requirements/pip.in
setuptools==80.9.0
# via -r requirements/pip.in
diff --git a/scripts/copy-node-modules.sh b/scripts/copy-node-modules.sh
index 16b38fc8fe59..0f5ae655401c 100755
--- a/scripts/copy-node-modules.sh
+++ b/scripts/copy-node-modules.sh
@@ -42,15 +42,6 @@ log "Ensuring vendor directories exist..."
log_and_run mkdir -p "$vendor_js"
log_and_run mkdir -p "$vendor_css"
-log "Copying studio-frontend JS & CSS from node_modules into vendor directores..."
-while read -r -d $'\0' src_file ; do
- if [[ "$src_file" = *.css ]] || [[ "$src_file" = *.css.map ]] ; then
- log_and_run cp --force "$src_file" "$vendor_css"
- else
- log_and_run cp --force "$src_file" "$vendor_js"
- fi
-done < <(find "$node_modules/@edx/studio-frontend/dist" -type f -print0)
-
log "Copying certain JS modules from node_modules into vendor directory..."
log_and_run cp --force \
"$node_modules/backbone.paginator/lib/backbone.paginator.js" \
diff --git a/scripts/structures_pruning/requirements/testing.txt b/scripts/structures_pruning/requirements/testing.txt
index 1b387d33c4d9..e5ce8b2a8133 100644
--- a/scripts/structures_pruning/requirements/testing.txt
+++ b/scripts/structures_pruning/requirements/testing.txt
@@ -18,7 +18,7 @@ dnspython==2.8.0
# pymongo
edx-opaque-keys==3.0.0
# via -r scripts/structures_pruning/requirements/base.txt
-iniconfig==2.1.0
+iniconfig==2.3.0
# via pytest
packaging==25.0
# via pytest
diff --git a/scripts/user_retirement/requirements/base.txt b/scripts/user_retirement/requirements/base.txt
index fc887f27d165..b726b8a49f40 100644
--- a/scripts/user_retirement/requirements/base.txt
+++ b/scripts/user_retirement/requirements/base.txt
@@ -10,13 +10,13 @@ attrs==25.4.0
# via zeep
backoff==2.2.1
# via -r scripts/user_retirement/requirements/base.in
-boto3==1.40.46
+boto3==1.40.62
# via -r scripts/user_retirement/requirements/base.in
-botocore==1.40.46
+botocore==1.40.62
# via
# boto3
# s3transfer
-cachetools==6.2.0
+cachetools==6.2.1
# via google-auth
certifi==2025.10.5
# via requests
@@ -24,7 +24,7 @@ cffi==2.0.0
# via
# cryptography
# pynacl
-charset-normalizer==3.4.3
+charset-normalizer==3.4.4
# via requests
click==8.3.0
# via
@@ -34,7 +34,7 @@ cryptography==45.0.7
# via
# -c requirements/constraints.txt
# pyjwt
-django==4.2.28
+django==5.2.7
# via
# -c requirements/common_constraints.txt
# -c requirements/constraints.txt
@@ -49,24 +49,24 @@ edx-django-utils==8.0.1
# via edx-rest-api-client
edx-rest-api-client==6.2.0
# via -r scripts/user_retirement/requirements/base.in
-google-api-core==2.25.2
+google-api-core==2.28.1
# via google-api-python-client
-google-api-python-client==2.184.0
+google-api-python-client==2.185.0
# via -r scripts/user_retirement/requirements/base.in
-google-auth==2.41.1
+google-auth==2.42.0
# via
# google-api-core
# google-api-python-client
# google-auth-httplib2
google-auth-httplib2==0.2.0
# via google-api-python-client
-googleapis-common-protos==1.70.0
+googleapis-common-protos==1.71.0
# via google-api-core
httplib2==0.31.0
# via
# google-api-python-client
# google-auth-httplib2
-idna==3.10
+idna==3.11
# via requests
isodate==0.7.2
# via zeep
@@ -82,16 +82,16 @@ lxml==5.3.2
# zeep
more-itertools==10.8.0
# via simple-salesforce
-platformdirs==4.4.0
+platformdirs==4.5.0
# via zeep
proto-plus==1.26.1
# via google-api-core
-protobuf==6.32.1
+protobuf==6.33.0
# via
# google-api-core
# googleapis-common-protos
# proto-plus
-psutil==7.1.0
+psutil==7.1.2
# via edx-django-utils
pyasn1==0.6.1
# via
@@ -127,7 +127,7 @@ requests==2.32.5
# requests-toolbelt
# simple-salesforce
# zeep
-requests-file==2.1.0
+requests-file==3.0.1
# via zeep
requests-toolbelt==1.0.0
# via zeep
diff --git a/scripts/user_retirement/requirements/testing.txt b/scripts/user_retirement/requirements/testing.txt
index 1c37620093ed..3299e4f8fd78 100644
--- a/scripts/user_retirement/requirements/testing.txt
+++ b/scripts/user_retirement/requirements/testing.txt
@@ -14,17 +14,17 @@ attrs==25.4.0
# zeep
backoff==2.2.1
# via -r scripts/user_retirement/requirements/base.txt
-boto3==1.40.46
+boto3==1.40.62
# via
# -r scripts/user_retirement/requirements/base.txt
# moto
-botocore==1.40.46
+botocore==1.40.62
# via
# -r scripts/user_retirement/requirements/base.txt
# boto3
# moto
# s3transfer
-cachetools==6.2.0
+cachetools==6.2.1
# via
# -r scripts/user_retirement/requirements/base.txt
# google-auth
@@ -37,7 +37,7 @@ cffi==2.0.0
# -r scripts/user_retirement/requirements/base.txt
# cryptography
# pynacl
-charset-normalizer==3.4.3
+charset-normalizer==3.4.4
# via
# -r scripts/user_retirement/requirements/base.txt
# requests
@@ -52,7 +52,7 @@ cryptography==45.0.7
# pyjwt
ddt==1.7.2
# via -r scripts/user_retirement/requirements/testing.in
-django==4.2.28
+django==5.2.7
# via
# -r scripts/user_retirement/requirements/base.txt
# django-crum
@@ -72,13 +72,13 @@ edx-django-utils==8.0.1
# edx-rest-api-client
edx-rest-api-client==6.2.0
# via -r scripts/user_retirement/requirements/base.txt
-google-api-core==2.25.2
+google-api-core==2.28.1
# via
# -r scripts/user_retirement/requirements/base.txt
# google-api-python-client
-google-api-python-client==2.184.0
+google-api-python-client==2.185.0
# via -r scripts/user_retirement/requirements/base.txt
-google-auth==2.41.1
+google-auth==2.42.0
# via
# -r scripts/user_retirement/requirements/base.txt
# google-api-core
@@ -88,7 +88,7 @@ google-auth-httplib2==0.2.0
# via
# -r scripts/user_retirement/requirements/base.txt
# google-api-python-client
-googleapis-common-protos==1.70.0
+googleapis-common-protos==1.71.0
# via
# -r scripts/user_retirement/requirements/base.txt
# google-api-core
@@ -97,11 +97,11 @@ httplib2==0.31.0
# -r scripts/user_retirement/requirements/base.txt
# google-api-python-client
# google-auth-httplib2
-idna==3.10
+idna==3.11
# via
# -r scripts/user_retirement/requirements/base.txt
# requests
-iniconfig==2.1.0
+iniconfig==2.3.0
# via pytest
isodate==0.7.2
# via
@@ -130,11 +130,11 @@ more-itertools==10.8.0
# via
# -r scripts/user_retirement/requirements/base.txt
# simple-salesforce
-moto==5.1.14
+moto==5.1.15
# via -r scripts/user_retirement/requirements/testing.in
packaging==25.0
# via pytest
-platformdirs==4.4.0
+platformdirs==4.5.0
# via
# -r scripts/user_retirement/requirements/base.txt
# zeep
@@ -144,13 +144,13 @@ proto-plus==1.26.1
# via
# -r scripts/user_retirement/requirements/base.txt
# google-api-core
-protobuf==6.32.1
+protobuf==6.33.0
# via
# -r scripts/user_retirement/requirements/base.txt
# google-api-core
# googleapis-common-protos
# proto-plus
-psutil==7.1.0
+psutil==7.1.2
# via
# -r scripts/user_retirement/requirements/base.txt
# edx-django-utils
@@ -211,7 +211,7 @@ requests==2.32.5
# responses
# simple-salesforce
# zeep
-requests-file==2.1.0
+requests-file==3.0.1
# via
# -r scripts/user_retirement/requirements/base.txt
# zeep
diff --git a/scripts/xblock/requirements.txt b/scripts/xblock/requirements.txt
index 23d2ac5b8eed..533cd92824e4 100644
--- a/scripts/xblock/requirements.txt
+++ b/scripts/xblock/requirements.txt
@@ -6,9 +6,9 @@
#
certifi==2025.10.5
# via requests
-charset-normalizer==3.4.3
+charset-normalizer==3.4.4
# via requests
-idna==3.10
+idna==3.11
# via requests
requests==2.32.5
# via -r scripts/xblock/requirements.in
diff --git a/scripts/xsslint/tests/test_linters.py b/scripts/xsslint/tests/test_linters.py
index 1c1589416172..2aba52ed78d7 100644
--- a/scripts/xsslint/tests/test_linters.py
+++ b/scripts/xsslint/tests/test_linters.py
@@ -1244,22 +1244,16 @@ def test_check_mako_expressions_in_mixed_contexts(self):
${x | h}
%static:require_module>
${x | h}
- <%static:studiofrontend page="${x}">
- ${x | h}
- %static:studiofrontend>
- ${x | h}
""")
linter._check_mako_file_is_safe(mako_template, results)
- assert len(results.violations) == 7
+ assert len(results.violations) == 5
assert results.violations[0].rule == MAKO_LINTER_RULESET.mako_unwanted_html_filter
assert results.violations[1].rule == MAKO_LINTER_RULESET.mako_invalid_js_filter
assert results.violations[2].rule == MAKO_LINTER_RULESET.mako_unwanted_html_filter
assert results.violations[3].rule == MAKO_LINTER_RULESET.mako_invalid_js_filter
assert results.violations[4].rule == MAKO_LINTER_RULESET.mako_unwanted_html_filter
- assert results.violations[5].rule == MAKO_LINTER_RULESET.mako_invalid_js_filter
- assert results.violations[6].rule == MAKO_LINTER_RULESET.mako_unwanted_html_filter
def test_check_mako_expressions_javascript_strings(self):
"""
diff --git a/scripts/xsslint/xsslint/linters.py b/scripts/xsslint/xsslint/linters.py
index a73e805dac8e..911c50a34f9c 100644
--- a/scripts/xsslint/xsslint/linters.py
+++ b/scripts/xsslint/xsslint/linters.py
@@ -1359,8 +1359,6 @@ def _get_contexts(self, mako_template):
%static:require_module(_async)?> | # require js script tag end (optionally the _async version)
<%static:webpack.*(? | # webpack script tag start
%static:webpack> | # webpack script tag end
- <%static:studiofrontend.*?(? | # studiofrontend script tag start
- %static:studiofrontend> | # studiofrontend script tag end
<%block[ ]*name=['"]requirejs['"]\w*(? | # require js tag start
%block> # require js tag end
""",
diff --git a/setup.cfg b/setup.cfg
index e4419bd149a9..06aea046d413 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -166,6 +166,7 @@ ignore_imports =
name = Do not depend on non-public API of isolated apps.
type = isolated_apps
isolated_apps =
+ cms.djangoapps.modulestore_migrator
openedx.core.djangoapps.agreements
openedx.core.djangoapps.bookmarks
openedx.core.djangoapps.content_libraries
diff --git a/setup.py b/setup.py
index 5b9f020ac3c5..eeb7b79f534d 100644
--- a/setup.py
+++ b/setup.py
@@ -139,7 +139,6 @@
],
"lms.djangoapp": [
"ace_common = openedx.core.djangoapps.ace_common.apps:AceCommonConfig",
- "announcements = openedx.features.announcements.apps:AnnouncementsConfig",
"content_libraries = openedx.core.djangoapps.content_libraries.apps:ContentLibrariesConfig",
"course_apps = openedx.core.djangoapps.course_apps.apps:CourseAppsConfig",
"course_live = openedx.core.djangoapps.course_live.apps:CourseLiveConfig",
@@ -158,7 +157,6 @@
"program_enrollments = lms.djangoapps.program_enrollments.apps:ProgramEnrollmentsConfig",
],
"cms.djangoapp": [
- "announcements = openedx.features.announcements.apps:AnnouncementsConfig",
"ace_common = openedx.core.djangoapps.ace_common.apps:AceCommonConfig",
"bookmarks = openedx.core.djangoapps.bookmarks.apps:BookmarksConfig",
"course_live = openedx.core.djangoapps.course_live.apps:CourseLiveConfig",
diff --git a/webpack.builtinblocks.config.js b/webpack.builtinblocks.config.js
index 1c5a9b1e0e9d..c0f2fdaeb4fa 100644
--- a/webpack.builtinblocks.config.js
+++ b/webpack.builtinblocks.config.js
@@ -79,14 +79,13 @@ module.exports = {
'./xmodule/js/src/xmodule.js',
'./xmodule/js/src/sequence/edit.js'
],
- VideoBlockDisplay: [
- './xmodule/js/src/xmodule.js',
- './xmodule/js/src/video/10_main.js'
- ],
VideoBlockEditor: [
'./xmodule/js/src/xmodule.js',
'./xmodule/js/src/tabs/tabs-aggregator.js'
],
+ VideoBlockDisplay: [
+ './xmodule/assets/video/public/js/10_main.js'
+ ],
WordCloudBlockDisplay: [
'./xmodule/js/src/xmodule.js',
'./xmodule/assets/word_cloud/src/js/word_cloud.js'
diff --git a/webpack.common.config.js b/webpack.common.config.js
index 36ac75708c23..ac283c3d40d8 100644
--- a/webpack.common.config.js
+++ b/webpack.common.config.js
@@ -134,7 +134,6 @@ module.exports = Merge.merge({
// Features
Currency: './openedx/features/course_experience/static/course_experience/js/currency.js',
- AnnouncementsView: './openedx/features/announcements/static/announcements/jsx/Announcements.jsx',
CookiePolicyBanner: './common/static/js/src/CookiePolicyBanner.jsx',
// Common
@@ -505,15 +504,6 @@ module.exports = Merge.merge({
}
]
},
- {
- test: /xmodule\/js\/src\/video\/10_main.js/,
- use: [
- {
- loader: 'imports-loader',
- options: 'this=>window'
- }
- ]
- },
/*
* END BUILT-IN XBLOCK ASSETS WITH GLOBAL DEFINITIONS
***************************************************************************************************** */
@@ -680,9 +670,11 @@ module.exports = Merge.merge({
$: 'jQuery',
backbone: 'Backbone',
canvas: 'canvas',
+ fs: 'fs',
gettext: 'gettext',
jquery: 'jQuery',
logger: 'Logger',
+ path: 'path',
underscore: '_',
URI: 'URI',
XBlockToXModuleShim: 'XBlockToXModuleShim',
diff --git a/xmodule/assets/library_content/public/js/library_content_edit.js b/xmodule/assets/library_content/public/js/library_content_edit.js
index fea66e266120..8dba248ca0dc 100644
--- a/xmodule/assets/library_content/public/js/library_content_edit.js
+++ b/xmodule/assets/library_content/public/js/library_content_edit.js
@@ -1,11 +1,39 @@
/* JavaScript for special editing operations that can be done on LibraryContentXBlock */
-window.LibraryContentAuthorView = function(runtime, element) {
+window.LibraryContentAuthorView = function(runtime, element, initArgs) {
'use strict';
var $element = $(element);
var usage_id = $element.data('usage-id');
// The "Update Now" button is not a child of 'element', as it is in the validation message area
// But it is still inside this xblock's wrapper element, which we can easily find:
var $wrapper = $element.parents('*[data-locator="' + usage_id + '"]');
+ var { is_root: isRoot = false } = initArgs;
+
+ function postMessageToParent(body, callbackFn = null) {
+ try {
+ window.parent.postMessage(body, document.referrer);
+ if (callbackFn) {
+ callbackFn();
+ }
+ } catch (e) {
+ console.error('Failed to post message:', e);
+ }
+ };
+
+ function reloadPreviewPage() {
+ if (window.self !== window.top) {
+ // We are inside iframe
+ // Normal location.reload() reloads the iframe but subsequent calls to
+ // postMessage fails. So we are using postMessage to tell the parent page
+ // to reload the iframe.
+ postMessageToParent({
+ type: 'refreshIframe',
+ message: 'Refresh Iframe',
+ payload: {},
+ })
+ } else {
+ location.reload();
+ }
+ }
$wrapper.on('click', '.library-update-btn', function(e) {
e.preventDefault();
@@ -20,17 +48,33 @@ window.LibraryContentAuthorView = function(runtime, element) {
state: 'end',
element: element
});
- if ($element.closest('.wrapper-xblock').is(':not(.level-page)')) {
- // We are on a course unit page. The notify('save') should refresh this block,
- // but that is only working on the container page view of this block.
- // Why? On the unit page, this XBlock's runtime has no reference to the
- // XBlockContainerPage - only the top-level XBlock (a vertical) runtime does.
- // But unfortunately there is no way to get a reference to our parent block's
- // JS 'runtime' object. So instead we must refresh the whole page:
- location.reload();
+ if (isRoot) {
+ // We are inside preview page where all children blocks are listed.
+ reloadPreviewPage();
}
});
});
+
+ $wrapper.on('click', '.library-block-migrate-btn', function(e) {
+ e.preventDefault();
+ // migrate library content block to item bank block
+ runtime.notify('save', {
+ state: 'start',
+ element: element,
+ message: gettext('Migrating to Problem Bank')
+ });
+ $.post(runtime.handlerUrl(element, 'upgrade_to_v2_library')).done(function() {
+ runtime.notify('save', {
+ state: 'end',
+ element: element
+ });
+ if (isRoot) {
+ // We are inside preview page where all children blocks are listed.
+ reloadPreviewPage();
+ }
+ });
+ });
+
// Hide loader and show element when update task finished.
var $loader = $wrapper.find('.ui-loading');
var $xblockHeader = $wrapper.find('.xblock-header');
diff --git a/xmodule/assets/video/public/js/00_async_process.js b/xmodule/assets/video/public/js/00_async_process.js
new file mode 100644
index 000000000000..a909e8225a2f
--- /dev/null
+++ b/xmodule/assets/video/public/js/00_async_process.js
@@ -0,0 +1,52 @@
+'use strict';
+
+/**
+ * Provides convenient way to process big amount of data without UI blocking.
+ *
+ * @param {array} list Array to process.
+ * @param {function} process Calls this function on each item in the list.
+ * @return {array} Returns a Promise object to observe when all actions of a
+ * certain type bound to the collection, queued or not, have finished.
+ */
+let AsyncProcess = {
+ array: function(list, process) {
+ if (!_.isArray(list)) {
+ return $.Deferred().reject().promise();
+ }
+
+ if (!_.isFunction(process) || !list.length) {
+ return $.Deferred().resolve(list).promise();
+ }
+
+ let MAX_DELAY = 50, // maximum amount of time that js code should be allowed to run continuously
+ dfd = $.Deferred();
+ let result = [];
+ let index = 0;
+ let len = list.length;
+
+ let getCurrentTime = function() {
+ return (new Date()).getTime();
+ };
+
+ let handler = function() {
+ let start = getCurrentTime();
+
+ do {
+ result[index] = process(list[index], index);
+ index++;
+ } while (index < len && getCurrentTime() - start < MAX_DELAY);
+
+ if (index < len) {
+ setTimeout(handler, 25);
+ } else {
+ dfd.resolve(result);
+ }
+ };
+
+ setTimeout(handler, 25);
+
+ return dfd.promise();
+ }
+};
+
+export default AsyncProcess;
diff --git a/xmodule/assets/video/public/js/00_component.js b/xmodule/assets/video/public/js/00_component.js
new file mode 100644
index 000000000000..2ac183b1982f
--- /dev/null
+++ b/xmodule/assets/video/public/js/00_component.js
@@ -0,0 +1,81 @@
+'use strict';
+
+import _ from 'underscore';
+
+
+/**
+ * Creates a new object with the specified prototype object and properties.
+ * @param {Object} o The object which should be the prototype of the
+ * newly-created object.
+ * @private
+ * @throws {TypeError, Error}
+ * @return {Object}
+ */
+let inherit = Object.create || (function() {
+ let F = function() {};
+
+ return function(o) {
+ if (arguments.length > 1) {
+ throw Error('Second argument not supported');
+ }
+ if (_.isNull(o) || _.isUndefined(o)) {
+ throw Error('Cannot set a null [[Prototype]]');
+ }
+ if (!_.isObject(o)) {
+ throw TypeError('Argument must be an object');
+ }
+
+ F.prototype = o;
+
+ return new F();
+ };
+}());
+
+/**
+ * Component module.
+ * @exports video/00_component.js
+ * @constructor
+ * @return {jquery Promise}
+ */
+let Component = function() {
+ if ($.isFunction(this.initialize)) {
+ // eslint-disable-next-line prefer-spread
+ return this.initialize.apply(this, arguments);
+ }
+};
+
+/**
+ * Returns new constructor that inherits form the current constructor.
+ * @static
+ * @param {Object} protoProps The object containing which will be added to
+ * the prototype.
+ * @return {Object}
+ */
+Component.extend = function(protoProps, staticProps) {
+ let Parent = this;
+ let Child = function() {
+ if ($.isFunction(this.initialize)) {
+ // eslint-disable-next-line prefer-spread
+ return this.initialize.apply(this, arguments);
+ }
+ };
+
+ // Inherit methods and properties from the Parent prototype.
+ Child.prototype = inherit(Parent.prototype);
+ Child.constructor = Parent;
+ // Provide access to parent's methods and properties
+ Child.__super__ = Parent.prototype;
+
+ // Extends inherited methods and properties by methods/properties
+ // passed as argument.
+ if (protoProps) {
+ $.extend(Child.prototype, protoProps);
+ }
+
+ // Inherit static methods and properties
+ $.extend(Child, Parent, staticProps);
+
+ return Child;
+};
+
+export default Component;
diff --git a/xmodule/assets/video/public/js/00_i18n.js b/xmodule/assets/video/public/js/00_i18n.js
new file mode 100644
index 000000000000..1962ed4ee8c4
--- /dev/null
+++ b/xmodule/assets/video/public/js/00_i18n.js
@@ -0,0 +1,35 @@
+'use strict';
+
+/**
+ * i18n module.
+ * @exports video/00_i18n.js
+ * @return {object}
+ */
+
+let i18n = {
+ Play: gettext('Play'),
+ Pause: gettext('Pause'),
+ Mute: gettext('Mute'),
+ Unmute: gettext('Unmute'),
+ 'Exit full browser': gettext('Exit full browser'),
+ 'Fill browser': gettext('Fill browser'),
+ Speed: gettext('Speed'),
+ 'Auto-advance': gettext('Auto-advance'),
+ Volume: gettext('Volume'),
+ // Translators: Volume level equals 0%.
+ Muted: gettext('Muted'),
+ // Translators: Volume level in range ]0,20]%
+ 'Very low': gettext('Very low'),
+ // Translators: Volume level in range ]20,40]%
+ Low: gettext('Low'),
+ // Translators: Volume level in range ]40,60]%
+ Average: gettext('Average'),
+ // Translators: Volume level in range ]60,80]%
+ Loud: gettext('Loud'),
+ // Translators: Volume level in range ]80,99]%
+ 'Very loud': gettext('Very loud'),
+ // Translators: Volume level equals 100%.
+ Maximum: gettext('Maximum')
+};
+
+export default i18n;
diff --git a/xmodule/assets/video/public/js/00_iterator.js b/xmodule/assets/video/public/js/00_iterator.js
new file mode 100644
index 000000000000..5b597f200ec5
--- /dev/null
+++ b/xmodule/assets/video/public/js/00_iterator.js
@@ -0,0 +1,83 @@
+'use strict';
+
+/**
+ * Provides convenient way to work with iterable data.
+ * @exports video/00_iterator.js
+ * @constructor
+ * @param {array} list Array to be iterated.
+ */
+let Iterator = function(list) {
+ this.list = list;
+ this.index = 0;
+ this.size = this.list.length;
+ this.lastIndex = this.list.length - 1;
+};
+
+Iterator.prototype = {
+
+ /**
+ * Checks validity of provided index for the iterator.
+ * @access protected
+ * @param {numebr} index
+ * @return {boolean}
+ */
+ _isValid: function(index) {
+ return _.isNumber(index) && index < this.size && index >= 0;
+ },
+
+ /**
+ * Returns next element.
+ * @param {number} [index] Updates current position.
+ * @return {any}
+ */
+ next: function(index) {
+ if (!(this._isValid(index))) {
+ index = this.index;
+ }
+
+ this.index = (index >= this.lastIndex) ? 0 : index + 1;
+
+ return this.list[this.index];
+ },
+
+ /**
+ * Returns previous element.
+ * @param {number} [index] Updates current position.
+ * @return {any}
+ */
+ prev: function(index) {
+ if (!(this._isValid(index))) {
+ index = this.index;
+ }
+
+ this.index = (index < 1) ? this.lastIndex : index - 1;
+
+ return this.list[this.index];
+ },
+
+ /**
+ * Returns last element in the list.
+ * @return {any}
+ */
+ last: function() {
+ return this.list[this.lastIndex];
+ },
+
+ /**
+ * Returns first element in the list.
+ * @return {any}
+ */
+ first: function() {
+ return this.list[0];
+ },
+
+ /**
+ * Returns `true` if current position is last for the iterator.
+ * @return {boolean}
+ */
+ isEnd: function() {
+ return this.index === this.lastIndex;
+ }
+};
+
+export default Iterator;
diff --git a/xmodule/assets/video/public/js/00_resizer.js b/xmodule/assets/video/public/js/00_resizer.js
new file mode 100644
index 000000000000..d892ec4d1873
--- /dev/null
+++ b/xmodule/assets/video/public/js/00_resizer.js
@@ -0,0 +1,236 @@
+'use strict';
+
+import _ from 'underscore';
+
+
+let Resizer = function(params) {
+ let defaults = {
+ container: window,
+ element: null,
+ containerRatio: null,
+ elementRatio: null
+ },
+ callbacksList = [],
+ delta = {
+ height: 0,
+ width: 0
+ },
+ module = {};
+ let mode = null,
+ config;
+
+ // eslint-disable-next-line no-shadow
+ let initialize = function(params) {
+ if (!config) {
+ config = defaults;
+ }
+
+ config = $.extend(true, {}, config, params);
+
+ if (!config.element) {
+ console.log(
+ 'Required parameter `element` is not passed.'
+ );
+ }
+
+ return module;
+ };
+
+ let getData = function() {
+ let $container = $(config.container),
+ containerWidth = $container.width() + delta.width,
+ containerHeight = $container.height() + delta.height;
+ let containerRatio = config.containerRatio;
+
+ let $element = $(config.element);
+ let elementRatio = config.elementRatio;
+
+ if (!containerRatio) {
+ containerRatio = containerWidth / containerHeight;
+ }
+
+ if (!elementRatio) {
+ elementRatio = $element.width() / $element.height();
+ }
+
+ return {
+ containerWidth: containerWidth,
+ containerHeight: containerHeight,
+ containerRatio: containerRatio,
+ element: $element,
+ elementRatio: elementRatio
+ };
+ };
+
+ let align = function() {
+ let data = getData();
+
+ switch (mode) {
+ case 'height':
+ alignByHeightOnly();
+ break;
+
+ case 'width':
+ alignByWidthOnly();
+ break;
+
+ default:
+ if (data.containerRatio >= data.elementRatio) {
+ alignByHeightOnly();
+ } else {
+ alignByWidthOnly();
+ }
+ break;
+ }
+
+ fireCallbacks();
+
+ return module;
+ };
+
+ let alignByWidthOnly = function() {
+ let data = getData(),
+ height = data.containerWidth / data.elementRatio;
+
+ data.element.css({
+ height: height,
+ width: data.containerWidth,
+ top: 0.5 * (data.containerHeight - height),
+ left: 0
+ });
+
+ return module;
+ };
+
+ let alignByHeightOnly = function() {
+ let data = getData(),
+ width = data.containerHeight * data.elementRatio;
+
+ data.element.css({
+ height: data.containerHeight,
+ width: data.containerHeight * data.elementRatio,
+ top: 0,
+ left: 0.5 * (data.containerWidth - width)
+ });
+
+ return module;
+ };
+
+ let setMode = function(param) {
+ if (_.isString(param)) {
+ mode = param;
+ align();
+ }
+
+ return module;
+ };
+
+ let setElement = function(element) {
+ config.element = element;
+
+ return module;
+ };
+
+ let addCallback = function(func) {
+ if ($.isFunction(func)) {
+ callbacksList.push(func);
+ } else {
+ console.error('[Video info]: TypeError: Argument is not a function.');
+ }
+
+ return module;
+ };
+
+ let addOnceCallback = function(func) {
+ if ($.isFunction(func)) {
+ let decorator = function() {
+ func();
+ removeCallback(func);
+ };
+
+ addCallback(decorator);
+ } else {
+ console.error('TypeError: Argument is not a function.');
+ }
+
+ return module;
+ };
+
+ let fireCallbacks = function() {
+ $.each(callbacksList, function(index, callback) {
+ callback();
+ });
+ };
+
+ let removeCallbacks = function() {
+ callbacksList.length = 0;
+
+ return module;
+ };
+
+ let removeCallback = function(func) {
+ let index = $.inArray(func, callbacksList);
+
+ if (index !== -1) {
+ return callbacksList.splice(index, 1);
+ }
+ };
+
+ let resetDelta = function() {
+ // eslint-disable-next-line no-multi-assign
+ delta.height = delta.width = 0;
+
+ return module;
+ };
+
+ let addDelta = function(value, side) {
+ if (_.isNumber(value) && _.isNumber(delta[side])) {
+ delta[side] += value;
+ }
+
+ return module;
+ };
+
+ let substractDelta = function(value, side) {
+ if (_.isNumber(value) && _.isNumber(delta[side])) {
+ delta[side] -= value;
+ }
+
+ return module;
+ };
+
+ let destroy = function() {
+ let data = getData();
+ data.element.css({
+ height: '', width: '', top: '', left: ''
+ });
+ removeCallbacks();
+ resetDelta();
+ mode = null;
+ };
+
+ initialize.apply(module, arguments);
+
+ return $.extend(true, module, {
+ align: align,
+ alignByWidthOnly: alignByWidthOnly,
+ alignByHeightOnly: alignByHeightOnly,
+ destroy: destroy,
+ setParams: initialize,
+ setMode: setMode,
+ setElement: setElement,
+ callbacks: {
+ add: addCallback,
+ once: addOnceCallback,
+ remove: removeCallback,
+ removeAll: removeCallbacks
+ },
+ delta: {
+ add: addDelta,
+ substract: substractDelta,
+ reset: resetDelta
+ }
+ });
+};
+
+export default Resizer;
diff --git a/xmodule/assets/video/public/js/00_sjson.js b/xmodule/assets/video/public/js/00_sjson.js
new file mode 100644
index 000000000000..99d870ff84a7
--- /dev/null
+++ b/xmodule/assets/video/public/js/00_sjson.js
@@ -0,0 +1,108 @@
+'use strict';
+
+let Sjson = function(data) {
+ let sjson = {
+ start: data.start.concat(),
+ text: data.text.concat()
+ },
+ module = {};
+
+ let getter = function(propertyName) {
+ return function() {
+ return sjson[propertyName];
+ };
+ };
+
+ let getStartTimes = getter('start');
+
+ let getCaptions = getter('text');
+
+ let size = function() {
+ return sjson.text.length;
+ };
+
+ function search(time, startTime, endTime) {
+ let start = getStartTimes(),
+ max = size() - 1,
+ min = 0,
+ results,
+ index;
+
+ // if we specify a start and end time to search,
+ // search the filtered list of captions in between
+ // the start / end times.
+ // Else, search the unfiltered list.
+ if (typeof startTime !== 'undefined'
+ && typeof endTime !== 'undefined') {
+ results = filter(startTime, endTime);
+ start = results.start;
+ max = results.captions.length - 1;
+ } else {
+ start = getStartTimes();
+ }
+ while (min < max) {
+ index = Math.ceil((max + min) / 2);
+
+ if (time < start[index]) {
+ max = index - 1;
+ }
+
+ if (time >= start[index]) {
+ min = index;
+ }
+ }
+
+ return min;
+ }
+
+ function filter(start, end) {
+ /* filters captions that occur between inputs
+ * `start` and `end`. Start and end should
+ * be Numbers (doubles) corresponding to the
+ * number of seconds elapsed since the beginning
+ * of the video.
+ *
+ * Returns an object with properties
+ * "start" and "captions" representing
+ * parallel arrays of start times and
+ * their corresponding captions.
+ */
+ let filteredTimes = [];
+ let filteredCaptions = [];
+ let startTimes = getStartTimes();
+ let captions = getCaptions();
+
+ if (startTimes.length !== captions.length) {
+ console.warn('video caption and start time arrays do not match in length');
+ }
+
+ // if end is null, then it's been set to
+ // some erroneous value, so filter using the
+ // entire array as long as it's not empty
+ if (end === null && startTimes.length) {
+ end = startTimes[startTimes.length - 1];
+ }
+
+ _.filter(startTimes, function(currentStartTime, i) {
+ if (currentStartTime >= start && currentStartTime <= end) {
+ filteredTimes.push(currentStartTime);
+ filteredCaptions.push(captions[i]);
+ }
+ });
+
+ return {
+ start: filteredTimes,
+ captions: filteredCaptions
+ };
+ }
+
+ return {
+ getCaptions: getCaptions,
+ getStartTimes: getStartTimes,
+ getSize: size,
+ filter: filter,
+ search: search
+ };
+};
+
+export default Sjson;
diff --git a/xmodule/assets/video/public/js/00_video_storage.js b/xmodule/assets/video/public/js/00_video_storage.js
new file mode 100644
index 000000000000..f2293336fe01
--- /dev/null
+++ b/xmodule/assets/video/public/js/00_video_storage.js
@@ -0,0 +1,96 @@
+'use strict';
+
+/**
+ * Provides convenient way to store key value pairs.
+ *
+ * @param {string} namespace Namespace that is used to store data.
+ * @return {object} VideoStorage API.
+ */
+let VideoStorage = function(namespace, id) {
+ /**
+ * Adds new value to the storage or rewrites existent.
+ *
+ * @param {string} name Identifier of the data.
+ * @param {any} value Data to store.
+ * @param {boolean} instanceSpecific Data with this flag will be added
+ * to instance specific storage.
+ */
+ let setItem = function(name, value, instanceSpecific) {
+ if (name) {
+ if (instanceSpecific) {
+ window[namespace][id][name] = value;
+ } else {
+ window[namespace][name] = value;
+ }
+ }
+ };
+
+ /**
+ * Returns the current value associated with the given name.
+ *
+ * @param {string} name Identifier of the data.
+ * @param {boolean} instanceSpecific Data with this flag will be added
+ * to instance specific storage.
+ * @return {any} The current value associated with the given name.
+ * If the given key does not exist in the list
+ * associated with the object then this method must return null.
+ */
+ let getItem = function(name, instanceSpecific) {
+ if (instanceSpecific) {
+ return window[namespace][id][name];
+ } else {
+ return window[namespace][name];
+ }
+ };
+
+ /**
+ * Removes the current value associated with the given name.
+ *
+ * @param {string} name Identifier of the data.
+ * @param {boolean} instanceSpecific Data with this flag will be added
+ * to instance specific storage.
+ */
+ let removeItem = function(name, instanceSpecific) {
+ if (instanceSpecific) {
+ delete window[namespace][id][name];
+ } else {
+ delete window[namespace][name];
+ }
+ };
+
+ /**
+ * Empties the storage.
+ *
+ */
+ let clear = function() {
+ window[namespace] = {};
+ window[namespace][id] = {};
+ };
+
+ /**
+ * Initializes the module: creates a storage with proper namespace.
+ *
+ * @private
+ */
+ (function initialize() {
+ if (!namespace) {
+ namespace = 'VideoStorage';
+ }
+ if (!id) {
+ // Generate random alpha-numeric string.
+ id = Math.random().toString(36).slice(2);
+ }
+
+ window[namespace] = window[namespace] || {};
+ window[namespace][id] = window[namespace][id] || {};
+ }());
+
+ return {
+ clear: clear,
+ getItem: getItem,
+ removeItem: removeItem,
+ setItem: setItem
+ };
+};
+
+export default VideoStorage;
diff --git a/xmodule/assets/video/public/js/01_initialize.js b/xmodule/assets/video/public/js/01_initialize.js
new file mode 100644
index 000000000000..85248b3f0266
--- /dev/null
+++ b/xmodule/assets/video/public/js/01_initialize.js
@@ -0,0 +1,845 @@
+/* eslint-disable no-console, no-param-reassign */
+/**
+ * @file Initialize module works with the JSON config, and sets up various
+ * settings, parameters, variables. After all setup actions are performed, it
+ * invokes the video player to play the specified video. This module must be
+ * invoked first. It provides several functions which do not fit in with other
+ * modules.
+ *
+ * @external VideoPlayer
+ *
+ * @module Initialize
+ */
+
+import VideoPlayer from './03_video_player.js';
+import i18n from './00_i18n.js';
+import _ from 'underscore';
+import moment from 'moment';
+
+/**
+ * @function
+ *
+ * Initialize module exports this function.
+ *
+ * @param {object} state The object containg the state of the video player.
+ * All other modules, their parameters, public variables, etc. are
+ * available via this object.
+ * @param {DOM element} element Container of the entire Video DOM element.
+ */
+let Initialize = function(state, element) {
+ _makeFunctionsPublic(state);
+
+ state.initialize(element)
+ .done(function() {
+ if (state.isYoutubeType()) {
+ state.parseSpeed();
+ }
+ // On iPhones and iPods native controls are used.
+ if (/iP(hone|od)/i.test(state.isTouch[0])) {
+ _hideWaitPlaceholder(state);
+ state.el.trigger('initialize', arguments);
+
+ return false;
+ }
+
+ _initializeModules(state, i18n)
+ .done(function() {
+ // On iPad ready state occurs just after start playing.
+ // We hide controls before video starts playing.
+ if (/iPad|Android/i.test(state.isTouch[0])) {
+ state.el.on('play', _.once(function() {
+ state.trigger('videoControl.show', null);
+ }));
+ } else {
+ // On PC show controls immediately.
+ state.trigger('videoControl.show', null);
+ }
+
+ _hideWaitPlaceholder(state);
+ state.el.trigger('initialize', arguments);
+ });
+ });
+};
+
+/* eslint-disable no-use-before-define */
+let methodsDict = {
+ bindTo: bindTo,
+ fetchMetadata: fetchMetadata,
+ getCurrentLanguage: getCurrentLanguage,
+ getDuration: getDuration,
+ getPlayerMode: getPlayerMode,
+ getVideoMetadata: getVideoMetadata,
+ initialize: initialize,
+ isHtml5Mode: isHtml5Mode,
+ isFlashMode: isFlashMode,
+ isYoutubeType: isYoutubeType,
+ parseSpeed: parseSpeed,
+ parseYoutubeStreams: parseYoutubeStreams,
+ setPlayerMode: setPlayerMode,
+ setSpeed: setSpeed,
+ setAutoAdvance: setAutoAdvance,
+ speedToString: speedToString,
+ trigger: trigger,
+ youtubeId: youtubeId,
+ loadHtmlPlayer: loadHtmlPlayer,
+ loadYoutubePlayer: loadYoutubePlayer,
+ loadYouTubeIFrameAPI: loadYouTubeIFrameAPI
+};
+/* eslint-enable no-use-before-define */
+
+let _youtubeApiDeferred = null;
+let _oldOnYouTubeIframeAPIReady;
+
+Initialize.prototype = methodsDict;
+
+export default Initialize;
+
+// ***************************************************************
+// Private functions start here. Private functions start with underscore.
+// ***************************************************************
+
+/**
+ * @function _makeFunctionsPublic
+ *
+ * Functions which will be accessible via 'state' object. When called,
+ * these functions will get the 'state'
+ * object as a context.
+ *
+ * @param {object} state The object containg the state (properties,
+ * methods, modules) of the Video player.
+ */
+function _makeFunctionsPublic(state) {
+ bindTo(methodsDict, state, state);
+}
+
+// function _renderElements(state)
+//
+// Create any necessary DOM elements, attach them, and set their
+// initial configuration. Also make the created DOM elements available
+// via the 'state' object. Much easier to work this way - you don't
+// have to do repeated jQuery element selects.
+function _renderElements(state) {
+ // Launch embedding of actual video content, or set it up so that it
+ // will be done as soon as the appropriate video player (YouTube or
+ // stand-alone HTML5) is loaded, and can handle embedding.
+ //
+ // Note that the loading of stand alone HTML5 player API is handled by
+ // Require JS. At the time when we reach this code, the stand alone
+ // HTML5 player is already loaded, so no further testing in that case
+ // is required.
+ let video;
+ let onYTApiReady;
+ let setupOnYouTubeIframeAPIReady;
+
+ if (state.videoType === 'youtube') {
+ state.youtubeApiAvailable = false;
+
+ onYTApiReady = function() {
+ console.log('[Video info]: YouTube API is available and is loaded.');
+ if (state.htmlPlayerLoaded) { return; }
+
+ console.log('[Video info]: Starting YouTube player.');
+ video = VideoPlayer(state);
+
+ state.modules.push(video);
+ state.__dfd__.resolve();
+ state.youtubeApiAvailable = true;
+ };
+
+ if (window.YT) {
+ // If we have a Deferred object responsible for calling OnYouTubeIframeAPIReady
+ // callbacks, make sure that they have all been called by trying to resolve the
+ // Deferred object. Upon resolving, all the OnYouTubeIframeAPIReady will be
+ // called. If the object has been already resolved, the callbacks will not
+ // be called a second time.
+ if (_youtubeApiDeferred) {
+ _youtubeApiDeferred.resolve();
+ }
+
+ window.YT.ready(onYTApiReady);
+ } else {
+ // There is only one global variable window.onYouTubeIframeAPIReady which
+ // is supposed to be a function that will be called by the YouTube API
+ // when it finished initializing. This function will update this global function
+ // so that it resolves our Deferred object, which will call all of the
+ // OnYouTubeIframeAPIReady callbacks.
+ //
+ // If this global function is already defined, we store it first, and make
+ // sure that it gets executed when our Deferred object is resolved.
+ setupOnYouTubeIframeAPIReady = function() {
+ _oldOnYouTubeIframeAPIReady = window.onYouTubeIframeAPIReady || undefined;
+
+ window.onYouTubeIframeAPIReady = function() {
+ _youtubeApiDeferred.resolve();
+ };
+
+ window.onYouTubeIframeAPIReady.done = _youtubeApiDeferred.done;
+
+ if (_oldOnYouTubeIframeAPIReady) {
+ window.onYouTubeIframeAPIReady.done(_oldOnYouTubeIframeAPIReady);
+ }
+ };
+
+ // If a Deferred object hasn't been created yet, create one now. It will
+ // be responsible for calling OnYouTubeIframeAPIReady callbacks once the
+ // YouTube API loads. After creating the Deferred object, load the YouTube
+ // API.
+ if (!_youtubeApiDeferred) {
+ _youtubeApiDeferred = $.Deferred();
+ setupOnYouTubeIframeAPIReady();
+ } else if (!window.onYouTubeIframeAPIReady || !window.onYouTubeIframeAPIReady.done) {
+ // The Deferred object could have been already defined in a previous
+ // initialization of the video module. However, since then the global variable
+ // window.onYouTubeIframeAPIReady could have been overwritten. If so,
+ // we should set it up again.
+ setupOnYouTubeIframeAPIReady();
+ }
+
+ // Attach a callback to our Deferred object to be called once the
+ // YouTube API loads.
+ window.onYouTubeIframeAPIReady.done(function() {
+ window.YT.ready(onYTApiReady);
+ });
+ }
+ } else {
+ video = VideoPlayer(state);
+
+ state.modules.push(video);
+ state.__dfd__.resolve();
+ state.htmlPlayerLoaded = true;
+ }
+}
+
+function _waitForYoutubeApi(state) {
+ console.log('[Video info]: Starting to wait for YouTube API to load.');
+ window.setTimeout(function() {
+ // If YouTube API will load OK, it will run `onYouTubeIframeAPIReady`
+ // callback, which will set `state.youtubeApiAvailable` to `true`.
+ // If something goes wrong at this stage, `state.youtubeApiAvailable` is
+ // `false`.
+ if (!state.youtubeApiAvailable) {
+ console.log('[Video info]: YouTube API is not available.');
+ if (!state.htmlPlayerLoaded) {
+ state.loadHtmlPlayer();
+ }
+ }
+ state.el.trigger('youtube_availability', [state.youtubeApiAvailable]);
+ }, state.config.ytTestTimeout);
+}
+
+function loadYouTubeIFrameAPI(scriptTag) {
+ let firstScriptTag = document.getElementsByTagName('script')[0];
+ firstScriptTag.parentNode.insertBefore(scriptTag, firstScriptTag);
+}
+
+// function _parseYouTubeIDs(state)
+// The function parse YouTube stream ID's.
+// @return
+// false: We don't have YouTube video IDs to work with; most likely
+// we have HTML5 video sources.
+// true: Parsing of YouTube video IDs went OK, and we can proceed
+// onwards to play YouTube videos.
+function _parseYouTubeIDs(state) {
+ if (state.parseYoutubeStreams(state.config.streams)) {
+ state.videoType = 'youtube';
+
+ return true;
+ }
+
+ console.log(
+ '[Video info]: Youtube Video IDs are incorrect or absent.'
+ );
+
+ return false;
+}
+
+/**
+ * Extract HLS video URLs from available video URLs.
+ *
+ * @param {object} state The object contaning the state (properties, methods, modules) of the Video player.
+ * @returns Array of available HLS video source urls.
+ */
+function extractHLSVideoSources(state) {
+ return _.filter(state.config.sources, function(source) {
+ return /\.m3u8(\?.*)?$/.test(source);
+ });
+}
+
+// function _prepareHTML5Video(state)
+// The function prepare HTML5 video, parse HTML5
+// video sources etc.
+function _prepareHTML5Video(state) {
+ state.speeds = ['0.75', '1.0', '1.25', '1.50', '2.0'];
+ // If none of the supported video formats can be played and there is no
+ // short-hand video links, than hide the spinner and show error message.
+ if (!state.config.sources.length) {
+ _hideWaitPlaceholder(state);
+ state.el
+ .find('.video-player div')
+ .addClass('hidden');
+ state.el
+ .find('.video-player .video-error')
+ .removeClass('is-hidden');
+
+ return false;
+ }
+
+ state.videoType = 'html5';
+
+ if (!_.keys(state.config.transcriptLanguages).length) {
+ state.config.showCaptions = false;
+ }
+ state.setSpeed(state.speed);
+
+ return true;
+}
+
+function _hideWaitPlaceholder(state) {
+ state.el
+ .addClass('is-initialized')
+ .find('.spinner')
+ .attr({
+ 'aria-hidden': 'true',
+ tabindex: -1
+ });
+}
+
+function _setConfigurations(state) {
+ state.setPlayerMode(state.config.mode);
+ // Possible value are: 'visible', 'hiding', and 'invisible'.
+ state.controlState = 'visible';
+ state.controlHideTimeout = null;
+ state.captionState = 'invisible';
+ state.captionHideTimeout = null;
+ state.HLSVideoSources = extractHLSVideoSources(state);
+}
+
+// eslint-disable-next-line no-shadow
+function _initializeModules(state, i18n) {
+ let dfd = $.Deferred(),
+ modulesList = $.map(state.modules, function(module) {
+ let options = state.options[module.moduleName] || {};
+ if (_.isFunction(module)) {
+ return module(state, i18n, options);
+ } else if ($.isPlainObject(module)) {
+ return module;
+ }
+ });
+
+ $.when.apply(null, modulesList)
+ .done(dfd.resolve);
+
+ return dfd.promise();
+}
+
+function _getConfiguration(data, storage) {
+ let isBoolean = function(value) {
+ let regExp = /^true$/i;
+ return regExp.test(value.toString());
+ },
+ // List of keys that will be extracted form the configuration.
+ extractKeys = [],
+ // Compatibility keys used to change names of some parameters in
+ // the final configuration.
+ compatKeys = {
+ start: 'startTime',
+ end: 'endTime'
+ },
+ // Conversions used to pre-process some configuration data.
+ conversions = {
+ showCaptions: isBoolean,
+ autoplay: isBoolean,
+ autohideHtml5: isBoolean,
+ autoAdvance: function(value) {
+ let shouldAutoAdvance = storage.getItem('auto_advance');
+ if (_.isUndefined(shouldAutoAdvance)) {
+ return isBoolean(value) || false;
+ } else {
+ return shouldAutoAdvance;
+ }
+ },
+ savedVideoPosition: function(value) {
+ return storage.getItem('savedVideoPosition', true)
+ || Number(value)
+ || 0;
+ },
+ speed: function(value) {
+ return storage.getItem('speed', true) || value;
+ },
+ generalSpeed: function(value) {
+ return storage.getItem('general_speed')
+ || value
+ || '1.0';
+ },
+ transcriptLanguage: function(value) {
+ return storage.getItem('language')
+ || value
+ || 'en';
+ },
+ ytTestTimeout: function(value) {
+ value = parseInt(value, 10);
+
+ if (!isFinite(value)) {
+ value = 1500;
+ }
+
+ return value;
+ },
+ startTime: function(value) {
+ value = parseInt(value, 10);
+ if (!isFinite(value) || value < 0) {
+ return 0;
+ }
+
+ return value;
+ },
+ endTime: function(value) {
+ value = parseInt(value, 10);
+
+ if (!isFinite(value) || value === 0) {
+ return null;
+ }
+
+ return value;
+ }
+ },
+ config = {};
+
+ data = _.extend({
+ startTime: 0,
+ endTime: null,
+ sub: '',
+ streams: ''
+ }, data);
+
+ $.each(data, function(option, value) {
+ // Extract option that is in `extractKeys`.
+ if ($.inArray(option, extractKeys) !== -1) {
+ return;
+ }
+
+ // Change option name to key that is in `compatKeys`.
+ if (compatKeys[option]) {
+ option = compatKeys[option];
+ }
+
+ // Pre-process data.
+ if (conversions[option]) {
+ if (_.isFunction(conversions[option])) {
+ value = conversions[option].call(this, value);
+ } else {
+ throw new TypeError(option + ' is not a function.');
+ }
+ }
+ config[option] = value;
+ });
+
+ return config;
+}
+
+// ***************************************************************
+// Public functions start here.
+// These are available via the 'state' object. Their context ('this'
+// keyword) is the 'state' object. The magic private function that makes
+// them available and sets up their context is makeFunctionsPublic().
+// ***************************************************************
+
+// function bindTo(methodsDict, obj, context, rewrite)
+// Creates a new function with specific context and assigns it to the provided
+// object.
+// eslint-disable-next-line no-shadow
+function bindTo(methodsDict, obj, context, rewrite) {
+ $.each(methodsDict, function(name, method) {
+ if (_.isFunction(method)) {
+ if (_.isUndefined(rewrite)) {
+ rewrite = true;
+ }
+
+ if (_.isUndefined(obj[name]) || rewrite) {
+ obj[name] = _.bind(method, context);
+ }
+ }
+ });
+}
+
+function loadYoutubePlayer() {
+ if (this.htmlPlayerLoaded) { return; }
+
+ console.log(
+ '[Video info]: Fetch metadata for YouTube video.'
+ );
+
+ this.fetchMetadata();
+ this.parseSpeed();
+}
+
+function loadHtmlPlayer() {
+ // When the youtube link doesn't work for any reason
+ // (for example, firewall) any
+ // alternate sources should automatically play.
+ if (!_prepareHTML5Video(this)) {
+ console.log(
+ '[Video info]: Continue loading '
+ + 'YouTube video.'
+ );
+
+ // Non-YouTube sources were not found either.
+
+ this.el.find('.video-player div')
+ .removeClass('hidden');
+ this.el.find('.video-player .video-error')
+ .addClass('is-hidden');
+
+ // If in reality the timeout was to short, try to
+ // continue loading the YouTube video anyways.
+ this.loadYoutubePlayer();
+ } else {
+ console.log(
+ '[Video info]: Start HTML5 player.'
+ );
+
+ // In-browser HTML5 player does not support quality
+ // control.
+ this.el.find('.quality_control').hide();
+ _renderElements(this);
+ }
+}
+
+// function initialize(element)
+// The function set initial configuration and preparation.
+
+function initialize(element) {
+ let self = this,
+ el = this.el,
+ id = this.id,
+ container = el.find('.video-wrapper'),
+ __dfd__ = $.Deferred(),
+ isTouch = onTouchBasedDevice() || '';
+
+ if (isTouch) {
+ el.addClass('is-touch');
+ }
+
+ $.extend(this, {
+ __dfd__: __dfd__,
+ container: container,
+ isFullScreen: false,
+ isTouch: isTouch
+ });
+
+ console.log('[Video info]: Initializing video with id "%s".', id);
+
+ // We store all settings passed to us by the server in one place. These
+ // are "read only", so don't modify them. All variable content lives in
+ // 'state' object.
+ // jQuery .data() return object with keys in lower camelCase format.
+ this.config = $.extend({}, _getConfiguration(this.metadata, this.storage), {
+ element: element,
+ fadeOutTimeout: 1400,
+ captionsFreezeTime: 10000,
+ mode: $.cookie('edX_video_player_mode'),
+ // Available HD qualities will only be accessible once the video has
+ // been played once, via player.getAvailableQualityLevels.
+ availableHDQualities: []
+ });
+
+ if (this.config.endTime < this.config.startTime) {
+ this.config.endTime = null;
+ }
+
+ this.lang = this.config.transcriptLanguage;
+ this.speed = this.speedToString(
+ this.config.speed || this.config.generalSpeed
+ );
+ this.auto_advance = this.config.autoAdvance;
+ this.htmlPlayerLoaded = false;
+ this.duration = this.metadata.duration;
+
+ _setConfigurations(this);
+
+ // If `prioritizeHls` is set to true than `hls` is the primary playback
+ if (this.config.prioritizeHls || !(_parseYouTubeIDs(this))) {
+ // If we do not have YouTube ID's, try parsing HTML5 video sources.
+ if (!_prepareHTML5Video(this)) {
+ __dfd__.reject();
+ // Non-YouTube sources were not found either.
+ return __dfd__.promise();
+ }
+
+ console.log('[Video info]: Start player in HTML5 mode.');
+ _renderElements(this);
+ } else {
+ _renderElements(this);
+
+ _waitForYoutubeApi(this);
+
+ let scriptTag = document.createElement('script');
+
+ scriptTag.src = this.config.ytApiUrl;
+ scriptTag.async = true;
+
+ $(scriptTag).on('load', function() {
+ self.loadYoutubePlayer();
+ });
+ $(scriptTag).on('error', function() {
+ console.log(
+ '[Video info]: YouTube returned an error for '
+ + 'video with id "' + self.id + '".'
+ );
+ // If the video is already loaded in `_waitForYoutubeApi` by the
+ // time we get here, then we shouldn't load it again.
+ if (!self.htmlPlayerLoaded) {
+ self.loadHtmlPlayer();
+ }
+ });
+
+ window.Video.loadYouTubeIFrameAPI(scriptTag);
+ }
+ return __dfd__.promise();
+}
+
+// function parseYoutubeStreams(state, youtubeStreams)
+//
+// Take a string in the form:
+// "iCawTYPtehk:0.75,KgpclqP-LBA:1.0,9-2670d5nvU:1.5"
+// parse it, and make it available via the 'state' object. If we are
+// not given a string, or it's length is zero, then we return false.
+//
+// @return
+// false: We don't have YouTube video IDs to work with; most likely
+// we have HTML5 video sources.
+// true: Parsing of YouTube video IDs went OK, and we can proceed
+// onwards to play YouTube videos.
+function parseYoutubeStreams(youtubeStreams) {
+ if (_.isUndefined(youtubeStreams) || !youtubeStreams.length) {
+ return false;
+ }
+
+ this.videos = {};
+
+ _.each(youtubeStreams.split(/,/), function(video) {
+ let speed;
+ video = video.split(/:/);
+ speed = this.speedToString(video[0]);
+ this.videos[speed] = video[1];
+ }, this);
+
+ return _.isString(this.videos['1.0']);
+}
+
+// function fetchMetadata()
+//
+// When dealing with YouTube videos, we must fetch meta data that has
+// certain key facts not available while the video is loading. For
+// example the length of the video can be determined from the meta
+// data.
+function fetchMetadata() {
+ let self = this,
+ metadataXHRs = [];
+
+ this.metadata = {};
+
+ metadataXHRs = _.map(this.videos, function(url, speed) {
+ return self.getVideoMetadata(url, function(data) {
+ if (data.items.length > 0) {
+ let metaDataItem = data.items[0];
+ self.metadata[metaDataItem.id] = metaDataItem.contentDetails;
+ }
+ });
+ });
+
+ $.when.apply(this, metadataXHRs).done(function() {
+ self.el.trigger('metadata_received');
+
+ // Not only do we trigger the "metadata_received" event, we also
+ // set a flag to notify that metadata has been received. This
+ // allows for code that will miss the "metadata_received" event
+ // to know that metadata has been received. This is important in
+ // cases when some code will subscribe to the "metadata_received"
+ // event after it has been triggered.
+ self.youtubeMetadataReceived = true;
+ });
+}
+
+// function parseSpeed()
+//
+// Create a separate array of available speeds.
+function parseSpeed() {
+ this.speeds = _.keys(this.videos).sort();
+}
+
+function setSpeed(newSpeed) {
+ // Possible speeds for each player type.
+ // HTML5 = [0.75, 1, 1.25, 1.5, 2]
+ // Youtube Flash = [0.75, 1, 1.25, 1.5]
+ // Youtube HTML5 = [0.25, 0.5, 1, 1.5, 2]
+ let map = {
+ 0.25: '0.75', // Youtube HTML5 -> HTML5 or Youtube Flash
+ '0.50': '0.75', // Youtube HTML5 -> HTML5 or Youtube Flash
+ 0.75: '0.50', // HTML5 or Youtube Flash -> Youtube HTML5
+ 1.25: '1.50', // HTML5 or Youtube Flash -> Youtube HTML5
+ 2.0: '1.50' // HTML5 or Youtube HTML5 -> Youtube Flash
+ };
+
+ if (_.contains(this.speeds, newSpeed)) {
+ this.speed = newSpeed;
+ } else {
+ newSpeed = map[newSpeed];
+ this.speed = _.contains(this.speeds, newSpeed) ? newSpeed : '1.0';
+ }
+ this.speed = parseFloat(this.speed);
+}
+
+function setAutoAdvance(enabled) {
+ this.auto_advance = enabled;
+}
+
+function getVideoMetadata(url, callback) {
+ let youTubeEndpoint;
+ if (!(_.isString(url))) {
+ url = this.videos['1.0'] || '';
+ }
+ // Will hit the API URL to get the youtube video metadata.
+ youTubeEndpoint = this.config.ytMetadataEndpoint; // The new runtime supports anonymous users
+ // and uses an XBlock handler to get YouTube metadata
+ if (!youTubeEndpoint) {
+ // The old runtime has a full/separate LMS API for getting YouTube metadata, but it doesn't
+ // support anonymous users nor videos that play in a sandboxed iframe.
+ youTubeEndpoint = [this.config.lmsRootURL, '/courses/yt_video_metadata', '?id=', url].join('');
+ }
+ return $.ajax({
+ url: youTubeEndpoint,
+ success: _.isFunction(callback) ? callback : null,
+ error: function() {
+ console.warn(
+ 'Unable to get youtube video metadata. Some video metadata may be unavailable.'
+ );
+ },
+ notifyOnError: false
+ });
+}
+
+function youtubeId(speed) {
+ let currentSpeed = this.isFlashMode() ? this.speed : '1.0';
+
+ return this.videos[speed]
+ || this.videos[currentSpeed]
+ || this.videos['1.0'];
+}
+
+function getDuration() {
+ try {
+ let safeMoment = typeof moment !== 'undefined' ? moment : window.moment;
+ return safeMoment.duration(this.metadata[this.youtubeId()].duration, safeMoment.ISO_8601).asSeconds();
+ } catch (err) {
+ return _.result(this.metadata[this.youtubeId('1.0')], 'duration') || 0;
+ }
+}
+
+/**
+ * Sets player mode.
+ *
+ * @param {string} mode Mode to set for the video player if it is supported.
+ * Otherwise, `html5` is used by default.
+ */
+function setPlayerMode(mode) {
+ let supportedModes = ['html5', 'flash'];
+
+ mode = _.contains(supportedModes, mode) ? mode : 'html5';
+ this.currentPlayerMode = mode;
+}
+
+/**
+ * Returns current player mode.
+ *
+ * @return {string} Returns string that describes player mode
+ */
+function getPlayerMode() {
+ return this.currentPlayerMode;
+}
+
+/**
+ * Checks if current player mode is Flash.
+ *
+ * @return {boolean} Returns `true` if current mode is `flash`, otherwise
+ * it returns `false`
+ */
+function isFlashMode() {
+ return this.getPlayerMode() === 'flash';
+}
+
+/**
+ * Checks if current player mode is Html5.
+ *
+ * @return {boolean} Returns `true` if current mode is `html5`, otherwise
+ * it returns `false`
+ */
+function isHtml5Mode() {
+ return this.getPlayerMode() === 'html5';
+}
+
+function isYoutubeType() {
+ return this.videoType === 'youtube';
+}
+
+function speedToString(speed) {
+ return parseFloat(speed).toFixed(2).replace(/\.00$/, '.0');
+}
+
+function getCurrentLanguage() {
+ let keys = _.keys(this.config.transcriptLanguages);
+
+ if (keys.length) {
+ if (!_.contains(keys, this.lang)) {
+ if (_.contains(keys, 'en')) {
+ this.lang = 'en';
+ } else {
+ this.lang = keys.pop();
+ }
+ }
+ } else {
+ return null;
+ }
+
+ return this.lang;
+}
+
+/*
+ * The trigger() function will assume that the @objChain is a complete
+ * chain with a method (function) at the end. It will call this function.
+ * So for example, when trigger() is called like so:
+ *
+ * state.trigger('videoPlayer.pause', {'param1': 10});
+ *
+ * Then trigger() will execute:
+ *
+ * state.videoPlayer.pause({'param1': 10});
+ */
+function trigger(objChain) {
+ let extraParameters = Array.prototype.slice.call(arguments, 1),
+ i, tmpObj, chain;
+
+ // Remember that 'this' is the 'state' object.
+ tmpObj = this;
+ chain = objChain.split('.');
+
+ // At the end of the loop the variable 'tmpObj' will either be the
+ // correct object/function to trigger/invoke. If the 'chain' chain of
+ // object is incorrect (one of the link is non-existent), then the loop
+ // will immediately exit.
+ while (chain.length) {
+ i = chain.shift();
+
+ if (tmpObj.hasOwnProperty(i)) {
+ tmpObj = tmpObj[i];
+ } else {
+ // An incorrect object chain was specified.
+
+ return false;
+ }
+ }
+
+ tmpObj.apply(this, extraParameters);
+
+ return true;
+}
diff --git a/xmodule/assets/video/public/js/025_focus_grabber.js b/xmodule/assets/video/public/js/025_focus_grabber.js
new file mode 100644
index 000000000000..48ec5527ad0e
--- /dev/null
+++ b/xmodule/assets/video/public/js/025_focus_grabber.js
@@ -0,0 +1,132 @@
+/*
+ * 025_focus_grabber.js
+ *
+ * Purpose: Provide a way to focus on autohidden Video controls.
+ *
+ *
+ * Because in HTML player mode we have a feature of autohiding controls on
+ * mouse inactivity, sometimes focus is lost from the currently selected
+ * control. What's more, when all controls are autohidden, we can't get to any
+ * of them because by default browser does not place hidden elements on the
+ * focus chain.
+ *
+ * To get around this minor annoyance, this module will manage 2 placeholder
+ * elements that will be invisible to the user's eye, but visible to the
+ * browser. This will allow for a sneaky stealing of focus and placing it where
+ * we need (on hidden controls).
+ *
+ * This code has been moved to a separate module because it provides a concrete
+ * block of functionality that can be turned on (off).
+ */
+
+/*
+ * "If you want to climb a mountain, begin at the top."
+ *
+ * ~ Zen saying
+ */
+
+
+
+// FocusGrabber module.
+let FocusGrabber = function(state) {
+ let dfd = $.Deferred();
+
+ state.focusGrabber = {};
+
+ _makeFunctionsPublic(state);
+ _renderElements(state);
+ _bindHandlers(state);
+
+ dfd.resolve();
+ return dfd.promise();
+};
+
+// Private functions.
+
+function _makeFunctionsPublic(state) {
+ let methodsDict = {
+ disableFocusGrabber: disableFocusGrabber,
+ enableFocusGrabber: enableFocusGrabber,
+ onFocus: onFocus
+ };
+
+ state.bindTo(methodsDict, state.focusGrabber, state);
+}
+
+function _renderElements(state) {
+ state.focusGrabber.elFirst = state.el.find('.focus_grabber.first');
+ state.focusGrabber.elLast = state.el.find('.focus_grabber.last');
+
+ // From the start, the Focus Grabber must be disabled so that
+ // tabbing (switching focus) does not land the user on one of the
+ // placeholder elements (elFirst, elLast).
+ state.focusGrabber.disableFocusGrabber();
+}
+
+function _bindHandlers(state) {
+ state.focusGrabber.elFirst.on('focus', state.focusGrabber.onFocus);
+ state.focusGrabber.elLast.on('focus', state.focusGrabber.onFocus);
+
+ // When the video container element receives programmatic focus, then
+ // on un-focus ('blur' event) we should trigger a 'mousemove' event so
+ // as to reveal autohidden controls.
+ state.el.on('blur', function() {
+ state.el.trigger('mousemove');
+ });
+}
+
+// Public functions.
+
+function enableFocusGrabber() {
+ let tabIndex;
+
+ // When the Focus Grabber is being enabled, there are two different
+ // scenarios:
+ //
+ // 1.) Currently focused element was inside the video player.
+ // 2.) Currently focused element was somewhere else on the page.
+ //
+ // In the first case we must make sure that the video player doesn't
+ // loose focus, even though the controls are autohidden.
+ if ($(document.activeElement).parents().hasClass('video')) {
+ tabIndex = -1;
+ } else {
+ tabIndex = 0;
+ }
+
+ this.focusGrabber.elFirst.attr('tabindex', tabIndex);
+ this.focusGrabber.elLast.attr('tabindex', tabIndex);
+
+ // Don't loose focus. We are inside video player on some control, but
+ // because we can't remain focused on a hidden element, we will shift
+ // focus to the main video element.
+ //
+ // Once the main element will receive the un-focus ('blur') event, a
+ // 'mousemove' event will be triggered, and the video controls will
+ // receive focus once again.
+ if (tabIndex === -1) {
+ this.el.focus();
+
+ this.focusGrabber.elFirst.attr('tabindex', 0);
+ this.focusGrabber.elLast.attr('tabindex', 0);
+ }
+}
+
+function disableFocusGrabber() {
+ // Only programmatic focusing on these elements will be available.
+ // We don't want the user to focus on them (for example with the 'Tab'
+ // key).
+ this.focusGrabber.elFirst.attr('tabindex', -1);
+ this.focusGrabber.elLast.attr('tabindex', -1);
+}
+
+function onFocus(event, params) {
+ // Once the Focus Grabber placeholder elements will gain focus, we will
+ // trigger 'mousemove' event so that the autohidden controls will
+ // become visible.
+ this.el.trigger('mousemove');
+
+ this.focusGrabber.disableFocusGrabber();
+}
+
+export default FocusGrabber;
diff --git a/xmodule/assets/video/public/js/02_html5_hls_video.js b/xmodule/assets/video/public/js/02_html5_hls_video.js
new file mode 100644
index 000000000000..b324476f7780
--- /dev/null
+++ b/xmodule/assets/video/public/js/02_html5_hls_video.js
@@ -0,0 +1,151 @@
+/* eslint-disable no-console, no-param-reassign */
+/**
+ * HTML5 video player module to support HLS video playback.
+ *
+ */
+
+'use strict';
+
+import _ from 'underscore';
+import HTML5Video from './02_html5_video.js';
+import HLS from 'hls.js';
+
+let HLSVideo = {};
+
+HLSVideo.Player = (function() {
+ /**
+ * Initialize HLS video player.
+ *
+ * @param {jQuery} el Reference to video player container element
+ * @param {Object} config Contains common config for video player
+ */
+ function Player(el, config) {
+ let self = this;
+
+ this.config = config;
+
+ // do common initialization independent of player type
+ this.init(el, config);
+
+ // set a default audio codec if not provided, this helps reduce issues
+ // switching audio codecs during playback
+ if (!this.config.defaultAudioCodec) {
+ this.config.defaultAudioCodec = "mp4a.40.5";
+ }
+
+ _.bindAll(this, 'playVideo', 'pauseVideo', 'onReady');
+
+ // If we have only HLS sources and browser doesn't support HLS then show error message.
+ if (config.HLSOnlySources && !config.canPlayHLS) {
+ this.showErrorMessage(null, '.video-hls-error');
+ return;
+ }
+
+ this.config.state.el.on('initialize', _.once(function() {
+ console.log('[HLS Video]: HLS Player initialized');
+ self.showPlayButton();
+ }));
+
+ // Safari has native support to play HLS videos
+ if (config.browserIsSafari) {
+ this.videoEl.attr('src', config.videoSources[0]);
+ } else {
+ // load auto start if auto_advance is enabled
+ if (config.state.auto_advance) {
+ this.hls = new HLS({autoStartLoad: true});
+ } else {
+ this.hls = new HLS({autoStartLoad: false});
+ }
+ this.hls.loadSource(config.videoSources[0]);
+ this.hls.attachMedia(this.video);
+
+ this.hls.on(HLS.Events.ERROR, this.onError.bind(this));
+
+ this.hls.on(HLS.Events.MANIFEST_PARSED, function(event, data) {
+ console.log(
+ '[HLS Video]: MANIFEST_PARSED, qualityLevelsInfo: ',
+ data.levels.map(function(level) {
+ return {
+ bitrate: level.bitrate,
+ resolution: level.width + 'x' + level.height
+ };
+ })
+ );
+ self.config.onReadyHLS();
+ });
+ this.hls.on(HLS.Events.LEVEL_SWITCHED, function(event, data) {
+ let level = self.hls.levels[data.level];
+ console.log(
+ '[HLS Video]: LEVEL_SWITCHED, qualityLevelInfo: ',
+ {
+ bitrate: level.bitrate,
+ resolution: level.width + 'x' + level.height
+ }
+ );
+ });
+ }
+ }
+
+ Player.prototype = Object.create(HTML5Video.Player.prototype);
+ Player.prototype.constructor = Player;
+
+ Player.prototype.playVideo = function() {
+ HTML5Video.Player.prototype.updatePlayerLoadingState.apply(this, ['show']);
+ if (!this.config.browserIsSafari) {
+ this.hls.startLoad();
+ }
+ HTML5Video.Player.prototype.playVideo.apply(this);
+ };
+
+ Player.prototype.pauseVideo = function() {
+ HTML5Video.Player.prototype.pauseVideo.apply(this);
+ HTML5Video.Player.prototype.updatePlayerLoadingState.apply(this, ['hide']);
+ };
+
+ Player.prototype.onPlaying = function() {
+ HTML5Video.Player.prototype.onPlaying.apply(this);
+ HTML5Video.Player.prototype.updatePlayerLoadingState.apply(this, ['hide']);
+ };
+
+ Player.prototype.onReady = function() {
+ this.config.events.onReady(null);
+ };
+
+ /**
+ * Handler for HLS video errors. This only takes care of fatal erros, non-fatal errors
+ * are automatically handled by hls.js
+ *
+ * @param {String} event `hlsError`
+ * @param {Object} data Contains the information regarding error occurred.
+ */
+ Player.prototype.onError = function(event, data) {
+ if (data.fatal) {
+ switch (data.type) {
+ case HLS.ErrorTypes.NETWORK_ERROR:
+ console.error(
+ '[HLS Video]: Fatal network error encountered, try to recover. Details: %s',
+ data.details
+ );
+ this.hls.startLoad();
+ break;
+ case HLS.ErrorTypes.MEDIA_ERROR:
+ console.error(
+ '[HLS Video]: Fatal media error encountered, try to recover. Details: %s',
+ data.details
+ );
+ this.hls.recoverMediaError();
+ break;
+ default:
+ console.error(
+ '[HLS Video]: Unrecoverable error encountered. Details: %s',
+ data.details
+ );
+ break;
+ }
+ }
+ };
+
+ return Player;
+}());
+
+export default HLSVideo;
diff --git a/xmodule/assets/video/public/js/02_html5_video.js b/xmodule/assets/video/public/js/02_html5_video.js
new file mode 100644
index 000000000000..839372054377
--- /dev/null
+++ b/xmodule/assets/video/public/js/02_html5_video.js
@@ -0,0 +1,380 @@
+/* eslint-disable no-console, no-param-reassign */
+/**
+ * @file HTML5 video player module. Provides methods to control the in-browser
+ * HTML5 video player.
+ *
+ * The goal was to write this module so that it closely resembles the YouTube
+ * API. The main reason for this is because initially the edX video player
+ * supported only YouTube videos. When HTML5 support was added, for greater
+ * compatibility, and to reduce the amount of code that needed to be modified,
+ * it was decided to write a similar API as the one provided by YouTube.
+ *
+ * @module HTML5Video
+ */
+
+import _ from 'underscore';
+
+let HTML5Video = {};
+
+HTML5Video.Player = (function() {
+ /*
+ * Constructor function for HTML5 Video player.
+ *
+ * @param {String|Object} el A DOM element where the HTML5 player will
+ * be inserted (as returned by jQuery(selector) function), or a
+ * selector string which will be used to select an element. This is a
+ * required parameter.
+ *
+ * @param config - An object whose properties will be used as
+ * configuration options for the HTML5 video player. This is an
+ * optional parameter. In the case if this parameter is missing, or
+ * some of the config object's properties are missing, defaults will be
+ * used. The available options (and their defaults) are as
+ * follows:
+ *
+ * config = {
+ *
+ * videoSources: [], // An array with properties being video
+ * // sources. The property name is the
+ * // video format of the source. Supported
+ * // video formats are: 'mp4', 'webm', and
+ * // 'ogg'.
+ * poster: Video poster URL
+ *
+ * browserIsSafari: Flag to tell if current browser is Safari
+ *
+ * events: { // Object's properties identify the
+ * // events that the API fires, and the
+ * // functions (event listeners) that the
+ * // API will call when those events occur.
+ * // If value is null, or property is not
+ * // specified, then no callback will be
+ * // called for that event.
+ *
+ * onReady: null,
+ * onStateChange: null
+ * }
+ * }
+ */
+ function Player(el, config) {
+ let errorMessage, lastSource, sourceList;
+
+ // Create HTML markup for individual sources of the HTML5 element.
+ sourceList = $.map(config.videoSources, function(source) {
+ return [
+ ' '
+ ].join('');
+ });
+
+ // do common initialization independent of player type
+ this.init(el, config);
+
+ // Create HTML markup for the element, populating it with
+ // sources from previous step. Set playback not supported error message.
+ errorMessage = [
+ gettext('This browser cannot play .mp4, .ogg, or .webm files.'),
+ gettext('Try using a different browser, such as Google Chrome.')
+ ].join('');
+ this.video.innerHTML = sourceList.join('') + errorMessage;
+
+ lastSource = this.videoEl.find('source').last();
+ lastSource.on('error', this.showErrorMessage.bind(this));
+ lastSource.on('error', this.onError.bind(this));
+ this.videoEl.on('error', this.onError.bind(this));
+ }
+
+ Player.prototype.showPlayButton = function() {
+ this.videoOverlayEl.removeClass('is-hidden');
+ };
+
+ Player.prototype.hidePlayButton = function() {
+ this.videoOverlayEl.addClass('is-hidden');
+ };
+
+ Player.prototype.showLoading = function() {
+ this.el
+ .removeClass('is-initialized')
+ .find('.spinner')
+ .removeAttr('tabindex')
+ .attr({'aria-hidden': 'false'});
+ };
+
+ Player.prototype.hideLoading = function() {
+ this.el
+ .addClass('is-initialized')
+ .find('.spinner')
+ .attr({'aria-hidden': 'false', tabindex: -1});
+ };
+
+ Player.prototype.updatePlayerLoadingState = function(state) {
+ if (state === 'show') {
+ this.hidePlayButton();
+ this.showLoading();
+ } else if (state === 'hide') {
+ this.hideLoading();
+ }
+ };
+
+ Player.prototype.callStateChangeCallback = function() {
+ if ($.isFunction(this.config.events.onStateChange)) {
+ this.config.events.onStateChange({
+ data: this.playerState
+ });
+ }
+ };
+
+ Player.prototype.pauseVideo = function() {
+ this.video.pause();
+ };
+
+ Player.prototype.seekTo = function(value) {
+ if (
+ typeof value === 'number'
+ && value <= this.video.duration
+ && value >= 0
+ ) {
+ this.video.currentTime = value;
+ }
+ };
+
+ Player.prototype.setVolume = function(value) {
+ if (typeof value === 'number' && value <= 100 && value >= 0) {
+ this.video.volume = value * 0.01;
+ }
+ };
+
+ Player.prototype.getCurrentTime = function() {
+ return this.video.currentTime;
+ };
+
+ Player.prototype.playVideo = function() {
+ this.video.play();
+ };
+
+ Player.prototype.getPlayerState = function() {
+ return this.playerState;
+ };
+
+ Player.prototype.getVolume = function() {
+ return this.video.volume;
+ };
+
+ Player.prototype.getDuration = function() {
+ if (isNaN(this.video.duration)) {
+ return 0;
+ }
+
+ return this.video.duration;
+ };
+
+ Player.prototype.setPlaybackRate = function(value) {
+ let newSpeed = parseFloat(value);
+
+ if (isFinite(newSpeed)) {
+ if (this.video.playbackRate !== value) {
+ this.video.playbackRate = value;
+ }
+ }
+ };
+
+ Player.prototype.getAvailablePlaybackRates = function() {
+ return [0.75, 1.0, 1.25, 1.5, 2.0];
+ };
+
+ // eslint-disable-next-line no-underscore-dangle
+ Player.prototype._getLogs = function() {
+ return this.logs;
+ };
+
+ Player.prototype.showErrorMessage = function(event, css) {
+ let cssSelecter = css || '.video-player .video-error';
+ this.el
+ .find('.video-player div')
+ .addClass('hidden')
+ .end()
+ .find(cssSelecter)
+ .removeClass('is-hidden')
+ .end()
+ .addClass('is-initialized')
+ .find('.spinner')
+ .attr({
+ 'aria-hidden': 'true',
+ tabindex: -1
+ });
+ };
+
+ Player.prototype.onError = function() {
+ if ($.isFunction(this.config.events.onError)) {
+ this.config.events.onError();
+ }
+ };
+
+ Player.prototype.destroy = function() {
+ this.video.removeEventListener('loadedmetadata', this.onLoadedMetadata, false);
+ this.video.removeEventListener('play', this.onPlay, false);
+ this.video.removeEventListener('playing', this.onPlaying, false);
+ this.video.removeEventListener('pause', this.onPause, false);
+ this.video.removeEventListener('ended', this.onEnded, false);
+ this.el
+ .find('.video-player div')
+ .removeClass('is-hidden')
+ .end()
+ .find('.video-player .video-error')
+ .addClass('is-hidden')
+ .end()
+ .removeClass('is-initialized')
+ .find('.spinner')
+ .attr({'aria-hidden': 'false'});
+ this.videoEl.off('remove');
+ this.videoEl.remove();
+ };
+
+ Player.prototype.onReady = function() {
+ this.config.events.onReady(null);
+ this.showPlayButton();
+ };
+
+ Player.prototype.onLoadedMetadata = function() {
+ this.playerState = HTML5Video.PlayerState.PAUSED;
+ if ($.isFunction(this.config.events.onReady)) {
+ this.onReady();
+ }
+ };
+
+ Player.prototype.onPlay = function() {
+ this.playerState = HTML5Video.PlayerState.BUFFERING;
+ this.callStateChangeCallback();
+ this.videoOverlayEl.addClass('is-hidden');
+ };
+
+ Player.prototype.onPlaying = function() {
+ this.playerState = HTML5Video.PlayerState.PLAYING;
+ this.callStateChangeCallback();
+ this.videoOverlayEl.addClass('is-hidden');
+ };
+
+ Player.prototype.onPause = function() {
+ this.playerState = HTML5Video.PlayerState.PAUSED;
+ this.callStateChangeCallback();
+ this.showPlayButton();
+ };
+
+ Player.prototype.onEnded = function() {
+ this.playerState = HTML5Video.PlayerState.ENDED;
+ this.callStateChangeCallback();
+ };
+
+ Player.prototype.init = function(el, config) {
+ let isTouch = window.onTouchBasedDevice() || '',
+ events = ['loadstart', 'progress', 'suspend', 'abort', 'error',
+ 'emptied', 'stalled', 'play', 'pause', 'loadedmetadata',
+ 'loadeddata', 'waiting', 'playing', 'canplay', 'canplaythrough',
+ 'seeking', 'seeked', 'timeupdate', 'ended', 'ratechange',
+ 'durationchange', 'volumechange'
+ ],
+ self = this,
+ callback;
+
+ this.config = config;
+ this.logs = [];
+ this.el = $(el);
+
+ // Because of problems with creating video element via jquery
+ // (http://bugs.jquery.com/ticket/9174) we create it using native JS.
+ this.video = document.createElement('video');
+
+ // Get the jQuery object and set error event handlers
+ this.videoEl = $(this.video);
+
+ // Video player overlay play button
+ this.videoOverlayEl = this.el.find('.video-wrapper .btn-play');
+
+ // The player state is used by other parts of the VideoPlayer to
+ // determine what the video is currently doing.
+ this.playerState = HTML5Video.PlayerState.UNSTARTED;
+
+ _.bindAll(this, 'onLoadedMetadata', 'onPlay', 'onPlaying', 'onPause', 'onEnded');
+
+ // Attach a 'click' event on the element. It will cause the
+ // video to pause/play.
+ callback = function() {
+ let PlayerState = HTML5Video.PlayerState;
+
+ if (self.playerState === PlayerState.PLAYING) {
+ self.playerState = PlayerState.PAUSED;
+ self.pauseVideo();
+ } else {
+ self.playerState = PlayerState.PLAYING;
+ self.playVideo();
+ }
+ };
+ this.videoEl.on('click', callback);
+ this.videoOverlayEl.on('click', callback);
+
+ this.debug = false;
+ $.each(events, function(index, eventName) {
+ self.video.addEventListener(eventName, function() {
+ self.logs.push({
+ 'event name': eventName,
+ state: self.playerState
+ });
+
+ if (self.debug) {
+ console.log(
+ 'event name:', eventName,
+ 'state:', self.playerState,
+ 'readyState:', self.video.readyState,
+ 'networkState:', self.video.networkState
+ );
+ }
+
+ el.trigger('html5:' + eventName, arguments);
+ });
+ });
+
+ // When the tag has been processed by the browser, and it
+ // is ready for playback, notify other parts of the VideoPlayer,
+ // and initially pause the video.
+ this.video.addEventListener('loadedmetadata', this.onLoadedMetadata, false);
+ this.video.addEventListener('play', this.onPlay, false);
+ this.video.addEventListener('playing', this.onPlaying, false);
+ this.video.addEventListener('pause', this.onPause, false);
+ this.video.addEventListener('ended', this.onEnded, false);
+
+ if (/iP(hone|od)/i.test(isTouch[0])) {
+ this.videoEl.prop('controls', true);
+ }
+
+ // Set video poster
+ if (this.config.poster) {
+ this.videoEl.prop('poster', this.config.poster);
+ }
+
+ // Place the element on the page.
+ this.videoEl.appendTo(el.find('.video-player > div:first-child'));
+ };
+
+ return Player;
+})();
+
+// The YouTube API presents several constants which describe the player's
+// state at a given moment. HTML5Video API will copy these constants so
+// that code which uses both the YouTube API and this API doesn't have to
+// change.
+HTML5Video.PlayerState = {
+ UNSTARTED: -1,
+ ENDED: 0,
+ PLAYING: 1,
+ PAUSED: 2,
+ BUFFERING: 3,
+ CUED: 5
+};
+
+export default HTML5Video;
diff --git a/xmodule/assets/video/public/js/035_video_accessible_menu.js b/xmodule/assets/video/public/js/035_video_accessible_menu.js
new file mode 100644
index 000000000000..ae2858156bed
--- /dev/null
+++ b/xmodule/assets/video/public/js/035_video_accessible_menu.js
@@ -0,0 +1,65 @@
+'use strict';
+
+import _ from 'underscore';
+
+/**
+ * Video Download Transcript control module.
+ * @exports video/035_video_accessible_menu.js
+ * @constructor
+ * @param {jquery Element} element
+ * @param {Object} options
+ */
+let VideoTranscriptDownloadHandler = function(element, options) {
+ if (!(this instanceof VideoTranscriptDownloadHandler)) {
+ return new VideoTranscriptDownloadHandler(element, options);
+ }
+
+ _.bindAll(this, 'clickHandler');
+
+ this.container = element;
+ this.options = options || {};
+
+ if (this.container.find('.wrapper-downloads .wrapper-download-transcripts')) {
+ this.initialize();
+ }
+
+ return false;
+};
+
+VideoTranscriptDownloadHandler.prototype = {
+ // Initializes the module.
+ initialize: function() {
+ this.value = this.options.storage.getItem('transcript_download_format');
+ this.el = this.container.find('.list-download-transcripts');
+ this.el.on('click', '.btn-link', this.clickHandler);
+ },
+
+ // Event handler. We delay link clicks until the file type is set
+ clickHandler: function(event) {
+ let that = this;
+ let fileType;
+ let data;
+ let downloadUrl;
+
+ event.preventDefault();
+
+ fileType = $(event.target).data('value');
+ data = {transcript_download_format: fileType};
+ downloadUrl = $(event.target).attr('href');
+
+ $.ajax({
+ url: this.options.saveStateUrl,
+ type: 'POST',
+ dataType: 'json',
+ data: data,
+ success: function() {
+ that.options.storage.setItem('transcript_download_format', fileType);
+ },
+ complete: function() {
+ document.location.href = downloadUrl;
+ }
+ });
+ }
+};
+
+export default VideoTranscriptDownloadHandler;
diff --git a/xmodule/assets/video/public/js/036_video_social_sharing.js b/xmodule/assets/video/public/js/036_video_social_sharing.js
new file mode 100644
index 000000000000..0ce61ba35665
--- /dev/null
+++ b/xmodule/assets/video/public/js/036_video_social_sharing.js
@@ -0,0 +1,85 @@
+// eslint-disable-next-line lines-around-directive
+'use strict';
+
+import _ from 'underscore';
+
+
+/**
+ * Video Social Sharing control module.
+ * @exports video/036_video_social_sharing.js
+ * @constructor
+ * @param {jquery Element} element
+ * @param {Object} options
+ */
+let VideoSocialSharingHandler = function(element, options) {
+ if (!(this instanceof VideoSocialSharingHandler)) {
+ return new VideoSocialSharingHandler(element, options);
+ }
+
+ _.bindAll(this, 'clickHandler');
+ _.bindAll(this, 'copyHandler');
+ _.bindAll(this, 'hideHandler');
+ _.bindAll(this, 'showHandler');
+
+ this.container = element;
+
+ if (this.container.find('.wrapper-downloads .wrapper-social-share')) {
+ this.initialize();
+ }
+
+ return false;
+};
+
+VideoSocialSharingHandler.prototype = {
+
+ // Initializes the module.
+ initialize: function() {
+ this.el = this.container.find('.wrapper-social-share');
+ this.baseVideoUrl = this.el.data('url');
+ this.course_id = this.container.data('courseId');
+ this.block_id = this.container.data('blockId');
+ this.el.on('click', '.social-share-link', this.clickHandler);
+
+ this.closeBtn = this.el.find('.close-btn');
+ this.toggleBtn = this.el.find('.social-toggle-btn');
+ this.copyBtn = this.el.find('.public-video-copy-btn');
+ this.shareContainer = this.el.find('.container-social-share');
+ this.closeBtn.on('click', this.hideHandler);
+ this.toggleBtn.on('click', this.showHandler);
+ this.copyBtn.on('click', this.copyHandler);
+ },
+
+ // Fire an analytics event on share button click.
+ clickHandler: function(event) {
+ let source = $(event.currentTarget).data('source');
+ this.sendAnalyticsEvent(source);
+ },
+
+ hideHandler: function(event) {
+ this.shareContainer.hide();
+ this.toggleBtn.show();
+ },
+
+ showHandler: function(event) {
+ this.shareContainer.show();
+ this.toggleBtn.hide();
+ },
+
+ copyHandler: function(event) {
+ navigator.clipboard.writeText(this.copyBtn.data('url'));
+ },
+
+ // Send an analytics event for share button tracking.
+ sendAnalyticsEvent: function(source) {
+ window.analytics.track(
+ 'edx.social.video.share_button.clicked',
+ {
+ source,
+ video_block_id: this.container.data('blockId'),
+ course_id: this.container.data('courseId'),
+ }
+ );
+ }
+};
+
+export default VideoSocialSharingHandler;
diff --git a/xmodule/assets/video/public/js/037_video_transcript_feedback.js b/xmodule/assets/video/public/js/037_video_transcript_feedback.js
new file mode 100644
index 000000000000..c5c1830b85ef
--- /dev/null
+++ b/xmodule/assets/video/public/js/037_video_transcript_feedback.js
@@ -0,0 +1,240 @@
+// VideoTranscriptFeedbackHandler module.
+'use strict';
+
+import _ from 'underscore';
+
+/**
+ * @desc VideoTranscriptFeedback module exports a function.
+ *
+ * @type {function}
+ * @access public
+ *
+ * @param {object} state - The object containing the state of the video
+ * player. All other modules, their parameters, public variables, etc.
+ * are available via this object.
+ *
+ * @this {object} The global window object.
+ *
+ */
+
+let VideoTranscriptFeedback = function(state) {
+ if (!(this instanceof VideoTranscriptFeedback)) {
+ return new VideoTranscriptFeedback(state);
+ }
+
+ _.bindAll(this, 'destroy', 'getFeedbackForCurrentTranscript', 'markAsPositiveFeedback', 'markAsNegativeFeedback', 'markAsEmptyFeedback',
+ 'selectThumbsUp', 'selectThumbsDown', 'unselectThumbsUp', 'unselectThumbsDown', 'thumbsUpClickHandler', 'thumbsDownClickHandler',
+ 'sendFeedbackForCurrentTranscript', 'onHideLanguageMenu', 'getCurrentLanguage', 'loadAndSetVisibility', 'showWidget', 'hideWidget'
+ );
+
+ this.state = state;
+ this.state.videoTranscriptFeedback = this;
+ this.currentTranscriptLanguage = this.state.lang;
+ this.transcriptLanguages = this.state.config.transcriptLanguages;
+
+ if (this.state.el.find('.wrapper-transcript-feedback').length) {
+ this.initialize();
+ }
+
+ return false;
+};
+
+VideoTranscriptFeedback.prototype = {
+ destroy: function () {
+ this.state.el.off(this.events);
+ },
+
+ initialize: function () {
+ this.el = this.state.el.find('.wrapper-transcript-feedback');
+
+ this.videoId = this.el.data('video-id');
+ this.userId = this.el.data('user-id');
+ this.aiTranslationsUrl = this.state.config.aiTranslationsUrl;
+
+ this.thumbsUpButton = this.el.find('.thumbs-up-btn');
+ this.thumbsDownButton = this.el.find('.thumbs-down-btn');
+ this.thumbsUpButton.on('click', this.thumbsUpClickHandler);
+ this.thumbsDownButton.on('click', this.thumbsDownClickHandler);
+
+ this.events = {
+ 'language_menu:hide': this.onHideLanguageMenu,
+ destroy: this.destroy
+ };
+ this.loadAndSetVisibility();
+ this.bindHandlers();
+ },
+
+ bindHandlers: function () {
+ this.state.el.on(this.events);
+ },
+
+ getFeedbackForCurrentTranscript: function () {
+ let self = this;
+ let url = self.aiTranslationsUrl + '/transcript-feedback' + '?transcript_language=' + self.currentTranscriptLanguage + '&video_id=' + self.videoId + '&user_id=' + self.userId;
+
+ $.ajax({
+ url: url,
+ type: 'GET',
+ success: function (data) {
+ if (data && data.value === true) {
+ self.markAsPositiveFeedback();
+ self.currentFeedback = true;
+ } else {
+ if (data && data.value === false) {
+ self.markAsNegativeFeedback();
+ self.currentFeedback = false;
+ } else {
+ self.markAsEmptyFeedback();
+ self.currentFeedback = null;
+ }
+ }
+ },
+ error: function (error) {
+ self.markAsEmptyFeedback();
+ self.currentFeedback = null;
+ }
+ });
+ },
+
+ markAsPositiveFeedback: function () {
+ this.selectThumbsUp();
+ this.unselectThumbsDown();
+ },
+
+ markAsNegativeFeedback: function () {
+ this.selectThumbsDown();
+ this.unselectThumbsUp();
+ },
+
+ markAsEmptyFeedback: function () {
+ this.unselectThumbsUp();
+ this.unselectThumbsDown();
+ },
+
+ selectThumbsUp: function () {
+ let thumbsUpIcon = this.thumbsUpButton.find('.thumbs-up-icon');
+ if (thumbsUpIcon[0].classList.contains('fa-thumbs-o-up')) {
+ thumbsUpIcon[0].classList.remove("fa-thumbs-o-up");
+ thumbsUpIcon[0].classList.add("fa-thumbs-up");
+ }
+ },
+
+ selectThumbsDown: function () {
+ let thumbsDownIcon = this.thumbsDownButton.find('.thumbs-down-icon');
+ if (thumbsDownIcon[0].classList.contains('fa-thumbs-o-down')) {
+ thumbsDownIcon[0].classList.remove("fa-thumbs-o-down");
+ thumbsDownIcon[0].classList.add("fa-thumbs-down");
+ }
+ },
+
+ unselectThumbsUp: function () {
+ let thumbsUpIcon = this.thumbsUpButton.find('.thumbs-up-icon');
+ if (thumbsUpIcon[0].classList.contains('fa-thumbs-up')) {
+ thumbsUpIcon[0].classList.remove("fa-thumbs-up");
+ thumbsUpIcon[0].classList.add("fa-thumbs-o-up");
+ }
+ },
+
+ unselectThumbsDown: function () {
+ let thumbsDownIcon = this.thumbsDownButton.find('.thumbs-down-icon');
+ if (thumbsDownIcon[0].classList.contains('fa-thumbs-down')) {
+ thumbsDownIcon[0].classList.remove("fa-thumbs-down");
+ thumbsDownIcon[0].classList.add("fa-thumbs-o-down");
+ }
+ },
+
+ thumbsUpClickHandler: function () {
+ if (this.currentFeedback) {
+ this.sendFeedbackForCurrentTranscript(null);
+ } else {
+ this.sendFeedbackForCurrentTranscript(true);
+ }
+ },
+
+ thumbsDownClickHandler: function () {
+ if (this.currentFeedback === false) {
+ this.sendFeedbackForCurrentTranscript(null);
+ } else {
+ this.sendFeedbackForCurrentTranscript(false);
+ }
+ },
+
+ sendFeedbackForCurrentTranscript: function (feedbackValue) {
+ let self = this;
+ let url = self.aiTranslationsUrl + '/transcript-feedback/';
+ $.ajax({
+ url: url,
+ type: 'POST',
+ dataType: 'json',
+ data: {
+ transcript_language: self.currentTranscriptLanguage,
+ video_id: self.videoId,
+ user_id: self.userId,
+ value: feedbackValue,
+ },
+ success: function (data) {
+ if (data && data.value === true) {
+ self.markAsPositiveFeedback();
+ self.currentFeedback = true;
+ } else {
+ if (data && data.value === false) {
+ self.markAsNegativeFeedback();
+ self.currentFeedback = false;
+ } else {
+ self.markAsEmptyFeedback();
+ self.currentFeedback = null;
+ }
+ }
+ },
+ error: function () {
+ self.markAsEmptyFeedback();
+ self.currentFeedback = null;
+ }
+ });
+ },
+
+ onHideLanguageMenu: function () {
+ let newLanguageSelected = this.getCurrentLanguage();
+ if (this.currentTranscriptLanguage !== newLanguageSelected) {
+ this.currentTranscriptLanguage = this.getCurrentLanguage();
+ this.loadAndSetVisibility();
+ }
+ },
+
+ getCurrentLanguage: function () {
+ let language = this.state.lang;
+ return language;
+ },
+
+ loadAndSetVisibility: function () {
+ let self = this;
+ let url = self.aiTranslationsUrl + '/video-transcript' + '?transcript_language=' + self.currentTranscriptLanguage + '&video_id=' + self.videoId;
+
+ $.ajax({
+ url: url,
+ type: 'GET',
+ async: false,
+ success: function (data) {
+ if (data && data.status === 'Completed') {
+ self.showWidget();
+ self.getFeedbackForCurrentTranscript();
+ } else {
+ self.hideWidget();
+ }
+ },
+ error: function (error) {
+ self.hideWidget();
+ }
+ });
+ },
+
+ showWidget: function () {
+ this.el.show();
+ },
+
+ hideWidget: function () {
+ this.el.hide();
+ }
+};
+
+export default VideoTranscriptFeedback;
diff --git a/xmodule/assets/video/public/js/03_video_player.js b/xmodule/assets/video/public/js/03_video_player.js
new file mode 100644
index 000000000000..bad0b45b77f9
--- /dev/null
+++ b/xmodule/assets/video/public/js/03_video_player.js
@@ -0,0 +1,911 @@
+/* eslint-disable no-console, no-param-reassign */
+import HTML5Video from './02_html5_video.js';
+import HTML5HLSVideo from './02_html5_hls_video.js';
+import Resizer from './00_resizer.js';
+import HLS from 'hls.js';
+import _ from 'underscore';
+import * as Time from './utils/time.js';
+
+let dfd = $.Deferred();
+
+let VideoPlayer = function(state) {
+ state.videoPlayer = {};
+ _makeFunctionsPublic(state);
+ _initialize(state);
+ // No callbacks to DOM events (click, mousemove, etc.).
+
+ return dfd.promise();
+};
+
+let methodsDict = {
+ destroy: destroy,
+ duration: duration,
+ handlePlaybackQualityChange: handlePlaybackQualityChange,
+
+ // Added for finer graded seeking control.
+ // Please see:
+ // https://developers.google.com/youtube/js_api_reference#Events
+ isBuffering: isBuffering,
+ // https://developers.google.com/youtube/js_api_reference#cueVideoById
+ isCued: isCued,
+
+ isEnded: isEnded,
+ isPlaying: isPlaying,
+ isUnstarted: isUnstarted,
+ onCaptionSeek: onSeek,
+ onEnded: onEnded,
+ onError: onError,
+ onPause: onPause,
+ onPlay: onPlay,
+ runTimer: runTimer,
+ stopTimer: stopTimer,
+ onLoadMetadataHtml5: onLoadMetadataHtml5,
+ onPlaybackQualityChange: onPlaybackQualityChange,
+ onReady: onReady,
+ onSlideSeek: onSeek,
+ onSpeedChange: onSpeedChange,
+ onAutoAdvanceChange: onAutoAdvanceChange,
+ onStateChange: onStateChange,
+ onUnstarted: onUnstarted,
+ onVolumeChange: onVolumeChange,
+ pause: pause,
+ play: play,
+ seekTo: seekTo,
+ setPlaybackRate: setPlaybackRate,
+ update: update,
+ figureOutStartEndTime: figureOutStartEndTime,
+ figureOutStartingTime: figureOutStartingTime,
+ updatePlayTime: updatePlayTime
+};
+/* eslint-enable no-use-before-define */
+
+VideoPlayer.prototype = methodsDict;
+
+export default VideoPlayer;
+
+// ***************************************************************
+// Private functions start here.
+// ***************************************************************
+
+// function _makeFunctionsPublic(state)
+//
+// Functions which will be accessible via 'state' object. When called,
+// these functions will get the 'state' object as a context.
+function _makeFunctionsPublic(state) {
+ let debouncedF = _.debounce(
+ function(params) {
+ // Can't cancel a queued debounced function on destroy
+ if (state.videoPlayer) {
+ return onSeek.call(this, params);
+ }
+ }.bind(state),
+ 300
+ );
+
+ state.bindTo(methodsDict, state.videoPlayer, state);
+
+ state.videoPlayer.onSlideSeek = debouncedF;
+ state.videoPlayer.onCaptionSeek = debouncedF;
+}
+
+// Updates players state, once metadata is loaded for html5 player.
+function onLoadMetadataHtml5() {
+ let player = this.videoPlayer.player.videoEl;
+ let videoWidth = player[0].videoWidth || player.width();
+ let videoHeight = player[0].videoHeight || player.height();
+
+ _resize(this, videoWidth, videoHeight);
+ _updateVcrAndRegion(this);
+}
+
+// function _initialize(state)
+//
+// Create any necessary DOM elements, attach them, and set their
+// initial configuration. Also make the created DOM elements available
+// via the 'state' object. Much easier to work this way - you don't
+// have to do repeated jQuery element selects.
+// eslint-disable-next-line no-underscore-dangle
+function _initialize(state) {
+ let youTubeId;
+ let player;
+ let userAgent;
+ let commonPlayerConfig;
+ let eventToBeTriggered = 'loadedmetadata';
+
+ // The function is called just once to apply pre-defined configurations
+ // by student before video starts playing. Waits until the video's
+ // metadata is loaded, which normally happens just after the video
+ // starts playing. Just after that configurations can be applied.
+ state.videoPlayer.ready = _.once(function() {
+ if (!state.isFlashMode() && state.speed != '1.0') {
+ state.videoPlayer.setPlaybackRate(state.speed);
+ }
+ });
+
+ if (state.isYoutubeType()) {
+ state.videoPlayer.PlayerState = YT.PlayerState;
+ state.videoPlayer.PlayerState.UNSTARTED = -1;
+ } else {
+ state.videoPlayer.PlayerState = HTML5Video.PlayerState;
+ }
+
+ state.videoPlayer.currentTime = 0;
+
+ state.videoPlayer.goToStartTime = true;
+ state.videoPlayer.stopAtEndTime = true;
+
+ state.videoPlayer.playerVars = {
+ controls: 0,
+ wmode: 'transparent',
+ rel: 0,
+ showinfo: 0,
+ enablejsapi: 1,
+ modestbranding: 1,
+ cc_load_policy: 0
+ };
+
+ if (!state.isFlashMode()) {
+ state.videoPlayer.playerVars.html5 = 1;
+ }
+
+ // Detect the current browser for several browser-specific work-arounds.
+ userAgent = navigator.userAgent.toLowerCase();
+ state.browserIsFirefox = userAgent.indexOf('firefox') > -1;
+ state.browserIsChrome = userAgent.indexOf('chrome') > -1;
+ // Chrome includes both "Chrome" and "Safari" in the user agent.
+ state.browserIsSafari = (userAgent.indexOf('safari') > -1
+ && !state.browserIsChrome);
+
+ // Browser can play HLS videos if either `Media Source Extensions`
+ // feature is supported or browser is safari (native HLS support)
+ state.canPlayHLS = state.HLSVideoSources.length > 0 && (HLS.isSupported() || state.browserIsSafari);
+ state.HLSOnlySources = state.config.sources.length > 0
+ && state.config.sources.length === state.HLSVideoSources.length;
+
+ commonPlayerConfig = {
+ playerVars: state.videoPlayer.playerVars,
+ videoSources: state.config.sources,
+ poster: state.config.poster,
+ browserIsSafari: state.browserIsSafari,
+ events: {
+ onReady: state.videoPlayer.onReady,
+ onStateChange: state.videoPlayer.onStateChange,
+ onError: state.videoPlayer.onError
+ }
+ };
+
+ if (state.videoType === 'html5') {
+ if (state.canPlayHLS || state.HLSOnlySources) {
+ state.videoPlayer.player = new HTML5HLSVideo.Player(
+ state.el,
+ _.extend({}, commonPlayerConfig, {
+ state: state,
+ onReadyHLS: function() { dfd.resolve(); },
+ videoSources: state.HLSVideoSources,
+ canPlayHLS: state.canPlayHLS,
+ HLSOnlySources: state.HLSOnlySources
+ })
+ );
+ // `loadedmetadata` event triggered too early on Safari due
+ // to which correct video dimensions were not calculated
+ eventToBeTriggered = state.browserIsSafari ? 'loadeddata' : eventToBeTriggered;
+ } else {
+ state.videoPlayer.player = new HTML5Video.Player(state.el, commonPlayerConfig);
+ }
+ // eslint-disable-next-line no-multi-assign
+ player = state.videoEl = state.videoPlayer.player.videoEl;
+ player[0].addEventListener(eventToBeTriggered, state.videoPlayer.onLoadMetadataHtml5, false);
+ player.on('remove', state.videoPlayer.destroy);
+ } else {
+ youTubeId = state.youtubeId();
+
+ state.videoPlayer.player = new YT.Player(state.id, {
+ playerVars: state.videoPlayer.playerVars,
+ videoId: youTubeId,
+ events: {
+ onReady: state.videoPlayer.onReady,
+ onStateChange: state.videoPlayer.onStateChange,
+ onPlaybackQualityChange: state.videoPlayer.onPlaybackQualityChange,
+ onError: state.videoPlayer.onError
+ }
+ });
+
+ state.el.on('initialize', function() {
+ // eslint-disable-next-line no-shadow, no-multi-assign
+ let player = state.videoEl = state.el.find('iframe');
+ let videoWidth = player.attr('width') || player.width();
+ let videoHeight = player.attr('height') || player.height();
+
+ player.on('remove', state.videoPlayer.destroy);
+
+ _resize(state, videoWidth, videoHeight);
+ _updateVcrAndRegion(state, true);
+ });
+ }
+
+ if (state.isTouch) {
+ dfd.resolve();
+ }
+}
+
+function _updateVcrAndRegion(state, isYoutube) {
+ // eslint-disable-next-line no-shadow
+ let update = function(state) {
+ // eslint-disable-next-line no-shadow
+ let duration = state.videoPlayer.duration();
+ let time = state.videoPlayer.figureOutStartingTime(duration);
+
+ // Update the VCR.
+ state.trigger(
+ 'videoControl.updateVcrVidTime',
+ {
+ time: time,
+ duration: duration
+ }
+ );
+
+ // Update the time slider.
+ state.trigger(
+ 'videoProgressSlider.updateStartEndTimeRegion',
+ {
+ duration: duration
+ }
+ );
+ state.trigger(
+ 'videoProgressSlider.updatePlayTime',
+ {
+ time: time,
+ duration: duration
+ }
+ );
+ };
+
+ // After initialization, update the VCR with total time.
+ // At this point only the metadata duration is available (not
+ // very precise), but it is better than having 00:00:00 for
+ // total time.
+ if (state.youtubeMetadataReceived || !isYoutube) {
+ // Metadata was already received, and is available.
+ update(state);
+ } else {
+ // We wait for metadata to arrive, before we request the update
+ // of the VCR video time, and of the start-end time region.
+ // Metadata contains duration of the video.
+ state.el.on('metadata_received', function() {
+ update(state);
+ });
+ }
+}
+
+function _resize(state, videoWidth, videoHeight) {
+ state.resizer = new Resizer({
+ element: state.videoEl,
+ elementRatio: videoWidth / videoHeight,
+ container: state.container
+ })
+ .callbacks.once(function() {
+ state.el.trigger('caption:resize');
+ })
+ .setMode('width');
+
+ // Update captions size when controls becomes visible on iPad or Android
+ if (/iPad|Android/i.test(state.isTouch[0])) {
+ state.el.on('controls:show', function() {
+ state.el.trigger('caption:resize');
+ });
+ }
+
+ $(window).on('resize.video', _.debounce(function() {
+ state.trigger('videoFullScreen.updateControlsHeight', null);
+ state.el.trigger('caption:resize');
+ state.resizer.align();
+ }, 100));
+}
+
+// function _restartUsingFlash(state)
+//
+// When we are about to play a YouTube video in HTML5 mode and discover
+// that we only have one available playback rate, we will switch to
+// Flash mode. In Flash speed switching is done by reloading videos
+// recorded at different frame rates.
+function _restartUsingFlash(state) {
+ // Remove from the page current iFrame with HTML5 video.
+ state.videoPlayer.player.destroy();
+
+ state.setPlayerMode('flash');
+
+ console.log('[Video info]: Changing YouTube player mode to "flash".');
+
+ // Removed configuration option that requests the HTML5 mode.
+ delete state.videoPlayer.playerVars.html5;
+
+ // Request for the creation of a new Flash player
+ state.videoPlayer.player = new YT.Player(state.id, {
+ playerVars: state.videoPlayer.playerVars,
+ videoId: state.youtubeId(),
+ events: {
+ onReady: state.videoPlayer.onReady,
+ onStateChange: state.videoPlayer.onStateChange,
+ onPlaybackQualityChange: state.videoPlayer.onPlaybackQualityChange,
+ onError: state.videoPlayer.onError
+ }
+ });
+
+ _updateVcrAndRegion(state, true);
+ state.el.trigger('caption:fetch');
+ state.resizer.setElement(state.el.find('iframe')).align();
+}
+
+// ***************************************************************
+// Public functions start here.
+// These are available via the 'state' object. Their context ('this'
+// keyword) is the 'state' object. The magic private function that makes
+// them available and sets up their context is makeFunctionsPublic().
+// ***************************************************************
+
+function destroy() {
+ let player = this.videoPlayer.player;
+ this.el.removeClass([
+ 'is-unstarted', 'is-playing', 'is-paused', 'is-buffered',
+ 'is-ended', 'is-cued'
+ ].join(' '));
+ $(window).off('.video');
+ this.el.trigger('destroy');
+ this.el.off();
+ this.videoPlayer.stopTimer();
+ if (this.resizer && this.resizer.destroy) {
+ this.resizer.destroy();
+ }
+ if (player && player.video) {
+ player.video.removeEventListener('loadedmetadata', this.videoPlayer.onLoadMetadataHtml5, false);
+ }
+ if (player && _.isFunction(player.destroy)) {
+ player.destroy();
+ }
+ if (this.canPlayHLS && player.hls) {
+ player.hls.destroy();
+ }
+ delete this.videoPlayer;
+}
+
+function pause() {
+ if (this.videoPlayer.player.pauseVideo) {
+ this.videoPlayer.player.pauseVideo();
+ }
+}
+
+function play() {
+ if (this.videoPlayer.player.playVideo) {
+ if (this.videoPlayer.isEnded()) {
+ // When the video will start playing again from the start, the
+ // start-time and end-time will come back into effect.
+ this.videoPlayer.goToStartTime = true;
+ }
+
+ this.videoPlayer.player.playVideo();
+ }
+}
+
+// This function gets the video's current play position in time
+// (currentTime) and its duration.
+// It is called at a regular interval when the video is playing.
+function update(time) {
+ this.videoPlayer.currentTime = time || this.videoPlayer.player.getCurrentTime();
+
+ if (isFinite(this.videoPlayer.currentTime)) {
+ this.videoPlayer.updatePlayTime(this.videoPlayer.currentTime);
+
+ // We need to pause the video if current time is smaller (or equal)
+ // than end-time. Also, we must make sure that this is only done
+ // once per video playing from start to end.
+ if (
+ this.videoPlayer.endTime !== null
+ && this.videoPlayer.endTime <= this.videoPlayer.currentTime
+ ) {
+ this.videoPlayer.pause();
+
+ this.trigger('videoProgressSlider.notifyThroughHandleEnd', {
+ end: true
+ });
+
+ this.el.trigger('stop');
+ }
+ this.el.trigger('timeupdate', [this.videoPlayer.currentTime]);
+ }
+}
+
+function setPlaybackRate(newSpeed) {
+ this.videoPlayer.player.setPlaybackRate(newSpeed);
+}
+
+function onSpeedChange(newSpeed) {
+ let time = this.videoPlayer.currentTime;
+
+ if (this.isFlashMode()) {
+ this.videoPlayer.currentTime = Time.convert(
+ time,
+ parseFloat(this.speed),
+ newSpeed
+ );
+ }
+
+ newSpeed = parseFloat(newSpeed);
+ this.setSpeed(newSpeed);
+ this.videoPlayer.setPlaybackRate(newSpeed);
+}
+
+function onAutoAdvanceChange(enabled) {
+ this.setAutoAdvance(enabled);
+}
+
+// Every 200 ms, if the video is playing, we call the function update, via
+// clearInterval. This interval is called updateInterval.
+// It is created on a onPlay event. Cleared on a onPause event.
+// Reinitialized on a onSeek event.
+function onSeek(params) {
+ let time = params.time,
+ type = params.type,
+ oldTime = this.videoPlayer.currentTime;
+ // After the user seeks, the video will start playing from
+ // the sought point, and stop playing at the end.
+ this.videoPlayer.goToStartTime = false;
+
+ this.videoPlayer.seekTo(time);
+
+ this.el.trigger('seek', [time, oldTime, type]);
+}
+
+function seekTo(time) {
+ // eslint-disable-next-line no-shadow
+ let duration = this.videoPlayer.duration();
+
+ if ((typeof time !== 'number') || (time > duration) || (time < 0)) {
+ return false;
+ }
+
+ this.el.off('play.seek');
+
+ if (this.videoPlayer.isPlaying()) {
+ this.videoPlayer.stopTimer();
+ }
+ let isUnplayed = this.videoPlayer.isUnstarted()
+ || this.videoPlayer.isCued();
+
+ // Use `cueVideoById` method for youtube video that is not played before.
+ if (isUnplayed && this.isYoutubeType()) {
+ this.videoPlayer.player.cueVideoById(this.youtubeId(), time);
+ } else {
+ // Youtube video cannot be rewinded during bufferization, so wait to
+ // finish bufferization and then rewind the video.
+ if (this.isYoutubeType() && this.videoPlayer.isBuffering()) {
+ this.el.on('play.seek', function() {
+ this.videoPlayer.player.seekTo(time, true);
+ }.bind(this));
+ } else {
+ // Otherwise, just seek the video
+ this.videoPlayer.player.seekTo(time, true);
+ }
+ }
+
+ this.videoPlayer.updatePlayTime(time, true);
+
+ // the timer is stopped above; restart it.
+ if (this.videoPlayer.isPlaying()) {
+ this.videoPlayer.runTimer();
+ }
+ // Update the the current time when user seek. (YoutubePlayer)
+ this.videoPlayer.currentTime = time;
+}
+
+function runTimer() {
+ if (!this.videoPlayer.updateInterval) {
+ this.videoPlayer.updateInterval = window.setInterval(
+ this.videoPlayer.update, 200
+ );
+
+ this.videoPlayer.update();
+ }
+}
+
+function stopTimer() {
+ window.clearInterval(this.videoPlayer.updateInterval);
+ delete this.videoPlayer.updateInterval;
+}
+
+function onEnded() {
+ let time = this.videoPlayer.duration();
+
+ this.trigger('videoProgressSlider.notifyThroughHandleEnd', {
+ end: true
+ });
+
+ if (this.videoPlayer.skipOnEndedStartEndReset) {
+ this.videoPlayer.skipOnEndedStartEndReset = undefined;
+ }
+ // Sometimes `onEnded` events fires when `currentTime` not equal
+ // `duration`. In this case, slider doesn't reach the end point of
+ // timeline.
+ this.videoPlayer.updatePlayTime(time);
+
+ // Emit 'pause_video' event when a video ends if Player is of Youtube
+ if (this.isYoutubeType()) {
+ this.el.trigger('pause', arguments);
+ }
+ this.el.trigger('ended', arguments);
+}
+
+function onPause() {
+ this.videoPlayer.stopTimer();
+ this.el.trigger('pause', arguments);
+}
+
+function onPlay() {
+ this.videoPlayer.runTimer();
+ this.trigger('videoProgressSlider.notifyThroughHandleEnd', {
+ end: false
+ });
+ this.videoPlayer.ready();
+ this.el.trigger('play', arguments);
+}
+
+function onUnstarted() { }
+
+function handlePlaybackQualityChange(value) {
+ this.videoPlayer.player.setPlaybackQuality(value);
+}
+
+function onPlaybackQualityChange() {
+ let quality;
+
+ quality = this.videoPlayer.player.getPlaybackQuality();
+
+ this.trigger('videoQualityControl.onQualityChange', quality);
+ this.el.trigger('qualitychange', arguments);
+}
+
+function onReady() {
+ let _this = this,
+ availablePlaybackRates, baseSpeedSubs,
+ player, videoWidth, videoHeight;
+
+ dfd.resolve();
+
+ this.el.on('speedchange', function(event, speed) {
+ _this.videoPlayer.onSpeedChange(speed);
+ });
+
+ this.el.on('autoadvancechange', function(event, enabled) {
+ _this.videoPlayer.onAutoAdvanceChange(enabled);
+ });
+
+ this.el.on('volumechange volumechange:silent', function(event, volume) {
+ _this.videoPlayer.onVolumeChange(volume);
+ });
+
+ availablePlaybackRates = this.videoPlayer.player
+ .getAvailablePlaybackRates();
+
+ // Because of problems with muting sound outside of range 0.25 and
+ // 5.0, we should filter our available playback rates.
+ // Issues:
+ // https://code.google.com/p/chromium/issues/detail?id=264341
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=840745
+ // https://developer.mozilla.org/en-US/docs/DOM/HTMLMediaElement
+
+ availablePlaybackRates = _.filter(
+ availablePlaybackRates,
+ function(item) {
+ let speed = Number(item);
+ return speed > 0.25 && speed <= 5;
+ }
+ );
+
+ // Because of a recent change in the YouTube API (not documented), sometimes
+ // HTML5 mode loads after Flash mode has been loaded. In this case we have
+ // multiple speeds available but the variable `this.currentPlayerMode` is
+ // set to "flash". This is impossible because in Flash mode we can have
+ // only one speed available. Therefore we must execute the following code
+ // block if we have multiple speeds or if `this.currentPlayerMode` is set to
+ // "html5". If any of the two conditions are true, we then set the variable
+ // `this.currentPlayerMode` to "html5".
+ //
+ // For more information, please see the PR that introduced this change:
+ // https://github.com/openedx/edx-platform/pull/2841
+ if (
+ (this.isHtml5Mode() || availablePlaybackRates.length > 1)
+&& this.isYoutubeType()
+ ) {
+ if (availablePlaybackRates.length === 1 && !this.isTouch) {
+ // This condition is needed in cases when Firefox version is
+ // less than 20. In those versions HTML5 playback could only
+ // happen at 1 speed (no speed changing). Therefore, in this
+ // case, we need to switch back to Flash.
+ //
+ // This might also happen in other browsers, therefore when we
+ // have 1 speed available, we fall back to Flash.
+
+ _restartUsingFlash(this);
+ return false;
+ } else if (availablePlaybackRates.length > 1) {
+ this.setPlayerMode('html5');
+
+ // We need to synchronize available frame rates with the ones
+ // that the user specified.
+
+ baseSpeedSubs = this.videos['1.0'];
+ // this.videos is a dictionary containing various frame rates
+ // and their associated subs.
+
+ // First clear the dictionary.
+ $.each(this.videos, function(index, value) {
+ delete _this.videos[index];
+ });
+ this.speeds = [];
+ // Recreate it with the supplied frame rates.
+ $.each(availablePlaybackRates, function(index, value) {
+ let key = value.toFixed(2).replace(/\.00$/, '.0');
+
+ _this.videos[key] = baseSpeedSubs;
+ _this.speeds.push(key);
+ });
+
+ this.setSpeed(this.speed);
+ this.el.trigger('speed:render', [this.speeds, this.speed]);
+ }
+ }
+
+ if (this.isFlashMode()) {
+ this.setSpeed(this.speed);
+ this.el.trigger('speed:set', [this.speed]);
+ }
+
+ if (this.isHtml5Mode()) {
+ this.videoPlayer.player.setPlaybackRate(this.speed);
+ }
+
+ // eslint-disable-next-line no-shadow
+ let duration = this.videoPlayer.duration(),
+ time = this.videoPlayer.figureOutStartingTime(duration);
+
+ // this.duration will be set initially only if duration is coming from edx-val
+ this.duration = this.duration || duration;
+
+ if (time > 0 && this.videoPlayer.goToStartTime) {
+ this.videoPlayer.seekTo(time);
+ }
+
+ this.el.trigger('ready', arguments);
+
+ if (this.config.autoplay) {
+ this.videoPlayer.play();
+ }
+}
+
+function onStateChange(event) {
+ this.el.removeClass([
+ 'is-unstarted', 'is-playing', 'is-paused', 'is-buffered',
+ 'is-ended', 'is-cued'
+ ].join(' '));
+
+ // eslint-disable-next-line default-case
+ switch (event.data) {
+ case this.videoPlayer.PlayerState.UNSTARTED:
+ this.el.addClass('is-unstarted');
+ this.videoPlayer.onUnstarted();
+ break;
+ case this.videoPlayer.PlayerState.PLAYING:
+ this.el.addClass('is-playing');
+ this.videoPlayer.onPlay();
+ break;
+ case this.videoPlayer.PlayerState.PAUSED:
+ this.el.addClass('is-paused');
+ this.videoPlayer.onPause();
+ break;
+ case this.videoPlayer.PlayerState.BUFFERING:
+ this.el.addClass('is-buffered');
+ this.el.trigger('buffering');
+ break;
+ case this.videoPlayer.PlayerState.ENDED:
+ this.el.addClass('is-ended');
+ this.videoPlayer.onEnded();
+ break;
+ case this.videoPlayer.PlayerState.CUED:
+ this.el.addClass('is-cued');
+ if (this.isFlashMode()) {
+ this.videoPlayer.play();
+ }
+ break;
+ }
+}
+
+function onError(code) {
+ this.el.trigger('error', [code]);
+}
+
+// eslint-disable-next-line no-shadow
+function figureOutStartEndTime(duration) {
+ let videoPlayer = this.videoPlayer;
+
+ videoPlayer.startTime = this.config.startTime;
+ if (videoPlayer.startTime >= duration) {
+ videoPlayer.startTime = 0;
+ } else if (this.isFlashMode()) {
+ videoPlayer.startTime /= Number(this.speed);
+ }
+
+ videoPlayer.endTime = this.config.endTime;
+ if (
+ videoPlayer.endTime <= videoPlayer.startTime
+ || videoPlayer.endTime >= duration
+ ) {
+ videoPlayer.endTime = null;
+ } else if (this.isFlashMode()) {
+ videoPlayer.endTime /= Number(this.speed);
+ }
+}
+
+// eslint-disable-next-line no-shadow
+function figureOutStartingTime(duration) {
+ let savedVideoPosition = this.config.savedVideoPosition,
+
+ // Default starting time is 0. This is the case when
+ // there is not start-time, no previously saved position,
+ // or one (or both) of those values is incorrect.
+ time = 0,
+
+ startTime, endTime;
+
+ this.videoPlayer.figureOutStartEndTime(duration);
+
+ startTime = this.videoPlayer.startTime;
+ endTime = this.videoPlayer.endTime;
+
+ if (startTime > 0) {
+ if (
+ startTime < savedVideoPosition
+&& (endTime > savedVideoPosition || endTime === null)
+
+// We do not want to jump to the end of the video.
+// We subtract 1 from the duration for a 1 second
+// safety net.
+&& savedVideoPosition < duration - 1
+ ) {
+ time = savedVideoPosition;
+ } else {
+ time = startTime;
+ }
+ } else if (
+ savedVideoPosition > 0
+&& (endTime > savedVideoPosition || endTime === null)
+
+// We do not want to jump to the end of the video.
+// We subtract 1 from the duration for a 1 second
+// safety net.
+&& savedVideoPosition < duration - 1
+ ) {
+ time = savedVideoPosition;
+ }
+
+ return time;
+}
+
+function updatePlayTime(time, skip_seek) {
+ let videoPlayer = this.videoPlayer,
+ endTime = this.videoPlayer.duration(),
+ youTubeId;
+
+ if (this.config.endTime) {
+ endTime = Math.min(this.config.endTime, endTime);
+ }
+
+ this.trigger(
+ 'videoProgressSlider.updatePlayTime',
+ {
+ time: time,
+ duration: endTime
+ }
+ );
+
+ this.trigger(
+ 'videoControl.updateVcrVidTime',
+ {
+ time: time,
+ duration: endTime
+ }
+ );
+
+ this.el.trigger('caption:update', [time]);
+}
+
+function isEnded() {
+ let playerState = this.videoPlayer.player.getPlayerState(),
+ ENDED = this.videoPlayer.PlayerState.ENDED;
+
+ return playerState === ENDED;
+}
+
+function isPlaying() {
+ let playerState = this.videoPlayer.player.getPlayerState();
+
+ return playerState === this.videoPlayer.PlayerState.PLAYING;
+}
+
+function isBuffering() {
+ let playerState = this.videoPlayer.player.getPlayerState();
+
+ return playerState === this.videoPlayer.PlayerState.BUFFERING;
+}
+
+function isCued() {
+ let playerState = this.videoPlayer.player.getPlayerState();
+
+ return playerState === this.videoPlayer.PlayerState.CUED;
+}
+
+function isUnstarted() {
+ let playerState = this.videoPlayer.player.getPlayerState();
+
+ return playerState === this.videoPlayer.PlayerState.UNSTARTED;
+}
+
+/*
+ * Return the duration of the video in seconds.
+ *
+ * First, try to use the native player API call to get the duration.
+ * If the value returned by the native function is not valid, resort to
+ * the value stored in the metadata for the video. Note that the metadata
+ * is available only for YouTube videos.
+ *
+ * IMPORTANT! It has been observed that sometimes, after initial playback
+ * of the video, when operations "pause" and "play" are performed (in that
+ * sequence), the function will start returning a slightly different value.
+ *
+ * For example: While playing for the first time, the function returns 31.
+ * After pausing the video and then resuming once more, the function will
+ * start returning 31.950656.
+ *
+ * This instability is internal to the player API (or browser internals).
+ */
+function duration() {
+ let dur;
+
+ // Sometimes the YouTube API doesn't finish instantiating all of it's
+ // methods, but the execution point arrives here.
+ //
+ // This happens when you have start-time and end-time set, and click "Edit"
+ // in Studio, and then "Save". The Video editor dialog closes, the
+ // video reloads, but the start-end range is not visible.
+ if (this.videoPlayer.player.getDuration) {
+ dur = this.videoPlayer.player.getDuration();
+ }
+
+ // For YouTube videos, before the video starts playing, the API
+ // function player.getDuration() will return 0. This means that the VCR
+ // will show total time as 0 when the page just loads (before the user
+ // clicks the Play button).
+ //
+ // We can do betterin a case when dur is 0 (or less than 0). We can ask
+ // the getDuration() function for total time, which will query the
+ // metadata for a duration.
+ //
+ // Be careful! Often the metadata duration is not very precise. It
+ // might differ by one or two seconds against the actual time as will
+ // be reported later on by the player.getDuration() API function.
+ if (!isFinite(dur) || dur <= 0) {
+ if (this.isYoutubeType()) {
+ dur = this.getDuration();
+ }
+ }
+
+ // Just in case the metadata is garbled, or something went wrong, we
+ // have a final check.
+ if (!isFinite(dur) || dur <= 0) {
+ dur = 0;
+ }
+
+ return Math.floor(dur);
+}
+
+function onVolumeChange(volume) {
+ this.videoPlayer.player.setVolume(volume);
+}
diff --git a/xmodule/assets/video/public/js/04_video_control.js b/xmodule/assets/video/public/js/04_video_control.js
new file mode 100644
index 000000000000..d928aeb6b895
--- /dev/null
+++ b/xmodule/assets/video/public/js/04_video_control.js
@@ -0,0 +1,164 @@
+'use strict';
+
+import * as Time from './utils/time.js';
+
+// VideoControl module.
+let VideoControl = function(state) {
+ let dfd = $.Deferred();
+
+ state.videoControl = {};
+
+ _makeFunctionsPublic(state);
+ _renderElements(state);
+ _bindHandlers(state);
+
+ dfd.resolve();
+ return dfd.promise();
+};
+
+// ***************************************************************
+// Private functions start here.
+// ***************************************************************
+
+// function _makeFunctionsPublic(state)
+//
+// Functions which will be accessible via 'state' object. When called, these functions will
+// get the 'state' object as a context.
+function _makeFunctionsPublic(state) {
+ let methodsDict = {
+ destroy: destroy,
+ hideControls: hideControls,
+ show: show,
+ showControls: showControls,
+ focusFirst: focusFirst,
+ updateVcrVidTime: updateVcrVidTime
+ };
+
+ state.bindTo(methodsDict, state.videoControl, state);
+}
+
+function destroy() {
+ this.el.off({
+ mousemove: this.videoControl.showControls,
+ keydown: this.videoControl.showControls,
+ destroy: this.videoControl.destroy,
+ initialize: this.videoControl.focusFirst
+ });
+
+ this.el.off('controls:show');
+ if (this.controlHideTimeout) {
+ clearTimeout(this.controlHideTimeout);
+ }
+ delete this.videoControl;
+}
+
+// function _renderElements(state)
+//
+// Create any necessary DOM elements, attach them, and set their initial configuration. Also
+// make the created DOM elements available via the 'state' object. Much easier to work this
+// way - you don't have to do repeated jQuery element selects.
+function _renderElements(state) {
+ state.videoControl.el = state.el.find('.video-controls');
+ state.videoControl.vidTimeEl = state.videoControl.el.find('.vidtime');
+
+ if ((state.videoType === 'html5') && (state.config.autohideHtml5)) {
+ state.videoControl.fadeOutTimeout = state.config.fadeOutTimeout;
+
+ state.videoControl.el.addClass('html5');
+ state.controlHideTimeout = setTimeout(state.videoControl.hideControls, state.videoControl.fadeOutTimeout);
+ }
+}
+
+// function _bindHandlers(state)
+//
+// Bind any necessary function callbacks to DOM events (click, mousemove, etc.).
+function _bindHandlers(state) {
+ if ((state.videoType === 'html5') && (state.config.autohideHtml5)) {
+ state.el.on({
+ mousemove: state.videoControl.showControls,
+ keydown: state.videoControl.showControls
+ });
+ }
+
+ if (state.config.focusFirstControl) {
+ state.el.on('initialize', state.videoControl.focusFirst);
+ }
+ state.el.on('destroy', state.videoControl.destroy);
+}
+
+// ***************************************************************
+// Public functions start here.
+// These are available via the 'state' object. Their context ('this' keyword) is the 'state' object.
+// The magic private function that makes them available and sets up their context is makeFunctionsPublic().
+// ***************************************************************
+
+function focusFirst() {
+ this.videoControl.el.find('.vcr a, .vcr button').first().focus();
+}
+
+function show() {
+ this.videoControl.el.removeClass('is-hidden');
+ this.el.trigger('controls:show', arguments);
+}
+
+function showControls(event) {
+ if (!this.controlShowLock) {
+ if (!this.captionsHidden) {
+ return;
+ }
+
+ this.controlShowLock = true;
+
+ if (this.controlState === 'invisible') {
+ this.videoControl.el.show();
+ this.controlState = 'visible';
+ } else if (this.controlState === 'hiding') {
+ this.videoControl.el.stop(true, false).css('opacity', 1).show();
+ this.controlState = 'visible';
+ } else if (this.controlState === 'visible') {
+ clearTimeout(this.controlHideTimeout);
+ }
+
+ this.controlHideTimeout = setTimeout(this.videoControl.hideControls, this.videoControl.fadeOutTimeout);
+ this.controlShowLock = false;
+ }
+}
+
+function hideControls() {
+ let _this = this;
+
+ this.controlHideTimeout = null;
+
+ if (!this.captionsHidden) {
+ return;
+ }
+
+ this.controlState = 'hiding';
+ this.videoControl.el.fadeOut(this.videoControl.fadeOutTimeout, function() {
+ _this.controlState = 'invisible';
+ // If the focus was on the video control or the volume control,
+ // then we must make sure to close these dialogs. Otherwise, after
+ // next autofocus, these dialogs will be open, but the focus will
+ // not be on them.
+ _this.videoVolumeControl.el.removeClass('open');
+ _this.videoSpeedControl.el.removeClass('open');
+
+ _this.focusGrabber.enableFocusGrabber();
+ });
+}
+
+function updateVcrVidTime(params) {
+ let endTime = (this.config.endTime !== null) ? this.config.endTime : params.duration,
+ startTime, currentTime;
+ // in case endTime is accidentally specified as being greater than the video
+ endTime = Math.min(endTime, params.duration);
+ startTime = this.config.startTime > 0 ? this.config.startTime : 0;
+ // if it's a subsection of video, use the clip duration as endTime
+ if (startTime && this.config.endTime) {
+ endTime = this.config.endTime - startTime;
+ }
+ currentTime = startTime ? params.time - startTime : params.time;
+ this.videoControl.vidTimeEl.text(Time.format(currentTime) + ' / ' + Time.format(endTime));
+}
+
+export default VideoControl;
diff --git a/xmodule/assets/video/public/js/04_video_full_screen.js b/xmodule/assets/video/public/js/04_video_full_screen.js
new file mode 100644
index 000000000000..4dd3f85eb18d
--- /dev/null
+++ b/xmodule/assets/video/public/js/04_video_full_screen.js
@@ -0,0 +1,309 @@
+import HtmlUtils from 'edx-ui-toolkit/js/utils/html-utils';
+
+let template = [
+ '',
+ ' ',
+ ' '
+].join('');
+
+// The following properties and functions enable cross-browser use of the
+// the Fullscreen Web API.
+//
+// function getVendorPrefixed(property)
+// function getFullscreenElement()
+// function exitFullscreen()
+// function requestFullscreen(element, options)
+//
+// For more information about the Fullscreen Web API see MDN:
+// https://developer.mozilla.org/en-US/docs/Web/API/Fullscreen_API
+let prefixedFullscreenProperties = (function() {
+ if ('fullscreenEnabled' in document) {
+ return {
+ fullscreenElement: 'fullscreenElement',
+ fullscreenEnabled: 'fullscreenEnabled',
+ requestFullscreen: 'requestFullscreen',
+ exitFullscreen: 'exitFullscreen',
+ fullscreenchange: 'fullscreenchange',
+ fullscreenerror: 'fullscreenerror'
+ };
+ }
+ if ('webkitFullscreenEnabled' in document) {
+ return {
+ fullscreenElement: 'webkitFullscreenElement',
+ fullscreenEnabled: 'webkitFullscreenEnabled',
+ requestFullscreen: 'webkitRequestFullscreen',
+ exitFullscreen: 'webkitExitFullscreen',
+ fullscreenchange: 'webkitfullscreenchange',
+ fullscreenerror: 'webkitfullscreenerror'
+ };
+ }
+ if ('mozFullScreenEnabled' in document) {
+ return {
+ fullscreenElement: 'mozFullScreenElement',
+ fullscreenEnabled: 'mozFullScreenEnabled',
+ requestFullscreen: 'mozRequestFullScreen',
+ exitFullscreen: 'mozCancelFullScreen',
+ fullscreenchange: 'mozfullscreenchange',
+ fullscreenerror: 'mozfullscreenerror'
+ };
+ }
+ if ('msFullscreenEnabled' in document) {
+ return {
+ fullscreenElement: 'msFullscreenElement',
+ fullscreenEnabled: 'msFullscreenEnabled',
+ requestFullscreen: 'msRequestFullscreen',
+ exitFullscreen: 'msExitFullscreen',
+ fullscreenchange: 'MSFullscreenChange',
+ fullscreenerror: 'MSFullscreenError'
+ };
+ }
+ return {};
+}());
+
+function getVendorPrefixed(property) {
+ return prefixedFullscreenProperties[property];
+}
+
+function getFullscreenElement() {
+ return document[getVendorPrefixed('fullscreenElement')];
+}
+
+function exitFullscreen() {
+ if (document[getVendorPrefixed('exitFullscreen')]) {
+ return document[getVendorPrefixed('exitFullscreen')]();
+ }
+ return null;
+}
+
+function requestFullscreen(element, options) {
+ if (element[getVendorPrefixed('requestFullscreen')]) {
+ return element[getVendorPrefixed('requestFullscreen')](options);
+ }
+ return null;
+}
+
+// ***************************************************************
+// Private functions start here.
+// ***************************************************************
+
+function destroy() {
+ $(document).off('keyup', this.videoFullScreen.exitHandler);
+ this.videoFullScreen.fullScreenEl.remove();
+ this.el.off({
+ destroy: this.videoFullScreen.destroy
+ });
+ document.removeEventListener(
+ getVendorPrefixed('fullscreenchange'),
+ this.videoFullScreen.handleFullscreenChange
+ );
+ if (this.isFullScreen) {
+ this.videoFullScreen.exit();
+ }
+ delete this.videoFullScreen;
+}
+
+// function renderElements(state)
+//
+// Create any necessary DOM elements, attach them, and set their initial configuration. Also
+// make the created DOM elements available via the 'state' object. Much easier to work this
+// way - you don't have to do repeated jQuery element selects.
+function renderElements(state) {
+ /* eslint-disable no-param-reassign */
+ state.videoFullScreen.fullScreenEl = $(template);
+ state.videoFullScreen.sliderEl = state.el.find('.slider');
+ state.videoFullScreen.fullScreenState = false;
+ HtmlUtils.append(state.el.find('.secondary-controls'), HtmlUtils.HTML(state.videoFullScreen.fullScreenEl));
+ state.videoFullScreen.updateControlsHeight();
+ /* eslint-enable no-param-reassign */
+}
+
+// function bindHandlers(state)
+//
+// Bind any necessary function callbacks to DOM events (click, mousemove, etc.).
+function bindHandlers(state) {
+ state.videoFullScreen.fullScreenEl.on('click', state.videoFullScreen.toggleHandler);
+ state.el.on({
+ destroy: state.videoFullScreen.destroy
+ });
+ $(document).on('keyup', state.videoFullScreen.exitHandler);
+ document.addEventListener(
+ getVendorPrefixed('fullscreenchange'),
+ state.videoFullScreen.handleFullscreenChange
+ );
+}
+
+function getControlsHeight(controls, slider) {
+ return controls.height() + 0.5 * slider.height();
+}
+
+// ***************************************************************
+// Public functions start here.
+// These are available via the 'state' object. Their context ('this' keyword) is the 'state' object.
+// The magic private function that makes them available and sets up their context is makeFunctionsPublic().
+// ***************************************************************
+
+function handleFullscreenChange() {
+ if (getFullscreenElement() !== this.el[0] && this.isFullScreen) {
+ // The video was fullscreen so this event must relate to this video
+ this.videoFullScreen.handleExit();
+ }
+}
+
+function updateControlsHeight() {
+ let controls = this.el.find('.video-controls');
+ let slider = this.videoFullScreen.sliderEl;
+ this.videoFullScreen.height = getControlsHeight(controls, slider);
+ return this.videoFullScreen.height;
+}
+
+function notifyParent(fullscreenOpen) {
+ if (window !== window.parent) {
+ // This is used by the Learning MFE to know about changing fullscreen mode.
+ // The MFE is then able to respond appropriately and scroll window to the previous position.
+ window.parent.postMessage({
+ type: 'plugin.videoFullScreen',
+ payload: {
+ open: fullscreenOpen
+ }
+ }, document.referrer
+ );
+ }
+}
+
+/**
+ * Event handler to toggle fullscreen mode.
+ * @param {jquery Event} event
+ */
+function toggleHandler(event) {
+ event.preventDefault();
+ this.videoCommands.execute('toggleFullScreen');
+}
+
+function handleExit() {
+ let fullScreenClassNameEl = this.el.add(document.documentElement);
+ let closedCaptionsEl = this.el.find('.closed-captions');
+
+ if (this.isFullScreen === false) {
+ return;
+ }
+
+ // eslint-disable-next-line no-multi-assign
+ this.videoFullScreen.fullScreenState = this.isFullScreen = false;
+ fullScreenClassNameEl.removeClass('video-fullscreen');
+ $(window).scrollTop(this.scrollPos);
+ this.videoFullScreen.fullScreenEl
+ .attr({title: gettext('Fill browser'), 'aria-label': gettext('Fill browser')})
+ .find('.icon')
+ .removeClass('fa-compress')
+ .addClass('fa-arrows-alt');
+
+ $(closedCaptionsEl).css({top: '70%', left: '5%'});
+ if (this.resizer) {
+ this.resizer.delta.reset().setMode('width');
+ }
+ this.el.trigger('fullscreen', [this.isFullScreen]);
+
+ this.videoFullScreen.notifyParent(false);
+}
+
+function handleEnter() {
+ let fullScreenClassNameEl = this.el.add(document.documentElement);
+ let closedCaptionsEl = this.el.find('.closed-captions');
+
+ if (this.isFullScreen === true) {
+ return;
+ }
+
+ this.videoFullScreen.notifyParent(true);
+
+ // eslint-disable-next-line no-multi-assign
+ this.videoFullScreen.fullScreenState = this.isFullScreen = true;
+ fullScreenClassNameEl.addClass('video-fullscreen');
+ this.videoFullScreen.fullScreenEl
+ .attr({title: gettext('Exit full browser'), 'aria-label': gettext('Exit full browser')})
+ .find('.icon')
+ .removeClass('fa-arrows-alt')
+ .addClass('fa-compress');
+
+ $(closedCaptionsEl).css({top: '70%', left: '5%'});
+ if (this.resizer) {
+ this.resizer.delta.substract(this.videoFullScreen.updateControlsHeight(), 'height').setMode('both');
+ }
+ this.el.trigger('fullscreen', [this.isFullScreen]);
+}
+
+function exit() {
+ if (getFullscreenElement() === this.el[0]) {
+ exitFullscreen();
+ } else {
+ // Else some other element is fullscreen or the fullscreen api does not exist.
+ this.videoFullScreen.handleExit();
+ }
+}
+
+function enter() {
+ this.scrollPos = $(window).scrollTop();
+ this.videoFullScreen.handleEnter();
+ requestFullscreen(this.el[0]);
+}
+
+/** Toggle fullscreen mode. */
+function toggle() {
+ if (this.videoFullScreen.fullScreenState) {
+ this.videoFullScreen.exit();
+ } else {
+ this.videoFullScreen.enter();
+ }
+}
+
+/**
+ * Event handler to exit from fullscreen mode.
+ * @param {jquery Event} event
+ */
+function exitHandler(event) {
+ if ((this.isFullScreen) && (event.keyCode === 27)) {
+ event.preventDefault();
+ this.videoCommands.execute('toggleFullScreen');
+ }
+}
+
+// function makeFunctionsPublic(state)
+//
+// Functions which will be accessible via 'state' object. When called, these functions will
+// get the 'state' object as a context.
+function makeFunctionsPublic(state) {
+ let methodsDict = {
+ destroy: destroy,
+ enter: enter,
+ exit: exit,
+ exitHandler: exitHandler,
+ handleExit: handleExit,
+ handleEnter: handleEnter,
+ handleFullscreenChange: handleFullscreenChange,
+ toggle: toggle,
+ toggleHandler: toggleHandler,
+ updateControlsHeight: updateControlsHeight,
+ notifyParent: notifyParent
+ };
+
+ state.bindTo(methodsDict, state.videoFullScreen, state);
+}
+
+// VideoControl() function - what this module "exports".
+export default function(state) {
+ let dfd = $.Deferred();
+
+ // eslint-disable-next-line no-param-reassign
+ state.videoFullScreen = {};
+
+ makeFunctionsPublic(state);
+ renderElements(state);
+ bindHandlers(state);
+
+ dfd.resolve();
+ return dfd.promise();
+}
diff --git a/xmodule/assets/video/public/js/05_video_quality_control.js b/xmodule/assets/video/public/js/05_video_quality_control.js
new file mode 100644
index 000000000000..1d2c6b1602a1
--- /dev/null
+++ b/xmodule/assets/video/public/js/05_video_quality_control.js
@@ -0,0 +1,176 @@
+'use strict';
+
+import HtmlUtils from 'edx-ui-toolkit/js/utils/html-utils';
+import _ from 'underscore';
+
+
+let template = HtmlUtils.interpolateHtml(
+ HtmlUtils.HTML([
+ '',
+ 'HD ',
+ '',
+ '{highDefinition}',
+ ' ',
+ '',
+ '{off}',
+ ' ',
+ ' '
+ ].join('')),
+ {
+ highDefinition: gettext('High Definition'),
+ off: gettext('off')
+ }
+);
+
+// VideoQualityControl() function - what this module "exports".
+let VideoQualityControl = function(state) {
+ let dfd = $.Deferred();
+
+ // Changing quality for now only works for YouTube videos.
+ if (state.videoType !== 'youtube') {
+ return;
+ }
+
+ state.videoQualityControl = {};
+
+ _makeFunctionsPublic(state);
+ _renderElements(state);
+ _bindHandlers(state);
+
+ dfd.resolve();
+ return dfd.promise();
+};
+
+// ***************************************************************
+// Private functions start here.
+// ***************************************************************
+
+// function _makeFunctionsPublic(state)
+//
+// Functions which will be accessible via 'state' object. When called, these functions will
+// get the 'state' object as a context.
+function _makeFunctionsPublic(state) {
+ let methodsDict = {
+ destroy: destroy,
+ fetchAvailableQualities: fetchAvailableQualities,
+ onQualityChange: onQualityChange,
+ showQualityControl: showQualityControl,
+ toggleQuality: toggleQuality
+ };
+
+ state.bindTo(methodsDict, state.videoQualityControl, state);
+}
+
+function destroy() {
+ this.videoQualityControl.el.off({
+ click: this.videoQualityControl.toggleQuality,
+ destroy: this.videoQualityControl.destroy
+ });
+ this.el.off('.quality');
+ this.videoQualityControl.el.remove();
+ delete this.videoQualityControl;
+}
+
+// function _renderElements(state)
+//
+// Create any necessary DOM elements, attach them, and set their initial configuration. Also
+// make the created DOM elements available via the 'state' object. Much easier to work this
+// way - you don't have to do repeated jQuery element selects.
+function _renderElements(state) {
+ // eslint-disable-next-line no-multi-assign
+ let element = state.videoQualityControl.el = $(template.toString());
+ state.videoQualityControl.quality = 'large';
+ HtmlUtils.append(state.el.find('.secondary-controls'), HtmlUtils.HTML(element));
+}
+
+// function _bindHandlers(state)
+//
+// Bind any necessary function callbacks to DOM events (click, mousemove, etc.).
+function _bindHandlers(state) {
+ state.videoQualityControl.el.on('click',
+ state.videoQualityControl.toggleQuality
+ );
+ state.el.on('play.quality', _.once(
+ state.videoQualityControl.fetchAvailableQualities
+ ));
+
+ state.el.on('destroy.quality', state.videoQualityControl.destroy);
+}
+
+// ***************************************************************
+// Public functions start here.
+// These are available via the 'state' object. Their context ('this' keyword) is the 'state' object.
+// The magic private function that makes them available and sets up their context is makeFunctionsPublic().
+// ***************************************************************
+
+/*
+ * @desc Shows quality control. This function will only be called if HD
+ * qualities are available.
+ *
+ * @public
+ */
+function showQualityControl() {
+ this.videoQualityControl.el.removeClass('is-hidden');
+}
+
+// This function can only be called once as _.once has been used.
+/*
+ * @desc Get the available qualities from YouTube API. Possible values are:
+ ['highres', 'hd1080', 'hd720', 'large', 'medium', 'small'].
+ HD are: ['highres', 'hd1080', 'hd720'].
+ *
+ * @public
+ */
+function fetchAvailableQualities() {
+ let qualities = this.videoPlayer.player.getAvailableQualityLevels();
+
+ this.config.availableHDQualities = _.intersection(
+ qualities, ['highres', 'hd1080', 'hd720']
+ );
+
+ // HD qualities are available, show video quality control.
+ if (this.config.availableHDQualities.length > 0) {
+ this.trigger('videoQualityControl.showQualityControl');
+ this.trigger('videoQualityControl.onQualityChange', this.videoQualityControl.quality);
+ }
+ // On initialization, force the video quality to be 'large' instead of
+ // 'default'. Otherwise, the player will sometimes switch to HD
+ // automatically, for example when the iframe resizes itself.
+ this.trigger('videoPlayer.handlePlaybackQualityChange',
+ this.videoQualityControl.quality
+ );
+}
+
+function onQualityChange(value) {
+ let controlStateStr;
+ this.videoQualityControl.quality = value;
+ if (_.contains(this.config.availableHDQualities, value)) {
+ controlStateStr = gettext('on');
+ this.videoQualityControl.el
+ .addClass('active')
+ .find('.control-text')
+ .text(controlStateStr);
+ } else {
+ controlStateStr = gettext('off');
+ this.videoQualityControl.el
+ .removeClass('active')
+ .find('.control-text')
+ .text(controlStateStr);
+ }
+}
+
+// This function toggles the quality of video only if HD qualities are
+// available.
+function toggleQuality(event) {
+ let value = this.videoQualityControl.quality;
+ let isHD = _.contains(this.config.availableHDQualities, value);
+ let newQuality = isHD ? 'large' : 'highres';
+
+ event.preventDefault();
+
+ this.trigger('videoPlayer.handlePlaybackQualityChange', newQuality);
+}
+
+export default VideoQualityControl;
diff --git a/xmodule/assets/video/public/js/06_video_progress_slider.js b/xmodule/assets/video/public/js/06_video_progress_slider.js
new file mode 100644
index 000000000000..93375084aab4
--- /dev/null
+++ b/xmodule/assets/video/public/js/06_video_progress_slider.js
@@ -0,0 +1,360 @@
+/*
+"This is as true in everyday life as it is in battle: we are given one life
+and the decision is ours whether to wait for circumstances to make up our
+mind, or whether to act, and in acting, to live."
+— Omar N. Bradley
+ */
+
+// VideoProgressSlider module.
+let template = [
+ '
'
+].join('');
+
+// VideoProgressSlider() function - what this module "exports".
+export default function(state) {
+ let dfd = $.Deferred();
+
+ state.videoProgressSlider = {};
+ _makeFunctionsPublic(state);
+ _renderElements(state);
+
+ dfd.resolve();
+ return dfd.promise();
+}
+
+// ***************************************************************
+// Private functions start here.
+// ***************************************************************
+
+// function _makeFunctionsPublic(state)
+//
+// Functions which will be accessible via 'state' object. When called,
+// these functions will get the 'state' object as a context.
+
+/* eslint-disable no-use-before-define */
+function _makeFunctionsPublic(state) {
+ let methodsDict = {
+ destroy: destroy,
+ buildSlider: buildSlider,
+ getRangeParams: getRangeParams,
+ onSlide: onSlide,
+ onStop: onStop,
+ updatePlayTime: updatePlayTime,
+ updateStartEndTimeRegion: updateStartEndTimeRegion,
+ notifyThroughHandleEnd: notifyThroughHandleEnd,
+ getTimeDescription: getTimeDescription,
+ focusSlider: focusSlider
+ };
+
+ state.bindTo(methodsDict, state.videoProgressSlider, state);
+}
+
+function destroy() {
+ this.videoProgressSlider.el.removeAttr('tabindex').slider('destroy');
+ this.el.off('destroy', this.videoProgressSlider.destroy);
+ delete this.videoProgressSlider;
+}
+
+function bindHandlers(state) {
+ state.videoProgressSlider.el.on('keypress', sliderToggle.bind(state));
+ state.el.on('destroy', state.videoProgressSlider.destroy);
+}
+/* eslint-enable no-use-before-define */
+
+// function _renderElements(state)
+//
+// Create any necessary DOM elements, attach them, and set their
+// initial configuration. Also make the created DOM elements available
+// via the 'state' object. Much easier to work this way - you don't
+// have to do repeated jQuery element selects.
+function _renderElements(state) {
+ state.videoProgressSlider.el = $(template);
+
+ state.el.find('.video-controls').prepend(state.videoProgressSlider.el);
+ state.videoProgressSlider.buildSlider();
+ _buildHandle(state);
+ bindHandlers(state);
+}
+
+function _buildHandle(state) {
+ state.videoProgressSlider.handle = state.videoProgressSlider.el
+ .find('.ui-slider-handle');
+
+ // ARIA
+ // We just want the knob to be selectable with keyboard
+ state.videoProgressSlider.el.attr({
+ tabindex: -1
+ });
+
+ // Let screen readers know that this div, representing the slider
+ // handle, behaves as a slider named 'video position'.
+ state.videoProgressSlider.handle.attr({
+ role: 'slider',
+ 'aria-disabled': false,
+ 'aria-valuetext': getTimeDescription(state.videoProgressSlider
+ .slider.slider('option', 'value')),
+ 'aria-valuemax': state.videoPlayer.duration(),
+ 'aria-valuemin': '0',
+ 'aria-valuenow': state.videoPlayer.currentTime,
+ tabindex: '0',
+ 'aria-label': gettext('Video position. Press space to toggle playback')
+ });
+}
+
+// ***************************************************************
+// Public functions start here.
+// These are available via the 'state' object. Their context ('this'
+// keyword) is the 'state' object. The magic private function that makes
+// them available and sets up their context is makeFunctionsPublic().
+// ***************************************************************
+
+function buildSlider() {
+ let sliderContents = edx.HtmlUtils.joinHtml(
+ edx.HtmlUtils.HTML('
')
+ );
+
+ // xss-lint: disable=javascript-jquery-append
+ this.videoProgressSlider.el.append(sliderContents.text);
+
+ this.videoProgressSlider.slider = this.videoProgressSlider.el
+ .slider({
+ range: 'min',
+ min: this.config.startTime,
+ max: this.config.endTime,
+ slide: this.videoProgressSlider.onSlide,
+ stop: this.videoProgressSlider.onStop,
+ step: 5
+ });
+
+ this.videoProgressSlider.sliderProgress = this.videoProgressSlider
+ .slider
+ .find('.ui-slider-range.ui-widget-header.ui-slider-range-min');
+}
+
+// Rebuild the slider start-end range (if it doesn't take up the
+// whole slider). Remember that endTime === null means the end-time
+// is set to the end of video by default.
+function updateStartEndTimeRegion(params) {
+ let start, end, duration, rangeParams;
+
+ // We must have a duration in order to determine the area of range.
+ // It also must be non-zero.
+ if (!params.duration) {
+ return;
+ } else {
+ duration = params.duration;
+ }
+
+ start = this.config.startTime;
+ end = this.config.endTime;
+
+ if (start > duration) {
+ start = 0;
+ } else if (this.isFlashMode()) {
+ start /= Number(this.speed);
+ }
+
+ // If end is set to null, or it is greater than the duration of the
+ // video, then we set it to the end of the video.
+ if (end === null || end > duration) {
+ end = duration;
+ } else if (this.isFlashMode()) {
+ end /= Number(this.speed);
+ }
+
+ // Don't build a range if it takes up the whole slider.
+ if (start === 0 && end === duration) {
+ return;
+ }
+
+ // Because JavaScript has weird rounding rules when a series of
+ // mathematical operations are performed in a single statement, we will
+ // split everything up into smaller statements.
+ //
+ // This will ensure that visually, the start-end range aligns nicely
+ // with actual starting and ending point of the video.
+
+ rangeParams = getRangeParams(start, end, duration);
+}
+
+function getRangeParams(startTime, endTime, duration) {
+ let step = 100 / duration;
+ let left = startTime * step;
+ let width = endTime * step - left;
+
+ return {
+ left: left + '%',
+ width: width + '%'
+ };
+}
+
+function onSlide(event, ui) {
+ let time = ui.value;
+ let endTime = this.videoPlayer.duration();
+
+ if (this.config.endTime) {
+ endTime = Math.min(this.config.endTime, endTime);
+ }
+
+ this.videoProgressSlider.frozen = true;
+
+ // Remember the seek to value so that we don't repeat ourselves on the
+ // 'stop' slider event.
+ this.videoProgressSlider.lastSeekValue = time;
+
+ this.trigger(
+ 'videoControl.updateVcrVidTime',
+ {
+ time: time,
+ duration: endTime
+ }
+ );
+
+ this.trigger(
+ 'videoPlayer.onSlideSeek',
+ {type: 'onSlideSeek', time: time}
+ );
+
+ // ARIA
+ this.videoProgressSlider.handle.attr(
+ 'aria-valuetext', getTimeDescription(this.videoPlayer.currentTime)
+ );
+}
+
+function onStop(event, ui) {
+ let _this = this;
+
+ this.videoProgressSlider.frozen = true;
+
+ // Only perform a seek if we haven't made a seek for the new slider value.
+ // This is necessary so that if the user only clicks on the slider, without
+ // dragging it, then only one seek is made, even when a 'slide' and a 'stop'
+ // events are triggered on the slider.
+ if (this.videoProgressSlider.lastSeekValue !== ui.value) {
+ this.trigger(
+ 'videoPlayer.onSlideSeek',
+ {type: 'onSlideSeek', time: ui.value}
+ );
+ }
+
+ // ARIA
+ this.videoProgressSlider.handle.attr(
+ 'aria-valuetext', getTimeDescription(this.videoPlayer.currentTime)
+ );
+
+ setTimeout(function() {
+ _this.videoProgressSlider.frozen = false;
+ }, 200);
+}
+
+function updatePlayTime(params) {
+ let time = Math.floor(params.time);
+ // params.duration could accidentally be construed as a floating
+ // point double. Since we're displaying this number, round down
+ // to nearest second
+ let endTime = Math.floor(params.duration);
+
+ if (this.config.endTime !== null) {
+ endTime = Math.min(this.config.endTime, endTime);
+ }
+
+ if (
+ this.videoProgressSlider.slider
+ && !this.videoProgressSlider.frozen
+ ) {
+ this.videoProgressSlider.slider
+ .slider('option', 'max', endTime)
+ .slider('option', 'value', time);
+ }
+
+ // Update aria values.
+ this.videoProgressSlider.handle.attr({
+ 'aria-valuemax': endTime,
+ 'aria-valuenow': time
+ });
+}
+
+// When the video stops playing (either because the end was reached, or
+// because endTime was reached), the screen reader must be notified that
+// the video is no longer playing. We do this by a little trick. Setting
+// the title attribute of the slider know to "video ended", and focusing
+// on it. The screen reader will read the attr text.
+//
+// The user can then tab their way forward, landing on the next control
+// element, the Play button.
+//
+// @param params - object with property `end`. If set to true, the
+// function must set the title attribute to
+// `video ended`;
+// if set to false, the function must reset the attr to
+// it's original state.
+//
+// This function will be triggered from VideoPlayer methods onEnded(),
+// onPlay(), and update() (update method handles endTime).
+function notifyThroughHandleEnd(params) {
+ if (params.end) {
+ this.videoProgressSlider.handle
+ .attr('title', gettext('Video ended'))
+ .focus();
+ } else {
+ this.videoProgressSlider.handle
+ .attr('title', gettext('Video position'));
+ }
+}
+
+// Returns a string describing the current time of video in
+// `%d hours %d minutes %d seconds` format.
+function getTimeDescription(time) {
+ let seconds = Math.floor(time);
+ let minutes = Math.floor(seconds / 60);
+ let hours = Math.floor(minutes / 60);
+ let i18n = function(value, word) {
+ let msg;
+
+ // eslint-disable-next-line default-case
+ switch (word) {
+ case 'hour':
+ msg = ngettext('%(value)s hour', '%(value)s hours', value);
+ break;
+ case 'minute':
+ msg = ngettext('%(value)s minute', '%(value)s minutes', value);
+ break;
+ case 'second':
+ msg = ngettext('%(value)s second', '%(value)s seconds', value);
+ break;
+ }
+ return interpolate(msg, {value: value}, true);
+ };
+
+ seconds %= 60;
+ minutes %= 60;
+
+ if (hours) {
+ return i18n(hours, 'hour') + ' '
+ + i18n(minutes, 'minute') + ' '
+ + i18n(seconds, 'second');
+ } else if (minutes) {
+ return i18n(minutes, 'minute') + ' '
+ + i18n(seconds, 'second');
+ }
+
+ return i18n(seconds, 'second');
+}
+
+// Shift focus to the progress slider container element.
+function focusSlider() {
+ this.videoProgressSlider.handle.attr(
+ 'aria-valuetext', getTimeDescription(this.videoPlayer.currentTime)
+ );
+ this.videoProgressSlider.el.trigger('focus');
+}
+
+// Toggle video playback when the spacebar is pushed.
+function sliderToggle(e) {
+ if (e.which === 32) {
+ e.preventDefault();
+ this.videoCommands.execute('togglePlayback');
+ }
+}
diff --git a/xmodule/assets/video/public/js/07_video_volume_control.js b/xmodule/assets/video/public/js/07_video_volume_control.js
new file mode 100644
index 000000000000..68924d15672d
--- /dev/null
+++ b/xmodule/assets/video/public/js/07_video_volume_control.js
@@ -0,0 +1,554 @@
+'use strict';
+
+import HtmlUtils from 'edx-ui-toolkit/js/utils/html-utils';
+
+/**
+ * Video volume control module.
+ * @exports video/07_video_volume_control.js
+ * @constructor
+ * @param {Object} state The object containing the state of the video
+ * @param {Object} i18n The object containing strings with translations.
+ * @return {jquery Promise}
+ */
+let VideoVolumeControl = function (state, i18n) {
+ if (!(this instanceof VideoVolumeControl)) {
+ return new VideoVolumeControl(state, i18n);
+ }
+
+ _.bindAll(this, 'keyDownHandler', 'updateVolumeSilently',
+ 'onVolumeChangeHandler', 'openMenu', 'closeMenu',
+ 'toggleMuteHandler', 'keyDownButtonHandler', 'destroy'
+ );
+ this.state = state;
+ this.state.videoVolumeControl = this;
+ this.i18n = i18n;
+ this.initialize();
+
+ return $.Deferred().resolve().promise();
+};
+
+
+VideoVolumeControl.prototype = {
+ /** Minimum value for the volume slider. */
+ min: 0,
+ /** Maximum value for the volume slider. */
+ max: 100,
+ /** Step to increase/decrease volume level via keyboard. */
+ step: 20,
+
+ videoVolumeControlHtml: HtmlUtils.interpolateHtml(
+ HtmlUtils.HTML([
+ '',
+ '
',
+ '{volumeInstructions}',
+ '
',
+ '
',
+ ' ',
+ ' ',
+ '
',
+ '
'].join('')),
+ {
+ volumeInstructions: gettext('Click on this button to mute or unmute this video or press UP or DOWN buttons to increase or decrease volume level.'), // eslint-disable-line max-len
+ adjustVideoVolume: gettext('Adjust video volume'),
+ volumeText: gettext('Volume')
+ }
+ ),
+
+ destroy: function () {
+ this.volumeSlider.slider('destroy');
+ this.state.el.find('iframe').removeAttr('tabindex');
+ this.a11y.destroy();
+ // eslint-disable-next-line no-multi-assign
+ this.cookie = this.a11y = null;
+ this.closeMenu();
+
+ this.state.el
+ .off('play.volume')
+ .off({
+ keydown: this.keyDownHandler,
+ volumechange: this.onVolumeChangeHandler
+ });
+ this.el.off({
+ mouseenter: this.openMenu,
+ mouseleave: this.closeMenu
+ });
+ this.button.off({
+ mousedown: this.toggleMuteHandler,
+ keydown: this.keyDownButtonHandler,
+ focus: this.openMenu,
+ blur: this.closeMenu
+ });
+ this.el.remove();
+ delete this.state.videoVolumeControl;
+ },
+
+ /** Initializes the module. */
+ initialize: function () {
+ let volume;
+
+ if (this.state.isTouch) {
+ // iOS doesn't support volume change
+ return false;
+ }
+
+ this.el = $(this.videoVolumeControlHtml.toString());
+ // Youtube iframe react on key buttons and has his own handlers.
+ // So, we disallow focusing on iframe.
+ this.state.el.find('iframe').attr('tabindex', -1);
+ this.button = this.el.children('.control');
+ this.cookie = new CookieManager(this.min, this.max);
+ this.a11y = new Accessibility(
+ this.button, this.min, this.max, this.i18n
+ );
+ volume = this.cookie.getVolume();
+ this.storedVolume = this.max;
+
+ this.render();
+ this.bindHandlers();
+ this.setVolume(volume, true, false);
+ this.checkMuteButtonStatus(volume);
+ },
+
+ /**
+ * Creates any necessary DOM elements, attach them, and set their,
+ * initial configuration.
+ */
+ render: function () {
+ let container = this.el.find('.volume-slider'),
+ instructionsId = 'volume-instructions-' + this.state.id;
+
+ HtmlUtils.append(container, HtmlUtils.HTML('
'));
+
+ this.volumeSlider = container.slider({
+ orientation: 'vertical',
+ range: 'min',
+ min: this.min,
+ max: this.max,
+ slide: this.onSlideHandler.bind(this)
+ });
+
+ // We provide an independent behavior to adjust volume level.
+ // Therefore, we do not need redundant focusing on slider in TAB
+ // order.
+ container.find('.volume-handle').attr('tabindex', -1);
+ this.state.el.find('.secondary-controls').append(this.el);
+
+ // set dynamic id for instruction element to avoid collisions
+ this.el.find('.instructions').attr('id', instructionsId);
+ this.button.attr('aria-describedby', instructionsId);
+ },
+
+ /** Bind any necessary function callbacks to DOM events. */
+ bindHandlers: function () {
+ this.state.el.on({
+ 'play.volume': _.once(this.updateVolumeSilently),
+ volumechange: this.onVolumeChangeHandler
+ });
+ this.state.el.find('.volume').on({
+ mouseenter: this.openMenu,
+ mouseleave: this.closeMenu
+ });
+ this.button.on({
+ keydown: this.keyDownHandler,
+ click: false,
+ mousedown: this.toggleMuteHandler,
+ focus: this.openMenu,
+ blur: this.closeMenu
+ });
+ this.state.el.on('destroy', this.destroy);
+ },
+
+ /**
+ * Updates volume level without updating view and triggering
+ * `volumechange` event.
+ */
+ updateVolumeSilently: function () {
+ this.state.el.trigger(
+ 'volumechange:silent', [this.getVolume()]
+ );
+ },
+
+ /**
+ * Returns current volume level.
+ * @return {Number}
+ */
+ getVolume: function () {
+ return this.volume;
+ },
+
+ /**
+ * Sets current volume level.
+ * @param {Number} volume Suggested volume level
+ * @param {Boolean} [silent] Sets the new volume level without
+ * triggering `volumechange` event and updating the cookie.
+ * @param {Boolean} [withoutSlider] Disables updating the slider.
+ */
+ setVolume: function (volume, silent, withoutSlider) {
+ if (volume === this.getVolume()) {
+ return false;
+ }
+
+ this.volume = volume;
+ this.a11y.update(this.getVolume());
+
+ if (!withoutSlider) {
+ this.updateSliderView(this.getVolume());
+ }
+
+ if (!silent) {
+ this.cookie.setVolume(this.getVolume());
+ this.state.el.trigger('volumechange', [this.getVolume()]);
+ }
+ },
+
+ /** Increases current volume level using previously defined step. */
+ increaseVolume: function () {
+ let volume = Math.min(this.getVolume() + this.step, this.max);
+
+ this.setVolume(volume, false, false);
+ this.el.find('.volume-slider')
+ .attr('aria-valuenow', volume);
+ },
+
+ /** Decreases current volume level using previously defined step. */
+ decreaseVolume: function () {
+ let volume = Math.max(this.getVolume() - this.step, this.min);
+
+ this.setVolume(volume, false, false);
+ this.el.find('.volume-slider')
+ .attr('aria-valuenow', volume);
+ },
+
+ /** Updates volume slider view. */
+ updateSliderView: function (volume) {
+ this.volumeSlider.slider('value', volume);
+ this.el.find('.volume-slider')
+ .attr('aria-valuenow', volume);
+ },
+
+ /**
+ * Mutes or unmutes volume.
+ * @param {Number} muteStatus Flag to mute/unmute volume.
+ */
+ mute: function (muteStatus) {
+ let volume;
+
+ this.updateMuteButtonView(muteStatus);
+
+ if (muteStatus) {
+ this.storedVolume = this.getVolume() || this.max;
+ }
+
+ volume = muteStatus ? 0 : this.storedVolume;
+ this.setVolume(volume, false, false);
+ this.el.find('.volume-slider')
+ .attr('aria-valuenow', volume);
+ },
+
+ /**
+ * Returns current volume state (is it muted or not?).
+ * @return {Boolean}
+ */
+ getMuteStatus: function () {
+ return this.getVolume() === 0;
+ },
+
+ /**
+ * Updates the volume button view.
+ * @param {Boolean} isMuted Flag to use muted or unmuted view.
+ */
+ updateMuteButtonView: function (isMuted) {
+ let action = isMuted ? 'addClass' : 'removeClass';
+
+ this.el[action]('is-muted');
+
+ if (isMuted) {
+ this.el
+ .find('.control .icon')
+ .removeClass('fa-volume-up')
+ .addClass('fa-volume-off');
+ } else {
+ this.el
+ .find('.control .icon')
+ .removeClass('fa-volume-off')
+ .addClass('fa-volume-up');
+ }
+ },
+
+ /** Toggles the state of the volume button. */
+ toggleMute: function () {
+ this.mute(!this.getMuteStatus());
+ },
+
+ /**
+ * Checks and updates the state of the volume button relatively to
+ * volume level.
+ * @param {Number} volume Volume level.
+ */
+ checkMuteButtonStatus: function (volume) {
+ if (volume <= this.min) {
+ this.updateMuteButtonView(true);
+ this.state.el.off('volumechange.is-muted');
+ this.state.el.on('volumechange.is-muted', _.once(function () {
+ this.updateMuteButtonView(false);
+ }.bind(this)));
+ }
+ },
+
+ /** Opens volume menu. */
+ openMenu: function () {
+ this.el.addClass('is-opened');
+ this.button.attr('aria-expanded', 'true');
+ },
+
+ /** Closes speed menu. */
+ closeMenu: function () {
+ this.el.removeClass('is-opened');
+ this.button.attr('aria-expanded', 'false');
+ },
+
+ /**
+ * Keydown event handler for the video container.
+ * @param {jquery Event} event
+ */
+ keyDownHandler: function (event) {
+ // ALT key is used to change (alternate) the function of
+ // other pressed keys. In this case, do nothing.
+ if (event.altKey) {
+ return true;
+ }
+
+ if ($(event.target).hasClass('ui-slider-handle')) {
+ return true;
+ }
+
+ let KEY = $.ui.keyCode,
+ keyCode = event.keyCode;
+
+ // eslint-disable-next-line default-case
+ switch (keyCode) {
+ case KEY.UP:
+ // Shift + Arrows keyboard shortcut might be used by
+ // screen readers. In this case, do nothing.
+ if (event.shiftKey) {
+ return true;
+ }
+
+ this.increaseVolume();
+ return false;
+ case KEY.DOWN:
+ // Shift + Arrows keyboard shortcut might be used by
+ // screen readers. In this case, do nothing.
+ if (event.shiftKey) {
+ return true;
+ }
+
+ this.decreaseVolume();
+ return false;
+
+ case KEY.SPACE:
+ case KEY.ENTER:
+ // Shift + Enter keyboard shortcut might be used by
+ // screen readers. In this case, do nothing.
+ if (event.shiftKey) {
+ return true;
+ }
+
+ this.toggleMute();
+ return false;
+ }
+
+ return true;
+ },
+
+ /**
+ * Keydown event handler for the volume button.
+ * @param {jquery Event} event
+ */
+ keyDownButtonHandler: function (event) {
+ // ALT key is used to change (alternate) the function of
+ // other pressed keys. In this case, do nothing.
+ if (event.altKey) {
+ return true;
+ }
+
+ let KEY = $.ui.keyCode,
+ keyCode = event.keyCode;
+
+ // eslint-disable-next-line default-case
+ switch (keyCode) {
+ case KEY.ENTER:
+ case KEY.SPACE:
+ this.toggleMute();
+ return false;
+ }
+
+ return true;
+ },
+
+ /**
+ * onSlide callback for the video slider.
+ * @param {jquery Event} event
+ * @param {jqueryuiSlider ui} ui
+ */
+ onSlideHandler: function (event, ui) {
+ this.setVolume(ui.value, false, true);
+ this.el.find('.volume-slider')
+ .attr('aria-valuenow', ui.volume);
+ },
+
+ /**
+ * Mousedown event handler for the volume button.
+ * @param {jquery Event} event
+ */
+ toggleMuteHandler: function (event) {
+ this.toggleMute();
+ event.preventDefault();
+ },
+
+ /**
+ * Volumechange event handler.
+ * @param {jquery Event} event
+ * @param {Number} volume Volume level.
+ */
+ onVolumeChangeHandler: function (event, volume) {
+ this.checkMuteButtonStatus(volume);
+ }
+};
+
+
+/**
+ * Module responsible for the accessibility of volume controls.
+ * @constructor
+ * @private
+ * @param {jquery $} button The volume button.
+ * @param {Number} min Minimum value for the volume slider.
+ * @param {Number} max Maximum value for the volume slider.
+ * @param {Object} i18n The object containing strings with translations.
+ */
+let Accessibility = function(button, min, max, i18n) {
+ this.min = min;
+ this.max = max;
+ this.button = button;
+ this.i18n = i18n;
+
+ this.initialize();
+};
+
+
+Accessibility.prototype = {
+ destroy: function () {
+ this.liveRegion.remove();
+ },
+
+ /** Initializes the module. */
+ initialize: function () {
+ this.liveRegion = $('
', {
+ class: 'sr video-live-region',
+ 'aria-hidden': 'false',
+ 'aria-live': 'polite'
+ });
+
+ this.button.after(HtmlUtils.HTML(this.liveRegion).toString());
+ },
+
+ /**
+ * Updates text of the live region.
+ * @param {Number} volume Volume level.
+ */
+ update: function (volume) {
+ this.liveRegion.text([
+ this.getVolumeDescription(volume),
+ this.i18n.Volume + '.'
+ ].join(' '));
+
+ $(this.button).parent().find('.volume-slider')
+ .attr('aria-valuenow', volume);
+ },
+
+ /**
+ * Returns a string describing the level of volume.
+ * @param {Number} volume Volume level.
+ */
+ getVolumeDescription: function (volume) {
+ if (volume === 0) {
+ return this.i18n.Muted;
+ } else if (volume <= 20) {
+ return this.i18n['Very low'];
+ } else if (volume <= 40) {
+ return this.i18n.Low;
+ } else if (volume <= 60) {
+ return this.i18n.Average;
+ } else if (volume <= 80) {
+ return this.i18n.Loud;
+ } else if (volume <= 99) {
+ return this.i18n['Very loud'];
+ }
+
+ return this.i18n.Maximum;
+ }
+};
+
+
+/**
+ * Module responsible for the work with volume cookie.
+ * @constructor
+ * @private
+ * @param {Number} min Minimum value for the volume slider.
+ * @param {Number} max Maximum value for the volume slider.
+ */
+let CookieManager = function (min, max) {
+ this.min = min;
+ this.max = max;
+ this.cookieName = 'video_player_volume_level';
+};
+
+
+CookieManager.prototype = {
+ /**
+ * Returns volume level from the cookie.
+ * @return {Number} Volume level.
+ */
+ getVolume: function () {
+ let volume = parseInt($.cookie(this.cookieName), 10);
+
+ if (_.isFinite(volume)) {
+ volume = Math.max(volume, this.min);
+ volume = Math.min(volume, this.max);
+ } else {
+ volume = this.max;
+ }
+
+ return volume;
+ },
+
+ /**
+ * Updates volume cookie.
+ * @param {Number} volume Volume level.
+ */
+ setVolume: function (value) {
+ $.cookie(this.cookieName, value, {
+ expires: 3650,
+ path: '/'
+ });
+ }
+};
+
+export default VideoVolumeControl;
diff --git a/xmodule/assets/video/public/js/08_video_auto_advance_control.js b/xmodule/assets/video/public/js/08_video_auto_advance_control.js
new file mode 100644
index 000000000000..7d350ee2df71
--- /dev/null
+++ b/xmodule/assets/video/public/js/08_video_auto_advance_control.js
@@ -0,0 +1,134 @@
+'use strict';
+
+import HtmlUtils from 'edx-ui-toolkit/js/utils/html-utils';
+import _ from 'underscore';
+
+
+/**
+ * Auto advance control module.
+ * @exports video/08_video_auto_advance_control.js
+ * @constructor
+ * @param {object} state The object containing the state of the video player.
+ * @return {jquery Promise}
+ */
+let AutoAdvanceControl = function(state) {
+ if (!(this instanceof AutoAdvanceControl)) {
+ return new AutoAdvanceControl(state);
+ }
+
+ _.bindAll(this, 'onClick', 'destroy', 'autoPlay', 'autoAdvance');
+ this.state = state;
+ this.state.videoAutoAdvanceControl = this;
+ this.initialize();
+
+ return $.Deferred().resolve().promise();
+};
+
+AutoAdvanceControl.prototype = {
+ template: HtmlUtils.interpolateHtml(
+ HtmlUtils.HTML([
+ '',
+ '',
+ '{autoAdvanceText}',
+ ' ',
+ ' '].join('')),
+ {
+ autoAdvanceText: gettext('Auto-advance')
+ }
+ ).toString(),
+
+ destroy: function() {
+ this.el.off({
+ click: this.onClick
+ });
+ this.el.remove();
+ this.state.el.off({
+ ready: this.autoPlay,
+ ended: this.autoAdvance,
+ destroy: this.destroy
+ });
+ delete this.state.videoAutoAdvanceControl;
+ },
+
+ /** Initializes the module. */
+ initialize: function() {
+ let state = this.state;
+
+ this.el = $(this.template);
+ this.render();
+ this.setAutoAdvance(state.auto_advance);
+ this.bindHandlers();
+
+ return true;
+ },
+
+ /**
+ * Creates any necessary DOM elements, attach them, and set their,
+ * initial configuration.
+ * @param {boolean} enabled Whether auto advance is enabled
+ */
+ render: function() {
+ this.state.el.find('.secondary-controls').prepend(this.el);
+ },
+
+ /**
+ * Bind any necessary function callbacks to DOM events (click,
+ * mousemove, etc.).
+ */
+ bindHandlers: function() {
+ this.el.on({
+ click: this.onClick
+ });
+ this.state.el.on({
+ ready: this.autoPlay,
+ ended: this.autoAdvance,
+ destroy: this.destroy
+ });
+ },
+
+ onClick: function(event) {
+ let enabled = !this.state.auto_advance;
+ event.preventDefault();
+ this.setAutoAdvance(enabled);
+ this.el.trigger('autoadvancechange', [enabled]);
+ },
+
+ /**
+ * Sets or unsets auto advance.
+ * @param {boolean} enabled Sets auto advance.
+ */
+ setAutoAdvance: function(enabled) {
+ if (enabled) {
+ this.el.addClass('active');
+ } else {
+ this.el.removeClass('active');
+ }
+ },
+
+ autoPlay: function() {
+ // Only autoplay the video if it's the first component of the unit.
+ // If a unit has more than one video, no more than one will autoplay.
+ let isFirstComponent = this.state.el.parents('.vert-0').length === 1;
+ if (this.state.auto_advance && isFirstComponent) {
+ this.state.videoCommands.execute('play');
+ }
+ },
+
+ autoAdvance: function() {
+ // We are posting a message to the MFE and then let the eventlistener
+ // in the MFE handle the action taken.
+ if (this.state.auto_advance) {
+ if (window !== window.parent) {
+ window.parent.postMessage({
+ type: 'plugin.autoAdvance',
+ payload: {}
+ }, document.referrer
+ );
+ }
+ }
+ }
+};
+
+export default AutoAdvanceControl;
diff --git a/xmodule/assets/video/public/js/08_video_speed_control.js b/xmodule/assets/video/public/js/08_video_speed_control.js
new file mode 100644
index 000000000000..f8a5ce3eb497
--- /dev/null
+++ b/xmodule/assets/video/public/js/08_video_speed_control.js
@@ -0,0 +1,417 @@
+'use strict';
+
+import Iterator from './00_iterator.js';
+import HtmlUtils from 'edx-ui-toolkit/js/utils/html-utils';
+
+/**
+ * Video speed control module.
+ * @exports video/08_video_speed_control.js
+ * @constructor
+ * @param {object} state The object containing the state of the video player.
+ * @return {jquery Promise}
+ */
+function VideoSpeedControl(state) {
+ if (!(this instanceof VideoSpeedControl)) {
+ return new VideoSpeedControl(state);
+ }
+
+ _.bindAll(this, 'onSetSpeed', 'onRenderSpeed', 'clickLinkHandler',
+ 'keyDownLinkHandler', 'mouseEnterHandler', 'mouseLeaveHandler',
+ 'clickMenuHandler', 'keyDownMenuHandler', 'destroy'
+ );
+ this.state = state;
+ this.state.videoSpeedControl = this;
+ this.initialize();
+
+ return $.Deferred().resolve().promise();
+};
+
+VideoSpeedControl.prototype = {
+ template: [
+ ''
+ ].join(''),
+
+ destroy: function () {
+ this.el.off({
+ mouseenter: this.mouseEnterHandler,
+ mouseleave: this.mouseLeaveHandler,
+ click: this.clickMenuHandler,
+ keydown: this.keyDownMenuHandler
+ });
+
+ this.state.el.off({
+ 'speed:set': this.onSetSpeed,
+ 'speed:render': this.onRenderSpeed
+ });
+ this.closeMenu(true);
+ this.speedsContainer.remove();
+ this.el.remove();
+ delete this.state.videoSpeedControl;
+ },
+
+ /** Initializes the module. */
+ initialize: function () {
+ let state = this.state;
+
+ if (!this.isPlaybackRatesSupported(state)) {
+ console.log(
+ '[Video info]: playbackRate is not supported.'
+ );
+
+ return false;
+ }
+ this.el = $(this.template);
+ this.speedsContainer = this.el.find('.video-speeds');
+ this.speedButton = this.el.find('.speed-button');
+ this.render(state.speeds, state.speed);
+ this.setSpeed(state.speed, true, true);
+ this.bindHandlers();
+
+ return true;
+ },
+
+ /**
+ * Creates any necessary DOM elements, attach them, and set their,
+ * initial configuration.
+ * @param {array} speeds List of speeds available for the player.
+ * @param {string} currentSpeed The current speed set to the player.
+ */
+ render: function (speeds, currentSpeed) {
+ let speedsContainer = this.speedsContainer,
+ reversedSpeeds = speeds.concat().reverse(),
+ instructionsId = 'speed-instructions-' + this.state.id,
+ speedsList = $.map(reversedSpeeds, function (speed) {
+ return HtmlUtils.interpolateHtml(
+ HtmlUtils.HTML(
+ [
+ '',
+ '',
+ '{speed}x',
+ ' ',
+ ' '
+ ].join('')
+ ),
+ {
+ speed: speed
+ }
+ ).toString();
+ });
+
+ HtmlUtils.setHtml(
+ speedsContainer,
+ HtmlUtils.HTML(speedsList)
+ );
+ this.speedLinks = new Iterator(speedsContainer.find('.speed-option'));
+ HtmlUtils.prepend(
+ this.state.el.find('.secondary-controls'),
+ HtmlUtils.HTML(this.el)
+ );
+ this.setActiveSpeed(currentSpeed);
+
+ // set dynamic id for instruction element to avoid collisions
+ this.el.find('.instructions').attr('id', instructionsId);
+ this.speedButton.attr('aria-describedby', instructionsId);
+ },
+
+ /**
+ * Bind any necessary function callbacks to DOM events (click,
+ * mousemove, etc.).
+ */
+ bindHandlers: function () {
+ // Attach various events handlers to the speed menu button.
+ this.el.on({
+ mouseenter: this.mouseEnterHandler,
+ mouseleave: this.mouseLeaveHandler,
+ click: this.openMenu,
+ keydown: this.keyDownMenuHandler
+ });
+
+ // Attach click and keydown event handlers to the individual speed
+ // entries.
+ this.speedsContainer.on({
+ click: this.clickLinkHandler,
+ keydown: this.keyDownLinkHandler
+ }, '.speed-option');
+
+ this.state.el.on({
+ 'speed:set': this.onSetSpeed,
+ 'speed:render': this.onRenderSpeed
+ });
+ this.state.el.on('destroy', this.destroy);
+ },
+
+ onSetSpeed: function (event, speed) {
+ this.setSpeed(speed, true);
+ },
+
+ onRenderSpeed: function (event, speeds, currentSpeed) {
+ this.render(speeds, currentSpeed);
+ },
+
+ /**
+ * Check if playbackRate supports by browser. If browser supports, 1.0
+ * should be returned by playbackRate property. In this case, function
+ * return True. Otherwise, False will be returned.
+ * iOS doesn't support speed change.
+ * @param {object} state The object containing the state of the video
+ * player.
+ * @return {boolean}
+ * true: Browser support playbackRate functionality.
+ * false: Browser doesn't support playbackRate functionality.
+ */
+ isPlaybackRatesSupported: function (state) {
+ let isHtml5 = state.videoType === 'html5',
+ isTouch = state.isTouch,
+ video = document.createElement('video');
+
+ // eslint-disable-next-line no-extra-boolean-cast
+ return !isTouch || (isHtml5 && !Boolean(video.playbackRate));
+ },
+
+ /**
+ * Opens speed menu.
+ * @param {boolean} [bindEvent] Click event will be attached on window.
+ */
+ openMenu: function (bindEvent) {
+ // When speed entries have focus, the menu stays open on
+ // mouseleave. A clickHandler is added to the window
+ // element to have clicks close the menu when they happen
+ // outside of it.
+ if (bindEvent) {
+ $(window).on('click.speedMenu', this.clickMenuHandler);
+ }
+
+ this.el.addClass('is-opened');
+ this.speedButton
+ .attr('tabindex', -1)
+ .attr('aria-expanded', 'true');
+ },
+
+ /**
+ * Closes speed menu.
+ * @param {boolean} [unBindEvent] Click event will be detached from window.
+ */
+ closeMenu: function (unBindEvent) {
+ // Remove the previously added clickHandler from window element.
+ if (unBindEvent) {
+ $(window).off('click.speedMenu');
+ }
+
+ this.el.removeClass('is-opened');
+ this.speedButton
+ .attr('tabindex', 0)
+ .attr('aria-expanded', 'false');
+ },
+
+ /**
+ * Sets new current speed for the speed control and triggers `speedchange`
+ * event if needed.
+ * @param {string|number} speed Speed to be set.
+ * @param {boolean} [silent] Sets the new speed without triggering
+ * `speedchange` event.
+ * @param {boolean} [forceUpdate] Updates the speed even if it's
+ * not differs from current speed.
+ */
+ setSpeed: function (speed, silent, forceUpdate) {
+ let newSpeed = this.state.speedToString(speed);
+ if (newSpeed !== this.currentSpeed || forceUpdate) {
+ this.speedsContainer
+ .find('li')
+ .siblings("li[data-speed='" + newSpeed + "']");
+
+ this.speedButton.find('.value').text(newSpeed + 'x');
+ this.currentSpeed = newSpeed;
+
+ if (!silent) {
+ this.el.trigger('speedchange', [newSpeed, this.state.speed]);
+ }
+ }
+
+ this.resetActiveSpeed();
+ this.setActiveSpeed(newSpeed);
+ },
+
+ resetActiveSpeed: function () {
+ let speedOptions = this.speedsContainer.find('li');
+
+ $(speedOptions).each(function (index, el) {
+ $(el).removeClass('is-active')
+ .find('.speed-option')
+ .attr('aria-pressed', 'false');
+ });
+ },
+
+ setActiveSpeed: function (speed) {
+ let speedOption = this.speedsContainer.find('li[data-speed="' + this.state.speedToString(speed) + '"]');
+
+ speedOption.addClass('is-active')
+ .find('.speed-option')
+ .attr('aria-pressed', 'true');
+
+ this.speedButton.attr('title', gettext('Video speed: ') + this.state.speedToString(speed) + 'x');
+ },
+
+ /**
+ * Click event handler for the menu.
+ * @param {jquery Event} event
+ */
+ clickMenuHandler: function () {
+ this.closeMenu();
+
+ return false;
+ },
+
+ /**
+ * Click event handler for speed links.
+ * @param {jquery Event} event
+ */
+ clickLinkHandler: function (event) {
+ let el = $(event.currentTarget).parent(),
+ speed = $(el).data('speed');
+
+ this.resetActiveSpeed();
+ this.setActiveSpeed(speed);
+ this.state.videoCommands.execute('speed', speed);
+ this.closeMenu(true);
+
+ return false;
+ },
+
+ /**
+ * Mouseenter event handler for the menu.
+ * @param {jquery Event} event
+ */
+ mouseEnterHandler: function () {
+ this.openMenu();
+
+ return false;
+ },
+
+ /**
+ * Mouseleave event handler for the menu.
+ * @param {jquery Event} event
+ */
+ mouseLeaveHandler: function () {
+ // Only close the menu is no speed entry has focus.
+ if (!this.speedLinks.list.is(':focus')) {
+ this.closeMenu();
+ }
+
+ return false;
+ },
+
+ /**
+ * Keydown event handler for the menu.
+ * @param {jquery Event} event
+ */
+ keyDownMenuHandler: function (event) {
+ let KEY = $.ui.keyCode,
+ keyCode = event.keyCode;
+
+ // eslint-disable-next-line default-case
+ switch (keyCode) {
+ // Open menu and focus on last element of list above it.
+ case KEY.ENTER:
+ case KEY.SPACE:
+ case KEY.UP:
+ this.openMenu(true);
+ this.speedLinks.last().focus();
+ break;
+ // Close menu.
+ case KEY.ESCAPE:
+ this.closeMenu(true);
+ break;
+ }
+ // We do not stop propagation and default behavior on a TAB
+ // keypress.
+ return event.keyCode === KEY.TAB;
+ },
+
+ /**
+ * Keydown event handler for speed links.
+ * @param {jquery Event} event
+ */
+ keyDownLinkHandler: function (event) {
+ // ALT key is used to change (alternate) the function of
+ // other pressed keys. In this, do nothing.
+ if (event.altKey) {
+ return true;
+ }
+
+ let KEY = $.ui.keyCode,
+ self = this,
+ parent = $(event.currentTarget).parent(),
+ index = parent.index(),
+ speed = parent.data('speed');
+
+ // eslint-disable-next-line default-case
+ switch (event.keyCode) {
+ // Close menu.
+ case KEY.TAB:
+ // Closes menu after 25ms delay to change `tabindex` after
+ // finishing default behavior.
+ setTimeout(function () {
+ self.closeMenu(true);
+ }, 25);
+
+ return true;
+ // Close menu and give focus to speed control.
+ case KEY.ESCAPE:
+ this.closeMenu(true);
+ this.speedButton.focus();
+
+ return false;
+ // Scroll up menu, wrapping at the top. Keep menu open.
+ case KEY.UP:
+ // Shift + Arrows keyboard shortcut might be used by
+ // screen readers. In this, do nothing.
+ if (event.shiftKey) {
+ return true;
+ }
+
+ this.speedLinks.prev(index).focus();
+ return false;
+ // Scroll down menu, wrapping at the bottom. Keep menu
+ // open.
+ case KEY.DOWN:
+ // Shift + Arrows keyboard shortcut might be used by
+ // screen readers. In this, do nothing.
+ if (event.shiftKey) {
+ return true;
+ }
+
+ this.speedLinks.next(index).focus();
+ return false;
+ // Close menu, give focus to speed control and change
+ // speed.
+ case KEY.ENTER:
+ case KEY.SPACE:
+ this.closeMenu(true);
+ this.speedButton.focus();
+ this.setSpeed(this.state.speedToString(speed));
+
+ return false;
+ }
+
+ return true;
+ }
+};
+
+export default VideoSpeedControl;
diff --git a/xmodule/assets/video/public/js/095_video_context_menu.js b/xmodule/assets/video/public/js/095_video_context_menu.js
new file mode 100644
index 000000000000..42544738fcab
--- /dev/null
+++ b/xmodule/assets/video/public/js/095_video_context_menu.js
@@ -0,0 +1,698 @@
+import _ from 'underscore';
+import Component from './00_component.js';
+
+let AbstractItem = Component.extend({
+ initialize: function (options) {
+ this.options = $.extend(true, {
+ label: '',
+ prefix: 'edx-',
+ dataAttrs: {menu: this},
+ attrs: {},
+ items: [],
+ callback: $.noop,
+ initialize: $.noop
+ }, options);
+
+ this.id = _.uniqueId();
+ this.element = this.createElement();
+ this.element.attr(this.options.attrs).data(this.options.dataAttrs);
+ this.children = [];
+ this.delegateEvents();
+ this.options.initialize.call(this, this);
+ },
+ destroy: function () {
+ _.invoke(this.getChildren(), 'destroy');
+ this.undelegateEvents();
+ this.getElement().remove();
+ },
+ open: function () {
+ this.getElement().addClass('is-opened');
+ return this;
+ },
+ close: function () {
+ },
+ closeSiblings: function () {
+ _.invoke(this.getSiblings(), 'close');
+ return this;
+ },
+ getElement: function () {
+ return this.element;
+ },
+ addChild: function (child) {
+ let firstChild = null,
+ lastChild = null;
+ if (this.hasChildren()) {
+ lastChild = this.getLastChild();
+ lastChild.next = child;
+ firstChild = this.getFirstChild();
+ firstChild.prev = child;
+ }
+ child.parent = this;
+ child.next = firstChild;
+ child.prev = lastChild;
+ this.children.push(child);
+ return this;
+ },
+ getChildren: function () {
+ // Returns the copy.
+ return this.children.concat();
+ },
+ hasChildren: function () {
+ return this.getChildren().length > 0;
+ },
+ getFirstChild: function () {
+ return _.first(this.children);
+ },
+ getLastChild: function () {
+ return _.last(this.children);
+ },
+ bindEvent: function (element, events, handler) {
+ $(element).on(this.addNamespace(events), handler);
+ return this;
+ },
+ getNext: function () {
+ let item = this.next;
+ while (item.isHidden() && this.id !== item.id) {
+ item = item.next;
+ }
+ return item;
+ },
+ getPrev: function () {
+ let item = this.prev;
+ while (item.isHidden() && this.id !== item.id) {
+ item = item.prev;
+ }
+ return item;
+ },
+ createElement: function () {
+ return null;
+ },
+ getRoot: function () {
+ let item = this;
+ while (item.parent) {
+ item = item.parent;
+ }
+ return item;
+ },
+ populateElement: function () {
+ },
+ focus: function () {
+ this.getElement().focus();
+ this.closeSiblings();
+ return this;
+ },
+ isHidden: function () {
+ return this.getElement().is(':hidden');
+ },
+ getSiblings: function () {
+ let items = [],
+ item = this;
+ while (item.next && item.next.id !== this.id) {
+ item = item.next;
+ items.push(item);
+ }
+ return items;
+ },
+ select: function () {
+ },
+ unselect: function () {
+ },
+ setLabel: function () {
+ },
+ itemHandler: function () {
+ },
+ keyDownHandler: function () {
+ },
+ delegateEvents: function () {
+ },
+ undelegateEvents: function () {
+ this.getElement().off('.' + this.id);
+ },
+ addNamespace: function (events) {
+ return _.map(events.split(/\s+/), function (event) {
+ return event + '.' + this.id;
+ }, this).join(' ');
+ }
+});
+
+let AbstractMenu = AbstractItem.extend({
+ delegateEvents: function () {
+ this.bindEvent(this.getElement(), 'keydown mouseleave mouseover', this.itemHandler.bind(this))
+ .bindEvent(this.getElement(), 'contextmenu', function (event) {
+ event.preventDefault();
+ });
+ return this;
+ },
+
+ populateElement: function () {
+ let fragment = document.createDocumentFragment();
+
+ _.each(this.getChildren(), function (child) {
+ fragment.appendChild(child.populateElement()[0]);
+ }, this);
+
+ this.appendContent([fragment]);
+ this.isRendered = true;
+ return this.getElement();
+ },
+
+ close: function () {
+ this.closeChildren();
+ this.getElement().removeClass('is-opened');
+ return this;
+ },
+
+ closeChildren: function () {
+ _.invoke(this.getChildren(), 'close');
+ return this;
+ },
+
+ itemHandler: function (event) {
+ event.preventDefault();
+ let item = $(event.target).data('menu');
+ // eslint-disable-next-line default-case
+ switch (event.type) {
+ case 'keydown':
+ this.keyDownHandler.call(this, event, item);
+ break;
+ case 'mouseover':
+ this.mouseOverHandler.call(this, event, item);
+ break;
+ case 'mouseleave':
+ this.mouseLeaveHandler.call(this, event, item);
+ break;
+ }
+ },
+
+ keyDownHandler: function () {
+ },
+ mouseOverHandler: function () {
+ },
+ mouseLeaveHandler: function () {
+ }
+});
+
+let Menu = AbstractMenu.extend({
+ initialize: function (options, contextmenuElement, container) {
+ this.contextmenuElement = $(contextmenuElement);
+ this.container = $(container);
+ this.overlay = this.getOverlay();
+ AbstractMenu.prototype.initialize.apply(this, arguments);
+ this.build(this, this.options.items);
+ },
+
+ createElement: function () {
+ return $(' ', {
+ class: ['contextmenu', this.options.prefix + 'contextmenu'].join(' '),
+ role: 'menu',
+ tabindex: -1
+ });
+ },
+
+ delegateEvents: function () {
+ AbstractMenu.prototype.delegateEvents.call(this);
+ this.bindEvent(this.contextmenuElement, 'contextmenu', this.contextmenuHandler.bind(this))
+ .bindEvent(window, 'resize', _.debounce(this.close.bind(this), 100));
+ return this;
+ },
+
+ destroy: function () {
+ AbstractMenu.prototype.destroy.call(this);
+ this.overlay.destroy();
+ this.contextmenuElement.removeData('contextmenu');
+ return this;
+ },
+
+ undelegateEvents: function () {
+ AbstractMenu.prototype.undelegateEvents.call(this);
+ this.contextmenuElement.off(this.addNamespace('contextmenu'));
+ this.overlay.undelegateEvents();
+ return this;
+ },
+
+ appendContent: function (content) {
+ let $content = $(content);
+ this.getElement().append($content);
+ return this;
+ },
+
+ addChild: function () {
+ AbstractMenu.prototype.addChild.apply(this, arguments);
+ this.next = this.getFirstChild();
+ this.prev = this.getLastChild();
+ return this;
+ },
+
+ build: function (container, items) {
+ _.each(items, function (item) {
+ let child;
+ if (_.has(item, 'items')) {
+ child = this.build((new Submenu(item, this.contextmenuElement)), item.items);
+ } else {
+ child = new MenuItem(item);
+ }
+ container.addChild(child);
+ }, this);
+ return container;
+ },
+
+ focus: function () {
+ this.getElement().focus();
+ return this;
+ },
+
+ open: function () {
+ let $menu = (this.isRendered) ? this.getElement() : this.populateElement();
+ this.container.append($menu);
+ AbstractItem.prototype.open.call(this);
+ this.overlay.show(this.container);
+ return this;
+ },
+
+ close: function () {
+ AbstractMenu.prototype.close.call(this);
+ this.getElement().detach();
+ this.overlay.hide();
+ return this;
+ },
+
+ position: function (event) {
+ this.getElement().position({
+ my: 'left top',
+ of: event,
+ collision: 'flipfit flipfit',
+ within: this.contextmenuElement
+ });
+
+ return this;
+ },
+
+ pointInContainerBox: function (x, y) {
+ let containerOffset = this.contextmenuElement.offset(),
+ containerBox = {
+ x0: containerOffset.left,
+ y0: containerOffset.top,
+ x1: containerOffset.left + this.contextmenuElement.outerWidth(),
+ y1: containerOffset.top + this.contextmenuElement.outerHeight()
+ };
+ return containerBox.x0 <= x && x <= containerBox.x1 && containerBox.y0 <= y && y <= containerBox.y1;
+ },
+
+ getOverlay: function () {
+ return new Overlay(
+ this.close.bind(this),
+ function (event) {
+ event.preventDefault();
+ if (this.pointInContainerBox(event.pageX, event.pageY)) {
+ this.position(event).focus();
+ this.closeChildren();
+ } else {
+ this.close();
+ }
+ }.bind(this)
+ );
+ },
+
+ contextmenuHandler: function (event) {
+ event.preventDefault();
+ event.stopPropagation();
+ this.open().position(event).focus();
+ },
+
+ keyDownHandler: function (event, item) {
+ let KEY = $.ui.keyCode,
+ keyCode = event.keyCode;
+
+ // eslint-disable-next-line default-case
+ switch (keyCode) {
+ case KEY.UP:
+ item.getPrev().focus();
+ event.stopPropagation();
+ break;
+ case KEY.DOWN:
+ item.getNext().focus();
+ event.stopPropagation();
+ break;
+ case KEY.TAB:
+ event.stopPropagation();
+ break;
+ case KEY.ESCAPE:
+ this.close();
+ break;
+ }
+
+ return false;
+ }
+});
+
+let Overlay = Component.extend({
+ ns: '.overlay',
+ initialize: function (clickHandler, contextmenuHandler) {
+ this.element = $('
', {
+ class: 'overlay'
+ });
+ this.clickHandler = clickHandler;
+ this.contextmenuHandler = contextmenuHandler;
+ },
+
+ destroy: function () {
+ this.getElement().remove();
+ this.undelegateEvents();
+ },
+
+ getElement: function () {
+ return this.element;
+ },
+
+ hide: function () {
+ this.getElement().detach();
+ this.undelegateEvents();
+ return this;
+ },
+
+ show: function (container) {
+ let $elem = $(this.getElement());
+ $(container).append($elem);
+ this.delegateEvents();
+ return this;
+ },
+
+ delegateEvents: function () {
+ let self = this;
+ $(document)
+ .on('click' + this.ns, function () {
+ if (_.isFunction(self.clickHandler)) {
+ self.clickHandler.apply(this, arguments);
+ }
+ self.hide();
+ })
+ .on('contextmenu' + this.ns, function () {
+ if (_.isFunction(self.contextmenuHandler)) {
+ self.contextmenuHandler.apply(this, arguments);
+ }
+ });
+ return this;
+ },
+
+ undelegateEvents: function () {
+ $(document).off(this.ns);
+ return this;
+ }
+});
+
+let Submenu = AbstractMenu.extend({
+ initialize: function (options, contextmenuElement) {
+ this.contextmenuElement = contextmenuElement;
+ AbstractMenu.prototype.initialize.apply(this, arguments);
+ },
+
+ createElement: function () {
+ let $spanElem,
+ $listElem,
+ $element = $(' ', {
+ class: ['submenu-item', 'menu-item', this.options.prefix + 'submenu-item'].join(' '),
+ 'aria-expanded': 'false',
+ 'aria-haspopup': 'true',
+ 'aria-labelledby': 'submenu-item-label-' + this.id,
+ role: 'menuitem',
+ tabindex: -1
+ });
+
+ $spanElem = $(' ', {
+ id: 'submenu-item-label-' + this.id,
+ text: this.options.label
+ });
+ this.label = $spanElem.appendTo($element);
+
+ $listElem = $(' ', {
+ class: ['submenu', this.options.prefix + 'submenu'].join(' '),
+ role: 'menu'
+ });
+
+ this.list = $listElem.appendTo($element);
+
+ return $element;
+ },
+
+ appendContent: function (content) {
+ let $content = $(content);
+ this.list.append($content);
+ return this;
+ },
+
+ setLabel: function (label) {
+ this.label.text(label);
+ return this;
+ },
+
+ openKeyboard: function () {
+ if (this.hasChildren()) {
+ this.open();
+ this.getFirstChild().focus();
+ }
+ return this;
+ },
+
+ keyDownHandler: function (event) {
+ let KEY = $.ui.keyCode,
+ keyCode = event.keyCode;
+
+ // eslint-disable-next-line default-case
+ switch (keyCode) {
+ case KEY.LEFT:
+ this.close().focus();
+ event.stopPropagation();
+ break;
+ case KEY.RIGHT:
+ case KEY.ENTER:
+ case KEY.SPACE:
+ this.openKeyboard();
+ event.stopPropagation();
+ break;
+ }
+
+ return false;
+ },
+
+ open: function () {
+ AbstractMenu.prototype.open.call(this);
+ this.getElement().attr({'aria-expanded': 'true'});
+ this.position();
+ return this;
+ },
+
+ close: function () {
+ AbstractMenu.prototype.close.call(this);
+ this.getElement().attr({'aria-expanded': 'false'});
+ return this;
+ },
+
+ position: function () {
+ this.list.position({
+ my: 'left top',
+ at: 'right top',
+ of: this.getElement(),
+ collision: 'flipfit flipfit',
+ within: this.contextmenuElement
+ });
+ return this;
+ },
+
+ mouseOverHandler: function () {
+ clearTimeout(this.timer);
+ this.timer = setTimeout(this.open.bind(this), 200);
+ this.focus();
+ },
+
+ mouseLeaveHandler: function () {
+ clearTimeout(this.timer);
+ this.timer = setTimeout(this.close.bind(this), 200);
+ this.focus();
+ }
+});
+
+let MenuItem = AbstractItem.extend({
+ createElement: function () {
+ let classNames = [
+ 'menu-item', this.options.prefix + 'menu-item',
+ this.options.isSelected ? 'is-selected' : ''
+ ].join(' ');
+
+ return $(' ', {
+ class: classNames,
+ 'aria-selected': this.options.isSelected ? 'true' : 'false',
+ role: 'menuitem',
+ tabindex: -1,
+ text: this.options.label
+ });
+ },
+
+ populateElement: function () {
+ return this.getElement();
+ },
+
+ delegateEvents: function () {
+ this.bindEvent(this.getElement(), 'click keydown contextmenu mouseover', this.itemHandler.bind(this));
+ return this;
+ },
+
+ setLabel: function (label) {
+ this.getElement().text(label);
+ return this;
+ },
+
+ select: function (event) {
+ this.options.callback.call(this, event, this, this.options);
+ this.getElement()
+ .addClass('is-selected')
+ .attr({'aria-selected': 'true'});
+ _.invoke(this.getSiblings(), 'unselect');
+ // Hide the menu.
+ this.getRoot().close();
+ return this;
+ },
+
+ unselect: function () {
+ this.getElement()
+ .removeClass('is-selected')
+ .attr({'aria-selected': 'false'});
+ return this;
+ },
+
+ itemHandler: function (event) {
+ event.preventDefault();
+ // eslint-disable-next-line default-case
+ switch (event.type) {
+ case 'contextmenu':
+ case 'click':
+ this.select();
+ break;
+ case 'mouseover':
+ this.focus();
+ event.stopPropagation();
+ break;
+ case 'keydown':
+ this.keyDownHandler.call(this, event, this);
+ break;
+ }
+ },
+
+ keyDownHandler: function (event) {
+ let KEY = $.ui.keyCode,
+ keyCode = event.keyCode;
+
+ // eslint-disable-next-line default-case
+ switch (keyCode) {
+ case KEY.RIGHT:
+ event.stopPropagation();
+ break;
+ case KEY.ENTER:
+ case KEY.SPACE:
+ this.select();
+ event.stopPropagation();
+ break;
+ }
+
+ return false;
+ }
+});
+
+let VideoContextMenu = function(state, i18n) {
+ let speedCallback = function(event, menuitem, options) {
+ let speed = parseFloat(options.label);
+ state.videoCommands.execute('speed', speed);
+ }
+ let options = {
+ items: [{
+ label: i18n.Play,
+ callback: function () {
+ state.videoCommands.execute('togglePlayback');
+ },
+ initialize: function (menuitem) {
+ state.el.on({
+ play: function () {
+ menuitem.setLabel(i18n.Pause);
+ },
+ pause: function () {
+ menuitem.setLabel(i18n.Play);
+ }
+ });
+ }
+ }, {
+ label: state.videoVolumeControl.getMuteStatus() ? i18n.Unmute : i18n.Mute,
+ callback: function () {
+ state.videoCommands.execute('toggleMute');
+ },
+ initialize: function (menuitem) {
+ state.el.on({
+ volumechange: function () {
+ if (state.videoVolumeControl.getMuteStatus()) {
+ menuitem.setLabel(i18n.Unmute);
+ } else {
+ menuitem.setLabel(i18n.Mute);
+ }
+ }
+ });
+ }
+ }, {
+ label: i18n['Fill browser'],
+ callback: function () {
+ state.videoCommands.execute('toggleFullScreen');
+ },
+ initialize: function (menuitem) {
+ state.el.on({
+ fullscreen: function (event, isFullscreen) {
+ if (isFullscreen) {
+ menuitem.setLabel(i18n['Exit full browser']);
+ } else {
+ menuitem.setLabel(i18n['Fill browser']);
+ }
+ }
+ });
+ }
+ }, {
+ label: i18n.Speed,
+ items: _.map(state.speeds, function (speed) {
+ let isSelected = parseFloat(speed) === state.speed;
+ return {
+ label: speed + 'x', callback: speedCallback, speed: speed, isSelected: isSelected
+ };
+ }),
+ initialize: function (menuitem) {
+ state.el.on({
+ speedchange: function (event, speed) {
+ // eslint-disable-next-line no-shadow
+ let item = menuitem.getChildren().filter(function (item) {
+ return item.options.speed === speed;
+ })[0];
+ if (item) {
+ item.select();
+ }
+ }
+ });
+ }
+ }
+ ]
+ };
+
+ // eslint-disable-next-line no-shadow
+ $.fn.contextmenu = function(container, options) {
+ return this.each(function () {
+ $(this).data('contextmenu', new Menu(options, this, container));
+ });
+ };
+
+ if (!state.isYoutubeType()) {
+ state.el.find('video').contextmenu(state.el, options);
+ state.el.on('destroy', function () {
+ let contextmenu = $(this).find('video').data('contextmenu');
+ if (contextmenu) {
+ contextmenu.destroy();
+ }
+ });
+ }
+
+ return $.Deferred().resolve().promise();
+}
+
+export default VideoContextMenu
diff --git a/xmodule/assets/video/public/js/09_bumper.js b/xmodule/assets/video/public/js/09_bumper.js
new file mode 100644
index 000000000000..d568c5369bb5
--- /dev/null
+++ b/xmodule/assets/video/public/js/09_bumper.js
@@ -0,0 +1,108 @@
+'use strict';
+
+/**
+ * VideoBumper module.
+ * @exports video/09_bumper.js
+ * @constructor
+ * @param {Object} player The player factory.
+ * @param {Object} state The object containing the state of the video
+ * @return {jquery Promise}
+ */
+let VideoBumper = function(player, state) {
+ if (!(this instanceof VideoBumper)) {
+ return new VideoBumper(player, state);
+ }
+
+ _.bindAll(
+ this, 'showMainVideoHandler', 'destroy', 'skipByDuration', 'destroyAndResolve'
+ );
+ this.dfd = $.Deferred();
+ this.element = state.el;
+ this.element.addClass('is-bumper');
+ this.player = player;
+ this.state = state;
+ this.doNotShowAgain = false;
+ this.state.videoBumper = this;
+ this.bindHandlers();
+ this.initialize();
+ this.maxBumperDuration = 35; // seconds
+};
+
+VideoBumper.prototype = {
+ initialize: function() {
+ this.player();
+ },
+
+ getPromise: function() {
+ return this.dfd.promise();
+ },
+
+ showMainVideoHandler: function() {
+ this.state.storage.setItem('isBumperShown', true);
+ setTimeout(function() {
+ this.saveState();
+ this.showMainVideo();
+ }.bind(this), 20);
+ },
+
+ destroyAndResolve: function() {
+ this.destroy();
+ this.dfd.resolve();
+ },
+
+ showMainVideo: function() {
+ if (this.state.videoPlayer) {
+ this.destroyAndResolve();
+ } else {
+ this.state.el.on('initialize', this.destroyAndResolve);
+ }
+ },
+
+ skip: function() {
+ this.element.trigger('skip', [this.doNotShowAgain]);
+ this.showMainVideoHandler();
+ },
+
+ skipAndDoNotShowAgain: function() {
+ this.doNotShowAgain = true;
+ this.skip();
+ },
+
+ skipByDuration: function(event, time) {
+ if (time > this.maxBumperDuration) {
+ this.element.trigger('ended');
+ }
+ },
+
+ bindHandlers: function() {
+ let events = ['ended', 'error'].join(' ');
+ this.element.on(events, this.showMainVideoHandler);
+ this.element.on('timeupdate', this.skipByDuration);
+ },
+
+ saveState: function() {
+ let info = {bumper_last_view_date: true};
+ if (this.doNotShowAgain) {
+ _.extend(info, {bumper_do_not_show_again: true});
+ }
+ if (this.state.videoSaveStatePlugin) {
+ this.state.videoSaveStatePlugin.saveState(true, info);
+ }
+ },
+
+ destroy: function() {
+ let events = ['ended', 'error'].join(' ');
+ this.element.off(events, this.showMainVideoHandler);
+ this.element.off({
+ timeupdate: this.skipByDuration,
+ initialize: this.destroyAndResolve
+ });
+ this.element.removeClass('is-bumper');
+ if (_.isFunction(this.state.videoPlayer.destroy)) {
+ this.state.videoPlayer.destroy();
+ }
+ delete this.state.videoBumper;
+ }
+};
+
+export default VideoBumper;
diff --git a/xmodule/assets/video/public/js/09_completion.js b/xmodule/assets/video/public/js/09_completion.js
new file mode 100644
index 000000000000..8d7faeb71b54
--- /dev/null
+++ b/xmodule/assets/video/public/js/09_completion.js
@@ -0,0 +1,201 @@
+'use strict';
+
+
+
+/**
+ * Completion handler
+ * @exports video/09_completion.js
+ * @constructor
+ * @param {Object} state The object containing the state of the video
+ * @return {jquery Promise}
+ */
+let VideoCompletionHandler = function(state) {
+ if (!(this instanceof VideoCompletionHandler)) {
+ return new VideoCompletionHandler(state);
+ }
+ this.state = state;
+ this.state.completionHandler = this;
+ this.initialize();
+ return $.Deferred().resolve().promise();
+};
+
+VideoCompletionHandler.prototype = {
+
+ /** Tears down the VideoCompletionHandler.
+ *
+ * * Removes backreferences from this.state to this.
+ * * Turns off signal handlers.
+ */
+ destroy: function() {
+ this.el.remove();
+ this.el.off('timeupdate.completion');
+ this.el.off('ended.completion');
+ delete this.state.completionHandler;
+ },
+
+ /** Initializes the VideoCompletionHandler.
+ *
+ * This sets all the instance variables needed to perform
+ * completion calculations.
+ */
+ initialize: function() {
+ // Attributes with "Time" in the name refer to the number of seconds since
+ // the beginning of the video, except for lastSentTime, which refers to a
+ // timestamp in seconds since the Unix epoch.
+ this.lastSentTime = undefined;
+ this.isComplete = false;
+ this.completionPercentage = this.state.config.completionPercentage;
+ this.startTime = this.state.config.startTime;
+ this.endTime = this.state.config.endTime;
+ this.isEnabled = this.state.config.completionEnabled;
+ if (this.endTime) {
+ this.completeAfterTime = this.calculateCompleteAfterTime(this.startTime, this.endTime);
+ }
+ if (this.isEnabled) {
+ this.bindHandlers();
+ }
+ },
+
+ /** Bind event handler callbacks.
+ *
+ * When ended is triggered, mark the video complete
+ * unconditionally.
+ *
+ * When timeupdate is triggered, check to see if the user has
+ * passed the completeAfterTime in the video, and if so, mark the
+ * video complete.
+ *
+ * When destroy is triggered, clean up outstanding resources.
+ */
+ bindHandlers: function() {
+ let self = this;
+
+ /** Event handler to check if the video is complete, and submit
+ * a completion if it is.
+ *
+ * If the timeupdate handler doesn't fire after the required
+ * percentage, this will catch any fully complete videos.
+ */
+ this.state.el.on('ended.completion', function() {
+ self.handleEnded();
+ });
+
+ /** Event handler to check video progress, and mark complete if
+ * greater than completionPercentage
+ */
+ this.state.el.on('timeupdate.completion', function(ev, currentTime) {
+ self.handleTimeUpdate(currentTime);
+ });
+
+ /** Event handler to receive youtube metadata (if we even are a youtube link),
+ * and mark complete, if youtube will insist on hosting the video itself.
+ */
+ this.state.el.on('metadata_received', function() {
+ self.checkMetadata();
+ });
+
+ /** Event handler to clean up resources when the video player
+ * is destroyed.
+ */
+ this.state.el.off('destroy', this.destroy);
+ },
+
+ /** Handler to call when the ended event is triggered */
+ handleEnded: function() {
+ if (this.isComplete) {
+ return;
+ }
+ this.markCompletion();
+ },
+
+ /** Handler to call when a timeupdate event is triggered */
+ handleTimeUpdate: function(currentTime) {
+ let duration;
+ if (this.isComplete) {
+ return;
+ }
+ if (this.lastSentTime !== undefined && currentTime - this.lastSentTime < this.repostDelaySeconds()) {
+ // Throttle attempts to submit in case of network issues
+ return;
+ }
+ if (this.completeAfterTime === undefined) {
+ // Duration is not available at initialization time
+ duration = this.state.videoPlayer.duration();
+ if (!duration) {
+ // duration is not yet set. Wait for another event,
+ // or fall back to 'ended' handler.
+ return;
+ }
+ this.completeAfterTime = this.calculateCompleteAfterTime(this.startTime, duration);
+ }
+
+ if (currentTime > this.completeAfterTime) {
+ this.markCompletion(currentTime);
+ }
+ },
+
+ /** Handler to call when youtube metadata is received */
+ checkMetadata: function() {
+ let metadata = this.state.metadata[this.state.youtubeId()];
+
+ // https://developers.google.com/youtube/v3/docs/videos#contentDetails.contentRating.ytRating
+ if (metadata && metadata.contentRating && metadata.contentRating.ytRating === 'ytAgeRestricted') {
+ // Age-restricted videos won't play in embedded players. Instead, they ask you to watch it on
+ // youtube itself. Which means we can't notice if they complete it. Rather than leaving an
+ // incompletable video in the course, let's just mark it complete right now.
+ if (!this.isComplete) {
+ this.markCompletion();
+ }
+ }
+ },
+
+ /** Submit completion to the LMS */
+ markCompletion: function(currentTime) {
+ let self = this;
+ let errmsg;
+ this.isComplete = true;
+ this.lastSentTime = currentTime;
+ this.state.el.trigger('complete');
+ if (this.state.config.publishCompletionUrl) {
+ $.ajax({
+ type: 'POST',
+ url: this.state.config.publishCompletionUrl,
+ contentType: 'application/json',
+ dataType: 'json',
+ data: JSON.stringify({completion: 1.0}),
+ success: function() {
+ self.state.el.off('timeupdate.completion');
+ self.state.el.off('ended.completion');
+ },
+ error: function(xhr) {
+ /* eslint-disable no-console */
+ self.complete = false;
+ errmsg = 'Failed to submit completion';
+ if (xhr.responseJSON !== undefined) {
+ errmsg += ': ' + xhr.responseJSON.error;
+ }
+ console.warn(errmsg);
+ /* eslint-enable no-console */
+ }
+ });
+ } else {
+ /* eslint-disable no-console */
+ console.warn('publishCompletionUrl not defined');
+ /* eslint-enable no-console */
+ }
+ },
+
+ /** Determine what point in the video (in seconds from the
+ * beginning) counts as complete.
+ */
+ calculateCompleteAfterTime: function(startTime, endTime) {
+ return startTime + (endTime - startTime) * this.completionPercentage;
+ },
+
+ /** How many seconds to wait after a POST fails to try again. */
+ repostDelaySeconds: function() {
+ return 3.0;
+ }
+};
+
+export default VideoCompletionHandler;
diff --git a/xmodule/assets/video/public/js/09_events_bumper_plugin.js b/xmodule/assets/video/public/js/09_events_bumper_plugin.js
new file mode 100644
index 000000000000..6e1b3fef3d15
--- /dev/null
+++ b/xmodule/assets/video/public/js/09_events_bumper_plugin.js
@@ -0,0 +1,112 @@
+'use strict';
+
+import _ from 'underscore';
+
+
+/**
+ * Events module.
+ * @exports video/09_events_bumper_plugin.js
+ * @constructor
+ * @param {Object} state The object containing the state of the video
+ * @param {Object} i18n The object containing strings with translations.
+ * @param {Object} options
+ * @return {jquery Promise}
+ */
+let EventsBumperPlugin = function(state, i18n, options) {
+ if (!(this instanceof EventsBumperPlugin)) {
+ return new EventsBumperPlugin(state, i18n, options);
+ }
+
+ _.bindAll(this, 'onReady', 'onPlay', 'onEnded', 'onShowLanguageMenu', 'onHideLanguageMenu', 'onSkip',
+ 'onShowCaptions', 'onHideCaptions', 'destroy');
+ this.state = state;
+ this.options = _.extend({}, options);
+ this.state.videoEventsBumperPlugin = this;
+ this.i18n = i18n;
+ this.initialize();
+
+ return $.Deferred().resolve().promise();
+};
+
+EventsBumperPlugin.moduleName = 'EventsBumperPlugin';
+EventsBumperPlugin.prototype = {
+ destroy: function() {
+ this.state.el.off(this.events);
+ delete this.state.videoEventsBumperPlugin;
+ },
+
+ initialize: function() {
+ this.events = {
+ ready: this.onReady,
+ play: this.onPlay,
+ 'ended stop': this.onEnded,
+ skip: this.onSkip,
+ 'language_menu:show': this.onShowLanguageMenu,
+ 'language_menu:hide': this.onHideLanguageMenu,
+ 'captions:show': this.onShowCaptions,
+ 'captions:hide': this.onHideCaptions,
+ destroy: this.destroy
+ };
+ this.bindHandlers();
+ },
+
+ bindHandlers: function() {
+ this.state.el.on(this.events);
+ },
+
+ onReady: function() {
+ this.log('edx.video.bumper.loaded');
+ },
+
+ onPlay: function() {
+ this.log('edx.video.bumper.played', {currentTime: this.getCurrentTime()});
+ },
+
+ onEnded: function() {
+ this.log('edx.video.bumper.stopped', {currentTime: this.getCurrentTime()});
+ },
+
+ onSkip: function(event, doNotShowAgain) {
+ let info = {currentTime: this.getCurrentTime()};
+ let eventName = 'edx.video.bumper.' + (doNotShowAgain ? 'dismissed' : 'skipped');
+ this.log(eventName, info);
+ },
+
+ onShowLanguageMenu: function() {
+ this.log('edx.video.bumper.transcript.menu.shown');
+ },
+
+ onHideLanguageMenu: function() {
+ this.log('edx.video.bumper.transcript.menu.hidden');
+ },
+
+ onShowCaptions: function() {
+ this.log('edx.video.bumper.transcript.shown', {currentTime: this.getCurrentTime()});
+ },
+
+ onHideCaptions: function() {
+ this.log('edx.video.bumper.transcript.hidden', {currentTime: this.getCurrentTime()});
+ },
+
+ getCurrentTime: function() {
+ let player = this.state.videoPlayer;
+ return player ? player.currentTime : 0;
+ },
+
+ getDuration: function() {
+ let player = this.state.videoPlayer;
+ return player ? player.duration() : 0;
+ },
+
+ log: function(eventName, data) {
+ let logInfo = _.extend({
+ host_component_id: this.state.id,
+ bumper_id: this.state.config.sources[0] || '',
+ duration: this.getDuration(),
+ code: 'html5'
+ }, data, this.options.data);
+ Logger.log(eventName, logInfo);
+ }
+};
+
+export default EventsBumperPlugin;
diff --git a/xmodule/assets/video/public/js/09_events_plugin.js b/xmodule/assets/video/public/js/09_events_plugin.js
new file mode 100644
index 000000000000..2febb99793c8
--- /dev/null
+++ b/xmodule/assets/video/public/js/09_events_plugin.js
@@ -0,0 +1,177 @@
+'use strict';
+
+import _ from 'underscore';
+
+
+/**
+ * Events module.
+ * @exports video/09_events_plugin.js
+ * @constructor
+ * @param {Object} state The object containing the state of the video
+ * @param {Object} i18n The object containing strings with translations.
+ * @param {Object} options
+ * @return {jquery Promise}
+ */
+let EventsPlugin = function(state, i18n, options) {
+ if (!(this instanceof EventsPlugin)) {
+ return new EventsPlugin(state, i18n, options);
+ }
+
+ _.bindAll(this, 'onReady', 'onPlay', 'onPause', 'onComplete', 'onEnded', 'onSeek',
+ 'onSpeedChange', 'onAutoAdvanceChange', 'onShowLanguageMenu', 'onHideLanguageMenu',
+ 'onSkip', 'onShowTranscript', 'onHideTranscript', 'onShowCaptions', 'onHideCaptions',
+ 'destroy');
+
+ this.state = state;
+ this.options = _.extend({}, options);
+ this.state.videoEventsPlugin = this;
+ this.i18n = i18n;
+ this.initialize();
+
+ return $.Deferred().resolve().promise();
+};
+
+EventsPlugin.moduleName = 'EventsPlugin';
+EventsPlugin.prototype = {
+ destroy: function() {
+ this.state.el.off(this.events);
+ delete this.state.videoEventsPlugin;
+ },
+
+ initialize: function() {
+ this.events = {
+ ready: this.onReady,
+ play: this.onPlay,
+ pause: this.onPause,
+ complete: this.onComplete,
+ 'ended stop': this.onEnded,
+ seek: this.onSeek,
+ skip: this.onSkip,
+ speedchange: this.onSpeedChange,
+ autoadvancechange: this.onAutoAdvanceChange,
+ 'language_menu:show': this.onShowLanguageMenu,
+ 'language_menu:hide': this.onHideLanguageMenu,
+ 'transcript:show': this.onShowTranscript,
+ 'transcript:hide': this.onHideTranscript,
+ 'captions:show': this.onShowCaptions,
+ 'captions:hide': this.onHideCaptions,
+ destroy: this.destroy
+ };
+ this.bindHandlers();
+ this.emitPlayVideoEvent = true;
+ },
+
+ bindHandlers: function() {
+ this.state.el.on(this.events);
+ },
+
+ onReady: function() {
+ this.log('load_video');
+ },
+
+ onPlay: function() {
+ if (this.emitPlayVideoEvent) {
+ this.log('play_video', {currentTime: this.getCurrentTime()});
+ this.emitPlayVideoEvent = false;
+ }
+ },
+
+ onPause: function() {
+ this.log('pause_video', {currentTime: this.getCurrentTime()});
+ this.emitPlayVideoEvent = true;
+ },
+
+ onComplete: function() {
+ this.log('complete_video', {currentTime: this.getCurrentTime()});
+ },
+
+ onEnded: function() {
+ this.log('stop_video', {currentTime: this.getCurrentTime()});
+ this.emitPlayVideoEvent = true;
+ },
+
+ onSkip: function(event, doNotShowAgain) {
+ let info = {currentTime: this.getCurrentTime()};
+ let eventName = doNotShowAgain ? 'do_not_show_again_video' : 'skip_video';
+ this.log(eventName, info);
+ },
+
+ onSeek: function(event, time, oldTime, type) {
+ this.log('seek_video', {
+ old_time: oldTime,
+ new_time: time,
+ type: type
+ });
+ this.emitPlayVideoEvent = true;
+ },
+
+ onSpeedChange: function(event, newSpeed, oldSpeed) {
+ this.log('speed_change_video', {
+ current_time: this.getCurrentTime(),
+ old_speed: this.state.speedToString(oldSpeed),
+ new_speed: this.state.speedToString(newSpeed)
+ });
+ },
+
+ onAutoAdvanceChange: function(event, enabled) {
+ this.log('auto_advance_change_video', {
+ enabled: enabled
+ });
+ },
+
+ onShowLanguageMenu: function() {
+ this.log('edx.video.language_menu.shown');
+ },
+
+ onHideLanguageMenu: function() {
+ this.log('edx.video.language_menu.hidden', {language: this.getCurrentLanguage()});
+ },
+
+ onShowTranscript: function() {
+ this.log('show_transcript', {current_time: this.getCurrentTime()});
+ },
+
+ onHideTranscript: function() {
+ this.log('hide_transcript', {current_time: this.getCurrentTime()});
+ },
+
+ onShowCaptions: function() {
+ this.log('edx.video.closed_captions.shown', {current_time: this.getCurrentTime()});
+ },
+
+ onHideCaptions: function() {
+ this.log('edx.video.closed_captions.hidden', {current_time: this.getCurrentTime()});
+ },
+
+ getCurrentTime: function() {
+ let player = this.state.videoPlayer;
+ let startTime = this.state.config.startTime;
+ let currentTime = player ? player.currentTime : 0;
+ // if video didn't start from 0(it's a subsection of video), subtract the additional time at start
+ if (startTime) {
+ currentTime = currentTime ? currentTime - startTime : 0;
+ }
+ return currentTime;
+ },
+
+ getCurrentLanguage: function() {
+ let language = this.state.lang;
+ return language;
+ },
+
+ log: function(eventName, data) {
+ // use startTime and endTime to calculate the duration to handle the case where only a subsection of video is used
+ let endTime = this.state.config.endTime || this.state.duration;
+ let startTime = this.state.config.startTime || 0;
+
+ let logInfo = _.extend({
+ id: this.state.id,
+ // eslint-disable-next-line no-nested-ternary
+ code: this.state.isYoutubeType() ? this.state.youtubeId() : this.state.canPlayHLS ? 'hls' : 'html5',
+ duration: endTime - startTime
+ }, data, this.options.data);
+ Logger.log(eventName, logInfo);
+ }
+};
+
+export default EventsPlugin;
diff --git a/xmodule/assets/video/public/js/09_play_pause_control.js b/xmodule/assets/video/public/js/09_play_pause_control.js
new file mode 100644
index 000000000000..c0e0844641b8
--- /dev/null
+++ b/xmodule/assets/video/public/js/09_play_pause_control.js
@@ -0,0 +1,96 @@
+'use strict';
+
+import _ from 'underscore';
+
+
+/**
+ * Play/pause control module.
+ * @exports video/09_play_pause_control.js
+ * @constructor
+ * @param {Object} state The object containing the state of the video
+ * @param {Object} i18n The object containing strings with translations.
+ * @return {jquery Promise}
+ */
+let PlayPauseControl = function(state, i18n) {
+ if (!(this instanceof PlayPauseControl)) {
+ return new PlayPauseControl(state, i18n);
+ }
+
+ _.bindAll(this, 'play', 'pause', 'onClick', 'destroy');
+ this.state = state;
+ this.state.videoPlayPauseControl = this;
+ this.i18n = i18n;
+ this.initialize();
+
+ return $.Deferred().resolve().promise();
+};
+
+PlayPauseControl.prototype = {
+ template: [
+ '',
+ ' ',
+ ' '
+ ].join(''),
+
+ destroy: function() {
+ this.el.remove();
+ this.state.el.off('destroy', this.destroy);
+ delete this.state.videoPlayPauseControl;
+ },
+
+ /** Initializes the module. */
+ initialize: function() {
+ this.el = $(this.template);
+ this.render();
+ this.bindHandlers();
+ },
+
+ /**
+ * Creates any necessary DOM elements, attach them, and set their,
+ * initial configuration.
+ */
+ render: function() {
+ this.state.el.find('.vcr').prepend(this.el);
+ },
+
+ /** Bind any necessary function callbacks to DOM events. */
+ bindHandlers: function() {
+ this.el.on({
+ click: this.onClick
+ });
+ this.state.el.on({
+ play: this.play,
+ 'pause ended': this.pause,
+ destroy: this.destroy
+ });
+ },
+
+ onClick: function(event) {
+ event.preventDefault();
+ this.state.videoCommands.execute('togglePlayback');
+ },
+
+ play: function() {
+ this.el
+ .addClass('pause')
+ .removeClass('play')
+ .attr({title: gettext('Pause'), 'aria-label': gettext('Pause')})
+ .find('.icon')
+ .removeClass('fa-play')
+ .addClass('fa-pause');
+ },
+
+ pause: function() {
+ this.el
+ .removeClass('pause')
+ .addClass('play')
+ .attr({title: gettext('Play'), 'aria-label': gettext('Play')})
+ .find('.icon')
+ .removeClass('fa-pause')
+ .addClass('fa-play');
+ }
+};
+
+export default PlayPauseControl;
diff --git a/xmodule/assets/video/public/js/09_play_placeholder.js b/xmodule/assets/video/public/js/09_play_placeholder.js
new file mode 100644
index 000000000000..47052ea06495
--- /dev/null
+++ b/xmodule/assets/video/public/js/09_play_placeholder.js
@@ -0,0 +1,84 @@
+'use strict';
+
+/**
+ * Play placeholder control module.
+ * @exports video/09_play_placeholder.js
+ * @constructor
+ * @param {Object} state The object containing the state of the video
+ * @param {Object} i18n The object containing strings with translations.
+ * @return {jquery Promise}
+ */
+let PlayPlaceholder = function(state, i18n) {
+ if (!(this instanceof PlayPlaceholder)) {
+ return new PlayPlaceholder(state, i18n);
+ }
+
+ _.bindAll(this, 'onClick', 'hide', 'show', 'destroy');
+ this.state = state;
+ this.state.videoPlayPlaceholder = this;
+ this.i18n = i18n;
+ this.initialize();
+
+ return $.Deferred().resolve().promise();
+};
+
+PlayPlaceholder.prototype = {
+ destroy: function() {
+ this.el.off('click', this.onClick);
+ this.state.el.on({
+ destroy: this.destroy,
+ play: this.hide,
+ 'ended pause': this.show
+ });
+ this.hide();
+ delete this.state.videoPlayPlaceholder;
+ },
+
+ /**
+ * Indicates whether the placeholder should be shown. We display it
+ * for html5 videos on iPad and Android devices.
+ * @return {Boolean}
+ */
+ shouldBeShown: function() {
+ return /iPad|Android/i.test(this.state.isTouch[0]) && !this.state.isYoutubeType();
+ },
+
+ /** Initializes the module. */
+ initialize: function() {
+ if (!this.shouldBeShown()) {
+ return false;
+ }
+
+ this.el = this.state.el.find('.btn-play');
+ this.bindHandlers();
+ this.show();
+ },
+
+ /** Bind any necessary function callbacks to DOM events. */
+ bindHandlers: function() {
+ this.el.on('click', this.onClick);
+ this.state.el.on({
+ destroy: this.destroy,
+ play: this.hide,
+ 'ended pause': this.show
+ });
+ },
+
+ onClick: function() {
+ this.state.videoCommands.execute('play');
+ },
+
+ hide: function() {
+ this.el
+ .addClass('is-hidden')
+ .attr({'aria-hidden': 'true', tabindex: -1});
+ },
+
+ show: function() {
+ this.el
+ .removeClass('is-hidden')
+ .attr({'aria-hidden': 'false', tabindex: 0});
+ }
+};
+
+export default PlayPlaceholder;
diff --git a/xmodule/assets/video/public/js/09_play_skip_control.js b/xmodule/assets/video/public/js/09_play_skip_control.js
new file mode 100644
index 000000000000..f1cb1bdb92de
--- /dev/null
+++ b/xmodule/assets/video/public/js/09_play_skip_control.js
@@ -0,0 +1,86 @@
+'use strict';
+
+/**
+ * Play/skip control module.
+ * @exports video/09_play_skip_control.js
+ * @constructor
+ * @param {Object} state The object containing the state of the video
+ * @param {Object} i18n The object containing strings with translations.
+ * @return {jquery Promise}
+ */
+let PlaySkipControl = function(state, i18n) {
+ if (!(this instanceof PlaySkipControl)) {
+ return new PlaySkipControl(state, i18n);
+ }
+
+ _.bindAll(this, 'play', 'onClick', 'destroy');
+ this.state = state;
+ this.state.videoPlaySkipControl = this;
+ this.i18n = i18n;
+ this.initialize();
+
+ return $.Deferred().resolve().promise();
+};
+
+PlaySkipControl.prototype = {
+ template: [
+ '',
+ ' ',
+ ' '
+ ].join(''),
+
+ destroy: function() {
+ this.el.remove();
+ this.state.el.off('destroy', this.destroy);
+ delete this.state.videoPlaySkipControl;
+ },
+
+ /** Initializes the module. */
+ initialize: function() {
+ this.el = $(this.template);
+ this.render();
+ this.bindHandlers();
+ },
+
+ /**
+ * Creates any necessary DOM elements, attach them, and set their,
+ * initial configuration.
+ */
+ render: function() {
+ this.state.el.find('.vcr').prepend(this.el);
+ },
+
+ /** Bind any necessary function callbacks to DOM events. */
+ bindHandlers: function() {
+ this.el.on('click', this.onClick);
+ this.state.el.on({
+ play: this.play,
+ destroy: this.destroy
+ });
+ },
+
+ onClick: function(event) {
+ event.preventDefault();
+ if (this.state.videoPlayer.isPlaying()) {
+ this.state.videoCommands.execute('skip');
+ } else {
+ this.state.videoCommands.execute('play');
+ }
+ },
+
+ play: function() {
+ this.el
+ .removeClass('play')
+ .addClass('skip')
+ .attr('title', gettext('Skip'))
+ .find('.icon')
+ .removeClass('fa-play')
+ .addClass('fa-step-forward');
+ // Disable possibility to pause the video.
+ this.state.el.find('video').off('click');
+ }
+};
+
+export default PlaySkipControl;
diff --git a/xmodule/assets/video/public/js/09_poster.js b/xmodule/assets/video/public/js/09_poster.js
new file mode 100644
index 000000000000..97e0cf388fd2
--- /dev/null
+++ b/xmodule/assets/video/public/js/09_poster.js
@@ -0,0 +1,62 @@
+'use strict';
+
+import _ from 'underscore';
+
+
+let VideoPoster = function(element, options) {
+ if (!(this instanceof VideoPoster)) {
+ return new VideoPoster(element, options);
+ }
+
+ _.bindAll(this, 'onClick', 'destroy');
+ this.element = element;
+ this.container = element.find('.video-player');
+ this.options = options || {};
+ this.initialize();
+};
+
+VideoPoster.moduleName = 'Poster';
+VideoPoster.prototype = {
+ template: _.template([
+ ')">',
+ '
',
+ ' ',
+ '', gettext('Play video'), ' ',
+ ' ',
+ '
'
+ ].join('')),
+
+ initialize: function() {
+ this.el = $(this.template({
+ url: this.options.poster.url,
+ type: this.options.poster.type
+ }));
+ this.element.addClass('is-pre-roll');
+ this.render();
+ this.bindHandlers();
+ },
+
+ bindHandlers: function() {
+ this.el.on('click', this.onClick);
+ this.element.on('destroy', this.destroy);
+ },
+
+ render: function() {
+ this.container.append(this.el);
+ },
+
+ onClick: function() {
+ if (_.isFunction(this.options.onClick)) {
+ this.options.onClick();
+ }
+ this.destroy();
+ },
+
+ destroy: function() {
+ this.element.off('destroy', this.destroy).removeClass('is-pre-roll');
+ this.el.remove();
+ }
+};
+
+export default VideoPoster;
diff --git a/xmodule/assets/video/public/js/09_save_state_plugin.js b/xmodule/assets/video/public/js/09_save_state_plugin.js
new file mode 100644
index 000000000000..c5de62628d9d
--- /dev/null
+++ b/xmodule/assets/video/public/js/09_save_state_plugin.js
@@ -0,0 +1,131 @@
+'use strict';
+
+import _ from 'underscore';
+import { convert, format, formatFull } from './utils/time.js';
+
+
+/**
+ * Save state module.
+ * @exports video/09_save_state_plugin.js
+ * @constructor
+ * @param {Object} state The object containing the state of the video
+ * @param {Object} i18n The object containing strings with translations.
+ * @param {Object} options
+ * @return {jquery Promise}
+ */
+let SaveStatePlugin = function(state, i18n, options) {
+ if (!(this instanceof SaveStatePlugin)) {
+ return new SaveStatePlugin(state, i18n, options);
+ }
+
+ _.bindAll(this, 'onSpeedChange', 'onAutoAdvanceChange', 'saveStateHandler', 'bindUnloadHandler', 'onUnload',
+ 'onYoutubeAvailability', 'onLanguageChange', 'destroy');
+ this.state = state;
+ this.options = _.extend({events: []}, options);
+ this.state.videoSaveStatePlugin = this;
+ this.i18n = i18n;
+ this.initialize();
+
+ return $.Deferred().resolve().promise();
+};
+
+SaveStatePlugin.moduleName = 'SaveStatePlugin';
+SaveStatePlugin.prototype = {
+ destroy: function() {
+ this.state.el.off(this.events).off('destroy', this.destroy);
+ $(window).off('unload', this.onUnload);
+ delete this.state.videoSaveStatePlugin;
+ },
+
+ initialize: function() {
+ this.events = {
+ speedchange: this.onSpeedChange,
+ autoadvancechange: this.onAutoAdvanceChange,
+ play: this.bindUnloadHandler,
+ 'pause destroy': this.saveStateHandler,
+ 'language_menu:change': this.onLanguageChange,
+ youtube_availability: this.onYoutubeAvailability
+ };
+ this.bindHandlers();
+ },
+
+ bindHandlers: function() {
+ if (this.options.events.length) {
+ _.each(this.options.events, function(eventName) {
+ let callback;
+ if (_.has(this.events, eventName)) {
+ callback = this.events[eventName];
+ this.state.el.on(eventName, callback);
+ }
+ }, this);
+ } else {
+ this.state.el.on(this.events);
+ }
+ this.state.el.on('destroy', this.destroy);
+ },
+
+ bindUnloadHandler: _.once(function() {
+ $(window).on('unload.video', this.onUnload);
+ }),
+
+ onSpeedChange: function(event, newSpeed) {
+ this.saveState(true, {speed: newSpeed});
+ this.state.storage.setItem('speed', newSpeed, true);
+ this.state.storage.setItem('general_speed', newSpeed);
+ },
+
+ onAutoAdvanceChange: function(event, enabled) {
+ this.saveState(true, {auto_advance: enabled});
+ this.state.storage.setItem('auto_advance', enabled);
+ },
+
+ saveStateHandler: function() {
+ this.saveState(true);
+ },
+
+ onUnload: function() {
+ this.saveState();
+ },
+
+ onLanguageChange: function(event, langCode) {
+ this.state.storage.setItem('language', langCode);
+ },
+
+ onYoutubeAvailability: function(event, youtubeIsAvailable) {
+ // Compare what the client-side code has determined Youtube
+ // availability to be (true/false) vs. what the LMS recorded for
+ // this user. The LMS will assume YouTube is available by default.
+ if (youtubeIsAvailable !== this.state.config.recordedYoutubeIsAvailable) {
+ this.saveState(true, {youtube_is_available: youtubeIsAvailable});
+ }
+ },
+
+ saveState: function(async, data) {
+ if (this.state.config.saveStateEnabled) {
+ if (!($.isPlainObject(data))) {
+ data = {
+ saved_video_position: this.state.videoPlayer.currentTime
+ };
+ }
+
+ if (data.speed) {
+ this.state.storage.setItem('speed', data.speed, true);
+ }
+
+ if (_.has(data, 'saved_video_position')) {
+ this.state.storage.setItem('savedVideoPosition', data.saved_video_position, true);
+ data.saved_video_position = formatFull(data.saved_video_position);
+ }
+
+ $.ajax({
+ url: this.state.config.saveStateUrl,
+ type: 'POST',
+ async: !!async,
+ dataType: 'json',
+ data: data
+ });
+ }
+ }
+};
+
+export default SaveStatePlugin;
diff --git a/xmodule/assets/video/public/js/09_skip_control.js b/xmodule/assets/video/public/js/09_skip_control.js
new file mode 100644
index 000000000000..17138e5f8882
--- /dev/null
+++ b/xmodule/assets/video/public/js/09_skip_control.js
@@ -0,0 +1,72 @@
+'use strict';
+
+import _ from 'underscore';
+
+
+/**
+ * Video skip control module.
+ * @exports video/09_skip_control.js
+ * @constructor
+ * @param {Object} state The object containing the state of the video
+ * @param {Object} i18n The object containing strings with translations.
+ * @return {jquery Promise}
+ */
+let SkipControl = function(state, i18n) {
+ if (!(this instanceof SkipControl)) {
+ return new SkipControl(state, i18n);
+ }
+
+ _.bindAll(this, 'onClick', 'render', 'destroy');
+ this.state = state;
+ this.state.videoSkipControl = this;
+ this.i18n = i18n;
+ this.initialize();
+
+ return $.Deferred().resolve().promise();
+};
+
+SkipControl.prototype = {
+ template: [
+ '',
+ ' ',
+ ' '
+ ].join(''),
+
+ destroy: function() {
+ this.el.remove();
+ this.state.el.off('.skip');
+ delete this.state.videoSkipControl;
+ },
+
+ /** Initializes the module. */
+ initialize: function() {
+ this.el = $(this.template);
+ this.bindHandlers();
+ },
+
+ /**
+ * Creates any necessary DOM elements, attach them, and set their,
+ * initial configuration.
+ */
+ render: function() {
+ this.state.el.find('.vcr .control').after(this.el);
+ },
+
+ /** Bind any necessary function callbacks to DOM events. */
+ bindHandlers: function() {
+ this.el.on('click', this.onClick);
+ this.state.el.on({
+ 'play.skip': _.once(this.render),
+ 'destroy.skip': this.destroy
+ });
+ },
+
+ onClick: function(event) {
+ event.preventDefault();
+ this.state.videoCommands.execute('skip', true);
+ }
+};
+
+export default SkipControl;
diff --git a/xmodule/assets/video/public/js/09_video_caption.js b/xmodule/assets/video/public/js/09_video_caption.js
new file mode 100644
index 000000000000..309abb70e8ea
--- /dev/null
+++ b/xmodule/assets/video/public/js/09_video_caption.js
@@ -0,0 +1,1459 @@
+// VideoCaption module.
+
+import Sjson from './00_sjson.js';
+import AsyncProcess from './00_async_process.js';
+import HtmlUtils from 'edx-ui-toolkit/js/utils/html-utils';
+import Draggabilly from 'draggabilly';
+import { convert } from './utils/time.js';
+import _ from 'underscore';
+
+'use strict';
+
+/**
+ * @desc VideoCaption module exports a function.
+ *
+ * @type {function}
+ * @access public
+ *
+ * @param {object} state - The object containing the state of the video
+ * player. All other modules, their parameters, public variables, etc.
+ * are available via this object.
+ *
+ * @this {object} The global window object.
+ *
+ * @returns {jquery Promise}
+ */
+let VideoCaption = function(state) {
+ if (!(this instanceof VideoCaption)) {
+ return new VideoCaption(state);
+ }
+
+ _.bindAll(this, 'toggleTranscript', 'onMouseEnter', 'onMouseLeave', 'onMovement',
+ 'onContainerMouseEnter', 'onContainerMouseLeave', 'fetchCaption',
+ 'onResize', 'pause', 'play', 'onCaptionUpdate', 'onCaptionHandler', 'destroy',
+ 'handleKeypress', 'handleKeypressLink', 'openLanguageMenu', 'closeLanguageMenu',
+ 'previousLanguageMenuItem', 'nextLanguageMenuItem', 'handleCaptionToggle',
+ 'showClosedCaptions', 'hideClosedCaptions', 'toggleClosedCaptions',
+ 'updateCaptioningCookie', 'handleCaptioningCookie', 'handleTranscriptToggle',
+ 'listenForDragDrop', 'setTranscriptVisibility', 'updateTranscriptCookie',
+ 'updateGoogleDisclaimer', 'toggleGoogleDisclaimer', 'updateProblematicCaptionsContent'
+ );
+
+ this.state = state;
+ this.state.videoCaption = this;
+ this.renderElements();
+ this.handleCaptioningCookie();
+ this.setTranscriptVisibility();
+ this.listenForDragDrop();
+
+ return $.Deferred().resolve().promise();
+};
+
+VideoCaption.prototype = {
+
+ destroy: function() {
+ this.state.el
+ .off({
+ 'caption:fetch': this.fetchCaption,
+ 'caption:resize': this.onResize,
+ 'caption:update': this.onCaptionUpdate,
+ ended: this.pause,
+ fullscreen: this.onResize,
+ pause: this.pause,
+ play: this.play,
+ destroy: this.destroy
+ })
+ .removeClass('is-captions-rendered');
+ if (this.fetchXHR && this.fetchXHR.abort) {
+ this.fetchXHR.abort();
+ }
+ if (this.availableTranslationsXHR && this.availableTranslationsXHR.abort) {
+ this.availableTranslationsXHR.abort();
+ }
+ this.subtitlesEl.remove();
+ this.container.remove();
+ delete this.state.videoCaption;
+ },
+ /**
+ * @desc Initiate rendering of elements, and set their initial configuration.
+ *
+ */
+ renderElements: function() {
+ let languages = this.state.config.transcriptLanguages;
+
+ let langHtml = HtmlUtils.interpolateHtml(
+ HtmlUtils.HTML(
+ [
+ '',
+ '',
+ ' ',
+ ' ',
+ '',
+ ' ',
+ ' ',
+ '',
+ '
'
+ ].join('')),
+ {
+ langTitle: gettext('Open language menu'),
+ courseId: this.state.id
+ }
+ );
+
+ let subtitlesHtml = HtmlUtils.interpolateHtml(
+ HtmlUtils.HTML(
+ [
+ '',
+ '
',
+ '',
+ ''
+ ].join('')),
+ {
+ courseId: this.state.id,
+ courseLang: this.state.lang
+ }
+ );
+
+ this.loaded = false;
+ this.subtitlesEl = $(HtmlUtils.ensureHtml(subtitlesHtml).toString());
+ this.subtitlesMenuEl = this.subtitlesEl.find('.subtitles-menu');
+ this.container = $(HtmlUtils.ensureHtml(langHtml).toString());
+ this.captionControlEl = this.container.find('.toggle-captions');
+ this.captionDisplayEl = this.state.el.find('.closed-captions');
+ this.transcriptControlEl = this.container.find('.toggle-transcript');
+ this.languageChooserEl = this.container.find('.lang');
+ this.menuChooserEl = this.languageChooserEl.parent();
+
+ if (_.keys(languages).length) {
+ this.renderLanguageMenu(languages);
+ this.fetchCaption();
+ }
+ },
+
+ /**
+ * @desc Bind any necessary function callbacks to DOM events (click,
+ * mousemove, etc.).
+ *
+ */
+ bindHandlers: function() {
+ let state = this.state,
+ events = [
+ 'mouseover', 'mouseout', 'mousedown', 'click', 'focus', 'blur',
+ 'keydown'
+ ].join(' ');
+
+ this.captionControlEl.on({
+ click: this.toggleClosedCaptions,
+ keydown: this.handleCaptionToggle
+ });
+ this.transcriptControlEl.on({
+ click: this.toggleTranscript,
+ keydown: this.handleTranscriptToggle
+ });
+ this.subtitlesMenuEl.on({
+ mouseenter: this.onMouseEnter,
+ mouseleave: this.onMouseLeave,
+ mousemove: this.onMovement,
+ mousewheel: this.onMovement,
+ DOMMouseScroll: this.onMovement
+ })
+ .on(events, 'span[data-index]', this.onCaptionHandler);
+ this.container.on({
+ mouseenter: this.onContainerMouseEnter,
+ mouseleave: this.onContainerMouseLeave
+ });
+
+ if (this.showLanguageMenu) {
+ this.languageChooserEl.on({
+ keydown: this.handleKeypress
+ }, '.language-menu');
+
+ this.languageChooserEl.on({
+ keydown: this.handleKeypressLink
+ }, '.control-lang');
+ }
+
+ state.el
+ .on({
+ 'caption:fetch': this.fetchCaption,
+ 'caption:resize': this.onResize,
+ 'caption:update': this.onCaptionUpdate,
+ ended: this.pause,
+ fullscreen: this.onResize,
+ pause: this.pause,
+ play: this.play,
+ destroy: this.destroy
+ });
+
+ if ((state.videoType === 'html5') && (state.config.autohideHtml5)) {
+ this.subtitlesMenuEl.on('scroll', state.videoControl.showControls);
+ }
+ },
+
+ onCaptionUpdate: function(event, time) {
+ this.updatePlayTime(time);
+ },
+
+ handleCaptionToggle: function(event) {
+ let KEY = $.ui.keyCode,
+ keyCode = event.keyCode;
+
+ switch (keyCode) {
+ case KEY.SPACE:
+ case KEY.ENTER:
+ event.preventDefault();
+ this.toggleClosedCaptions(event);
+ // no default
+ }
+ },
+
+ handleTranscriptToggle: function(event) {
+ let KEY = $.ui.keyCode,
+ keyCode = event.keyCode;
+
+ switch (keyCode) {
+ case KEY.SPACE:
+ case KEY.ENTER:
+ event.preventDefault();
+ this.toggleTranscript(event);
+ // no default
+ }
+ },
+
+ handleKeypressLink: function(event) {
+ let KEY = $.ui.keyCode,
+ keyCode = event.keyCode,
+ focused, index, total;
+
+ switch (keyCode) {
+ case KEY.UP:
+ event.preventDefault();
+ focused = $(':focus').parent();
+ index = this.languageChooserEl.find('li').index(focused);
+ total = this.languageChooserEl.find('li').size() - 1;
+
+ this.previousLanguageMenuItem(event, index);
+ break;
+
+ case KEY.DOWN:
+ event.preventDefault();
+ focused = $(':focus').parent();
+ index = this.languageChooserEl.find('li').index(focused);
+ total = this.languageChooserEl.find('li').size() - 1;
+
+ this.nextLanguageMenuItem(event, index, total);
+ break;
+
+ case KEY.ESCAPE:
+ this.closeLanguageMenu(event);
+ break;
+
+ case KEY.ENTER:
+ case KEY.SPACE:
+ return true;
+ // no default
+ }
+ return true;
+ },
+
+ handleKeypress: function(event) {
+ let KEY = $.ui.keyCode,
+ keyCode = event.keyCode;
+
+ switch (keyCode) {
+ // Handle keypresses
+ case KEY.ENTER:
+ case KEY.SPACE:
+ case KEY.UP:
+ event.preventDefault();
+ this.openLanguageMenu(event);
+ break;
+
+ case KEY.ESCAPE:
+ this.closeLanguageMenu(event);
+ break;
+ // no default
+ }
+
+ return event.keyCode === KEY.TAB;
+ },
+
+ nextLanguageMenuItem: function(event, index, total) {
+ event.preventDefault();
+
+ if (event.altKey || event.shiftKey) {
+ return true;
+ }
+
+ if (index === total) {
+ this.languageChooserEl
+ .find('.control-lang').first()
+ .focus();
+ } else {
+ this.languageChooserEl
+ .find('li:eq(' + index + ')')
+ .next()
+ .find('.control-lang')
+ .focus();
+ }
+
+ return false;
+ },
+
+ previousLanguageMenuItem: function(event, index) {
+ event.preventDefault();
+
+ if (event.altKey || event.shiftKey) {
+ return true;
+ }
+
+ if (index === 0) {
+ this.languageChooserEl
+ .find('.control-lang').last()
+ .focus();
+ } else {
+ this.languageChooserEl
+ .find('li:eq(' + index + ')')
+ .prev()
+ .find('.control-lang')
+ .focus();
+ }
+
+ return false;
+ },
+
+ openLanguageMenu: function(event) {
+ let button = this.languageChooserEl,
+ menu = button.parent().find('.menu');
+
+ event.preventDefault();
+
+ button
+ .addClass('is-opened');
+
+ menu
+ .find('.control-lang').last()
+ .focus();
+ },
+
+ closeLanguageMenu: function(event) {
+ let button = this.languageChooserEl;
+ event.preventDefault();
+
+ button
+ .removeClass('is-opened')
+ .find('.language-menu')
+ .focus();
+ },
+
+ onCaptionHandler: function(event) {
+ switch (event.type) {
+ case 'mouseover':
+ case 'mouseout':
+ this.captionMouseOverOut(event);
+ break;
+ case 'mousedown':
+ this.captionMouseDown(event);
+ break;
+ case 'click':
+ this.captionClick(event);
+ break;
+ case 'focusin':
+ this.captionFocus(event);
+ break;
+ case 'focusout':
+ this.captionBlur(event);
+ break;
+ case 'keydown':
+ this.captionKeyDown(event);
+ break;
+ // no default
+ }
+ },
+
+ /**
+ * @desc Opens language menu.
+ *
+ * @param {jquery Event} event
+ */
+ onContainerMouseEnter: function(event) {
+ event.preventDefault();
+ $(event.currentTarget).find('.lang').addClass('is-opened');
+
+ // We only want to fire the analytics event if a menu is
+ // present instead of on the container hover, since it wraps
+ // the "CC" and "Transcript" buttons as well.
+ if ($(event.currentTarget).find('.lang').length) {
+ this.state.el.trigger('language_menu:show');
+ }
+ },
+
+ /**
+ * @desc Closes language menu.
+ *
+ * @param {jquery Event} event
+ */
+ onContainerMouseLeave: function(event) {
+ event.preventDefault();
+ $(event.currentTarget).find('.lang').removeClass('is-opened');
+
+ // We only want to fire the analytics event if a menu is
+ // present instead of on the container hover, since it wraps
+ // the "CC" and "Transcript" buttons as well.
+ if ($(event.currentTarget).find('.lang').length) {
+ this.state.el.trigger('language_menu:hide');
+ }
+ },
+
+ /**
+ * @desc Freezes moving of captions when mouse is over them.
+ *
+ * @param {jquery Event} event
+ */
+ onMouseEnter: function() {
+ if (this.frozen) {
+ clearTimeout(this.frozen);
+ }
+
+ this.frozen = setTimeout(
+ this.onMouseLeave,
+ this.state.config.captionsFreezeTime
+ );
+ },
+
+ /**
+ * @desc Unfreezes moving of captions when mouse go out.
+ *
+ * @param {jquery Event} event
+ */
+ onMouseLeave: function() {
+ if (this.frozen) {
+ clearTimeout(this.frozen);
+ }
+
+ this.frozen = null;
+
+ if (this.playing) {
+ this.scrollCaption();
+ }
+ },
+
+ /**
+ * @desc Freezes moving of captions when mouse is moving over them.
+ *
+ * @param {jquery Event} event
+ */
+ onMovement: function() {
+ this.onMouseEnter();
+ },
+
+ /**
+ * @desc Gets the correct start and end times from the state configuration
+ *
+ * @returns {array} if [startTime, endTime] are defined
+ */
+ getStartEndTimes: function() {
+ // due to the way config.startTime/endTime are
+ // processed in 03_video_player.js, we assume
+ // endTime can be an integer or null,
+ // and startTime is an integer > 0
+ let config = this.state.config;
+ let startTime = config.startTime * 1000;
+ let endTime = (config.endTime !== null) ? config.endTime * 1000 : null;
+ return [startTime, endTime];
+ },
+
+ /**
+ * @desc Gets captions within the start / end times stored within this.state.config
+ *
+ * @returns {object} {start, captions} parallel arrays of
+ * start times and corresponding captions
+ */
+ getBoundedCaptions: function() {
+ // get start and caption. If startTime and endTime
+ // are specified, filter by that range.
+ let times = this.getStartEndTimes();
+ // eslint-disable-next-line prefer-spread
+ let results = this.sjson.filter.apply(this.sjson, times);
+ let start = results.start;
+ let captions = results.captions;
+
+ return {
+ start: start,
+ captions: captions
+ };
+ },
+
+ /**
+ * @desc Sets whether or not the Google disclaimer should be shown based on captions
+ * being AI generated, and shows/hides based on the above and if ClosedCaptions are being shown.
+ *
+ * @param {array} captions List of captions for the video.
+ *
+ * @returns {boolean}
+ */
+ updateGoogleDisclaimer: function(captions) {
+ const aIGeneratedSpanText = '\w+)["']/;
+ let self = this,
+ state = this.state,
+ aiGeneratedSpan = captions.find(caption => caption.includes(aIGeneratedSpanText)),
+ captionsAIGenerated = !(aiGeneratedSpan === undefined),
+ aiCaptionProviderIsGoogle = true;
+
+ if (captionsAIGenerated) {
+ const providerMatch = aiProviderRegexp.exec(aiGeneratedSpan);
+ if (providerMatch !== null) {
+ aiCaptionProviderIsGoogle = providerMatch.groups['provider'] === 'gcp';
+ }
+ // If there is no provider tag, it was generated before we added those,
+ // so it must be Google
+ }
+ // This field is whether or not, in general, this video should show the google disclaimer
+ self.shouldShowGoogleDisclaimer = captionsAIGenerated && aiCaptionProviderIsGoogle;
+ // Should we, right now, on load, show the google disclaimer
+ self.toggleGoogleDisclaimer(!self.hideCaptionsOnLoad && !state.captionsHidden);
+ },
+
+ /**
+ * @desc Show or hide the google translate disclaimer based on the passed param
+ * and whether or not we are currently showing a google translated transcript.
+ * @param {boolean} [show] Show if true, hide if false - if we are showing a google
+ * translated transcript. If not, this will always hide.
+ */
+ toggleGoogleDisclaimer: function(show) {
+ let self = this,
+ state = this.state;
+ if (show && self.shouldShowGoogleDisclaimer) {
+ state.el.find('.google-disclaimer').show();
+ } else {
+ state.el.find('.google-disclaimer').hide();
+ }
+ },
+
+ /**
+ * @desc Replaces content in a caption
+ *
+ * @param {array} captions List of captions for the video.
+ * @param {string} content content to be replaced
+ * @param {string} replacementContent the replace string
+ *
+ * @returns {array} captions List of captions for the video.
+ */
+ updateProblematicCaptionsContent: function(captions, content = '', replacementContent = '') {
+ let updatedCaptions = captions.map(caption => caption.replace(content, replacementContent));
+
+ return updatedCaptions;
+ },
+
+ /**
+ * @desc Fetch the caption file specified by the user. Upon successful
+ * receipt of the file, the captions will be rendered.
+ * @param {boolean} [fetchWithYoutubeId] Fetch youtube captions if true.
+ * @returns {boolean}
+ * true: The user specified a caption file. NOTE: if an error happens
+ * while the specified file is being retrieved (for example the
+ * file is missing on the server), this function will still return
+ * true.
+ * false: No caption file was specified, or an empty string was
+ * specified for the Youtube type player.
+ */
+ fetchCaption: function(fetchWithYoutubeId) {
+ let self = this,
+ state = this.state,
+ language = state.getCurrentLanguage(),
+ url = state.config.transcriptTranslationUrl.replace('__lang__', language),
+ data, youtubeId;
+
+ if (this.loaded) {
+ this.hideCaptions(false);
+ }
+
+ if (this.fetchXHR && this.fetchXHR.abort) {
+ this.fetchXHR.abort();
+ }
+
+ if (state.videoType === 'youtube' || fetchWithYoutubeId) {
+ try {
+ youtubeId = state.youtubeId('1.0');
+ } catch (err) {
+ youtubeId = null;
+ }
+
+ if (!youtubeId) {
+ return false;
+ }
+
+ data = {videoId: youtubeId};
+ }
+
+ state.el.removeClass('is-captions-rendered');
+ // Fetch the captions file. If no file was specified, or if an error
+ // occurred, then we hide the captions panel, and the "Transcript" button
+ this.fetchXHR = $.ajaxWithPrefix({
+ url: url,
+ notifyOnError: false,
+ data: data,
+ success: function(sjson) {
+ let results, start, captions;
+ self.sjson = new Sjson(sjson);
+ results = self.getBoundedCaptions();
+ start = results.start;
+ captions = results.captions;
+ let contentToReplace = CAPTIONS_CONTENT_TO_REPLACE,
+ replacementContent = CAPTIONS_CONTENT_REPLACEMENT;
+
+ captions = self.updateProblematicCaptionsContent(captions, contentToReplace, replacementContent);
+
+ self.updateGoogleDisclaimer(captions);
+
+ if (self.loaded) {
+ if (self.rendered) {
+ self.renderCaption(start, captions);
+ self.updatePlayTime(state.videoPlayer.currentTime);
+ }
+ } else {
+ if (state.isTouch) {
+ HtmlUtils.setHtml(
+ self.subtitlesEl.find('.subtitles-menu'),
+ HtmlUtils.joinHtml(
+ HtmlUtils.HTML(''),
+ gettext('Transcript will be displayed when you start playing the video.'),
+ HtmlUtils.HTML(' ')
+ )
+ );
+ } else {
+ self.renderCaption(start, captions);
+ }
+ self.hideCaptions(self.hideCaptionsOnLoad);
+ HtmlUtils.append(
+ self.state.el.find('.video-wrapper').parent(),
+ HtmlUtils.HTML(self.subtitlesEl)
+ );
+ HtmlUtils.append(
+ self.state.el.find('.secondary-controls'),
+ HtmlUtils.HTML(self.container)
+ );
+ self.bindHandlers();
+ }
+
+ self.loaded = true;
+ },
+ error: function(jqXHR, textStatus, errorThrown) {
+ let canFetchWithYoutubeId;
+ console.log('[Video info]: ERROR while fetching captions.');
+ console.log(
+ '[Video info]: STATUS:', textStatus
+ + ', MESSAGE:', '' + errorThrown
+ );
+ // If initial list of languages has more than 1 item, check
+ // for availability other transcripts.
+ // If player mode is html5 and there are no initial languages
+ // then try to fetch youtube version of transcript with
+ // youtubeId.
+ if (_.keys(state.config.transcriptLanguages).length > 1) {
+ self.fetchAvailableTranslations();
+ } else if (!fetchWithYoutubeId && state.videoType === 'html5') {
+ canFetchWithYoutubeId = self.fetchCaption(true);
+ if (canFetchWithYoutubeId) {
+ console.log('[Video info]: Html5 mode fetching caption with youtubeId.'); // eslint-disable-line max-len, no-console
+ } else {
+ self.hideCaptions(true);
+ self.languageChooserEl.hide();
+ self.hideClosedCaptions();
+ }
+ } else {
+ self.hideCaptions(true);
+ self.languageChooserEl.hide();
+ self.hideClosedCaptions();
+ }
+ }
+ });
+
+ return true;
+ },
+
+ /**
+ * @desc Fetch the list of available language codes. Upon successful receipt
+ * the list of available languages will be updated.
+ *
+ * @returns {jquery Promise}
+ */
+ fetchAvailableTranslations: function() {
+ let self = this,
+ state = this.state;
+
+ this.availableTranslationsXHR = $.ajaxWithPrefix({
+ url: state.config.transcriptAvailableTranslationsUrl,
+ notifyOnError: false,
+ success: function(response) {
+ let currentLanguages = state.config.transcriptLanguages,
+ newLanguages = _.pick(currentLanguages, response);
+
+ // Update property with available currently translations.
+ state.config.transcriptLanguages = newLanguages;
+ // Remove an old language menu.
+ self.container.find('.langs-list').remove();
+
+ if (_.keys(newLanguages).length) {
+ self.renderLanguageMenu(newLanguages);
+ }
+ },
+ error: function() {
+ self.hideCaptions(true);
+ self.languageChooserEl.hide();
+ }
+ });
+
+ return this.availableTranslationsXHR;
+ },
+
+ /**
+ * @desc Recalculates and updates the height of the container of captions.
+ *
+ */
+ onResize: function() {
+ this.subtitlesEl
+ .find('.spacing').first()
+ .height(this.topSpacingHeight());
+
+ this.subtitlesEl
+ .find('.spacing').last()
+ .height(this.bottomSpacingHeight());
+
+ this.scrollCaption();
+ this.setSubtitlesHeight();
+ },
+
+ /**
+ * @desc Create any necessary DOM elements, attach them, and set their
+ * initial configuration for the Language menu.
+ *
+ * @param {object} languages Dictionary where key is language code,
+ * value - language label
+ *
+ */
+ renderLanguageMenu: function(languages) {
+ let self = this,
+ state = this.state,
+ $menu = $('