diff --git a/openedx/core/djangoapps/notifications/templates/notifications/email_digest_preference_update.html b/openedx/core/djangoapps/notifications/templates/notifications/email_digest_preference_update.html
new file mode 100644
index 000000000000..79e88012f313
--- /dev/null
+++ b/openedx/core/djangoapps/notifications/templates/notifications/email_digest_preference_update.html
@@ -0,0 +1,13 @@
+
+
+
+
+ {{ _("Email Digest Preferences Updated") }}
+
+
+
+
+
diff --git a/openedx/core/djangoapps/notifications/tests/test_filters.py b/openedx/core/djangoapps/notifications/tests/test_filters.py
index 4391d48852d0..c8e0bdfccfb7 100644
--- a/openedx/core/djangoapps/notifications/tests/test_filters.py
+++ b/openedx/core/djangoapps/notifications/tests/test_filters.py
@@ -28,8 +28,8 @@
CourseRoleAudienceFilter,
CohortAudienceFilter,
TeamAudienceFilter,
+ NotificationFilter,
)
-from openedx.core.djangoapps.notifications.filters import NotificationFilter
from openedx.core.djangoapps.notifications.handlers import calculate_course_wide_notification_audience
from openedx.features.course_duration_limits.models import CourseDurationLimitConfig
from openedx.features.course_experience.tests.views.helpers import add_course_mode
@@ -96,7 +96,8 @@ def test_audit_expired_filter_with_no_role(
@mock.patch("openedx.core.djangoapps.course_date_signals.utils.get_course_run_details")
@mock.patch(
- "openedx.core.djangoapps.notifications.filters.NotificationFilter.filter_audit_expired_users_with_no_role")
+ "openedx.core.djangoapps.notifications.audience_filters.NotificationFilter"
+ ".filter_audit_expired_users_with_no_role")
def test_apply_filter(
self,
mock_filter_audit_expired,
diff --git a/openedx/core/djangoapps/notifications/tests/test_tasks.py b/openedx/core/djangoapps/notifications/tests/test_tasks.py
index 036d4d326198..706ae2989842 100644
--- a/openedx/core/djangoapps/notifications/tests/test_tasks.py
+++ b/openedx/core/djangoapps/notifications/tests/test_tasks.py
@@ -2,12 +2,12 @@
Tests for notifications tasks.
"""
+import datetime
from unittest.mock import patch
-import datetime
import ddt
-from django.core.exceptions import ValidationError
from django.conf import settings
+from django.core.exceptions import ValidationError
from edx_toggles.toggles.testutils import override_waffle_flag
from common.djangoapps.student.models import CourseEnrollment
@@ -15,7 +15,6 @@
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory
-from .utils import create_notification
from ..config.waffle import ENABLE_NOTIFICATIONS
from ..models import CourseNotificationPreference, Notification
from ..tasks import (
@@ -24,6 +23,7 @@
send_notifications,
update_user_preference
)
+from .utils import create_notification
@patch('openedx.core.djangoapps.notifications.models.COURSE_NOTIFICATION_CONFIG_VERSION', 1)
@@ -225,7 +225,6 @@ def test_notification_not_created_when_context_is_incomplete(self):
@ddt.ddt
-@patch('openedx.core.djangoapps.notifications.tasks.ENABLE_NOTIFICATIONS_FILTERS.is_enabled', lambda x: False)
class SendBatchNotificationsTest(ModuleStoreTestCase):
"""
Test that notification and notification preferences are created in batches
@@ -255,9 +254,9 @@ def _create_users(self, num_of_users):
@override_waffle_flag(ENABLE_NOTIFICATIONS, active=True)
@ddt.data(
- (settings.NOTIFICATION_CREATION_BATCH_SIZE, 1, 2),
- (settings.NOTIFICATION_CREATION_BATCH_SIZE + 10, 2, 4),
- (settings.NOTIFICATION_CREATION_BATCH_SIZE - 10, 1, 2),
+ (settings.NOTIFICATION_CREATION_BATCH_SIZE, 7, 3),
+ (settings.NOTIFICATION_CREATION_BATCH_SIZE + 10, 9, 6),
+ (settings.NOTIFICATION_CREATION_BATCH_SIZE - 10, 7, 3),
)
@ddt.unpack
def test_notification_is_send_in_batch(self, creation_size, prefs_query_count, notifications_query_count):
@@ -307,7 +306,7 @@ def test_preference_not_created_for_default_off_preference(self):
"username": "Test Author"
}
with override_waffle_flag(ENABLE_NOTIFICATIONS, active=True):
- with self.assertNumQueries(1):
+ with self.assertNumQueries(7):
send_notifications(user_ids, str(self.course.id), notification_app, notification_type,
context, "http://test.url")
@@ -326,7 +325,7 @@ def test_preference_created_for_default_on_preference(self):
"replier_name": "Replier Name"
}
with override_waffle_flag(ENABLE_NOTIFICATIONS, active=True):
- with self.assertNumQueries(3):
+ with self.assertNumQueries(9):
send_notifications(user_ids, str(self.course.id), notification_app, notification_type,
context, "http://test.url")
@@ -391,7 +390,7 @@ def test_app_name_param(self):
"""
assert not Notification.objects.all()
create_notification(self.user, self.course_1.id, app_name='discussion', notification_type='new_comment')
- create_notification(self.user, self.course_1.id, app_name='updates', notification_type='course_update')
+ create_notification(self.user, self.course_1.id, app_name='updates', notification_type='course_updates')
delete_notifications({'app_name': 'discussion'})
assert not Notification.objects.filter(app_name='discussion')
assert Notification.objects.filter(app_name='updates')
diff --git a/openedx/core/djangoapps/notifications/tests/test_views.py b/openedx/core/djangoapps/notifications/tests/test_views.py
index ca5572ed9f55..e40e52078989 100644
--- a/openedx/core/djangoapps/notifications/tests/test_views.py
+++ b/openedx/core/djangoapps/notifications/tests/test_views.py
@@ -7,6 +7,7 @@
import ddt
from django.conf import settings
+from django.test.utils import override_settings
from django.urls import reverse
from edx_toggles.toggles.testutils import override_waffle_flag
from openedx_events.learning.data import CourseData, CourseEnrollmentData, UserData, UserPersonalData
@@ -19,20 +20,21 @@
from common.djangoapps.student.roles import CourseStaffRole
from common.djangoapps.student.tests.factories import UserFactory
from lms.djangoapps.discussion.django_comment_client.tests.factories import RoleFactory
-from lms.djangoapps.discussion.toggles import ENABLE_REPORTED_CONTENT_NOTIFICATIONS
from openedx.core.djangoapps.content.course_overviews.tests.factories import CourseOverviewFactory
from openedx.core.djangoapps.django_comment_common.models import (
FORUM_ROLE_ADMINISTRATOR,
FORUM_ROLE_COMMUNITY_TA,
FORUM_ROLE_MODERATOR
)
-from openedx.core.djangoapps.notifications.config.waffle import (
- ENABLE_COURSEWIDE_NOTIFICATIONS,
- ENABLE_NOTIFICATIONS,
- SHOW_NOTIFICATIONS_TRAY
+from openedx.core.djangoapps.notifications.config.waffle import ENABLE_NOTIFICATIONS
+from openedx.core.djangoapps.notifications.email_notifications import EmailCadence
+from openedx.core.djangoapps.notifications.models import (
+ CourseNotificationPreference,
+ Notification,
+ get_course_notification_preference_config_version
)
-from openedx.core.djangoapps.notifications.models import CourseNotificationPreference, Notification
from openedx.core.djangoapps.notifications.serializers import NotificationCourseEnrollmentSerializer
+from openedx.core.djangoapps.notifications.email.utils import encrypt_object, encrypt_string
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory
@@ -80,26 +82,23 @@ def setUp(self):
)
@override_waffle_flag(ENABLE_NOTIFICATIONS, active=True)
- @ddt.data((False,), (True,))
@ddt.unpack
- def test_course_enrollment_list_view(self, show_notifications_tray):
+ def test_course_enrollment_list_view(self):
"""
Test the CourseEnrollmentListView.
"""
self.client.login(username=self.user.username, password=self.TEST_PASSWORD)
- # Enable or disable the waffle flag based on the test case data
- with override_waffle_flag(SHOW_NOTIFICATIONS_TRAY, active=show_notifications_tray):
- url = reverse('enrollment-list')
- response = self.client.get(url)
+ url = reverse('enrollment-list')
+ response = self.client.get(url)
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- data = response.data['results']
- enrollments = CourseEnrollment.objects.filter(user=self.user, is_active=True)
- expected_data = NotificationCourseEnrollmentSerializer(enrollments, many=True).data
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ data = response.data['results']
+ enrollments = CourseEnrollment.objects.filter(user=self.user, is_active=True)
+ expected_data = NotificationCourseEnrollmentSerializer(enrollments, many=True).data
- self.assertEqual(len(data), 1)
- self.assertEqual(data, expected_data)
- self.assertEqual(response.data['show_preferences'], show_notifications_tray)
+ self.assertEqual(len(data), 1)
+ self.assertEqual(data, expected_data)
+ self.assertEqual(response.data['show_preferences'], True)
def test_course_enrollment_api_permission(self):
"""
@@ -172,7 +171,6 @@ def test_course_enrollment_post_save(self):
@override_waffle_flag(ENABLE_NOTIFICATIONS, active=True)
-@override_waffle_flag(ENABLE_REPORTED_CONTENT_NOTIFICATIONS, active=True)
@ddt.ddt
class UserNotificationPreferenceAPITest(ModuleStoreTestCase):
"""
@@ -281,9 +279,9 @@ def _expected_api_response(self, course=None):
'enabled': True,
'core_notification_types': [],
'notification_types': {
- 'course_update': {
+ 'course_updates': {
'web': True,
- 'email': True,
+ 'email': False,
'push': True,
'email_cadence': 'Daily',
'info': ''
@@ -321,11 +319,6 @@ def _expected_api_response(self, course=None):
}
}
}
- if not ENABLE_COURSEWIDE_NOTIFICATIONS.is_enabled(course.id):
- app_prefs = response['notification_preference_config']['discussion']
- notification_types = app_prefs['notification_types']
- for notification_type in ['new_discussion_post', 'new_question_post']:
- notification_types.pop(notification_type)
return response
def test_get_user_notification_preference_without_login(self):
@@ -336,7 +329,6 @@ def test_get_user_notification_preference_without_login(self):
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
@mock.patch("eventtracking.tracker.emit")
- @override_waffle_flag(ENABLE_COURSEWIDE_NOTIFICATIONS, active=True)
def test_get_user_notification_preference(self, mock_emit):
"""
Test get user notification preference.
@@ -351,7 +343,6 @@ def test_get_user_notification_preference(self, mock_emit):
self.assertEqual(event_name, 'edx.notifications.preferences.viewed')
@mock.patch("eventtracking.tracker.emit")
- @override_waffle_flag(ENABLE_COURSEWIDE_NOTIFICATIONS, active=True)
@mock.patch.dict(COURSE_NOTIFICATION_TYPES, {
**COURSE_NOTIFICATION_TYPES,
**{
@@ -472,207 +463,6 @@ def test_info_is_not_saved_in_json(self):
assert 'info' not in type_prefs.keys()
-@override_waffle_flag(ENABLE_NOTIFICATIONS, active=True)
-@override_waffle_flag(ENABLE_REPORTED_CONTENT_NOTIFICATIONS, active=True)
-@ddt.ddt
-class UserNotificationChannelPreferenceAPITest(ModuleStoreTestCase):
- """
- Test for user notification channel preference API.
- """
-
- def setUp(self):
- super().setUp()
- self.user = UserFactory()
- self.course = CourseFactory.create(
- org='testorg',
- number='testcourse',
- run='testrun'
- )
-
- course_overview = CourseOverviewFactory.create(id=self.course.id, org='AwesomeOrg')
- self.course_enrollment = CourseEnrollment.objects.create(
- user=self.user,
- course=course_overview,
- is_active=True,
- mode='audit'
- )
- self.client = APIClient()
- self.path = reverse('notification-channel-preferences', kwargs={'course_key_string': self.course.id})
-
- enrollment_data = CourseEnrollmentData(
- user=UserData(
- pii=UserPersonalData(
- username=self.user.username,
- email=self.user.email,
- name=self.user.profile.name,
- ),
- id=self.user.id,
- is_active=self.user.is_active,
- ),
- course=CourseData(
- course_key=self.course.id,
- display_name=self.course.display_name,
- ),
- mode=self.course_enrollment.mode,
- is_active=self.course_enrollment.is_active,
- creation_date=self.course_enrollment.created,
- )
- COURSE_ENROLLMENT_CREATED.send_event(
- enrollment=enrollment_data
- )
-
- def _expected_api_response(self, course=None):
- """
- Helper method to return expected API response.
- """
- if course is None:
- course = self.course
- response = {
- 'id': 1,
- 'course_name': 'course-v1:testorg+testcourse+testrun Course',
- 'course_id': 'course-v1:testorg+testcourse+testrun',
- 'notification_preference_config': {
- 'discussion': {
- 'enabled': True,
- 'core_notification_types': [
- 'new_comment_on_response',
- 'new_comment',
- 'new_response',
- 'response_on_followed_post',
- 'comment_on_followed_post',
- 'response_endorsed_on_thread',
- 'response_endorsed'
- ],
- 'notification_types': {
- 'core': {
- 'web': True,
- 'email': True,
- 'push': True,
- 'email_cadence': 'Daily',
- 'info': 'Notifications for responses and comments on your posts, and the ones you’re '
- 'following, including endorsements to your responses and on your posts.'
- },
- 'new_discussion_post': {
- 'web': False,
- 'email': False,
- 'push': False,
- 'email_cadence': 'Daily',
- 'info': ''
- },
- 'new_question_post': {
- 'web': False,
- 'email': False,
- 'push': False,
- 'email_cadence': 'Daily',
- 'info': ''
- },
- 'content_reported': {
- 'web': True,
- 'email': True,
- 'push': True,
- 'email_cadence': 'Daily',
- 'info': ''
- },
- },
- 'non_editable': {
- 'core': ['web']
- }
- },
- 'updates': {
- 'enabled': True,
- 'core_notification_types': [
-
- ],
- 'notification_types': {
- 'course_update': {
- 'web': True,
- 'email': True,
- 'push': True,
- 'email_cadence': 'Daily',
- 'info': ''
- },
- 'core': {
- 'web': True,
- 'email': True,
- 'push': True,
- 'email_cadence': 'Daily',
- 'info': 'Notifications for new announcements and updates from the course team.'
- }
- },
- 'non_editable': {}
- },
- 'grading': {
- 'enabled': True,
- 'core_notification_types': [],
- 'notification_types': {
- 'ora_staff_notification': {
- 'web': False,
- 'email': False,
- 'push': False,
- 'email_cadence': 'Daily',
- 'info': ''
- },
- 'core': {
- 'web': True,
- 'email': True,
- 'push': True,
- 'email_cadence': 'Daily',
- 'info': 'Notifications for submission grading.'
- }
- },
- 'non_editable': {}
- }
- }
- }
- if not ENABLE_COURSEWIDE_NOTIFICATIONS.is_enabled(course.id):
- app_prefs = response['notification_preference_config']['discussion']
- notification_types = app_prefs['notification_types']
- for notification_type in ['new_discussion_post', 'new_question_post']:
- notification_types.pop(notification_type)
- return response
-
- @ddt.data(
- ('discussion', 'web', True, status.HTTP_200_OK),
- ('discussion', 'web', False, status.HTTP_200_OK),
-
- ('invalid_notification_app', 'web', False, status.HTTP_400_BAD_REQUEST),
- ('discussion', 'invalid_notification_channel', False, status.HTTP_400_BAD_REQUEST),
- )
- @ddt.unpack
- @mock.patch("eventtracking.tracker.emit")
- def test_patch_user_notification_preference(
- self, notification_app, notification_channel, value, expected_status, mock_emit,
- ):
- """
- Test update of user notification channel preference.
- """
- self.client.login(username=self.user.username, password=self.TEST_PASSWORD)
- payload = {
- 'notification_app': notification_app,
- 'value': value,
- }
- if notification_channel:
- payload['notification_channel'] = notification_channel
-
- response = self.client.patch(self.path, json.dumps(payload), content_type='application/json')
- self.assertEqual(response.status_code, expected_status)
-
- if expected_status == status.HTTP_200_OK:
- expected_data = self._expected_api_response()
- expected_app_prefs = expected_data['notification_preference_config'][notification_app]
- for notification_type, __ in expected_app_prefs['notification_types'].items():
- non_editable_channels = expected_app_prefs['non_editable'].get(notification_type, [])
- if notification_channel not in non_editable_channels:
- expected_app_prefs['notification_types'][notification_type][notification_channel] = value
- expected_data = remove_notifications_with_visibility_settings(expected_data)
- self.assertEqual(response.data, expected_data)
- event_name, event_data = mock_emit.call_args[0]
- self.assertEqual(event_name, 'edx.notifications.preferences.updated')
- self.assertEqual(event_data['notification_app'], notification_app)
- self.assertEqual(event_data['notification_channel'], notification_channel)
- self.assertEqual(event_data['value'], value)
-
-
@ddt.ddt
class NotificationListAPIViewTest(APITestCase):
"""
@@ -910,24 +700,21 @@ def setUp(self):
Notification.objects.create(user=self.user, app_name='App Name 2', notification_type='Type A')
Notification.objects.create(user=self.user, app_name='App Name 3', notification_type='Type C')
- @ddt.data((False,), (True,))
+ @override_waffle_flag(ENABLE_NOTIFICATIONS, active=True)
@ddt.unpack
- def test_get_unseen_notifications_count_with_show_notifications_tray(self, show_notifications_tray_enabled):
+ def test_get_unseen_notifications_count_with_show_notifications_tray(self):
"""
Test that the endpoint returns the correct count of unseen notifications and show_notifications_tray value.
"""
self.client.login(username=self.user.username, password=self.TEST_PASSWORD)
+ # Make a request to the view
+ response = self.client.get(self.url)
- # Enable or disable the waffle flag based on the test case data
- with override_waffle_flag(SHOW_NOTIFICATIONS_TRAY, active=show_notifications_tray_enabled):
- # Make a request to the view
- response = self.client.get(self.url)
-
- self.assertEqual(response.status_code, 200)
- self.assertEqual(response.data['count'], 4)
- self.assertEqual(response.data['count_by_app_name'], {
- 'App Name 1': 2, 'App Name 2': 1, 'App Name 3': 1, 'discussion': 0, 'updates': 0, 'grading': 0})
- self.assertEqual(response.data['show_notifications_tray'], show_notifications_tray_enabled)
+ self.assertEqual(response.status_code, 200)
+ self.assertEqual(response.data['count'], 4)
+ self.assertEqual(response.data['count_by_app_name'], {
+ 'App Name 1': 2, 'App Name 2': 1, 'App Name 3': 1, 'discussion': 0, 'updates': 0, 'grading': 0})
+ self.assertEqual(response.data['show_notifications_tray'], True)
def test_get_unseen_notifications_count_for_unauthenticated_user(self):
"""
@@ -1105,6 +892,64 @@ def test_mark_notification_read_without_app_name_and_notification_id(self):
self.assertEqual(response.data, {'error': 'Invalid app_name or notification_id.'})
+@ddt.ddt
+class UpdatePreferenceFromEncryptedDataView(ModuleStoreTestCase):
+ """
+ Tests if preference is updated when encrypted url is hit
+ """
+ def setUp(self):
+ """
+ Setup test case
+ """
+ super().setUp()
+ password = 'password'
+ self.user = UserFactory(password=password)
+ self.client.login(username=self.user.username, password=password)
+ self.course = CourseFactory.create(display_name='test course 1', run="Testing_course_1")
+ CourseNotificationPreference(course_id=self.course.id, user=self.user).save()
+
+ @override_settings(LMS_BASE="")
+ @ddt.data('get', 'post')
+ def test_if_preference_is_updated(self, request_type):
+ """
+ Tests if preference is updated when url is hit
+ """
+ user_hash = encrypt_string(self.user.username)
+ patch_hash = encrypt_object({'channel': 'email', 'value': False})
+ url_params = {
+ "username": user_hash,
+ "patch": patch_hash
+ }
+ url = reverse("preference_update_from_encrypted_username_view", kwargs=url_params)
+ func = getattr(self.client, request_type)
+ response = func(url)
+ assert response.status_code == status.HTTP_200_OK
+ preference = CourseNotificationPreference.objects.get(user=self.user, course_id=self.course.id)
+ config = preference.notification_preference_config
+ for app_name, app_prefs in config.items():
+ for type_prefs in app_prefs['notification_types'].values():
+ assert type_prefs['email'] is False
+ assert type_prefs['email_cadence'] == EmailCadence.NEVER
+
+ def test_if_config_version_is_updated(self):
+ """
+ Tests if preference version is updated before applying patch data
+ """
+ preference = CourseNotificationPreference.objects.get(user=self.user, course_id=self.course.id)
+ preference.config_version -= 1
+ preference.save()
+ user_hash = encrypt_string(self.user.username)
+ patch_hash = encrypt_object({'channel': 'email', 'value': False})
+ url_params = {
+ "username": user_hash,
+ "patch": patch_hash
+ }
+ url = reverse("preference_update_from_encrypted_username_view", kwargs=url_params)
+ self.client.get(url)
+ preference = CourseNotificationPreference.objects.get(user=self.user, course_id=self.course.id)
+ assert preference.config_version == get_course_notification_preference_config_version()
+
+
def remove_notifications_with_visibility_settings(expected_response):
"""
Remove notifications with visibility settings from the expected response.
diff --git a/openedx/core/djangoapps/notifications/urls.py b/openedx/core/djangoapps/notifications/urls.py
index 89b04443a581..7f611bc2c4ca 100644
--- a/openedx/core/djangoapps/notifications/urls.py
+++ b/openedx/core/djangoapps/notifications/urls.py
@@ -11,7 +11,8 @@
NotificationCountView,
NotificationListAPIView,
NotificationReadAPIView,
- UserNotificationPreferenceView, UserNotificationChannelPreferenceView,
+ UserNotificationPreferenceView,
+ preference_update_from_encrypted_username_view,
)
router = routers.DefaultRouter()
@@ -24,11 +25,6 @@
UserNotificationPreferenceView.as_view(),
name='notification-preferences'
),
- re_path(
- fr'^channel/configurations/{settings.COURSE_KEY_PATTERN}$',
- UserNotificationChannelPreferenceView.as_view(),
- name='notification-channel-preferences'
- ),
path('', NotificationListAPIView.as_view(), name='notifications-list'),
path('count/', NotificationCountView.as_view(), name='notifications-count'),
path(
@@ -37,7 +33,8 @@
name='mark-notifications-seen'
),
path('read/', NotificationReadAPIView.as_view(), name='notifications-read'),
-
+ path('preferences/update///', preference_update_from_encrypted_username_view,
+ name='preference_update_from_encrypted_username_view'),
]
urlpatterns += router.urls
diff --git a/openedx/core/djangoapps/notifications/utils.py b/openedx/core/djangoapps/notifications/utils.py
index 5ce592bb413d..249eaf874995 100644
--- a/openedx/core/djangoapps/notifications/utils.py
+++ b/openedx/core/djangoapps/notifications/utils.py
@@ -3,13 +3,11 @@
"""
from typing import Dict, List
-from common.djangoapps.student.models import CourseEnrollment, CourseAccessRole
-from lms.djangoapps.discussion.toggles import ENABLE_REPORTED_CONTENT_NOTIFICATIONS
+from common.djangoapps.student.models import CourseAccessRole, CourseEnrollment
from openedx.core.djangoapps.django_comment_common.models import Role
+from openedx.core.djangoapps.notifications.config.waffle import ENABLE_NOTIFICATIONS
from openedx.core.lib.cache_utils import request_cached
-from .config.waffle import ENABLE_COURSEWIDE_NOTIFICATIONS, SHOW_NOTIFICATIONS_TRAY
-
def find_app_in_normalized_apps(app_name, apps_list):
"""
@@ -42,7 +40,7 @@ def get_show_notifications_tray(user):
).values_list('course_id', flat=True)
for course_id in learner_enrollments_course_ids:
- if SHOW_NOTIFICATIONS_TRAY.is_enabled(course_id):
+ if ENABLE_NOTIFICATIONS.is_enabled(course_id):
show_notifications_tray = True
break
@@ -58,27 +56,6 @@ def get_list_in_batches(input_list, batch_size):
yield input_list[index: index + batch_size]
-def filter_course_wide_preferences(course_key, preferences):
- """
- If course wide notifications is disabled for course, it filters course_wide
- preferences from response
- """
- if ENABLE_COURSEWIDE_NOTIFICATIONS.is_enabled(course_key):
- return preferences
- course_wide_notification_types = ['new_discussion_post', 'new_question_post']
-
- if not ENABLE_REPORTED_CONTENT_NOTIFICATIONS.is_enabled(course_key):
- course_wide_notification_types.append('content_reported')
-
- config = preferences['notification_preference_config']
- for app_prefs in config.values():
- notification_types = app_prefs['notification_types']
- for course_wide_type in course_wide_notification_types:
- if course_wide_type in notification_types.keys():
- notification_types.pop(course_wide_type)
- return preferences
-
-
def get_user_forum_roles(user_id: int, course_id: str) -> List[str]:
"""
Get forum roles for the given user in the specified course.
diff --git a/openedx/core/djangoapps/notifications/views.py b/openedx/core/djangoapps/notifications/views.py
index cc830dbb8d7f..fdc91c12a9e0 100644
--- a/openedx/core/djangoapps/notifications/views.py
+++ b/openedx/core/djangoapps/notifications/views.py
@@ -10,11 +10,13 @@
from opaque_keys.edx.keys import CourseKey
from pytz import UTC
from rest_framework import generics, status
+from rest_framework.decorators import api_view
from rest_framework.generics import UpdateAPIView
from rest_framework.response import Response
from rest_framework.views import APIView
from common.djangoapps.student.models import CourseEnrollment
+from openedx.core.djangoapps.notifications.email.utils import update_user_preferences_from_patch
from openedx.core.djangoapps.notifications.models import (
CourseNotificationPreference,
get_course_notification_preference_config_version
@@ -35,7 +37,7 @@
NotificationCourseEnrollmentSerializer,
NotificationSerializer,
UserCourseNotificationPreferenceSerializer,
- UserNotificationPreferenceUpdateSerializer, UserNotificationChannelPreferenceUpdateSerializer,
+ UserNotificationPreferenceUpdateSerializer,
)
from .utils import get_show_notifications_tray
@@ -236,55 +238,6 @@ def patch(self, request, course_key_string):
return Response(serializer.data, status=status.HTTP_200_OK)
-@allow_any_authenticated_user()
-class UserNotificationChannelPreferenceView(APIView):
- """
- Supports retrieving and patching the UserNotificationPreference
- model.
- **Example Requests**
- PATCH /api/notifications/configurations/{course_id}
- """
-
- def patch(self, request, course_key_string):
- """
- Update an existing user notification preference for an entire channel with the data in the request body.
-
- Parameters:
- request (Request): The request object
- course_key_string (int): The ID of the course of the notification preference to be updated.
- Returns:
- 200: The updated preference, serialized using the UserNotificationPreferenceSerializer
- 404: If the preference does not exist
- 403: If the user does not have permission to update the preference
- 400: Validation error
- """
- course_id = CourseKey.from_string(course_key_string)
- user_course_notification_preference = CourseNotificationPreference.objects.get(
- user=request.user,
- course_id=course_id,
- is_active=True,
- )
- if user_course_notification_preference.config_version != get_course_notification_preference_config_version():
- return Response(
- {'error': _('The notification preference config version is not up to date.')},
- status=status.HTTP_409_CONFLICT,
- )
-
- preference_update = UserNotificationChannelPreferenceUpdateSerializer(
- user_course_notification_preference, data=request.data, partial=True
- )
- preference_update.is_valid(raise_exception=True)
- updated_notification_preferences = preference_update.save()
- notification_preference_update_event(request.user, course_id, preference_update.validated_data)
- serializer_context = {
- 'course_id': course_id,
- 'user': request.user
- }
- serializer = UserCourseNotificationPreferenceSerializer(updated_notification_preferences,
- context=serializer_context)
- return Response(serializer.data, status=status.HTTP_200_OK)
-
-
@allow_any_authenticated_user()
class NotificationListAPIView(generics.ListAPIView):
"""
@@ -375,7 +328,7 @@ def get(self, request):
.annotate(count=Count('*'))
)
count_total = 0
- show_notifications_tray = get_show_notifications_tray(request.user)
+ show_notifications_tray = get_show_notifications_tray(self.request.user)
count_by_app_name_dict = {
app_name: 0
for app_name in COURSE_NOTIFICATION_APPS
@@ -479,3 +432,13 @@ def patch(self, request, *args, **kwargs):
return Response({'message': _('Notifications marked read.')}, status=status.HTTP_200_OK)
return Response({'error': _('Invalid app_name or notification_id.')}, status=status.HTTP_400_BAD_REQUEST)
+
+
+@api_view(['GET', 'POST'])
+def preference_update_from_encrypted_username_view(request, username, patch):
+ """
+ View to update user preferences from encrypted username and patch.
+ username and patch must be string
+ """
+ update_user_preferences_from_patch(username, patch)
+ return Response({"result": "success"}, status=status.HTTP_200_OK)
diff --git a/openedx/core/djangoapps/programs/README.rst b/openedx/core/djangoapps/programs/README.rst
new file mode 100644
index 000000000000..6c5a3946c26e
--- /dev/null
+++ b/openedx/core/djangoapps/programs/README.rst
@@ -0,0 +1,21 @@
+Status: Maintenance
+
+Responsibilities
+================
+The Programs app is responsible (along with the `credentials app`_)
+for communicating with the `credentials service`_, which is
+the system of record for a learner's Program Certificates, and which (when enabled by the edX
+instance) is the system of record for accessing all of a learner's credentials.
+
+It also hosts program discussion forum and program live configuration.
+
+.. _credentials service: https://github.com/openedx/credentials
+
+.. _credentials app: https://github.com/openedx/edx-platform/tree/master/openedx/core/djangoapps/credentials
+
+See Also
+========
+
+* ``lms/djangoapps/learner_dashboard/``, which hosts the program dashboard.
+* ``openedx/core/djangoapps/credentials``
+
diff --git a/openedx/core/djangoapps/programs/docs/decisions/0001-sync-certificate-available-dates.rst b/openedx/core/djangoapps/programs/docs/decisions/0001-sync-certificate-available-dates.rst
index dfe9fcb49264..c9bd660b31c6 100644
--- a/openedx/core/djangoapps/programs/docs/decisions/0001-sync-certificate-available-dates.rst
+++ b/openedx/core/djangoapps/programs/docs/decisions/0001-sync-certificate-available-dates.rst
@@ -4,7 +4,9 @@ Sync certificate_available_date and visible_date for certificates
Status
------
-Review
+Superseded by Credentials ADR `0001 Certificate Available Date`_.
+
+.. _0001 Certificate Available Date: https://github.com/openedx/edx-platform/blob/master/openedx/core/djangoapps/oauth_dispatch/docs/decisions/0005-restricted-application-for-SSO.rst
Context
-------
diff --git a/openedx/core/djangoapps/programs/signals.py b/openedx/core/djangoapps/programs/signals.py
index 89292f565b66..0a85cbf84efd 100644
--- a/openedx/core/djangoapps/programs/signals.py
+++ b/openedx/core/djangoapps/programs/signals.py
@@ -2,7 +2,6 @@
This module contains signals / handlers related to programs.
"""
-
import logging
from django.dispatch import receiver
@@ -14,7 +13,7 @@
COURSE_CERT_AWARDED,
COURSE_CERT_CHANGED,
COURSE_CERT_DATE_CHANGE,
- COURSE_CERT_REVOKED
+ COURSE_CERT_REVOKED,
)
LOGGER = logging.getLogger(__name__)
@@ -39,11 +38,10 @@ def handle_course_cert_awarded(sender, user, course_key, mode, status, **kwargs)
if not is_credentials_enabled():
return
- LOGGER.debug(
- f"Handling COURSE_CERT_AWARDED: user={user}, course_key={course_key}, mode={mode}, status={status}"
- )
+ LOGGER.debug(f"Handling COURSE_CERT_AWARDED: user={user}, course_key={course_key}, mode={mode}, status={status}")
# import here, because signal is registered at startup, but items in tasks are not yet able to be loaded
from openedx.core.djangoapps.programs.tasks import award_program_certificates
+
award_program_certificates.delay(user.username)
@@ -68,7 +66,7 @@ def handle_course_cert_changed(sender, user, course_key, mode, status, **kwargs)
Returns:
None
"""
- verbose = kwargs.get('verbose', False)
+ verbose = kwargs.get("verbose", False)
if verbose:
LOGGER.info(
f"Starting handle_course_cert_changed with params: sender [{sender}], user [{user}], course_key "
@@ -87,6 +85,7 @@ def handle_course_cert_changed(sender, user, course_key, mode, status, **kwargs)
LOGGER.debug(f"Handling COURSE_CERT_CHANGED: user={user}, course_key={course_key}, mode={mode}, status={status}")
# import here, because signal is registered at startup, but items in tasks are not yet able to be loaded
from openedx.core.djangoapps.programs.tasks import award_course_certificate
+
award_course_certificate.delay(user.username, str(course_key))
@@ -112,16 +111,16 @@ def handle_course_cert_revoked(sender, user, course_key, mode, status, **kwargs)
LOGGER.info(f"Handling COURSE_CERT_REVOKED: user={user}, course_key={course_key}, mode={mode}, status={status}")
# import here, because signal is registered at startup, but items in tasks are not yet able to be loaded
from openedx.core.djangoapps.programs.tasks import revoke_program_certificates
+
revoke_program_certificates.delay(user.username, str(course_key))
-@receiver(COURSE_CERT_DATE_CHANGE, dispatch_uid='course_certificate_date_change_handler')
+@receiver(COURSE_CERT_DATE_CHANGE, dispatch_uid="course_certificate_date_change_handler")
def handle_course_cert_date_change(sender, course_key, **kwargs): # pylint: disable=unused-argument
"""
When a course run's configuration has been updated, and the system has detected an update related to the display
behavior or availability date of the certificates issued in that course, we should enqueue celery tasks responsible
for:
- - updating the `visible_date` attribute of any previously awarded certificates the Credentials IDA manages
- updating the certificate available date of the course run's course certificate configuration in Credentials
Args:
@@ -137,8 +136,7 @@ def handle_course_cert_date_change(sender, course_key, **kwargs): # pylint: dis
LOGGER.info(f"Handling COURSE_CERT_DATE_CHANGE for course {course_key}")
# import here, because signal is registered at startup, but items in tasks are not yet loaded
from openedx.core.djangoapps.programs.tasks import update_certificate_available_date_on_course_update
- from openedx.core.djangoapps.programs.tasks import update_certificate_visible_date_on_course_update
- update_certificate_visible_date_on_course_update.delay(str(course_key))
+
update_certificate_available_date_on_course_update.delay(str(course_key))
@@ -163,6 +161,5 @@ def handle_course_pacing_change(sender, updated_course_overview, **kwargs): # p
LOGGER.info(f"Handling COURSE_PACING_CHANGED for course {course_id}")
# import here, because signal is registered at startup, but items in tasks are not yet loaded
from openedx.core.djangoapps.programs.tasks import update_certificate_available_date_on_course_update
- from openedx.core.djangoapps.programs.tasks import update_certificate_visible_date_on_course_update
+
update_certificate_available_date_on_course_update.delay(course_id)
- update_certificate_visible_date_on_course_update.delay(course_id)
diff --git a/openedx/core/djangoapps/programs/tasks.py b/openedx/core/djangoapps/programs/tasks.py
index 72ccc79bddfc..42548fd494ce 100644
--- a/openedx/core/djangoapps/programs/tasks.py
+++ b/openedx/core/djangoapps/programs/tasks.py
@@ -2,7 +2,8 @@
This file contains celery tasks and utility functions responsible for syncing course and program certificate metadata
between the monolith and the Credentials IDA.
"""
-from typing import Dict, List
+
+from typing import TYPE_CHECKING, Dict, List, Optional
from urllib.parse import urljoin
from celery import shared_task
@@ -13,11 +14,11 @@
from django.contrib.sites.models import Site
from django.core.exceptions import ObjectDoesNotExist
from edx_django_utils.monitoring import set_code_owner_attribute
+from opaque_keys import InvalidKeyError
from opaque_keys.edx.keys import CourseKey
from requests.exceptions import HTTPError
from common.djangoapps.course_modes.models import CourseMode
-from lms.djangoapps.certificates.api import available_date_for_certificate
from lms.djangoapps.certificates.models import GeneratedCertificate
from openedx.core.djangoapps.content.course_overviews.api import get_course_overview_or_none
from openedx.core.djangoapps.credentials.api import is_credentials_enabled
@@ -30,6 +31,12 @@
from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers
from xmodule.data import CertificatesDisplayBehaviors
+if TYPE_CHECKING:
+ from datetime import datetime
+
+ from django.contrib.auth.models import User as UserType # pylint: disable=imported-auth-user
+ from requests import Session
+
User = get_user_model()
LOGGER = get_task_logger(__name__)
@@ -42,7 +49,7 @@
DATE_FORMAT = "%Y-%m-%dT%H:%M:%SZ"
-def get_completed_programs(site, student):
+def get_completed_programs(site: Site, student: "UserType") -> Dict:
"""
Given a set of completed courses, determine which programs are completed.
@@ -51,14 +58,14 @@ def get_completed_programs(site, student):
student (User): Representing the student whose completed programs to check for.
Returns:
- dict of {program_UUIDs: visible_dates}
+ Dict of program_UUIDs:availability dates
"""
meter = ProgramProgressMeter(site, student)
return meter.completed_programs_with_available_dates
-def get_inverted_programs(student):
+def get_inverted_programs(student: "UserType"):
"""
Get programs keyed by course run ID.
@@ -77,7 +84,7 @@ def get_inverted_programs(student):
return inverted_programs
-def get_certified_programs(student: User, raise_on_error: bool = False) -> List[str]:
+def get_certified_programs(student: "UserType", raise_on_error: bool = False) -> List[str]:
"""
Find the UUIDs of all the programs for which the student has already been awarded
a certificate.
@@ -102,7 +109,7 @@ def get_certified_programs(student: User, raise_on_error: bool = False) -> List[
return certified_programs
-def get_revokable_program_uuids(course_specific_programs: List[Dict], student: User) -> List[str]:
+def get_revokable_program_uuids(course_specific_programs: List[Dict], student: "UserType") -> List[str]:
"""
Get program uuids for which certificate to be revoked.
@@ -131,7 +138,7 @@ def get_revokable_program_uuids(course_specific_programs: List[Dict], student: U
return program_uuids_to_revoke
-def award_program_certificate(client, user, program_uuid, visible_date):
+def award_program_certificate(client: "Session", user: "UserType", program_uuid: "str") -> None:
"""
Issue a new certificate of completion to the given student for the given program.
@@ -142,8 +149,6 @@ def award_program_certificate(client, user, program_uuid, visible_date):
The student's user data
program_uuid:
uuid of the completed program
- visible_date:
- when the program credential should be visible to user
Returns:
None
@@ -156,7 +161,6 @@ def award_program_certificate(client, user, program_uuid, visible_date):
"username": user.username,
"lms_user_id": user.id,
"credential": {"type": PROGRAM_CERTIFICATE, "program_uuid": program_uuid},
- "attributes": [{"name": "visible_date", "value": visible_date.strftime(DATE_FORMAT)}],
},
)
response.raise_for_status()
@@ -188,7 +192,13 @@ def revoke_program_certificate(client, username, program_uuid):
response.raise_for_status()
-def post_course_certificate(client, username, certificate, visible_date, date_override=None, org=None):
+def post_course_certificate(
+ client: "Session",
+ username: str,
+ certificate: GeneratedCertificate,
+ date_override: Optional["datetime"] = None,
+ org: Optional[str] = None,
+):
"""
POST a certificate that has been updated to Credentials
"""
@@ -206,7 +216,6 @@ def post_course_certificate(client, username, certificate, visible_date, date_ov
"type": COURSE_CERTIFICATE,
},
"date_override": {"date": date_override.strftime(DATE_FORMAT)} if date_override else None,
- "attributes": [{"name": "visible_date", "value": visible_date.strftime(DATE_FORMAT)}],
},
)
response.raise_for_status()
@@ -226,15 +235,16 @@ def post_course_certificate_configuration(client, cert_config, certificate_avail
"""
credentials_api_base_url = get_credentials_api_base_url()
credentials_api_url = urljoin(f"{credentials_api_base_url}/", "course_certificates/")
+ certificate_config = {
+ "course_id": cert_config["course_id"],
+ "certificate_type": cert_config["mode"],
+ "certificate_available_date": certificate_available_date,
+ "is_active": True,
+ }
response = client.post(
credentials_api_url,
- json={
- "course_id": cert_config["course_id"],
- "certificate_type": cert_config["mode"],
- "certificate_available_date": certificate_available_date,
- "is_active": True,
- },
+ json=certificate_config,
)
# Sometimes helpful error context is swallowed when calling `raise_for_status()`. We try to print out any additional
@@ -244,13 +254,22 @@ def post_course_certificate_configuration(client, cert_config, certificate_avail
# 201 on a successful call.
if response.status_code != 201:
LOGGER.error(
- "Error creating or updating a course certificate configuration in the Credentials IDA. Additional details: "
- f"{response.text}"
+ "Error creating or updating a course certificate configuration in the Credentials IDA.\n"
+ f"config sent: {certificate_config}\nAdditional details: {response.text}"
)
response.raise_for_status()
-@shared_task(bind=True, ignore_result=True)
+# pylint: disable=unused-argument
+@shared_task(
+ bind=True,
+ ignore_result=True,
+ autoretry_for=(Exception,),
+ max_retries=10,
+ retry_backoff=30,
+ retry_backoff_max=600,
+ retry_jitter=True,
+)
@set_code_owner_attribute
def award_program_certificates(self, username): # lint-amnesty, pylint: disable=too-many-statements
"""
@@ -272,23 +291,14 @@ def award_program_certificates(self, username): # lint-amnesty, pylint: disable
Returns:
None
"""
- def _retry_with_custom_exception(username, reason, countdown):
- exception = MaxRetriesExceededError(
- f"Failed to award a program certificate to user {username}. Reason: {reason}"
- )
- return self.retry(exc=exception, countdown=countdown, max_retries=MAX_RETRIES)
-
- countdown = 2**self.request.retries
-
# If the credentials config model is disabled for this feature, it may indicate a condition where processing of such
- # tasks has been temporarily disabled. Since this is a recoverable situation, mark this task for retry instead of
- # failing it altogether.
+ # tasks has been temporarily disabled. This is a recoverable situation, so let celery retry.
if not is_credentials_enabled():
error_msg = (
"Task award_program_certificates cannot be executed, use of the Credentials service is disabled by config"
)
LOGGER.warning(error_msg)
- raise _retry_with_custom_exception(username=username, reason=error_msg, countdown=countdown)
+ raise MaxRetriesExceededError(f"Failed to award a program certificate. Reason: {error_msg}")
try:
student = User.objects.get(username=username)
@@ -323,7 +333,9 @@ def _retry_with_custom_exception(username, reason, countdown):
except Exception as exc:
error_msg = f"Failed to determine program certificates to be awarded for user {student}: {exc}"
LOGGER.exception(error_msg)
- raise _retry_with_custom_exception(username=username, reason=error_msg, countdown=countdown) from exc
+ raise MaxRetriesExceededError(
+ f"Failed to award a program certificate to user {student.id}. Reason: {error_msg}"
+ ) from exc
# For each completed program for which the student doesn't already have a certificate, award one now.
#
@@ -339,18 +351,15 @@ def _retry_with_custom_exception(username, reason, countdown):
except Exception as exc:
error_msg = "Failed to create a credentials API client to award program certificates"
LOGGER.exception(error_msg)
- # Retry because a misconfiguration could be fixed
- raise _retry_with_custom_exception(username=username, reason=error_msg, countdown=countdown) from exc
+ # A misconfiguration could be fixed; let celery retry.
+ raise MaxRetriesExceededError(
+ f"Failed to award a program certificate to user {student.id}. Reason: {error_msg}"
+ ) from exc
failed_program_certificate_award_attempts = []
for program_uuid in new_program_uuids:
- visible_date = completed_programs[program_uuid]
try:
- LOGGER.info(
- f"Visible date for program certificate awarded to user {student} in program {program_uuid} is "
- f"{visible_date}"
- )
- award_program_certificate(credentials_client, student, program_uuid, visible_date)
+ award_program_certificate(credentials_client, student, program_uuid)
LOGGER.info(f"Awarded program certificate to user {student} in program {program_uuid}")
except HTTPError as exc:
if exc.response.status_code == 404:
@@ -360,17 +369,13 @@ def _retry_with_custom_exception(username, reason, countdown):
"not be configured correctly in Credentials"
)
elif exc.response.status_code == 429:
- rate_limit_countdown = 60
+ # Let celery handle retry attempts and backoff
error_msg = (
- f"Rate limited. Retrying task to award certificate to user {student} in program "
- f"{program_uuid} in {rate_limit_countdown} seconds"
+ f"Rate limited. Attempting to award certificate to user {student} in program {program_uuid}."
)
LOGGER.warning(error_msg)
- # Retry after 60 seconds, when we should be in a new throttling window
- raise _retry_with_custom_exception(
- username=username,
- reason=error_msg,
- countdown=rate_limit_countdown,
+ raise MaxRetriesExceededError(
+ f"Failed to award a program certificate to user {student.id}. Reason: {error_msg}"
) from exc
else:
LOGGER.warning(
@@ -378,7 +383,7 @@ def _retry_with_custom_exception(username, reason, countdown):
"might not be configured correctly in Credentials"
)
except Exception as exc: # pylint: disable=broad-except
- # keep trying to award other certs, but retry the whole task to fix any missing entries
+ # keep trying to award other certs, but let celery retry the whole task to fix any missing entries
LOGGER.exception(
f"Failed to award program certificate to user {student} in program {program_uuid}: {exc}"
)
@@ -394,7 +399,9 @@ def _retry_with_custom_exception(username, reason, countdown):
f"Failed to award program certificate(s) for user {student} in programs "
f"{failed_program_certificate_award_attempts}"
)
- raise _retry_with_custom_exception(username=username, reason=error_msg, countdown=countdown)
+ raise MaxRetriesExceededError(
+ f"Failed to award a program certificate to user {student.id}. Reason: {error_msg}"
+ )
else:
LOGGER.warning(f"User {student} is not eligible for any new program certificates")
@@ -402,16 +409,22 @@ def _retry_with_custom_exception(username, reason, countdown):
# pylint: disable=W0613
-@shared_task(bind=True, ignore_result=True)
+@shared_task(
+ bind=True,
+ ignore_result=True,
+ autoretry_for=(Exception,),
+ max_retries=10,
+ retry_backoff=30,
+ retry_backoff_max=600,
+ retry_jitter=True,
+)
@set_code_owner_attribute
def update_credentials_course_certificate_configuration_available_date(
self, course_key, certificate_available_date=None
):
"""
This task will update the CourseCertificate configuration's available date
- in Credentials. This is different from the "visible_date" attribute. This
- date will always either be the available date that is set in Studio for a
- given course, or it will be None.
+ in Credentials.
Arguments:
course_run_key (str): The course run key to award the certificate for
@@ -427,7 +440,8 @@ def update_credentials_course_certificate_configuration_available_date(
course_modes = CourseMode.objects.filter(course_id=course_key)
# There should only ever be one certificate relevant mode per course run
modes = [
- mode.slug for mode in course_modes
+ mode.slug
+ for mode in course_modes
if mode.slug in CourseMode.CERTIFICATE_RELEVANT_MODES or CourseMode.is_eligible_for_certificate(mode.slug)
]
if len(modes) != 1:
@@ -448,7 +462,15 @@ def update_credentials_course_certificate_configuration_available_date(
)
-@shared_task(bind=True, ignore_result=True)
+@shared_task(
+ bind=True,
+ ignore_result=True,
+ autoretry_for=(Exception,),
+ max_retries=10,
+ retry_backoff=30,
+ retry_backoff_max=600,
+ retry_jitter=True,
+)
@set_code_owner_attribute
def award_course_certificate(self, username, course_run_key):
"""
@@ -457,35 +479,21 @@ def award_course_certificate(self, username, course_run_key):
It can be called independently for a username and a course_run, but is invoked on each GeneratedCertificate.save.
- If this function is moved, make sure to update it's entry in EXPLICIT_QUEUES in the settings files so it runs in the
+ If this function is moved, make sure to update its entry in EXPLICIT_QUEUES in the settings files so it runs in the
correct queue.
Arguments:
username (str): The user to award the Credentials course cert to
course_run_key (str): The course run key to award the certificate for
"""
- def _retry_with_custom_exception(username, course_run_key, reason, countdown):
- exception = MaxRetriesExceededError(
- f"Failed to award course certificate for user {username} for course {course_run_key}. Reason: {reason}"
- )
- return self.retry(exc=exception, countdown=countdown, max_retries=MAX_RETRIES)
-
- countdown = 2**self.request.retries
-
# If the credentials config model is disabled for this feature, it may indicate a condition where processing of such
- # tasks has been temporarily disabled. Since this is a recoverable situation,
- # mark this task for retry instead of failing it altogether.
+ # tasks has been temporarily disabled. This is a recoverable situation, let celery retry.
if not is_credentials_enabled():
error_msg = (
"Task award_course_certificate cannot be executed when credentials issuance is disabled in API config"
)
LOGGER.warning(error_msg)
- raise _retry_with_custom_exception(
- username=username,
- course_run_key=course_run_key,
- reason=error_msg,
- countdown=countdown,
- )
+ raise MaxRetriesExceededError(f"Failed to award course certificate. Reason: {error_msg}")
try:
user = User.objects.get(username=username)
@@ -499,71 +507,91 @@ def _retry_with_custom_exception(username, course_run_key, reason, countdown):
LOGGER.info(f"Running task award_course_certificate for user {user}")
try:
course_key = CourseKey.from_string(course_run_key)
- # Get the cert for the course key and username if it's both passing and available in professional/verified
- try:
- certificate = GeneratedCertificate.eligible_certificates.get(
- user=user.id,
- course_id=course_key,
- )
- except GeneratedCertificate.DoesNotExist:
- LOGGER.warning(
- f"Task award_course_certificate was called for user {user} in course run {course_key} but this learner "
- "has not earned a course certificate in this course run"
- )
- return
+ except InvalidKeyError as exc:
+ error_msg = "Failed to determine course key"
+ LOGGER.warning(
+ f"Failed to award course certificate for user {user.id} for course {course_run_key}. Reason: {error_msg}"
+ )
+ return
+
+ # Get the cert for the course key and username if it's both passing and available in professional/verified
+ try:
+ certificate = GeneratedCertificate.eligible_certificates.get(
+ user=user.id,
+ course_id=course_key,
+ )
+ except GeneratedCertificate.DoesNotExist:
+ LOGGER.warning(
+ f"Task award_course_certificate was called for user {user.id} in course run {course_key} but this learner "
+ "has not earned a course certificate in this course run"
+ )
+ return
+ try:
if (
- certificate.mode in CourseMode.CERTIFICATE_RELEVANT_MODES
- or CourseMode.is_eligible_for_certificate(certificate.mode)
+ certificate.mode not in CourseMode.CERTIFICATE_RELEVANT_MODES
+ and not CourseMode.is_eligible_for_certificate(certificate.mode)
):
- course_overview = get_course_overview_or_none(course_key)
- if not course_overview:
- LOGGER.warning(
- f"Task award_course_certificate was called for user {user} in course {course_key} but no course "
- "overview could be retrieved for the course run"
- )
- return
-
- visible_date = available_date_for_certificate(course_overview, certificate)
- LOGGER.info(
- f"Task award_course_certificate will award a course certificate to user {user} in course run "
- f"{course_key} with a visible date of {visible_date}"
+ LOGGER.warning(
+ f"Task award_course_certificate was called for user {user.id} in course run {course_key} but "
+ f"this course has an ineligible mode of {certificate.mode} for a certificate on this instance."
)
+ return
+ except Exception as exc:
+ error_msg = f"Failed to determine course mode certificate eligibility for {certificate}."
+ LOGGER.error(error_msg)
+ raise MaxRetriesExceededError(
+ f"Failed to award course certificate for user {user.id} for course {course_run_key}. Reason: {error_msg}"
+ ) from exc
- # If the certificate has an associated CertificateDateOverride, send it along
- try:
- date_override = certificate.date_override.date
- LOGGER.info(
- f"Task award_course_certificate will award a course certificate to user {user} in course run "
- f"{course_key} with an override date of {date_override}"
- )
- except ObjectDoesNotExist:
- date_override = None
+ course_overview = get_course_overview_or_none(course_key)
+ if not course_overview:
+ LOGGER.warning(
+ f"Task award_course_certificate was called for user {user.id} in course {course_key} but no course "
+ "overview could be retrieved for the course run"
+ )
+ return
- credentials_client = get_credentials_api_client(
- User.objects.get(username=settings.CREDENTIALS_SERVICE_USERNAME),
- )
- post_course_certificate(
- credentials_client,
- username,
- certificate,
- visible_date,
- date_override,
- org=course_key.org,
- )
- LOGGER.info(f"Awarded a course certificate to user {user} in course run {course_key}")
+ # If the certificate has an associated CertificateDateOverride, send it along
+ try:
+ date_override = certificate.date_override.date # type: Optional["datetime"]
+ LOGGER.info(
+ f"Task award_course_certificate will award a course certificate to user {user.id} in course run "
+ f"{course_key} with an override date of {date_override}"
+ )
+ except ObjectDoesNotExist:
+ date_override = None # type: Optional["datetime"]
+
+ try:
+ credentials_client = get_credentials_api_client(
+ User.objects.get(username=settings.CREDENTIALS_SERVICE_USERNAME),
+ )
+ post_course_certificate(
+ credentials_client,
+ username,
+ certificate,
+ date_override,
+ org=course_key.org,
+ )
except Exception as exc:
- error_msg = f"Failed to determine course certificates to be awarded for user {user}."
- LOGGER.exception(error_msg)
- raise _retry_with_custom_exception(
- username=username,
- course_run_key=course_run_key,
- reason=error_msg,
- countdown=countdown,
+ error_msg = f"Failed to post course certificate to be awarded for user {user}."
+ raise MaxRetriesExceededError(
+ f"Failed to award course certificate for user {user.id} for course {course_run_key}. Reason: {error_msg}"
) from exc
+ # Successfully posted the cert to credentials
+ LOGGER.info(f"Awarded a course certificate to user {user.id} in course run {course_key}")
-@shared_task(bind=True, ignore_result=True)
+
+@shared_task(
+ bind=True,
+ ignore_result=True,
+ autoretry_for=(Exception,),
+ max_retries=10,
+ retry_backoff=30,
+ retry_backoff_max=600,
+ retry_jitter=True,
+)
@set_code_owner_attribute
def revoke_program_certificates(self, username, course_key): # lint-amnesty, pylint: disable=too-many-statements
"""
@@ -572,7 +600,7 @@ def revoke_program_certificates(self, username, course_key): # lint-amnesty, py
It will consult with a variety of APIs to determine whether or not the specified user's certificate should be
revoked in one or more programs, and use the credentials service to revoke the said certificates if so.
- If this function is moved, make sure to update it's entry in EXPLICIT_QUEUES in the settings files so it runs in the
+ If this function is moved, make sure to update its entry in EXPLICIT_QUEUES in the settings files so it runs in the
correct queue.
Args:
@@ -582,28 +610,14 @@ def revoke_program_certificates(self, username, course_key): # lint-amnesty, py
Returns:
None
"""
- def _retry_with_custom_exception(username, course_key, reason, countdown):
- exception = MaxRetriesExceededError(
- f"Failed to revoke program certificate for user {username} for course {course_key}. Reason: {reason}"
- )
- return self.retry(exc=exception, countdown=countdown, max_retries=MAX_RETRIES)
-
- countdown = 2**self.request.retries
-
# If the credentials config model is disabled for this feature, it may indicate a condition where processing of such
- # tasks has been temporarily disabled. Since this is a recoverable situation, mark this task for retry instead of
- # failing it altogether.
+ # tasks has been temporarily disabled. Since this is a recoverable situation, let celery retry.
if not is_credentials_enabled():
error_msg = (
"Task revoke_program_certificates cannot be executed, use of the Credentials service is disabled by config"
)
LOGGER.warning(error_msg)
- raise _retry_with_custom_exception(
- username=username,
- course_key=course_key,
- reason=error_msg,
- countdown=countdown,
- )
+ raise MaxRetriesExceededError(f"Failed to revoke program certificate. Reason: {error_msg}")
try:
student = User.objects.get(username=username)
@@ -633,11 +647,8 @@ def _retry_with_custom_exception(username, course_key, reason, countdown):
f"revoked from user {student}"
)
LOGGER.exception(error_msg)
- raise _retry_with_custom_exception(
- username=username,
- course_key=course_key,
- reason=error_msg,
- countdown=countdown,
+ raise MaxRetriesExceededError(
+ f"Failed to revoke program certificate for user {student.id} for course {course_key}. Reason: {error_msg}"
) from exc
if program_uuids_to_revoke:
@@ -648,8 +659,10 @@ def _retry_with_custom_exception(username, course_key, reason, countdown):
except Exception as exc:
error_msg = "Failed to create a credentials API client to revoke program certificates"
LOGGER.exception(error_msg)
- # Retry because a misconfiguration could be fixed
- raise _retry_with_custom_exception(username, course_key, reason=exc, countdown=countdown) from exc
+ # Stil retryable because a misconfiguration could be fixed
+ raise MaxRetriesExceededError(
+ f"Failed to revoke program certificate for user {student.id} for course {course_key}. Reason: {exc}"
+ ) from exc
failed_program_certificate_revoke_attempts = []
for program_uuid in program_uuids_to_revoke:
@@ -663,25 +676,21 @@ def _retry_with_custom_exception(username, course_key, reason, countdown):
"program certificate could not be found"
)
elif exc.response.status_code == 429:
- rate_limit_countdown = 60
+ # Let celery handle retry attempts and backoff
error_msg = (
- f"Rate limited. Retrying task to revoke a program certificate from user {student} in program "
- f"{program_uuid} in {rate_limit_countdown} seconds"
+ f"Rate limited. Attempting to revoke a program certificate from user {student} in program "
+ f"{program_uuid}."
)
LOGGER.warning(error_msg)
- # Retry after 60 seconds, when we should be in a new throttling window
- raise _retry_with_custom_exception(
- username,
- course_key,
- reason=error_msg,
- countdown=rate_limit_countdown,
+ raise MaxRetriesExceededError(
+ f"Failed to revoke program certificate for user {student.id} Reason: {error_msg}"
) from exc
else:
LOGGER.warning(
f"Unable to revoke program certificate from user {student} in program {program_uuid}"
)
except Exception as exc: # pylint: disable=broad-except
- # keep trying to revoke other certs, but retry the whole task to fix any missing entries
+ # keep trying to revoke other certs, but let celery retry the whole task to fix any missing entries
LOGGER.exception(
f"Failed to revoke program certificate from user {student} in program {program_uuid}: {exc}"
)
@@ -689,7 +698,7 @@ def _retry_with_custom_exception(username, course_key, reason, countdown):
if failed_program_certificate_revoke_attempts:
# N.B. This logic assumes that this task is idempotent
- LOGGER.info(f"Retrying failed task to revoke program certificate(s) from user {student}")
+ LOGGER.info(f"Failed task to revoke program certificate(s) from user {student}")
# The error message may change on each reattempt but will never be raised until the max number of retries
# have been exceeded. It is unlikely that this list will change by the time it reaches its maximimum number
# of attempts.
@@ -697,61 +706,25 @@ def _retry_with_custom_exception(username, course_key, reason, countdown):
f"Failed to revoke program certificate(s) from user {student} for programs "
f"{failed_program_certificate_revoke_attempts}"
)
- raise _retry_with_custom_exception(username, course_key, reason=error_msg, countdown=countdown)
+ raise MaxRetriesExceededError(
+ f"Failed to revoke program certificate for user {student.id} for course {course_key}. "
+ f"Reason: {error_msg}"
+ )
else:
LOGGER.info(f"No program certificates to revoke from user {student}")
LOGGER.info(f"Successfully completed the task revoke_program_certificates for user {student}")
-@shared_task(bind=True, ignore_result=True)
-@set_code_owner_attribute
-def update_certificate_visible_date_on_course_update(self, course_key):
- """
- This task is designed to be called whenever a course-run's `certificate_available_date` is updated.
-
- When executed, this task will first get a list of all learners within the course-run that have earned a certificate.
- Next, we will enqueue an additional `award_course_certificate` task for each learner in this list. These subtasks
- will be responsible for updating the `visible_date` attribute on each certificate the Credentials IDA knows about.
-
- If this function is moved, make sure to update it's entry in EXPLICIT_QUEUES in the settings files so it runs in the
- correct queue.
-
- Arguments:
- course_key(str): The course identifier
- """
- countdown = 2**self.request.retries
-
- # If the CredentialsApiConfig configuration model is disabled for this feature, it may indicate a condition where
- # processing of such tasks has been temporarily disabled. Since this is a recoverable situation, mark this task for
- # retry instead of failing it.
- if not is_credentials_enabled():
- error_msg = (
- "Cannot execute the `update_certificate_visible_date_on_course_update` task. Issuing user credentials "
- "through the Credentials IDA is disabled."
- )
- LOGGER.warning(error_msg)
- exception = MaxRetriesExceededError(
- f"Failed to update the `visible_date` attribute for certificates in course {course_key}. Reason: "
- f"{error_msg}"
- )
- raise self.retry(exc=exception, countdown=countdown, max_retries=MAX_RETRIES)
-
- # Retrieve a list of all usernames of learners who have a certificate record in this course-run. The
- # Credentials IDA REST API still requires a username as the main identifier for the learner.
- users_with_certificates_in_course = GeneratedCertificate.eligible_available_certificates.filter(
- course_id=course_key
- ).values_list("user__username", flat=True)
-
- LOGGER.info(
- f"Resending course certificates for learners in course {course_key} to the Credentials service. Queueing "
- f"{len(users_with_certificates_in_course)} `award_course_certificate` tasks."
- )
- for user in users_with_certificates_in_course:
- award_course_certificate.delay(user, str(course_key))
-
-
-@shared_task(bind=True, ignore_result=True)
+@shared_task(
+ bind=True,
+ ignore_result=True,
+ autoretry_for=(Exception,),
+ max_retries=10,
+ retry_backoff=30,
+ retry_backoff_max=600,
+ retry_jitter=True,
+)
@set_code_owner_attribute
def update_certificate_available_date_on_course_update(self, course_key):
"""
@@ -764,22 +737,19 @@ def update_certificate_available_date_on_course_update(self, course_key):
Args:
course_key(str): The course run's identifier
"""
- countdown = 2**self.request.retries
-
# If the CredentialsApiConfig configuration model is disabled for this feature, it may indicate a condition where
# processing of such tasks has been temporarily disabled. Since this is a recoverable situation, mark this task for
# retry instead of failing it.
if not is_credentials_enabled():
error_msg = (
- "Cannot execute the `update_certificate_visible_date_on_course_update` task. Issuing user credentials "
+ "Cannot execute the `update_certificate_available_date_on_course_update` task. Issuing user credentials "
"through the Credentials IDA is disabled."
)
LOGGER.warning(error_msg)
- exception = MaxRetriesExceededError(
+ raise MaxRetriesExceededError(
"Failed to update the `certificate_available_date` in the Credentials service for course-run "
f"{course_key}. Reason: {error_msg}"
)
- raise self.retry(exc=exception, countdown=countdown, max_retries=MAX_RETRIES)
course_overview = get_course_overview_or_none(course_key)
if not course_overview:
@@ -818,6 +788,5 @@ def update_certificate_available_date_on_course_update(self, course_key):
new_certificate_available_date = None
update_credentials_course_certificate_configuration_available_date.delay(
- str(course_key),
- new_certificate_available_date
+ str(course_key), new_certificate_available_date
)
diff --git a/openedx/core/djangoapps/programs/tests/test_signals.py b/openedx/core/djangoapps/programs/tests/test_signals.py
index afb128a5069c..fae9b1dff22a 100644
--- a/openedx/core/djangoapps/programs/tests/test_signals.py
+++ b/openedx/core/djangoapps/programs/tests/test_signals.py
@@ -1,17 +1,15 @@
"""
This module contains tests for programs-related signals and signal handlers.
"""
-import datetime
+import datetime
from unittest import mock
from django.test import TestCase
from opaque_keys.edx.keys import CourseKey
from common.djangoapps.student.tests.factories import UserFactory
-from openedx.core.djangoapps.content.course_overviews.tests.factories import (
- CourseOverviewFactory,
-)
+from openedx.core.djangoapps.content.course_overviews.tests.factories import CourseOverviewFactory
from openedx.core.djangoapps.programs.signals import (
handle_course_cert_awarded,
handle_course_cert_changed,
@@ -28,15 +26,15 @@
from openedx.core.djangoapps.site_configuration.tests.factories import SiteConfigurationFactory
from openedx.core.djangolib.testing.utils import skip_unless_lms
-TEST_USERNAME = 'test-user'
-TEST_COURSE_KEY = CourseKey.from_string('course-v1:edX+test_course+1')
+TEST_USERNAME = "test-user"
+TEST_COURSE_KEY = CourseKey.from_string("course-v1:edX+test_course+1")
# The credentials app isn't installed for the CMS.
@skip_unless_lms
-@mock.patch('openedx.core.djangoapps.programs.tasks.award_program_certificates.delay')
+@mock.patch("openedx.core.djangoapps.programs.tasks.award_program_certificates.delay")
@mock.patch(
- 'openedx.core.djangoapps.credentials.models.CredentialsApiConfig.is_learner_issuance_enabled',
+ "openedx.core.djangoapps.credentials.models.CredentialsApiConfig.is_learner_issuance_enabled",
new_callable=mock.PropertyMock,
return_value=False,
)
@@ -54,8 +52,8 @@ def signal_kwargs(self):
sender=self.__class__,
user=UserFactory.create(username=TEST_USERNAME),
course_key=TEST_COURSE_KEY,
- mode='test-mode',
- status='test-status',
+ mode="test-mode",
+ status="test-status",
)
def test_signal_received(self, mock_is_learner_issuance_enabled, mock_task): # pylint: disable=unused-argument
@@ -95,9 +93,9 @@ def test_programs_enabled(self, mock_is_learner_issuance_enabled, mock_task):
# The credentials app isn't installed for the CMS.
@skip_unless_lms
-@mock.patch('openedx.core.djangoapps.programs.tasks.award_course_certificate.delay')
+@mock.patch("openedx.core.djangoapps.programs.tasks.award_course_certificate.delay")
@mock.patch(
- 'openedx.core.djangoapps.credentials.models.CredentialsApiConfig.is_learner_issuance_enabled',
+ "openedx.core.djangoapps.credentials.models.CredentialsApiConfig.is_learner_issuance_enabled",
new_callable=mock.PropertyMock,
return_value=False,
)
@@ -119,8 +117,8 @@ def signal_kwargs(self):
sender=self.__class__,
user=self.user,
course_key=TEST_COURSE_KEY,
- mode='test-mode',
- status='test-status',
+ mode="test-mode",
+ status="test-status",
)
def test_signal_received(self, mock_is_learner_issuance_enabled, mock_task): # pylint: disable=unused-argument
@@ -160,9 +158,7 @@ def test_credentials_enabled(self, mock_is_learner_issuance_enabled, mock_task):
def test_records_enabled(self, mock_is_learner_issuance_enabled, mock_task):
mock_is_learner_issuance_enabled.return_value = True
- site_config = SiteConfigurationFactory.create(
- site_values={'course_org_filter': ['edX']}
- )
+ site_config = SiteConfigurationFactory.create(site_values={"course_org_filter": ["edX"]})
# Correctly sent
handle_course_cert_changed(**self.signal_kwargs)
@@ -170,7 +166,7 @@ def test_records_enabled(self, mock_is_learner_issuance_enabled, mock_task):
mock_task.reset_mock()
# Correctly not sent
- site_config.site_values['ENABLE_LEARNER_RECORDS'] = False
+ site_config.site_values["ENABLE_LEARNER_RECORDS"] = False
site_config.save()
handle_course_cert_changed(**self.signal_kwargs)
assert not mock_task.called
@@ -178,9 +174,9 @@ def test_records_enabled(self, mock_is_learner_issuance_enabled, mock_task):
# The credentials app isn't installed for the CMS.
@skip_unless_lms
-@mock.patch('openedx.core.djangoapps.programs.tasks.revoke_program_certificates.delay')
+@mock.patch("openedx.core.djangoapps.programs.tasks.revoke_program_certificates.delay")
@mock.patch(
- 'openedx.core.djangoapps.credentials.models.CredentialsApiConfig.is_learner_issuance_enabled',
+ "openedx.core.djangoapps.credentials.models.CredentialsApiConfig.is_learner_issuance_enabled",
new_callable=mock.PropertyMock,
return_value=False,
)
@@ -198,8 +194,8 @@ def signal_kwargs(self):
sender=self.__class__,
user=UserFactory.create(username=TEST_USERNAME),
course_key=TEST_COURSE_KEY,
- mode='test-mode',
- status='test-status',
+ mode="test-mode",
+ status="test-status",
)
def test_signal_received(self, mock_is_learner_issuance_enabled, mock_task): # pylint: disable=unused-argument
@@ -238,10 +234,9 @@ def test_programs_enabled(self, mock_is_learner_issuance_enabled, mock_task):
@skip_unless_lms
-@mock.patch('openedx.core.djangoapps.programs.tasks.update_certificate_visible_date_on_course_update.delay')
-@mock.patch('openedx.core.djangoapps.programs.tasks.update_certificate_available_date_on_course_update.delay')
+@mock.patch("openedx.core.djangoapps.programs.tasks.update_certificate_available_date_on_course_update.delay")
@mock.patch(
- 'openedx.core.djangoapps.credentials.models.CredentialsApiConfig.is_learner_issuance_enabled',
+ "openedx.core.djangoapps.credentials.models.CredentialsApiConfig.is_learner_issuance_enabled",
new_callable=mock.PropertyMock,
return_value=False,
)
@@ -255,37 +250,26 @@ def signal_kwargs(self):
"""
DRY helper.
"""
- return {
- 'sender': self.__class__,
- 'course_key': TEST_COURSE_KEY,
- 'available_date': datetime.datetime.now()
- }
+ return {"sender": self.__class__, "course_key": TEST_COURSE_KEY, "available_date": datetime.datetime.now()}
- def test_signal_received(
- self,
- mock_is_learner_issuance_enabled,
- mock_visible_date_task,
- mock_cad_task
- ):
+ def test_signal_received(self, mock_is_learner_issuance_enabled, mock_cad_task):
"""
Ensures the receiver function is invoked when COURSE_CERT_DATE_CHANGE is sent.
"""
COURSE_CERT_DATE_CHANGE.send(**self.signal_kwargs)
assert mock_is_learner_issuance_enabled.call_count == 1
- assert mock_visible_date_task.call_count == 0
assert mock_cad_task.call_count == 0
- def test_programs_disabled(self, mock_is_learner_issuance_enabled, mock_visible_date_task, mock_cad_task):
+ def test_programs_disabled(self, mock_is_learner_issuance_enabled, mock_cad_task):
"""
Ensures that the receiver function does not queue any celery tasks if the system is not configured to use the
Credentials service.
"""
handle_course_cert_date_change(**self.signal_kwargs)
assert mock_is_learner_issuance_enabled.call_count == 1
- assert mock_visible_date_task.call_count == 0
assert mock_cad_task.call_count == 0
- def test_programs_enabled(self, mock_is_learner_issuance_enabled, mock_visible_date_task, mock_cad_task):
+ def test_programs_enabled(self, mock_is_learner_issuance_enabled, mock_cad_task):
"""
Ensures that the receiver function enqueues the expected celery tasks when the system is configured to use the
Credentials IDA.
@@ -294,14 +278,12 @@ def test_programs_enabled(self, mock_is_learner_issuance_enabled, mock_visible_d
handle_course_cert_date_change(**self.signal_kwargs)
assert mock_is_learner_issuance_enabled.call_count == 1
- assert mock_visible_date_task.call_count == 1
assert mock_cad_task.call_count == 1
@skip_unless_lms
-@mock.patch('openedx.core.djangoapps.programs.tasks.update_certificate_visible_date_on_course_update.delay')
-@mock.patch('openedx.core.djangoapps.programs.tasks.update_certificate_available_date_on_course_update.delay')
-@mock.patch('openedx.core.djangoapps.programs.signals.is_credentials_enabled')
+@mock.patch("openedx.core.djangoapps.programs.tasks.update_certificate_available_date_on_course_update.delay")
+@mock.patch("openedx.core.djangoapps.programs.signals.is_credentials_enabled")
class CoursePacingChangedReceiverTest(TestCase):
"""
Tests for the `handle_course_pacing_change` signal handler function.
@@ -319,15 +301,14 @@ def signal_kwargs(self):
DRY helper.
"""
return {
- 'sender': self.__class__,
- 'updated_course_overview': self.course_overview,
+ "sender": self.__class__,
+ "updated_course_overview": self.course_overview,
}
def test_handle_course_pacing_change_credentials_disabled(
self,
mock_is_creds_enabled,
mock_cad_task,
- mock_visible_date_task,
):
"""
Test the verifies the behavior of the `handle_course_pacing_change` signal receiver when use of the Credentials
@@ -337,14 +318,12 @@ def test_handle_course_pacing_change_credentials_disabled(
handle_course_pacing_change(**self.signal_kwargs)
assert mock_is_creds_enabled.call_count == 1
- assert mock_visible_date_task.call_count == 0
assert mock_cad_task.call_count == 0
def test_handle_course_pacing_change_credentials_enabled(
self,
mock_is_creds_enabled,
mock_cad_task,
- mock_visible_date_task
):
"""
Test that verifies the behavior of the `handle_course_pacing_change` signal receiver when use of the Credentials
@@ -355,5 +334,4 @@ def test_handle_course_pacing_change_credentials_enabled(
handle_course_pacing_change(**self.signal_kwargs)
assert mock_is_creds_enabled.call_count == 1
- assert mock_visible_date_task.call_count == 1
assert mock_cad_task.call_count == 1
diff --git a/openedx/core/djangoapps/programs/tests/test_tasks.py b/openedx/core/djangoapps/programs/tests/test_tasks.py
index 4b3a143fc944..d30cc4e8689e 100644
--- a/openedx/core/djangoapps/programs/tests/test_tasks.py
+++ b/openedx/core/djangoapps/programs/tests/test_tasks.py
@@ -2,7 +2,6 @@
Tests for programs celery tasks.
"""
-
import json
import logging
from datetime import datetime, timedelta
@@ -22,21 +21,13 @@
from common.djangoapps.course_modes.tests.factories import CourseModeFactory
from common.djangoapps.student.tests.factories import UserFactory
-from lms.djangoapps.certificates.tests.factories import (
- CertificateDateOverrideFactory,
- GeneratedCertificateFactory,
-)
+from lms.djangoapps.certificates.tests.factories import CertificateDateOverrideFactory, GeneratedCertificateFactory
from openedx.core.djangoapps.catalog.tests.mixins import CatalogIntegrationMixin
-from openedx.core.djangoapps.content.course_overviews.tests.factories import (
- CourseOverviewFactory,
-)
+from openedx.core.djangoapps.content.course_overviews.tests.factories import CourseOverviewFactory
from openedx.core.djangoapps.credentials.tests.mixins import CredentialsApiConfigMixin
from openedx.core.djangoapps.oauth_dispatch.tests.factories import ApplicationFactory
from openedx.core.djangoapps.programs import tasks
-from openedx.core.djangoapps.site_configuration.tests.factories import (
- SiteConfigurationFactory,
- SiteFactory,
-)
+from openedx.core.djangoapps.site_configuration.tests.factories import SiteConfigurationFactory, SiteFactory
from openedx.core.djangolib.testing.utils import skip_unless_lms
from xmodule.data import CertificatesDisplayBehaviors
@@ -109,7 +100,7 @@ def test_award_program_certificate(self, mock_get_api_base_url):
"http://test-server/credentials/",
)
- tasks.award_program_certificate(test_client, student, 123, datetime(2010, 5, 30))
+ tasks.award_program_certificate(test_client, student, 123)
expected_body = {
"username": student.username,
@@ -118,17 +109,41 @@ def test_award_program_certificate(self, mock_get_api_base_url):
"program_uuid": 123,
"type": tasks.PROGRAM_CERTIFICATE,
},
- "attributes": [
- {
- "name": "visible_date",
- "value": "2010-05-30T00:00:00Z",
- }
- ],
}
last_request_body = httpretty.last_request().body.decode("utf-8")
assert json.loads(last_request_body) == expected_body
+@skip_unless_lms
+@ddt.ddt
+@override_settings(CREDENTIALS_SERVICE_USERNAME="test-service-username")
+class AwardProgramCertificatesUtilitiesTestCase(CatalogIntegrationMixin, CredentialsApiConfigMixin, TestCase):
+ """
+ Tests for the utility methods for the 'award_program_certificates' celery task.
+ """
+
+ def setUp(self):
+ super().setUp()
+ self.create_credentials_config()
+ self.student = UserFactory.create(username="test-student")
+ self.site = SiteFactory()
+ self.site_configuration = SiteConfigurationFactory(site=self.site)
+ self.catalog_integration = self.create_catalog_integration()
+ ApplicationFactory.create(name="credentials")
+ UserFactory.create(username=settings.CREDENTIALS_SERVICE_USERNAME)
+
+ def test_get_completed_programs(self):
+ """get_completed_programs returns result of ProgramProgressMeter.completed_programs_with_available_dates"""
+ expected = {1: 1, 2: 2, 3: 3}
+ with mock.patch(
+ TASKS_MODULE + ".ProgramProgressMeter.completed_programs_with_available_dates",
+ new_callable=mock.PropertyMock,
+ ) as mock_completed_programs_with_available_dates:
+ mock_completed_programs_with_available_dates.return_value = expected
+ completed_programs = tasks.get_completed_programs(self.site, self.student)
+ assert expected == completed_programs
+
+
@skip_unless_lms
@ddt.ddt
@mock.patch(TASKS_MODULE + ".award_program_certificate")
@@ -189,10 +204,6 @@ def test_awarding_certs(
actual_program_uuids = [call[0][2] for call in mock_award_program_certificate.call_args_list]
assert actual_program_uuids == expected_awarded_program_uuids
- actual_visible_dates = [call[0][3] for call in mock_award_program_certificate.call_args_list]
- assert actual_visible_dates == expected_awarded_program_uuids
- # program uuids are same as mock dates
-
@mock.patch("openedx.core.djangoapps.site_configuration.helpers.get_current_site_configuration")
def test_awarding_certs_with_skip_program_certificate(
self,
@@ -224,9 +235,6 @@ def test_awarding_certs_with_skip_program_certificate(
tasks.award_program_certificates.delay(self.student.username).get()
actual_program_uuids = [call[0][2] for call in mock_award_program_certificate.call_args_list]
assert actual_program_uuids == expected_awarded_program_uuids
- actual_visible_dates = [call[0][3] for call in mock_award_program_certificate.call_args_list]
- assert actual_visible_dates == expected_awarded_program_uuids
- # program uuids are same as mock dates
@ddt.data(
("credentials", "enable_learner_issuance"),
@@ -311,7 +319,7 @@ def test_failure_to_create_api_client_retries(
tasks.award_program_certificates.delay(self.student.username).get()
assert mock_exception.called
- assert mock_get_api_client.call_count == (tasks.MAX_RETRIES + 1)
+ assert mock_get_api_client.call_count == (tasks.MAX_RETRIES)
assert not mock_award_program_certificate.called
def _make_side_effect(self, side_effects):
@@ -480,9 +488,7 @@ def test_post_course_certificate(self, mock_get_api_base_url):
"http://test-server/credentials/",
)
- visible_date = datetime.now()
-
- tasks.post_course_certificate(test_client, self.student.username, self.certificate, visible_date)
+ tasks.post_course_certificate(test_client, self.student.username, self.certificate)
expected_body = {
"username": self.student.username,
@@ -493,12 +499,6 @@ def test_post_course_certificate(self, mock_get_api_base_url):
"type": tasks.COURSE_CERTIFICATE,
},
"date_override": None,
- "attributes": [
- {
- "name": "visible_date",
- "value": visible_date.strftime("%Y-%m-%dT%H:%M:%SZ"), # text representation of date
- }
- ],
}
last_request_body = httpretty.last_request().body.decode("utf-8")
assert json.loads(last_request_body) == expected_body
@@ -564,7 +564,6 @@ def test_award_course_certificates(self, mode, mock_post_course_certificate):
call_args, _ = mock_post_course_certificate.call_args
assert call_args[1] == self.student.username
assert call_args[2] == self.certificate
- assert call_args[3] == self.certificate.modified_date
def test_award_course_certificates_available_date(self, mock_post_course_certificate):
"""
@@ -576,7 +575,6 @@ def test_award_course_certificates_available_date(self, mock_post_course_certifi
call_args, _ = mock_post_course_certificate.call_args
assert call_args[1] == self.student.username
assert call_args[2] == self.certificate
- assert call_args[3] == self.available_date
def test_award_course_certificates_override_date(self, mock_post_course_certificate):
"""
@@ -587,8 +585,7 @@ def test_award_course_certificates_override_date(self, mock_post_course_certific
call_args, _ = mock_post_course_certificate.call_args
assert call_args[1] == self.student.username
assert call_args[2] == self.certificate
- assert call_args[3] == self.certificate.modified_date
- assert call_args[4] == self.certificate.date_override.date
+ assert call_args[3] == self.certificate.date_override.date
def test_award_course_cert_not_called_if_disabled(self, mock_post_course_certificate):
"""
@@ -611,6 +608,17 @@ def test_award_course_cert_not_called_if_user_not_found(self, mock_post_course_c
assert mock_exception.called
assert not mock_post_course_certificate.called
+ def test_award_course_cert_not_called_if_course_overview_not_found(self, mock_post_course_certificate):
+ """
+ Test that the post method is never called if the CourseOverview isn't found
+ """
+ self.course.delete()
+ with mock.patch(TASKS_MODULE + ".LOGGER.warning") as mock_exception:
+ # Use the certificate course id here since the course will be deleted
+ tasks.award_course_certificate.delay(self.student.username, str(self.certificate.course_id)).get()
+ assert mock_exception.called
+ assert not mock_post_course_certificate.called
+
def test_award_course_cert_not_called_if_certificate_not_found(self, mock_post_course_certificate):
"""
Test that the post method is never called if the certificate doesn't exist for the user and course
@@ -621,16 +629,21 @@ def test_award_course_cert_not_called_if_certificate_not_found(self, mock_post_c
assert mock_exception.called
assert not mock_post_course_certificate.called
- def test_award_course_cert_not_called_if_course_overview_not_found(self, mock_post_course_certificate):
+ def test_award_course_cert_not_called_if_course_run_key_is_bad(self, mock_post_course_certificate):
"""
- Test that the post method is never called if the CourseOverview isn't found
+ Test that the post method is never called if the course run key is invalid
"""
- self.course.delete()
- with mock.patch(TASKS_MODULE + ".LOGGER.warning") as mock_exception:
- # Use the certificate course id here since the course will be deleted
- tasks.award_course_certificate.delay(self.student.username, str(self.certificate.course_id)).get()
- assert mock_exception.called
- assert not mock_post_course_certificate.called
+ bad_course_run_key = "I/Am/The/Keymaster"
+ expected_message = (
+ f"Failed to award course certificate for user {self.student.id} for course "
+ f"{bad_course_run_key}. Reason: Failed to determine course key"
+ )
+ with LogCapture(level=logging.WARNING) as log_capture:
+ tasks.award_course_certificate.delay(self.student.username, bad_course_run_key).get()
+ assert not mock_post_course_certificate.called
+ log_capture.check_present(
+ ("openedx.core.djangoapps.programs.tasks", "WARNING", expected_message),
+ )
def test_award_course_cert_not_called_if_certificated_not_verified_mode(self, mock_post_course_certificate):
"""
@@ -922,7 +935,7 @@ def test_get_api_client_failure_retries(
with pytest.raises(MaxRetriesExceededError):
tasks.revoke_program_certificates.delay(self.student.username, self.course_key).get()
assert mock_exception.called
- assert mock_get_api_client.call_count == (tasks.MAX_RETRIES + 1)
+ assert mock_get_api_client.call_count == (tasks.MAX_RETRIES)
assert not mock_revoke_program_certificate.called
@@ -1002,74 +1015,6 @@ def test_post_course_certificate_configuration(self, mock_get_api_base_url):
assert json.loads(last_request_body) == expected_body
-@skip_unless_lms
-class UpdateCertificateVisibleDatesOnCourseUpdateTestCase(CredentialsApiConfigMixin, TestCase):
- """
- Tests for the `update_certificate_visible_date_on_course_update` task.
- """
-
- def setUp(self):
- super().setUp()
- self.credentials_api_config = self.create_credentials_config(enabled=False)
- # setup course
- self.course = CourseOverviewFactory.create()
- # setup users
- self.student1 = UserFactory.create(username="test-student1")
- self.student2 = UserFactory.create(username="test-student2")
- self.student3 = UserFactory.create(username="test-student3")
- # award certificates to users in course we created
- self.certificate_student1 = GeneratedCertificateFactory.create(
- user=self.student1,
- mode="verified",
- course_id=self.course.id,
- status="downloadable",
- )
- self.certificate_student2 = GeneratedCertificateFactory.create(
- user=self.student2,
- mode="verified",
- course_id=self.course.id,
- status="downloadable",
- )
- self.certificate_student3 = GeneratedCertificateFactory.create(
- user=self.student3,
- mode="verified",
- course_id=self.course.id,
- status="downloadable",
- )
-
- def tearDown(self):
- super().tearDown()
- self.credentials_api_config = self.create_credentials_config(enabled=False)
-
- def test_update_visible_dates_but_credentials_config_disabled(self):
- """
- This test verifies the behavior of the `update_certificate_visible_date_on_course_update` task when the
- CredentialsApiConfig is disabled.
-
- If the system is configured to _not_ use the Credentials IDA, we should expect this task to eventually throw an
- exception when the max number of retries has reached.
- """
- with pytest.raises(MaxRetriesExceededError):
- tasks.update_certificate_visible_date_on_course_update(self.course.id) # pylint: disable=no-value-for-parameter
-
- def test_update_visible_dates(self):
- """
- Happy path test.
-
- This test verifies the behavior of the `update_certificate_visible_date_on_course_update` task. This test
- verifies attempts by the system to queue a number of `award_course_certificate` tasks to ensure the
- `visible_date` attribute is updated on all eligible course certificates.
- """
- # enable the CredentialsApiConfig to issue certificates using the Credentials service
- self.credentials_api_config.enabled = True
- self.credentials_api_config.enable_learner_issuance = True
-
- with mock.patch(f"{TASKS_MODULE}.award_course_certificate.delay") as award_course_cert:
- tasks.update_certificate_visible_date_on_course_update(self.course.id) # pylint: disable=no-value-for-parameter
-
- assert award_course_cert.call_count == 3
-
-
@skip_unless_lms
class UpdateCertificateAvailableDateOnCourseUpdateTestCase(CredentialsApiConfigMixin, TestCase):
"""
@@ -1117,7 +1062,8 @@ def test_update_certificate_available_date_credentials_config_disabled(self):
)
with pytest.raises(MaxRetriesExceededError):
- tasks.update_certificate_available_date_on_course_update(course_overview.id) # pylint: disable=no-value-for-parameter
+ # pylint: disable=no-value-for-parameter
+ tasks.update_certificate_available_date_on_course_update(course_overview.id)
@mock.patch(f"{TASKS_MODULE}.update_credentials_course_certificate_configuration_available_date.delay")
def test_update_certificate_available_date_instructor_paced_cdb_early_no_info(self, mock_update):
@@ -1143,7 +1089,8 @@ def test_update_certificate_available_date_instructor_paced_cdb_early_no_info(se
self.end_date,
)
- tasks.update_certificate_available_date_on_course_update(course_overview.id) # pylint: disable=no-value-for-parameter
+ # pylint: disable=no-value-for-parameter
+ tasks.update_certificate_available_date_on_course_update(course_overview.id)
mock_update.assert_called_once_with(str(course_overview.id), None)
@mock.patch(f"{TASKS_MODULE}.update_credentials_course_certificate_configuration_available_date.delay")
@@ -1168,7 +1115,8 @@ def test_update_certificate_available_date_instructor_paced_cdb_end(self, mock_u
self.end_date,
)
- tasks.update_certificate_available_date_on_course_update(course_overview.id) # pylint: disable=no-value-for-parameter
+ # pylint: disable=no-value-for-parameter
+ tasks.update_certificate_available_date_on_course_update(course_overview.id)
mock_update.assert_called_once_with(str(course_overview.id), str(self.end_date))
@mock.patch(f"{TASKS_MODULE}.update_credentials_course_certificate_configuration_available_date.delay")
@@ -1196,7 +1144,8 @@ def test_update_certificate_available_date_instructor_paced_cdb_end_with_date(se
self.end_date,
)
- tasks.update_certificate_available_date_on_course_update(course_overview.id) # pylint: disable=no-value-for-parameter
+ # pylint: disable=no-value-for-parameter
+ tasks.update_certificate_available_date_on_course_update(course_overview.id)
mock_update.assert_called_once_with(str(course_overview.id), str(certificate_available_date))
@mock.patch(f"{TASKS_MODULE}.update_credentials_course_certificate_configuration_available_date.delay")
@@ -1228,7 +1177,8 @@ def test_update_certificate_available_date_self_paced(self, mock_update):
self.end_date,
)
- tasks.update_certificate_available_date_on_course_update(course_overview.id) # pylint: disable=no-value-for-parameter
+ # pylint: disable=no-value-for-parameter
+ tasks.update_certificate_available_date_on_course_update(course_overview.id)
mock_update.assert_called_once_with(str(course_overview.id), None)
def test_update_certificate_available_date_no_course_overview(self):
@@ -1246,8 +1196,9 @@ def test_update_certificate_available_date_no_course_overview(self):
self._update_credentials_api_config(True)
with LogCapture(level=logging.WARNING) as log_capture:
- tasks.update_certificate_available_date_on_course_update(bad_course_run_key) # pylint: disable=no-value-for-parameter
+ # pylint: disable=no-value-for-parameter
+ tasks.update_certificate_available_date_on_course_update(bad_course_run_key)
log_capture.check_present(
- ('openedx.core.djangoapps.programs.tasks', 'WARNING', expected_message),
+ ("openedx.core.djangoapps.programs.tasks", "WARNING", expected_message),
)
diff --git a/openedx/core/djangoapps/programs/tests/test_utils.py b/openedx/core/djangoapps/programs/tests/test_utils.py
index b2ddf6d9045f..264f1a6aeebd 100644
--- a/openedx/core/djangoapps/programs/tests/test_utils.py
+++ b/openedx/core/djangoapps/programs/tests/test_utils.py
@@ -6,7 +6,6 @@
from collections import namedtuple
from copy import deepcopy
from unittest import mock
-from urllib.parse import urlencode
import ddt
import httpretty
@@ -44,10 +43,8 @@
ProgramDataExtender,
ProgramMarketingDataExtender,
ProgramProgressMeter,
- get_buy_subscription_url,
get_certificates,
get_logged_in_program_certificate_url,
- get_programs_subscription_data,
is_user_enrolled_in_program_type
)
from openedx.core.djangoapps.site_configuration.tests.factories import SiteFactory
@@ -1759,100 +1756,3 @@ def test_user_with_entitlement_no_enrollment(self, mock_get_programs_by_type):
)
mock_get_programs_by_type.return_value = [self.program]
assert is_user_enrolled_in_program_type(user=self.user, program_type_slug=self.MICROBACHELORS)
-
-
-@skip_unless_lms
-class TestGetProgramsSubscriptionData(TestCase):
- """
- Tests for the get_programs_subscription_data utility function.
- """
- @classmethod
- def setUpClass(cls):
- super().setUpClass()
-
- cls.mock_program_subscription_data = [
- {'id': uuid.uuid4(), 'resource_id': uuid.uuid4(),
- 'resource_type': 'program', 'resource_data': None, 'trial_end': '1970-01-01T00:02:03Z',
- 'price': '100.00', 'currency': 'USD', 'sub_type': 'stripe', 'identifier': 'dummy_1',
- 'current_period_end': '1970-01-01T00:02:03Z', 'status': 'active',
- 'customer': 1, 'subscription_state': 'active'},
- {'id': uuid.uuid4(), 'resource_id': uuid.uuid4(),
- 'resource_type': 'program', 'resource_data': None, 'trial_end': '1970-01-01T03:25:12Z',
- 'price': '1000.00', 'currency': 'USD', 'sub_type': 'stripe', 'identifier': 'dummy_2',
- 'current_period_end': '1970-05-23T12:05:21Z', 'status': 'subscription_initiated',
- 'customer': 1, 'subscription_state': 'notStarted'}
- ]
-
- @mock.patch(UTILS_MODULE + ".get_subscription_api_client")
- @mock.patch(UTILS_MODULE + ".log.info")
- def test_get_programs_subscription_data(self, mock_log, mock_get_subscription_api_client):
- # mock return values
- mock_client = mock.Mock()
- mock_get_subscription_api_client.return_value = mock_client
- mock_response = {"results": self.mock_program_subscription_data, "next": None}
- mock_client.get.return_value = mock.Mock(json=lambda: mock_response, raise_for_status=lambda: None)
-
- # call the function
- user = mock.Mock()
- result = get_programs_subscription_data(user)
-
- # assert expected behavior
- mock_log.assert_called_once_with(f"B2C_SUBSCRIPTIONS: Requesting Program subscription data for user: {user}")
- mock_get_subscription_api_client.assert_called_once_with(user)
- mock_client.get.assert_called_once_with(settings.SUBSCRIPTIONS_API_PATH, params={"page": 1})
- assert result == self.mock_program_subscription_data
-
- @mock.patch(UTILS_MODULE + ".get_subscription_api_client")
- @mock.patch(UTILS_MODULE + ".log.info")
- def test_get_programs_subscription_data_with_uuid(self, mock_log, mock_get_subscription_api_client):
- mock_client = mock.Mock()
- mock_get_subscription_api_client.return_value = mock_client
- subscription_data = self.mock_program_subscription_data[0]
- program_uuid = subscription_data['resource_id']
-
- mock_response = {"results": subscription_data, "next": None}
- mock_client.get.return_value = mock.Mock(json=lambda: mock_response, raise_for_status=lambda: None)
-
- user = mock.Mock()
- result = get_programs_subscription_data(user, program_uuid=program_uuid)
-
- mock_log.assert_called_once_with(f"B2C_SUBSCRIPTIONS: Requesting Program subscription data for user: {user}"
- f" for program_uuid: {str(program_uuid)}")
- mock_get_subscription_api_client.assert_called_once_with(user)
- mock_client.get.assert_called_once_with(
- settings.SUBSCRIPTIONS_API_PATH,
- params={
- "most_active_and_recent": 'true',
- "resource_id": program_uuid,
- }
- )
- assert result == subscription_data
-
-
-@override_settings(SUBSCRIPTIONS_BUY_SUBSCRIPTION_URL='http://subscription_buy_url/')
-@ddt.ddt
-class TestBuySubscriptionUrl(TestCase):
- """
- Tests for the BuySubscriptionUrl utility function.
- """
- @ddt.data(
- {
- 'skus': ['TESTSKU'],
- 'program_uuid': '12345678-9012-3456-7890-123456789012'
- },
- {
- 'skus': ['TESTSKU1', 'TESTSKU2', 'TESTSKU3'],
- 'program_uuid': '12345678-9012-3456-7890-123456789012'
- },
- {
- 'skus': [],
- 'program_uuid': '12345678-9012-3456-7890-123456789012'
- }
- )
- @ddt.unpack
- def test_get_buy_subscription_url(self, skus, program_uuid):
- """ Verify the subscription purchase page URL is properly constructed and returned. """
- url = get_buy_subscription_url(program_uuid, skus)
- formatted_skus = urlencode({'sku': skus}, doseq=True)
- expected_url = f'{settings.SUBSCRIPTIONS_BUY_SUBSCRIPTION_URL}{program_uuid}/?{formatted_skus}'
- assert url == expected_url
diff --git a/openedx/core/djangoapps/programs/utils.py b/openedx/core/djangoapps/programs/utils.py
index c9858a7bd27c..b842b8b363b6 100644
--- a/openedx/core/djangoapps/programs/utils.py
+++ b/openedx/core/djangoapps/programs/utils.py
@@ -1,14 +1,12 @@
"""Helper functions for working with Programs."""
-
import datetime
import logging
from collections import defaultdict
from copy import deepcopy
from itertools import chain
-from urllib.parse import urlencode, urljoin, urlparse, urlunparse
+from urllib.parse import urljoin, urlparse, urlunparse
-import requests
from dateutil.parser import parse
from django.conf import settings
from django.contrib.auth import get_user_model
@@ -16,7 +14,6 @@
from django.core.cache import cache
from django.urls import reverse
from django.utils.functional import cached_property
-from edx_rest_api_client.auth import SuppliedJwtAuth
from opaque_keys.edx.keys import CourseKey
from pytz import utc
from requests.exceptions import RequestException
@@ -36,14 +33,13 @@
from openedx.core.djangoapps.catalog.utils import (
get_fulfillable_course_runs_for_entitlement,
get_pathways,
- get_programs
+ get_programs,
)
from openedx.core.djangoapps.commerce.utils import get_ecommerce_api_base_url, get_ecommerce_api_client
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
from openedx.core.djangoapps.credentials.utils import get_credentials, get_credentials_records_url
from openedx.core.djangoapps.enrollments.api import get_enrollments
from openedx.core.djangoapps.enrollments.permissions import ENROLL_IN_COURSE
-from openedx.core.djangoapps.oauth_dispatch.jwt import create_jwt_for_user
from openedx.core.djangoapps.programs import ALWAYS_CALCULATE_PROGRAM_PRICE_AS_ANONYMOUS_USER
from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers
from xmodule.modulestore.django import modulestore
@@ -65,20 +61,12 @@ def get_program_and_course_data(site, user, program_uuid, mobile_only=False):
return program_data, course_data
-def get_buy_subscription_url(program_uuid, skus):
- """
- Returns the URL to the Subscription Purchase page for the given program UUID and course Skus.
- """
- formatted_skus = urlencode({'sku': skus}, doseq=True)
- url = f'{settings.SUBSCRIPTIONS_BUY_SUBSCRIPTION_URL}{program_uuid}/?{formatted_skus}'
- return url
-
-
def get_program_urls(program_data):
"""Returns important urls of program."""
from lms.djangoapps.learner_dashboard.utils import FAKE_COURSE_KEY, strip_course_id
- program_uuid = program_data.get('uuid')
- skus = program_data.get('skus')
+
+ program_uuid = program_data.get("uuid")
+ skus = program_data.get("skus")
ecommerce_service = EcommerceService()
# TODO: Don't have business logic of course-certificate==record-available here in LMS.
@@ -87,17 +75,11 @@ def get_program_urls(program_data):
# a waffle flag. This feature is in active developoment.
program_record_url = get_credentials_records_url(program_uuid=program_uuid)
urls = {
- 'program_listing_url': reverse('program_listing_view'),
- 'track_selection_url': strip_course_id(
- reverse('course_modes_choose', kwargs={'course_id': FAKE_COURSE_KEY})
- ),
- 'commerce_api_url': reverse('commerce_api:v0:baskets:create'),
- 'buy_button_url': ecommerce_service.get_checkout_page_url(*skus),
- 'program_record_url': program_record_url,
- 'buy_subscription_url': get_buy_subscription_url(program_uuid, skus),
- 'manage_subscription_url': settings.SUBSCRIPTIONS_MANAGE_SUBSCRIPTION_URL,
- 'orders_and_subscriptions_url': settings.ORDER_HISTORY_MICROFRONTEND_URL,
- 'subscriptions_learner_help_center_url': settings.SUBSCRIPTIONS_LEARNER_HELP_CENTER_URL
+ "program_listing_url": reverse("program_listing_view"),
+ "track_selection_url": strip_course_id(reverse("course_modes_choose", kwargs={"course_id": FAKE_COURSE_KEY})),
+ "commerce_api_url": reverse("commerce_api:v0:baskets:create"),
+ "buy_button_url": ecommerce_service.get_checkout_page_url(*skus),
+ "program_record_url": program_record_url,
}
return urls
@@ -107,12 +89,12 @@ def get_industry_and_credit_pathways(program_data, site):
industry_pathways = []
credit_pathways = []
try:
- for pathway_id in program_data['pathway_ids']:
+ for pathway_id in program_data["pathway_ids"]:
pathway = get_pathways(site, pathway_id)
- if pathway and pathway['email']:
- if pathway['pathway_type'] == PathwayType.CREDIT.value:
+ if pathway and pathway["email"]:
+ if pathway["pathway_type"] == PathwayType.CREDIT.value:
credit_pathways.append(pathway)
- elif pathway['pathway_type'] == PathwayType.INDUSTRY.value:
+ elif pathway["pathway_type"] == PathwayType.INDUSTRY.value:
industry_pathways.append(pathway)
# if pathway caching did not complete fully (no pathway_ids)
except KeyError:
@@ -124,22 +106,13 @@ def get_industry_and_credit_pathways(program_data, site):
def get_program_marketing_url(programs_config, mobile_only=False):
"""Build a URL used to link to programs on the marketing site."""
if mobile_only:
- marketing_url = 'edxapp://course?programs'
+ marketing_url = "edxapp://course?programs"
else:
- marketing_url = urljoin(settings.MKTG_URLS.get('ROOT'), programs_config.marketing_path).rstrip('/')
+ marketing_url = urljoin(settings.MKTG_URLS.get("ROOT"), programs_config.marketing_path).rstrip("/")
return marketing_url
-def get_program_subscriptions_marketing_url():
- """Build a URL used to link to subscription eligible programs on the marketing site."""
- marketing_urls = settings.MKTG_URLS
- return urljoin(
- marketing_urls.get('ROOT'),
- marketing_urls.get('PROGRAM_SUBSCRIPTIONS'),
- )
-
-
def attach_program_detail_url(programs, mobile_only=False):
"""Extend program representations by attaching a URL to be used when linking to program details.
@@ -153,13 +126,13 @@ def attach_program_detail_url(programs, mobile_only=False):
"""
for program in programs:
if mobile_only:
- detail_fragment_url = reverse('program_details_fragment_view', kwargs={'program_uuid': program['uuid']})
- path_id = detail_fragment_url.replace('/dashboard/', '')
- detail_url = f'edxapp://enrolled_program_info?path_id={path_id}'
+ detail_fragment_url = reverse("program_details_fragment_view", kwargs={"program_uuid": program["uuid"]})
+ path_id = detail_fragment_url.replace("/dashboard/", "")
+ detail_url = f"edxapp://enrolled_program_info?path_id={path_id}"
else:
- detail_url = reverse('program_details_view', kwargs={'program_uuid': program['uuid']})
+ detail_url = reverse("program_details_view", kwargs={"program_uuid": program["uuid"]})
- program['detail_url'] = detail_url
+ program["detail_url"] = detail_url
return programs
@@ -220,14 +193,14 @@ def invert_programs(self):
inverted_programs = defaultdict(list)
for program in self.programs:
- for course in program['courses']:
- course_uuid = course['uuid']
+ for course in program["courses"]:
+ course_uuid = course["uuid"]
if course_uuid in self.course_uuids:
program_list = inverted_programs[course_uuid]
if program not in program_list:
program_list.append(program)
- for course_run in course['course_runs']:
- course_run_id = course_run['key']
+ for course_run in course["course_runs"]:
+ course_run_id = course_run["key"]
if course_run_id in self.course_run_ids:
program_list = inverted_programs[course_run_id]
if program not in program_list:
@@ -235,7 +208,7 @@ def invert_programs(self):
# Sort programs by title for consistent presentation.
for program_list in inverted_programs.values():
- program_list.sort(key=lambda p: p['title'])
+ program_list.sort(key=lambda p: p["title"])
return inverted_programs
@@ -280,25 +253,22 @@ def _is_course_in_progress(self, now, course):
Returns:
bool, indicating whether the course is in progress.
"""
- enrolled_runs = [run for run in course['course_runs'] if run['key'] in self.course_run_ids]
+ enrolled_runs = [run for run in course["course_runs"] if run["key"] in self.course_run_ids]
# Check if the user is enrolled in a required run and mode/seat.
- runs_with_required_mode = [
- run for run in enrolled_runs
- if run['type'] == self.enrolled_run_modes[run['key']]
- ]
+ runs_with_required_mode = [run for run in enrolled_runs if run["type"] == self.enrolled_run_modes[run["key"]]]
if runs_with_required_mode:
- not_failed_runs = [run for run in runs_with_required_mode if run['key'] not in self.failed_course_runs]
+ not_failed_runs = [run for run in runs_with_required_mode if run["key"] not in self.failed_course_runs]
if not_failed_runs:
return True
# Check if seats required for course completion are still available.
upgrade_deadlines = []
for run in enrolled_runs:
- for seat in run['seats']:
- if seat['type'] == run['type'] and run['type'] != self.enrolled_run_modes[run['key']]:
- upgrade_deadlines.append(seat['upgrade_deadline'])
+ for seat in run["seats"]:
+ if seat["type"] == run["type"] and run["type"] != self.enrolled_run_modes[run["key"]]:
+ upgrade_deadlines.append(seat["upgrade_deadline"])
# An upgrade deadline of None means the course is always upgradeable.
return any(not deadline or deadline and parse(deadline) > now for deadline in upgrade_deadlines)
@@ -326,24 +296,21 @@ def progress(self, programs=None, count_only=True):
program_copy = deepcopy(program)
completed, in_progress, not_started = [], [], []
- for course in program_copy['courses']:
+ for course in program_copy["courses"]:
active_entitlement = CourseEntitlement.get_entitlement_if_active(
- user=self.user,
- course_uuid=course['uuid']
+ user=self.user, course_uuid=course["uuid"]
)
if self._is_course_complete(course):
completed.append(course)
elif self._is_course_enrolled(course) or active_entitlement:
# Show all currently enrolled courses and active entitlements as in progress
if active_entitlement:
- course['course_runs'] = get_fulfillable_course_runs_for_entitlement(
- active_entitlement,
- course['course_runs']
+ course["course_runs"] = get_fulfillable_course_runs_for_entitlement(
+ active_entitlement, course["course_runs"]
)
- course['user_entitlement'] = active_entitlement.to_dict()
- course['enroll_url'] = reverse(
- 'entitlements_api:v1:enrollments',
- args=[str(active_entitlement.uuid)]
+ course["user_entitlement"] = active_entitlement.to_dict()
+ course["enroll_url"] = reverse(
+ "entitlements_api:v1:enrollments", args=[str(active_entitlement.uuid)]
)
in_progress.append(course)
else:
@@ -351,20 +318,20 @@ def progress(self, programs=None, count_only=True):
if course_in_progress:
in_progress.append(course)
else:
- course['expired'] = not course_in_progress
+ course["expired"] = not course_in_progress
not_started.append(course)
else:
not_started.append(course)
- progress.append({
- 'uuid': program_copy['uuid'],
- 'completed': len(completed) if count_only else completed,
- 'in_progress': len(in_progress) if count_only else in_progress,
- 'not_started': len(not_started) if count_only else not_started,
- 'all_unenrolled': all(
- not self._is_course_enrolled(course) for course in program_copy['courses']
- ),
- })
+ progress.append(
+ {
+ "uuid": program_copy["uuid"],
+ "completed": len(completed) if count_only else completed,
+ "in_progress": len(in_progress) if count_only else in_progress,
+ "not_started": len(not_started) if count_only else not_started,
+ "all_unenrolled": all(not self._is_course_enrolled(course) for course in program_copy["courses"]),
+ }
+ )
return progress
@@ -383,7 +350,7 @@ def completed_programs_with_available_dates(self):
for program in self.programs:
available_date = self._available_date_for_program(program, certificates_by_run)
if available_date:
- completed[program['uuid']] = available_date
+ completed[program["uuid"]] = available_date
return completed
def _available_date_for_program(self, program_data, certificates):
@@ -397,11 +364,11 @@ def _available_date_for_program(self, program_data, certificates):
Returns a datetime object or None if the program is not complete.
"""
program_available_date = None
- for course in program_data['courses']:
+ for course in program_data["courses"]:
earliest_course_run_date = None
- for course_run in course['course_runs']:
- key = CourseKey.from_string(course_run['key'])
+ for course_run in course["course_runs"]:
+ key = CourseKey.from_string(course_run["key"])
# Get a certificate if one exists
certificate = certificates.get(key)
@@ -409,20 +376,15 @@ def _available_date_for_program(self, program_data, certificates):
continue
# Modes must match (see _is_course_complete() comments for why)
- course_run_mode = self._course_run_mode_translation(course_run['type'])
+ course_run_mode = self._course_run_mode_translation(course_run["type"])
certificate_mode = self._certificate_mode_translation(certificate.mode)
modes_match = course_run_mode == certificate_mode
# Grab the available date and keep it if it's the earliest one for this catalog course.
if modes_match and CertificateStatuses.is_passing_status(certificate.status):
course_overview = CourseOverview.get_from_id(key)
- available_date = certificate_api.available_date_for_certificate(
- course_overview,
- certificate
- )
- earliest_course_run_date = min(
- date for date in [available_date, earliest_course_run_date] if date
- )
+ available_date = certificate_api.available_date_for_certificate(course_overview, certificate)
+ earliest_course_run_date = min(date for date in [available_date, earliest_course_run_date] if date)
# If we're missing a cert for a course, the program isn't completed and we should just bail now
if earliest_course_run_date is None:
@@ -477,7 +439,7 @@ def reshape(course_run):
with course run certificates.
"""
return {
- 'course_run_id': course_run['key'],
+ "course_run_id": course_run["key"],
# A course run's type is assumed to indicate which mode must be
# completed in order for the run to count towards program completion.
# This supports the same flexible program construction allowed by the
@@ -485,10 +447,10 @@ def reshape(course_run):
# count towards completion of a course in a program). This may change
# in the future to make use of the more rigid set of "applicable seat
# types" associated with each program type in the catalog.
- 'type': self._course_run_mode_translation(course_run['type']),
+ "type": self._course_run_mode_translation(course_run["type"]),
}
- return any(reshape(course_run) in self.completed_course_runs for course_run in course['course_runs'])
+ return any(reshape(course_run) in self.completed_course_runs for course_run in course["course_runs"])
@cached_property
def completed_course_runs(self):
@@ -498,7 +460,7 @@ def completed_course_runs(self):
Returns:
list of dicts, each representing a course run certificate
"""
- return self.course_runs_with_state['completed']
+ return self.course_runs_with_state["completed"]
@cached_property
def failed_course_runs(self):
@@ -508,7 +470,7 @@ def failed_course_runs(self):
Returns:
list of strings, each a course run ID
"""
- return [run['course_run_id'] for run in self.course_runs_with_state['failed']]
+ return [run["course_run_id"] for run in self.course_runs_with_state["failed"]]
@cached_property
def course_runs_with_state(self):
@@ -525,10 +487,10 @@ def course_runs_with_state(self):
completed_runs, failed_runs = [], []
for certificate in course_run_certificates:
- course_key = certificate['course_key']
+ course_key = certificate["course_key"]
course_data = {
- 'course_run_id': str(course_key),
- 'type': self._certificate_mode_translation(certificate['type']),
+ "course_run_id": str(course_key),
+ "type": self._certificate_mode_translation(certificate["type"]),
}
try:
@@ -538,15 +500,12 @@ def course_runs_with_state(self):
else:
may_certify = certificate_api.certificates_viewable_for_course(course_overview)
- if (
- CertificateStatuses.is_passing_status(certificate['status'])
- and may_certify
- ):
+ if CertificateStatuses.is_passing_status(certificate["status"]) and may_certify:
completed_runs.append(course_data)
else:
failed_runs.append(course_data)
- return {'completed': completed_runs, 'failed': failed_runs}
+ return {"completed": completed_runs, "failed": failed_runs}
def _is_course_enrolled(self, course):
"""Check if a user is enrolled in a course.
@@ -560,7 +519,7 @@ def _is_course_enrolled(self, course):
Returns:
bool, indicating whether the course is in progress.
"""
- return any(course_run['key'] in self.course_run_ids for course_run in course['course_runs'])
+ return any(course_run["key"] in self.course_run_ids for course_run in course["course_runs"])
# pylint: disable=missing-docstring
@@ -578,7 +537,7 @@ def __init__(self, program_data, user, mobile_only=False):
self.data = program_data
self.user = user
self.mobile_only = mobile_only
- self.data.update({'is_mobile_only': self.mobile_only})
+ self.data.update({"is_mobile_only": self.mobile_only})
self.course_run_key = None
self.course_overview = None
@@ -586,7 +545,7 @@ def __init__(self, program_data, user, mobile_only=False):
def extend(self):
"""Execute extension handlers, returning the extended data."""
- self._execute('_extend')
+ self._execute("_extend")
self._collect_one_click_purchase_eligibility_data()
return self.data
@@ -601,51 +560,55 @@ def _handlers(cls, prefix):
def _extend_course_runs(self):
"""Execute course run data handlers."""
- for course in self.data['courses']:
- for course_run in course['course_runs']:
+ for course in self.data["courses"]:
+ for course_run in course["course_runs"]:
# State to be shared across handlers.
- self.course_run_key = CourseKey.from_string(course_run['key'])
+ self.course_run_key = CourseKey.from_string(course_run["key"])
# Some (old) course runs may exist for a program which do not exist in LMS. In that case,
# continue without the course run.
try:
self.course_overview = CourseOverview.get_from_id(self.course_run_key)
except CourseOverview.DoesNotExist:
- log.warning('Failed to get course overview for course run key: %s', course_run.get('key'))
+ log.warning("Failed to get course overview for course run key: %s", course_run.get("key"))
else:
self.enrollment_start = self.course_overview.enrollment_start or DEFAULT_ENROLLMENT_START_DATE
- self._execute('_attach_course_run', course_run)
+ self._execute("_attach_course_run", course_run)
def _attach_course_run_certificate_url(self, run_mode):
certificate_data = certificate_api.certificate_downloadable_status(self.user, self.course_run_key)
- certificate_uuid = certificate_data.get('uuid')
- run_mode['certificate_url'] = certificate_api.get_certificate_url(
- user_id=self.user.id, # Providing user_id allows us to fall back to PDF certificates
- # if web certificates are not configured for a given course.
- course_id=self.course_run_key,
- uuid=certificate_uuid,
- ) if certificate_uuid else None
+ certificate_uuid = certificate_data.get("uuid")
+ run_mode["certificate_url"] = (
+ certificate_api.get_certificate_url(
+ user_id=self.user.id, # Providing user_id allows us to fall back to PDF certificates
+ # if web certificates are not configured for a given course.
+ course_id=self.course_run_key,
+ uuid=certificate_uuid,
+ )
+ if certificate_uuid
+ else None
+ )
def _attach_course_run_course_url(self, run_mode):
if self.mobile_only:
- run_mode['course_url'] = 'edxapp://enrolled_course_info?course_id={}'.format(run_mode.get('key'))
+ run_mode["course_url"] = "edxapp://enrolled_course_info?course_id={}".format(run_mode.get("key"))
else:
- run_mode['course_url'] = reverse('course_root', args=[self.course_run_key])
+ run_mode["course_url"] = reverse("course_root", args=[self.course_run_key])
def _attach_course_run_enrollment_open_date(self, run_mode):
- run_mode['enrollment_open_date'] = strftime_localized(self.enrollment_start, 'SHORT_DATE')
+ 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)
+ run_mode["is_course_ended"] = end_date < datetime.datetime.now(utc)
def _attach_course_run_is_enrolled(self, run_mode):
- run_mode['is_enrolled'] = CourseEnrollment.is_enrolled(self.user, self.course_run_key)
+ 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
+ run_mode["is_enrollment_open"] = self.enrollment_start <= datetime.datetime.now(utc) < enrollment_end
def _attach_course_run_advertised_start(self, run_mode):
"""
@@ -654,10 +617,10 @@ def _attach_course_run_advertised_start(self, run_mode):
to start on December 1, 2016, the author might provide 'Winter 2016' as
the advertised start.
"""
- run_mode['advertised_start'] = self.course_overview.advertised_start
+ run_mode["advertised_start"] = self.course_overview.advertised_start
def _attach_course_run_upgrade_url(self, run_mode):
- required_mode_slug = run_mode['type']
+ required_mode_slug = run_mode["type"]
enrolled_mode_slug, _ = CourseEnrollment.enrollment_mode_for_user(self.user, self.course_run_key)
is_mode_mismatch = required_mode_slug != enrolled_mode_slug
is_upgrade_required = is_mode_mismatch and CourseEnrollment.is_enrolled(self.user, self.course_run_key)
@@ -666,19 +629,19 @@ def _attach_course_run_upgrade_url(self, run_mode):
# Requires that the ecommerce service be in use.
required_mode = CourseMode.mode_for_course(self.course_run_key, required_mode_slug)
ecommerce = EcommerceService()
- sku = getattr(required_mode, 'sku', None)
+ sku = getattr(required_mode, "sku", None)
if ecommerce.is_enabled(self.user) and sku:
- run_mode['upgrade_url'] = ecommerce.get_checkout_page_url(required_mode.sku)
+ run_mode["upgrade_url"] = ecommerce.get_checkout_page_url(required_mode.sku)
else:
- run_mode['upgrade_url'] = None
+ run_mode["upgrade_url"] = None
else:
- run_mode['upgrade_url'] = None
+ run_mode["upgrade_url"] = None
def _attach_course_run_may_certify(self, run_mode):
- run_mode['may_certify'] = certificate_api.certificates_viewable_for_course(self.course_overview)
+ run_mode["may_certify"] = certificate_api.certificates_viewable_for_course(self.course_overview)
def _attach_course_run_is_mobile_only(self, run_mode):
- run_mode['is_mobile_only'] = self.mobile_only
+ run_mode["is_mobile_only"] = self.mobile_only
def _filter_out_courses_with_entitlements(self, courses):
"""
@@ -694,17 +657,17 @@ def _filter_out_courses_with_entitlements(self, courses):
Returns:
A subset of the given list of course dicts
"""
- course_uuids = {course['uuid'] for course in courses}
+ course_uuids = {course["uuid"] for course in courses}
# Filter the entitlements' modes with a case-insensitive match against applicable seat_types
entitlements = self.user.courseentitlement_set.filter(
- mode__in=self.data['applicable_seat_types'],
+ mode__in=self.data["applicable_seat_types"],
course_uuid__in=course_uuids,
)
# Here we check the entitlements' expired_at_datetime property rather than filter by the expired_at attribute
# to ensure that the expiration status is as up to date as possible
entitlements = [e for e in entitlements if not e.expired_at_datetime]
courses_with_entitlements = {str(entitlement.course_uuid) for entitlement in entitlements}
- return [course for course in courses if course['uuid'] not in courses_with_entitlements]
+ return [course for course in courses if course["uuid"] not in courses_with_entitlements]
def _filter_out_courses_with_enrollments(self, courses):
"""
@@ -717,14 +680,11 @@ def _filter_out_courses_with_enrollments(self, courses):
Returns:
A subset of the given list of course dicts
"""
- enrollments = self.user.courseenrollment_set.filter(
- is_active=True,
- mode__in=self.data['applicable_seat_types']
- )
+ enrollments = self.user.courseenrollment_set.filter(is_active=True, mode__in=self.data["applicable_seat_types"])
course_runs_with_enrollments = {str(enrollment.course_id) for enrollment in enrollments}
courses_without_enrollments = []
for course in courses:
- if all(str(run['key']) not in course_runs_with_enrollments for run in course['course_runs']):
+ if all(str(run["key"]) not in course_runs_with_enrollments for run in course["course_runs"]):
courses_without_enrollments.append(course)
return courses_without_enrollments
@@ -734,40 +694,40 @@ def _collect_one_click_purchase_eligibility_data(self): # lint-amnesty, pylint:
Extend the program data with data about learner's eligibility for one click purchase,
discount data of the program and SKUs of seats that should be added to basket.
"""
- if 'professional' in self.data['applicable_seat_types']:
- self.data['applicable_seat_types'].append('no-id-professional')
- applicable_seat_types = {seat for seat in self.data['applicable_seat_types'] if seat != 'credit'}
+ if "professional" in self.data["applicable_seat_types"]:
+ self.data["applicable_seat_types"].append("no-id-professional")
+ applicable_seat_types = {seat for seat in self.data["applicable_seat_types"] if seat != "credit"}
- is_learner_eligible_for_one_click_purchase = self.data['is_program_eligible_for_one_click_purchase']
- bundle_uuid = self.data.get('uuid')
+ is_learner_eligible_for_one_click_purchase = self.data["is_program_eligible_for_one_click_purchase"]
+ bundle_uuid = self.data.get("uuid")
skus = []
- bundle_variant = 'full'
+ bundle_variant = "full"
if is_learner_eligible_for_one_click_purchase: # lint-amnesty, pylint: disable=too-many-nested-blocks
- courses = self.data['courses']
+ courses = self.data["courses"]
if not self.user.is_anonymous:
courses = self._filter_out_courses_with_enrollments(courses)
courses = self._filter_out_courses_with_entitlements(courses)
- if len(courses) < len(self.data['courses']):
- bundle_variant = 'partial'
+ if len(courses) < len(self.data["courses"]):
+ bundle_variant = "partial"
for course in courses:
entitlement_product = False
- for entitlement in course.get('entitlements', []):
+ for entitlement in course.get("entitlements", []):
# We add the first entitlement product found with an applicable seat type because, at this time,
# we are assuming that, for any given course, there is at most one paid entitlement available.
- if entitlement['mode'] in applicable_seat_types:
- skus.append(entitlement['sku'])
+ if entitlement["mode"] in applicable_seat_types:
+ skus.append(entitlement["sku"])
entitlement_product = True
break
if not entitlement_product:
- course_runs = course.get('course_runs', [])
- published_course_runs = [run for run in course_runs if run['status'] == 'published']
+ course_runs = course.get("course_runs", [])
+ published_course_runs = [run for run in course_runs if run["status"] == "published"]
if len(published_course_runs) == 1:
- for seat in published_course_runs[0]['seats']:
- if seat['type'] in applicable_seat_types and seat['sku']:
- skus.append(seat['sku'])
+ for seat in published_course_runs[0]["seats"]:
+ if seat["type"] in applicable_seat_types and seat["sku"]:
+ skus.append(seat["sku"])
break
else:
# If a course in the program has more than 1 published course run
@@ -798,36 +758,36 @@ def _collect_one_click_purchase_eligibility_data(self): # lint-amnesty, pylint:
params = dict(sku=skus, is_anonymous=True)
else:
if bundle_uuid:
- params = dict(
- sku=skus, username=self.user.username, bundle=bundle_uuid
- )
+ params = dict(sku=skus, username=self.user.username, bundle=bundle_uuid)
else:
params = dict(sku=skus, username=self.user.username)
response = api_client.get(api_url, params=params)
response.raise_for_status()
discount_data = response.json()
- program_discounted_price = discount_data['total_incl_tax']
- program_full_price = discount_data['total_incl_tax_excl_discounts']
- discount_data['is_discounted'] = program_discounted_price < program_full_price
- discount_data['discount_value'] = program_full_price - program_discounted_price
-
- self.data.update({
- 'discount_data': discount_data,
- 'full_program_price': discount_data['total_incl_tax'],
- 'variant': bundle_variant
- })
+ program_discounted_price = discount_data["total_incl_tax"]
+ program_full_price = discount_data["total_incl_tax_excl_discounts"]
+ discount_data["is_discounted"] = program_discounted_price < program_full_price
+ discount_data["discount_value"] = program_full_price - program_discounted_price
+
+ self.data.update(
+ {
+ "discount_data": discount_data,
+ "full_program_price": discount_data["total_incl_tax"],
+ "variant": bundle_variant,
+ }
+ )
except RequestException:
- log.exception('Failed to get discount price for following product SKUs: %s ', ', '.join(skus))
- self.data.update({
- 'discount_data': {'is_discounted': False}
- })
+ log.exception("Failed to get discount price for following product SKUs: %s ", ", ".join(skus))
+ self.data.update({"discount_data": {"is_discounted": False}})
else:
is_learner_eligible_for_one_click_purchase = False
- self.data.update({
- 'is_learner_eligible_for_one_click_purchase': is_learner_eligible_for_one_click_purchase,
- 'skus': skus,
- })
+ self.data.update(
+ {
+ "is_learner_eligible_for_one_click_purchase": is_learner_eligible_for_one_click_purchase,
+ "skus": skus,
+ }
+ )
def get_certificates(user, extended_program):
@@ -846,42 +806,43 @@ def get_certificates(user, extended_program):
"""
certificates = []
- for course in extended_program['courses']:
- for course_run in course['course_runs']:
- url = course_run.get('certificate_url')
- if url and course_run.get('may_certify'):
- certificates.append({
- 'type': 'course',
- 'title': course_run['title'],
- 'url': url,
- })
+ for course in extended_program["courses"]:
+ for course_run in course["course_runs"]:
+ url = course_run.get("certificate_url")
+ if url and course_run.get("may_certify"):
+ certificates.append(
+ {
+ "type": "course",
+ "title": course_run["title"],
+ "url": url,
+ }
+ )
# We only want one certificate per course to be returned.
break
- program_credentials = get_credentials(user, program_uuid=extended_program['uuid'], credential_type='program')
+ program_credentials = get_credentials(user, program_uuid=extended_program["uuid"], credential_type="program")
# only include a program certificate if a certificate is available for every course
- if program_credentials and (len(certificates) == len(extended_program['courses'])):
- enabled_force_program_cert_auth = configuration_helpers.get_value(
- 'force_program_cert_auth',
- True
- )
- cert_url = program_credentials[0]['certificate_url']
+ if program_credentials and (len(certificates) == len(extended_program["courses"])):
+ enabled_force_program_cert_auth = configuration_helpers.get_value("force_program_cert_auth", True)
+ cert_url = program_credentials[0]["certificate_url"]
url = get_logged_in_program_certificate_url(cert_url) if enabled_force_program_cert_auth else cert_url
- certificates.append({
- 'type': 'program',
- 'title': extended_program['title'],
- 'url': url,
- })
+ certificates.append(
+ {
+ "type": "program",
+ "title": extended_program["title"],
+ "url": url,
+ }
+ )
return certificates
def get_logged_in_program_certificate_url(certificate_url):
parsed_url = urlparse(certificate_url)
- query_string = 'next=' + parsed_url.path
- url_parts = (parsed_url.scheme, parsed_url.netloc, '/login/', '', query_string, '')
+ query_string = "next=" + parsed_url.path
+ url_parts = (parsed_url.scheme, parsed_url.netloc, "/login/", "", query_string, "")
return urlunparse(url_parts)
@@ -903,49 +864,44 @@ def __init__(self, program_data, user):
self.instructors = []
# Values for programs' price calculation.
- self.data['avg_price_per_course'] = 0.0
- self.data['number_of_courses'] = 0
- self.data['full_program_price'] = 0.0
+ self.data["avg_price_per_course"] = 0.0
+ self.data["number_of_courses"] = 0
+ self.data["full_program_price"] = 0.0
def _extend_program(self):
"""Aggregates data from the program data structure."""
- cache_key = 'program.instructors.{uuid}'.format(
- uuid=self.data['uuid']
- )
+ cache_key = "program.instructors.{uuid}".format(uuid=self.data["uuid"])
program_instructors = cache.get(cache_key)
- for course in self.data['courses']:
- self._execute('_collect_course', course)
+ for course in self.data["courses"]:
+ self._execute("_collect_course", course)
if not program_instructors:
- for course_run in course['course_runs']:
- self._execute('_collect_instructors', course_run)
+ for course_run in course["course_runs"]:
+ self._execute("_collect_instructors", course_run)
if not program_instructors:
# We cache the program instructors list to avoid repeated modulestore queries
program_instructors = self.instructors
cache.set(cache_key, program_instructors, 3600)
- if 'instructor_ordering' not in self.data:
+ if "instructor_ordering" not in self.data:
# If no instructor ordering is set in discovery, it doesn't populate this key
- self.data['instructor_ordering'] = []
+ self.data["instructor_ordering"] = []
sorted_instructor_names = [
- ' '.join([name for name in (instructor['given_name'], instructor['family_name']) if name])
- for instructor in self.data['instructor_ordering']
+ " ".join([name for name in (instructor["given_name"], instructor["family_name"]) if name])
+ for instructor in self.data["instructor_ordering"]
]
instructors_to_be_sorted = [
- instructor for instructor in program_instructors
- if instructor['name'] in sorted_instructor_names
+ instructor for instructor in program_instructors if instructor["name"] in sorted_instructor_names
]
instructors_to_not_be_sorted = [
- instructor for instructor in program_instructors
- if instructor['name'] not in sorted_instructor_names
+ instructor for instructor in program_instructors if instructor["name"] not in sorted_instructor_names
]
sorted_instructors = sorted(
- instructors_to_be_sorted,
- key=lambda item: sorted_instructor_names.index(item['name'])
+ instructors_to_be_sorted, key=lambda item: sorted_instructor_names.index(item["name"])
)
- self.data['instructors'] = sorted_instructors + instructors_to_not_be_sorted
+ self.data["instructors"] = sorted_instructors + instructors_to_not_be_sorted
def extend(self):
"""Execute extension handlers, returning the extended data."""
@@ -961,7 +917,7 @@ def _handlers(cls, prefix):
return {name for name in chain(cls.__dict__, ProgramDataExtender.__dict__) if name.startswith(prefix)}
def _attach_course_run_can_enroll(self, run_mode):
- run_mode['can_enroll'] = bool(self.user.has_perm(ENROLL_IN_COURSE, self.course_overview))
+ run_mode["can_enroll"] = bool(self.user.has_perm(ENROLL_IN_COURSE, self.course_overview))
def _attach_course_run_certificate_url(self, run_mode):
"""
@@ -977,16 +933,16 @@ def _attach_course_run_upgrade_url(self, run_mode):
if not self.user.is_anonymous:
super()._attach_course_run_upgrade_url(run_mode)
else:
- run_mode['upgrade_url'] = None
+ run_mode["upgrade_url"] = None
def _collect_course_pricing(self, course):
- self.data['number_of_courses'] += 1
- course_runs = course['course_runs']
+ self.data["number_of_courses"] += 1
+ course_runs = course["course_runs"]
if course_runs:
- seats = course_runs[0]['seats']
+ seats = course_runs[0]["seats"]
if seats:
- self.data['full_program_price'] += float(seats[0]['price'])
- self.data['avg_price_per_course'] = self.data['full_program_price'] / self.data['number_of_courses']
+ self.data["full_program_price"] += float(seats[0]["price"])
+ self.data["avg_price_per_course"] = self.data["full_program_price"] / self.data["number_of_courses"]
def _collect_instructors(self, course_run):
"""
@@ -996,19 +952,21 @@ def _collect_instructors(self, course_run):
instructor data into the catalog, retrieve it via the catalog API, and remove this code.
"""
module_store = modulestore()
- course_run_key = CourseKey.from_string(course_run['key'])
+ course_run_key = CourseKey.from_string(course_run["key"])
course_block = module_store.get_course(course_run_key)
if course_block:
- course_instructors = getattr(course_block, 'instructor_info', {})
+ course_instructors = getattr(course_block, "instructor_info", {})
# Deduplicate program instructors using instructor name
- curr_instructors_names = [instructor.get('name', '').strip() for instructor in self.instructors]
- for instructor in course_instructors.get('instructors', []):
- if instructor.get('name', '').strip() not in curr_instructors_names:
+ curr_instructors_names = [instructor.get("name", "").strip() for instructor in self.instructors]
+ for instructor in course_instructors.get("instructors", []):
+ if instructor.get("name", "").strip() not in curr_instructors_names:
self.instructors.append(instructor)
-def is_user_enrolled_in_program_type(user, program_type_slug, paid_modes_only=False, enrollments=None, entitlements=None): # lint-amnesty, pylint: disable=line-too-long
+def is_user_enrolled_in_program_type(
+ user, program_type_slug, paid_modes_only=False, enrollments=None, entitlements=None
+): # lint-amnesty, pylint: disable=line-too-long
"""
This method will look at the learners Enrollments and Entitlements to determine
if a learner is enrolled in a Program of the given type.
@@ -1036,10 +994,10 @@ def is_user_enrolled_in_program_type(user, program_type_slug, paid_modes_only=Fa
return False
for program in programs:
- for course in program.get('courses', []):
- course_uuids.add(course.get('uuid'))
- for course_run in course.get('course_runs', []):
- course_runs.add(course_run['key'])
+ for course in program.get("courses", []):
+ course_uuids.add(course.get("uuid"))
+ for course_run in course.get("course_runs", []):
+ course_runs.add(course_run["key"])
# Check Entitlements first, because there will be less Course Entitlements than
# Course Run Enrollments.
@@ -1050,60 +1008,12 @@ def is_user_enrolled_in_program_type(user, program_type_slug, paid_modes_only=Fa
student_enrollments = enrollments if enrollments is not None else get_enrollments(user.username)
for enrollment in student_enrollments:
- course_run_id = enrollment['course_details']['course_id']
+ course_run_id = enrollment["course_details"]["course_id"]
if paid_modes_only:
course_run_key = CourseKey.from_string(course_run_id)
paid_modes = [mode.slug for mode in get_paid_modes_for_course(course_run_key)]
- if enrollment['mode'] in paid_modes and course_run_id in course_runs:
+ if enrollment["mode"] in paid_modes and course_run_id in course_runs:
return True
elif course_run_id in course_runs:
return True
return False
-
-
-def get_subscription_api_client(user):
- """
- Returns an API client which can be used to make Subscriptions API requests.
- """
- scopes = [
- 'user_id',
- 'email',
- 'profile'
- ]
- jwt = create_jwt_for_user(user, scopes=scopes)
- client = requests.Session()
- client.auth = SuppliedJwtAuth(jwt)
-
- return client
-
-
-def get_programs_subscription_data(user, program_uuid=None):
- """
- Returns the subscription data for a user's program if uuid is specified
- else return data for user's all subscriptions.
- """
- client = get_subscription_api_client(user)
- api_path = f"{settings.SUBSCRIPTIONS_API_PATH}"
- subscription_data = []
-
- log.info(f"B2C_SUBSCRIPTIONS: Requesting Program subscription data for user: {user}" +
- (f" for program_uuid: {program_uuid}" if program_uuid is not None else ""))
-
- try:
- if program_uuid:
- response = client.get(api_path, params={'resource_id': program_uuid, 'most_active_and_recent': 'true'})
- response.raise_for_status()
- subscription_data = response.json().get('results', [])
- else:
- next_page = 1
- while next_page:
- response = client.get(api_path, params=dict(page=next_page))
- response.raise_for_status()
- subscription_data.extend(response.json().get('results', []))
- next_page = response.json().get('next')
- except Exception as exc: # pylint: disable=broad-except
- log.exception(
- f"B2C_SUBSCRIPTIONS: Failed to retrieve Program Subscription Data for user: {user} with error: {exc}"
- + f" for program_uuid: {str(program_uuid)}" if program_uuid is not None else ""
- )
- return subscription_data
diff --git a/openedx/core/djangoapps/schedules/resolvers.py b/openedx/core/djangoapps/schedules/resolvers.py
index 3498042415ff..5f769e6af299 100644
--- a/openedx/core/djangoapps/schedules/resolvers.py
+++ b/openedx/core/djangoapps/schedules/resolvers.py
@@ -526,6 +526,7 @@ def get_schedules(self):
'week_num': week_num,
'week_highlights': week_highlights,
# This is used by the bulk email optout policy
+ 'course_id': str(course.id),
'course_ids': [str(course.id)],
'unsubscribe_url': unsubscribe_url,
})
diff --git a/openedx/core/djangoapps/schedules/tests/test_resolvers.py b/openedx/core/djangoapps/schedules/tests/test_resolvers.py
index 5881861b2282..5c2b592b8e25 100644
--- a/openedx/core/djangoapps/schedules/tests/test_resolvers.py
+++ b/openedx/core/djangoapps/schedules/tests/test_resolvers.py
@@ -293,6 +293,7 @@ def test_schedule_context(self):
'contact_email': 'info@example.com',
'contact_mailing_address': '123 Sesame Street',
'course_ids': [str(self.course.id)],
+ 'course_id': str(self.course.id),
'course_name': self.course.display_name,
'course_url': f'http://learning-mfe/course/{self.course.id}/home',
'dashboard_url': '/dashboard',
diff --git a/openedx/core/djangoapps/user_api/accounts/serializers.py b/openedx/core/djangoapps/user_api/accounts/serializers.py
index b17f479b452d..f7ffe15d2a4b 100644
--- a/openedx/core/djangoapps/user_api/accounts/serializers.py
+++ b/openedx/core/djangoapps/user_api/accounts/serializers.py
@@ -47,12 +47,24 @@
class PhoneNumberSerializer(serializers.BaseSerializer): # lint-amnesty, pylint: disable=abstract-method
"""
- Class to serialize phone number into a digit only representation
+ Class to serialize phone number into a digit only representation.
+
+ This serializer removes all non-numeric characters from the phone number,
+ allowing '+' only at the beginning of the number.
"""
def to_internal_value(self, data):
- """Remove all non numeric characters in phone number"""
- return re.sub("[^0-9]", "", data) or None
+ """
+ Remove all non-numeric characters from the phone number.
+
+ Args:
+ data (str): The input phone number string.
+
+ Returns:
+ str or None: The cleaned phone number string containing only digits,
+ with an optional '+' at the beginning.
+ """
+ return re.sub(r'(?!^)\+|[^0-9+]', "", data) or None
class LanguageProficiencySerializer(serializers.ModelSerializer):
diff --git a/openedx/core/djangoapps/user_api/accounts/views.py b/openedx/core/djangoapps/user_api/accounts/views.py
index 720b3ba96af7..103c5bf24f6c 100644
--- a/openedx/core/djangoapps/user_api/accounts/views.py
+++ b/openedx/core/djangoapps/user_api/accounts/views.py
@@ -21,7 +21,6 @@
from edx_ace import ace
from edx_ace.recipient import Recipient
from edx_rest_framework_extensions.auth.jwt.authentication import JwtAuthentication
-from openedx.core.lib.api.authentication import BearerAuthentication
from edx_rest_framework_extensions.auth.session.authentication import SessionAuthenticationAllowInactiveUser
from enterprise.models import EnterpriseCourseEnrollment, EnterpriseCustomerUser, PendingEnterpriseCustomerUser
from integrated_channels.degreed.models import DegreedLearnerDataTransmissionAudit
@@ -50,9 +49,10 @@
get_retired_email_by_email,
get_retired_username_by_username,
is_email_retired,
- is_username_retired
+ is_username_retired,
)
from common.djangoapps.student.models_api import confirm_name_change, do_name_change_request, get_pending_name_change
+from lms.djangoapps.certificates.api import clear_pii_from_certificate_records_for_user
from openedx.core.djangoapps.ace_common.template_context import get_base_template_context
from openedx.core.djangoapps.api_admin.models import ApiAccessRequest
from openedx.core.djangoapps.course_groups.models import UnregisteredLearnerCohortAssignments
@@ -64,9 +64,8 @@
from openedx.core.djangoapps.user_api.accounts.image_helpers import get_profile_image_names, set_has_profile_image
from openedx.core.djangoapps.user_api.accounts.utils import handle_retirement_cancellation
from openedx.core.djangoapps.user_authn.exceptions import AuthFailedError
-from openedx.core.lib.api.authentication import BearerAuthenticationAllowInactiveUser
+from openedx.core.lib.api.authentication import BearerAuthentication, BearerAuthenticationAllowInactiveUser
from openedx.core.lib.api.parsers import MergePatchParser
-from lms.djangoapps.certificates.api import clear_pii_from_certificate_records_for_user
from ..errors import AccountUpdateError, AccountValidationError, UserNotAuthorized, UserNotFound
from ..message_types import DeletionNotificationMessage
@@ -75,7 +74,7 @@
RetirementStateError,
UserOrgTag,
UserRetirementPartnerReportingStatus,
- UserRetirementStatus
+ UserRetirementStatus,
)
from .api import get_account_settings, update_account_settings
from .permissions import (
@@ -83,13 +82,13 @@
CanDeactivateUser,
CanGetAccountInfo,
CanReplaceUsername,
- CanRetireUser
+ CanRetireUser,
)
from .serializers import (
PendingNameChangeSerializer,
UserRetirementPartnerReportSerializer,
UserRetirementStatusSerializer,
- UserSearchEmailSerializer
+ UserSearchEmailSerializer,
)
from .signals import USER_RETIRE_LMS_CRITICAL, USER_RETIRE_LMS_MISC, USER_RETIRE_MAILINGS
from .utils import create_retirement_request_and_deactivate_account, username_suffix_generator
@@ -97,16 +96,16 @@
log = logging.getLogger(__name__)
USER_PROFILE_PII = {
- 'name': '',
- 'meta': '',
- 'location': '',
- 'year_of_birth': None,
- 'gender': None,
- 'mailing_address': None,
- 'city': None,
- 'country': None,
- 'bio': None,
- 'phone_number': None,
+ "name": "",
+ "meta": "",
+ "location": "",
+ "year_of_birth": None,
+ "gender": None,
+ "mailing_address": None,
+ "city": None,
+ "country": None,
+ "bio": None,
+ "phone_number": None,
}
@@ -118,12 +117,9 @@ def request_requires_username(function):
@wraps(function)
def wrapper(self, request): # pylint: disable=missing-docstring
- username = request.data.get('username', None)
+ username = request.data.get("username", None)
if not username:
- return Response(
- status=status.HTTP_404_NOT_FOUND,
- data={'message': 'The user was not specified.'}
- )
+ return Response(status=status.HTTP_404_NOT_FOUND, data={"message": "The user was not specified."})
return function(self, request)
return wrapper
@@ -131,177 +127,183 @@ def wrapper(self, request): # pylint: disable=missing-docstring
class AccountViewSet(ViewSet):
"""
- **Use Cases**
-
- Get or update a user's account information. Updates are supported
- only through merge patch.
-
- **Example Requests**
-
- GET /api/user/v1/me[?view=shared]
- GET /api/user/v1/accounts?usernames={username1,username2}[?view=shared]
- GET /api/user/v1/accounts?email={user_email}
- GET /api/user/v1/accounts/{username}/[?view=shared]
-
- PATCH /api/user/v1/accounts/{username}/{"key":"value"} "application/merge-patch+json"
-
- POST /api/user/v1/accounts/search_emails "application/json"
-
- **Notes for PATCH requests to /accounts endpoints**
- * Requested updates to social_links are automatically merged with
- previously set links. That is, any newly introduced platforms are
- add to the previous list. Updated links to pre-existing platforms
- replace their values in the previous list. Pre-existing platforms
- can be removed by setting the value of the social_link to an
- empty string ("").
-
- **Response Values for GET requests to the /me endpoint**
- If the user is not logged in, an HTTP 401 "Not Authorized" response
- is returned.
-
- Otherwise, an HTTP 200 "OK" response is returned. The response
- contains the following value:
-
- * username: The username associated with the account.
-
- **Response Values for GET requests to /accounts endpoints**
-
- If no user exists with the specified username, or email, an HTTP 404 "Not
- Found" response is returned.
-
- If the user makes the request for her own account, or makes a
- request for another account and has "is_staff" access, an HTTP 200
- "OK" response is returned. The response contains the following
- values.
-
- * id: numerical lms user id in db
- * activation_key: auto-genrated activation key when signed up via email
- * bio: null or textual representation of user biographical
- information ("about me").
- * country: An ISO 3166 country code or null.
- * date_joined: The date the account was created, in the string
- format provided by datetime. For example, "2014-08-26T17:52:11Z".
- * last_login: The latest date the user logged in, in the string datetime format.
- * email: Email address for the user. New email addresses must be confirmed
- via a confirmation email, so GET does not reflect the change until
- the address has been confirmed.
- * secondary_email: A secondary email address for the user. Unlike
- the email field, GET will reflect the latest update to this field
- even if changes have yet to be confirmed.
- * verified_name: Approved verified name of the learner present in name affirmation plugin
- * gender: One of the following values:
-
- * null
- * "f"
- * "m"
- * "o"
-
- * goals: The textual representation of the user's goals, or null.
- * is_active: Boolean representation of whether a user is active.
- * language: The user's preferred language, or null.
- * language_proficiencies: Array of language preferences. Each
- preference is a JSON object with the following keys:
-
- * "code": string ISO 639-1 language code e.g. "en".
-
- * level_of_education: One of the following values:
-
- * "p": PhD or Doctorate
- * "m": Master's or professional degree
- * "b": Bachelor's degree
- * "a": Associate's degree
- * "hs": Secondary/high school
- * "jhs": Junior secondary/junior high/middle school
- * "el": Elementary/primary school
- * "none": None
- * "o": Other
- * null: The user did not enter a value
-
- * mailing_address: The textual representation of the user's mailing
- address, or null.
- * name: The full name of the user.
- * profile_image: A JSON representation of a user's profile image
- information. This representation has the following keys.
-
- * "has_image": Boolean indicating whether the user has a profile
- image.
- * "image_url_*": Absolute URL to various sizes of a user's
- profile image, where '*' matches a representation of the
- corresponding image size, such as 'small', 'medium', 'large',
- and 'full'. These are configurable via PROFILE_IMAGE_SIZES_MAP.
-
- * requires_parental_consent: True if the user is a minor
- requiring parental consent.
- * social_links: Array of social links, sorted alphabetically by
- "platform". Each preference is a JSON object with the following keys:
-
- * "platform": A particular social platform, ex: 'facebook'
- * "social_link": The link to the user's profile on the particular platform
-
- * username: The username associated with the account.
- * year_of_birth: The year the user was born, as an integer, or null.
-
- * account_privacy: The user's setting for sharing her personal
- profile. Possible values are "all_users", "private", or "custom".
- If "custom", the user has selectively chosen a subset of shareable
- fields to make visible to others via the User Preferences API.
-
- * phone_number: The phone number for the user. String of numbers with
- an optional `+` sign at the start.
-
- * pending_name_change: If the user has an active name change request, returns the
- requested name.
-
- For all text fields, plain text instead of HTML is supported. The
- data is stored exactly as specified. Clients must HTML escape
- rendered values to avoid script injections.
-
- If a user who does not have "is_staff" access requests account
- information for a different user, only a subset of these fields is
- returned. The returned fields depend on the
- ACCOUNT_VISIBILITY_CONFIGURATION configuration setting and the
- visibility preference of the user for whom data is requested.
-
- Note that a user can view which account fields they have shared
- with other users by requesting their own username and providing
- the "view=shared" URL parameter.
-
- **Response Values for PATCH**
-
- Users can only modify their own account information. If the
- requesting user does not have the specified username and has staff
- access, the request returns an HTTP 403 "Forbidden" response. If
- the requesting user does not have staff access, the request
- returns an HTTP 404 "Not Found" response to avoid revealing the
- existence of the account.
-
- If no user exists with the specified username, an HTTP 404 "Not
- Found" response is returned.
-
- If "application/merge-patch+json" is not the specified content
- type, a 415 "Unsupported Media Type" response is returned.
-
- If validation errors prevent the update, this method returns a 400
- "Bad Request" response that includes a "field_errors" field that
- lists all error messages.
-
- If a failure at the time of the update prevents the update, a 400
- "Bad Request" error is returned. The JSON collection contains
- specific errors.
-
- If the update is successful, updated user account data is returned.
+ **Use Cases**
+
+ Get or update a user's account information. Updates are supported
+ only through merge patch.
+
+ **Example Requests**
+
+ GET /api/user/v1/me[?view=shared]
+ GET /api/user/v1/accounts?usernames={username1,username2}[?view=shared]
+ GET /api/user/v1/accounts?email={user_email}
+ GET /api/user/v1/accounts/{username}/[?view=shared]
+
+ PATCH /api/user/v1/accounts/{username}/{"key":"value"} "application/merge-patch+json"
+
+ POST /api/user/v1/accounts/search_emails "application/json"
+
+ **Notes for PATCH requests to /accounts endpoints**
+ * Requested updates to social_links are automatically merged with
+ previously set links. That is, any newly introduced platforms are
+ add to the previous list. Updated links to pre-existing platforms
+ replace their values in the previous list. Pre-existing platforms
+ can be removed by setting the value of the social_link to an
+ empty string ("").
+
+ **Response Values for GET requests to the /me endpoint**
+ If the user is not logged in, an HTTP 401 "Not Authorized" response
+ is returned.
+
+ Otherwise, an HTTP 200 "OK" response is returned. The response
+ contains the following value:
+
+ * username: The username associated with the account.
+
+ **Response Values for GET requests to /accounts endpoints**
+
+ If no user exists with the specified username, or email, an HTTP 404 "Not
+ Found" response is returned.
+
+ If the user makes the request for her own account, or makes a
+ request for another account and has "is_staff" access, an HTTP 200
+ "OK" response is returned. The response contains the following
+ values.
+
+ * id: numerical lms user id in db
+ * activation_key: auto-genrated activation key when signed up via email
+ * bio: null or textual representation of user biographical
+ information ("about me").
+ * country: An ISO 3166 country code or null.
+ * date_joined: The date the account was created, in the string
+ format provided by datetime. For example, "2014-08-26T17:52:11Z".
+ * last_login: The latest date the user logged in, in the string datetime format.
+ * email: Email address for the user. New email addresses must be confirmed
+ via a confirmation email, so GET does not reflect the change until
+ the address has been confirmed.
+ * secondary_email: A secondary email address for the user. Unlike
+ the email field, GET will reflect the latest update to this field
+ even if changes have yet to be confirmed.
+ * verified_name: Approved verified name of the learner present in name affirmation plugin
+ * gender: One of the following values:
+
+ * null
+ * "f"
+ * "m"
+ * "o"
+
+ * goals: The textual representation of the user's goals, or null.
+ * is_active: Boolean representation of whether a user is active.
+ * language: The user's preferred language, or null.
+ * language_proficiencies: Array of language preferences. Each
+ preference is a JSON object with the following keys:
+
+ * "code": string ISO 639-1 language code e.g. "en".
+
+ * level_of_education: One of the following values:
+
+ * "p": PhD or Doctorate
+ * "m": Master's or professional degree
+ * "b": Bachelor's degree
+ * "a": Associate's degree
+ * "hs": Secondary/high school
+ * "jhs": Junior secondary/junior high/middle school
+ * "el": Elementary/primary school
+ * "none": None
+ * "o": Other
+ * null: The user did not enter a value
+
+ * mailing_address: The textual representation of the user's mailing
+ address, or null.
+ * name: The full name of the user.
+ * profile_image: A JSON representation of a user's profile image
+ information. This representation has the following keys.
+
+ * "has_image": Boolean indicating whether the user has a profile
+ image.
+ * "image_url_*": Absolute URL to various sizes of a user's
+ profile image, where '*' matches a representation of the
+ corresponding image size, such as 'small', 'medium', 'large',
+ and 'full'. These are configurable via PROFILE_IMAGE_SIZES_MAP.
+
+ * requires_parental_consent: True if the user is a minor
+ requiring parental consent.
+ * social_links: Array of social links, sorted alphabetically by
+ "platform". Each preference is a JSON object with the following keys:
+
+ * "platform": A particular social platform, ex: 'facebook'
+ * "social_link": The link to the user's profile on the particular platform
+
+ * username: The username associated with the account.
+ * year_of_birth: The year the user was born, as an integer, or null.
+
+ * account_privacy: The user's setting for sharing her personal
+ profile. Possible values are "all_users", "private", or "custom".
+ If "custom", the user has selectively chosen a subset of shareable
+ fields to make visible to others via the User Preferences API.
+
+ * phone_number: The phone number for the user. String of numbers with
+ an optional `+` sign at the start.
+
+ * pending_name_change: If the user has an active name change request, returns the
+ requested name.
+
+ For all text fields, plain text instead of HTML is supported. The
+ data is stored exactly as specified. Clients must HTML escape
+ rendered values to avoid script injections.
+
+ If a user who does not have "is_staff" access requests account
+ information for a different user, only a subset of these fields is
+ returned. The returned fields depend on the
+ ACCOUNT_VISIBILITY_CONFIGURATION configuration setting and the
+ visibility preference of the user for whom data is requested.
+
+ Note that a user can view which account fields they have shared
+ with other users by requesting their own username and providing
+ the "view=shared" URL parameter.
+
+ **Response Values for PATCH**
+
+ Users can only modify their own account information. If the
+ requesting user does not have the specified username and has staff
+ access, the request returns an HTTP 403 "Forbidden" response. If
+ the requesting user does not have staff access, the request
+ returns an HTTP 404 "Not Found" response to avoid revealing the
+ existence of the account.
+
+ If no user exists with the specified username, an HTTP 404 "Not
+ Found" response is returned.
+
+ If "application/merge-patch+json" is not the specified content
+ type, a 415 "Unsupported Media Type" response is returned.
+
+ If validation errors prevent the update, this method returns a 400
+ "Bad Request" response that includes a "field_errors" field that
+ lists all error messages.
+
+ If a failure at the time of the update prevents the update, a 400
+ "Bad Request" error is returned. The JSON collection contains
+ specific errors.
+
+ If the update is successful, updated user account data is returned.
"""
+
authentication_classes = (
- JwtAuthentication, BearerAuthenticationAllowInactiveUser, SessionAuthenticationAllowInactiveUser
+ JwtAuthentication,
+ BearerAuthenticationAllowInactiveUser,
+ SessionAuthenticationAllowInactiveUser,
)
permission_classes = (permissions.IsAuthenticated, CanGetAccountInfo)
- parser_classes = (JSONParser, MergePatchParser,)
+ parser_classes = (
+ JSONParser,
+ MergePatchParser,
+ )
def get(self, request):
"""
GET /api/user/v1/me
"""
- return Response({'username': request.user.username})
+ return Response({"username": request.user.username})
def list(self, request):
"""
@@ -309,13 +311,13 @@ def list(self, request):
GET /api/user/v1/accounts?email={user_email} (Staff Only)
GET /api/user/v1/accounts?lms_user_id={lms_user_id} (Staff Only)
"""
- usernames = request.GET.get('username')
- user_email = request.GET.get('email')
- lms_user_id = request.GET.get('lms_user_id')
+ usernames = request.GET.get("username")
+ user_email = request.GET.get("email")
+ lms_user_id = request.GET.get("lms_user_id")
search_usernames = []
if usernames:
- search_usernames = usernames.strip(',').split(',')
+ search_usernames = usernames.strip(",").split(",")
elif user_email:
if is_email_retired(user_email):
can_cancel_retirement = True
@@ -325,22 +327,20 @@ def list(self, request):
retirement_status = UserRetirementStatus.objects.get(
created__gt=earliest_datetime,
created__lt=datetime.datetime.now(pytz.UTC),
- original_email=user_email
+ original_email=user_email,
)
retirement_id = retirement_status.id
except UserRetirementStatus.DoesNotExist:
can_cancel_retirement = False
context = {
- 'error_msg': accounts.RETIRED_EMAIL_MSG,
- 'can_cancel_retirement': can_cancel_retirement,
- 'retirement_id': retirement_id
+ "error_msg": accounts.RETIRED_EMAIL_MSG,
+ "can_cancel_retirement": can_cancel_retirement,
+ "retirement_id": retirement_id,
}
- return Response(
- context, status=status.HTTP_404_NOT_FOUND
- )
- user_email = user_email.strip('')
+ return Response(context, status=status.HTTP_404_NOT_FOUND)
+ user_email = user_email.strip("")
try:
user = User.objects.get(email=user_email)
except (UserNotFound, User.DoesNotExist):
@@ -355,9 +355,7 @@ def list(self, request):
return Response(status=status.HTTP_400_BAD_REQUEST)
search_usernames = [user.username]
try:
- account_settings = get_account_settings(
- request, search_usernames, view=request.query_params.get('view')
- )
+ account_settings = get_account_settings(request, search_usernames, view=request.query_params.get("view"))
except UserNotFound:
return Response(status=status.HTTP_404_NOT_FOUND)
@@ -386,23 +384,15 @@ def search_emails(self, request):
"""
if not request.user.is_staff:
return Response(
- {
- 'developer_message': 'not_found',
- 'user_message': 'Not Found'
- },
- status=status.HTTP_404_NOT_FOUND
+ {"developer_message": "not_found", "user_message": "Not Found"}, status=status.HTTP_404_NOT_FOUND
)
try:
- user_emails = request.data['emails']
+ user_emails = request.data["emails"]
except KeyError as error:
- error_message = f'{error} field is required'
+ error_message = f"{error} field is required"
return Response(
- {
- 'developer_message': error_message,
- 'user_message': error_message
- },
- status=status.HTTP_400_BAD_REQUEST
+ {"developer_message": error_message, "user_message": error_message}, status=status.HTTP_400_BAD_REQUEST
)
users = User.objects.filter(email__in=user_emails)
data = UserSearchEmailSerializer(users, many=True).data
@@ -413,8 +403,7 @@ def retrieve(self, request, username):
GET /api/user/v1/accounts/{username}/
"""
try:
- account_settings = get_account_settings(
- request, [username], view=request.query_params.get('view'))
+ account_settings = get_account_settings(request, [username], view=request.query_params.get("view"))
except UserNotFound:
return Response(status=status.HTTP_404_NOT_FOUND)
@@ -443,11 +432,8 @@ def partial_update(self, request, username):
return Response({"field_errors": err.field_errors}, status=status.HTTP_400_BAD_REQUEST)
except AccountUpdateError as err:
return Response(
- {
- "developer_message": err.developer_message,
- "user_message": err.user_message
- },
- status=status.HTTP_400_BAD_REQUEST
+ {"developer_message": err.developer_message, "user_message": err.user_message},
+ status=status.HTTP_400_BAD_REQUEST,
)
return Response(account_settings)
@@ -457,6 +443,7 @@ class NameChangeView(ViewSet):
"""
Viewset to manage profile name change requests.
"""
+
permission_classes = (permissions.IsAuthenticated,)
def create(self, request):
@@ -472,10 +459,10 @@ def create(self, request):
}
"""
user = request.user
- new_name = request.data.get('name', None)
- rationale = f'Name change requested through account API by {user.username}'
+ new_name = request.data.get("name", None)
+ rationale = f"Name change requested through account API by {user.username}"
- serializer = PendingNameChangeSerializer(data={'new_name': new_name})
+ serializer = PendingNameChangeSerializer(data={"new_name": new_name})
if serializer.is_valid():
pending_name_change = do_name_change_request(user, new_name, rationale)[0]
@@ -483,8 +470,8 @@ def create(self, request):
return Response(status=status.HTTP_201_CREATED)
else:
return Response(
- {'new_name': 'The profile name given was identical to the current name.'},
- status=status.HTTP_400_BAD_REQUEST
+ {"new_name": "The profile name given was identical to the current name."},
+ status=status.HTTP_400_BAD_REQUEST,
)
return Response(status=status.HTTP_400_BAD_REQUEST, data=serializer.errors)
@@ -514,6 +501,7 @@ class AccountDeactivationView(APIView):
Account deactivation viewset. Currently only supports POST requests.
Only admins can deactivate accounts.
"""
+
permission_classes = (permissions.IsAuthenticated, CanDeactivateUser)
def post(self, request, username):
@@ -559,6 +547,7 @@ class DeactivateLogoutView(APIView):
- Log the user out
- Create a row in the retirement table for that user
"""
+
# BearerAuthentication is added here to support account deletion
# from the mobile app until it moves to JWT Auth.
# See mobile roadmap issue https://github.com/openedx/edx-platform/issues/33307.
@@ -575,7 +564,7 @@ def post(self, request):
# Ensure the account deletion is not disable
enable_account_deletion = configuration_helpers.get_value(
- 'ENABLE_ACCOUNT_DELETION', settings.FEATURES.get('ENABLE_ACCOUNT_DELETION', False)
+ "ENABLE_ACCOUNT_DELETION", settings.FEATURES.get("ENABLE_ACCOUNT_DELETION", False)
)
if not enable_account_deletion:
@@ -595,11 +584,9 @@ def post(self, request):
# Send notification email to user
site = Site.objects.get_current()
notification_context = get_base_template_context(site)
- notification_context.update({'full_name': request.user.profile.name})
+ notification_context.update({"full_name": request.user.profile.name})
language_code = request.user.preferences.model.get_value(
- request.user,
- LANGUAGE_KEY,
- default=settings.LANGUAGE_CODE
+ request.user, LANGUAGE_KEY, default=settings.LANGUAGE_CODE
)
notification = DeletionNotificationMessage().personalize(
recipient=Recipient(lms_user_id=0, email_address=user_email),
@@ -608,22 +595,20 @@ def post(self, request):
)
ace.send(notification)
except Exception as exc:
- log.exception('Error sending out deletion notification email')
+ log.exception("Error sending out deletion notification email")
raise exc
# Log the user out.
logout(request)
return Response(status=status.HTTP_204_NO_CONTENT)
except KeyError:
- log.exception(f'Username not specified {request.user}')
- return Response('Username not specified.', status=status.HTTP_404_NOT_FOUND)
+ log.exception(f"Username not specified {request.user}")
+ return Response("Username not specified.", status=status.HTTP_404_NOT_FOUND)
except user_model.DoesNotExist:
log.exception(f'The user "{request.user.username}" does not exist.')
- return Response(
- f'The user "{request.user.username}" does not exist.', status=status.HTTP_404_NOT_FOUND
- )
+ return Response(f'The user "{request.user.username}" does not exist.', status=status.HTTP_404_NOT_FOUND)
except Exception as exc: # pylint: disable=broad-except
- log.exception(f'500 error deactivating account {exc}')
+ log.exception(f"500 error deactivating account {exc}")
return Response(str(exc), status=status.HTTP_500_INTERNAL_SERVER_ERROR)
def _verify_user_password(self, request):
@@ -636,7 +621,7 @@ def _verify_user_password(self, request):
"""
try:
self._check_excessive_login_attempts(request.user)
- user = authenticate(username=request.user.username, password=request.POST['password'], request=request)
+ user = authenticate(username=request.user.username, password=request.POST["password"], request=request)
if user:
if LoginFailures.is_feature_enabled():
LoginFailures.clear_lockout_counter(user)
@@ -644,9 +629,7 @@ def _verify_user_password(self, request):
else:
self._handle_failed_authentication(request.user)
except AuthFailedError as err:
- log.exception(
- f"The user password to deactivate was incorrect. {request.user.username}"
- )
+ log.exception(f"The user password to deactivate was incorrect. {request.user.username}")
return Response(str(err), status=status.HTTP_403_FORBIDDEN)
except Exception as err: # pylint: disable=broad-except
return Response(f"Could not verify user password: {err}", status=status.HTTP_400_BAD_REQUEST)
@@ -657,8 +640,9 @@ def _check_excessive_login_attempts(self, user):
"""
if user and LoginFailures.is_feature_enabled():
if LoginFailures.is_user_locked_out(user):
- raise AuthFailedError(_('This account has been temporarily locked due '
- 'to excessive login failures. Try again later.'))
+ raise AuthFailedError(
+ _("This account has been temporarily locked due to excessive login failures. Try again later.")
+ )
def _handle_failed_authentication(self, user):
"""
@@ -667,7 +651,7 @@ def _handle_failed_authentication(self, user):
if user and LoginFailures.is_feature_enabled():
LoginFailures.increment_lockout_counter(user)
- raise AuthFailedError(_('Email or password is incorrect.'))
+ raise AuthFailedError(_("Email or password is incorrect."))
def _set_unusable_password(user):
@@ -684,15 +668,19 @@ class AccountRetirementPartnerReportView(ViewSet):
Provides API endpoints for managing partner reporting of retired
users.
"""
- DELETION_COMPLETED_KEY = 'deletion_completed'
- ORGS_CONFIG_KEY = 'orgs_config'
- ORGS_CONFIG_ORG_KEY = 'org'
- ORGS_CONFIG_FIELD_HEADINGS_KEY = 'field_headings'
- ORIGINAL_EMAIL_KEY = 'original_email'
- ORIGINAL_NAME_KEY = 'original_name'
- STUDENT_ID_KEY = 'student_id'
-
- permission_classes = (permissions.IsAuthenticated, CanRetireUser,)
+
+ DELETION_COMPLETED_KEY = "deletion_completed"
+ ORGS_CONFIG_KEY = "orgs_config"
+ ORGS_CONFIG_ORG_KEY = "org"
+ ORGS_CONFIG_FIELD_HEADINGS_KEY = "field_headings"
+ ORIGINAL_EMAIL_KEY = "original_email"
+ ORIGINAL_NAME_KEY = "original_name"
+ STUDENT_ID_KEY = "student_id"
+
+ permission_classes = (
+ permissions.IsAuthenticated,
+ CanRetireUser,
+ )
parser_classes = (JSONParser,)
serializer_class = UserRetirementStatusSerializer
@@ -706,7 +694,7 @@ def _get_orgs_for_user(user):
org = enrollment.course_id.org
# Org can conceivably be blank or this bogus default value
- if org and org != 'outdated_entry':
+ if org and org != "outdated_entry":
orgs.add(org)
return orgs
@@ -718,9 +706,9 @@ def retirement_partner_report(self, request): # pylint: disable=unused-argument
that are not already being processed and updates their status
to indicate they are currently being processed.
"""
- retirement_statuses = UserRetirementPartnerReportingStatus.objects.filter(
- is_being_processed=False
- ).order_by('id')
+ retirement_statuses = UserRetirementPartnerReportingStatus.objects.filter(is_being_processed=False).order_by(
+ "id"
+ )
retirements = []
for retirement_status in retirement_statuses:
@@ -737,12 +725,12 @@ def _get_retirement_for_partner_report(self, retirement_status):
Get the retirement for this retirement_status. The retirement info will be included in the partner report.
"""
retirement = {
- 'user_id': retirement_status.user.pk,
- 'original_username': retirement_status.original_username,
+ "user_id": retirement_status.user.pk,
+ "original_username": retirement_status.original_username,
AccountRetirementPartnerReportView.ORIGINAL_EMAIL_KEY: retirement_status.original_email,
AccountRetirementPartnerReportView.ORIGINAL_NAME_KEY: retirement_status.original_name,
- 'orgs': self._get_orgs_for_user(retirement_status.user),
- 'created': retirement_status.created,
+ "orgs": self._get_orgs_for_user(retirement_status.user),
+ "created": retirement_status.created,
}
return retirement
@@ -761,7 +749,7 @@ def retirement_partner_status_create(self, request):
Creates a UserRetirementPartnerReportingStatus object for the given user
as part of the retirement pipeline.
"""
- username = request.data['username']
+ username = request.data["username"]
try:
retirement = UserRetirementStatus.get_retirement_for_retirement_action(username)
@@ -771,10 +759,10 @@ def retirement_partner_status_create(self, request):
UserRetirementPartnerReportingStatus.objects.get_or_create(
user=retirement.user,
defaults={
- 'original_username': retirement.original_username,
- 'original_email': retirement.original_email,
- 'original_name': retirement.original_name
- }
+ "original_username": retirement.original_username,
+ "original_email": retirement.original_email,
+ "original_name": retirement.original_name,
+ },
)
return Response(status=status.HTTP_204_NO_CONTENT)
@@ -790,14 +778,13 @@ def retirement_partner_cleanup(self, request):
Deletes UserRetirementPartnerReportingStatus objects for a list of users
that have been reported on.
"""
- usernames = [u['original_username'] for u in request.data]
+ usernames = [u["original_username"] for u in request.data]
if not usernames:
- return Response('No original_usernames given.', status=status.HTTP_400_BAD_REQUEST)
+ return Response("No original_usernames given.", status=status.HTTP_400_BAD_REQUEST)
retirement_statuses = UserRetirementPartnerReportingStatus.objects.filter(
- is_being_processed=True,
- original_username__in=usernames
+ is_being_processed=True, original_username__in=usernames
)
# Need to de-dupe usernames that differ only by case to find the exact right match
@@ -809,15 +796,15 @@ def retirement_partner_cleanup(self, request):
# to disambiguate them in Python, which will respect case in the comparison.
if len(usernames) != len(retirement_statuses_clean):
return Response(
- '{} original_usernames given, {} found!\n'
- 'Given usernames:\n{}\n'
- 'Found UserRetirementReportingStatuses:\n{}'.format(
+ "{} original_usernames given, {} found!\n"
+ "Given usernames:\n{}\n"
+ "Found UserRetirementReportingStatuses:\n{}".format(
len(usernames),
len(retirement_statuses_clean),
usernames,
- ', '.join([rs.original_username for rs in retirement_statuses_clean])
+ ", ".join([rs.original_username for rs in retirement_statuses_clean]),
),
- status=status.HTTP_400_BAD_REQUEST
+ status=status.HTTP_400_BAD_REQUEST,
)
retirement_statuses.delete()
@@ -829,7 +816,11 @@ class CancelAccountRetirementStatusView(ViewSet):
"""
Provides API endpoints for canceling retirement process for a user's account.
"""
- permission_classes = (permissions.IsAuthenticated, CanCancelUserRetirement,)
+
+ permission_classes = (
+ permissions.IsAuthenticated,
+ CanCancelUserRetirement,
+ )
def cancel_retirement(self, request):
"""
@@ -839,26 +830,23 @@ def cancel_retirement(self, request):
This also handles the top level error handling, and permissions.
"""
try:
- retirement_id = request.data['retirement_id']
+ retirement_id = request.data["retirement_id"]
except KeyError:
- return Response(
- status=status.HTTP_400_BAD_REQUEST,
- data={'message': 'retirement_id must be specified.'}
- )
+ return Response(status=status.HTTP_400_BAD_REQUEST, data={"message": "retirement_id must be specified."})
try:
retirement = UserRetirementStatus.objects.get(id=retirement_id)
except UserRetirementStatus.DoesNotExist:
- return Response(data={"message": 'Retirement does not exist!'}, status=status.HTTP_400_BAD_REQUEST)
+ return Response(data={"message": "Retirement does not exist!"}, status=status.HTTP_400_BAD_REQUEST)
- if retirement.current_state.state_name != 'PENDING':
+ if retirement.current_state.state_name != "PENDING":
return Response(
status=status.HTTP_400_BAD_REQUEST,
data={
"message": f"Retirement requests can only be cancelled for users in the PENDING state. Current "
- f"request state for '{retirement.original_username}': "
- f"{retirement.current_state.state_name}"
- }
+ f"request state for '{retirement.original_username}': "
+ f"{retirement.current_state.state_name}"
+ },
)
handle_retirement_cancellation(retirement)
@@ -870,7 +858,11 @@ class AccountRetirementStatusView(ViewSet):
"""
Provides API endpoints for managing the user retirement process.
"""
- permission_classes = (permissions.IsAuthenticated, CanRetireUser,)
+
+ permission_classes = (
+ permissions.IsAuthenticated,
+ CanRetireUser,
+ )
parser_classes = (JSONParser,)
serializer_class = UserRetirementStatusSerializer
@@ -883,37 +875,35 @@ def retirement_queue(self, request):
created in the retirement queue at least `cool_off_days` ago.
"""
try:
- cool_off_days = int(request.GET['cool_off_days'])
+ cool_off_days = int(request.GET["cool_off_days"])
if cool_off_days < 0:
- raise RetirementStateError('Invalid argument for cool_off_days, must be greater than 0.')
+ raise RetirementStateError("Invalid argument for cool_off_days, must be greater than 0.")
- states = request.GET.getlist('states')
+ states = request.GET.getlist("states")
if not states:
raise RetirementStateError('Param "states" required with at least one state.')
state_objs = RetirementState.objects.filter(state_name__in=states)
if state_objs.count() != len(states):
found = [s.state_name for s in state_objs]
- raise RetirementStateError(f'Unknown state. Requested: {states} Found: {found}')
+ raise RetirementStateError(f"Unknown state. Requested: {states} Found: {found}")
- limit = request.GET.get('limit')
+ limit = request.GET.get("limit")
if limit:
try:
limit_count = int(limit)
except ValueError:
return Response(
- f'Limit could not be parsed: {limit}, please ensure this is an integer',
- status=status.HTTP_400_BAD_REQUEST
+ f"Limit could not be parsed: {limit}, please ensure this is an integer",
+ status=status.HTTP_400_BAD_REQUEST,
)
earliest_datetime = datetime.datetime.now(pytz.UTC) - datetime.timedelta(days=cool_off_days)
- retirements = UserRetirementStatus.objects.select_related(
- 'user', 'current_state', 'last_state'
- ).filter(
- current_state__in=state_objs, created__lt=earliest_datetime
- ).order_by(
- 'id'
+ retirements = (
+ UserRetirementStatus.objects.select_related("user", "current_state", "last_state")
+ .filter(current_state__in=state_objs, created__lt=earliest_datetime)
+ .order_by("id")
)
if limit:
retirements = retirements[:limit_count]
@@ -921,10 +911,9 @@ def retirement_queue(self, request):
return Response(serializer.data)
# This should only occur on the int() conversion of cool_off_days at this point
except ValueError:
- return Response('Invalid cool_off_days, should be integer.', status=status.HTTP_400_BAD_REQUEST)
+ return Response("Invalid cool_off_days, should be integer.", status=status.HTTP_400_BAD_REQUEST)
except KeyError as exc:
- return Response(f'Missing required parameter: {str(exc)}',
- status=status.HTTP_400_BAD_REQUEST)
+ return Response(f"Missing required parameter: {str(exc)}", status=status.HTTP_400_BAD_REQUEST)
except RetirementStateError as exc:
return Response(str(exc), status=status.HTTP_400_BAD_REQUEST)
@@ -939,36 +928,33 @@ 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)
+ 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)
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.')
+ raise RetirementStateError("Dates must be today or earlier, and start must be earlier than end.")
# Add a day to make sure we get all the way to 23:59:59.999, this is compared "lt" in the query
# not "lte".
end_date += datetime.timedelta(days=1)
- state = request.GET['state']
+ state = request.GET["state"]
state_obj = RetirementState.objects.get(state_name=state)
- retirements = UserRetirementStatus.objects.select_related(
- 'user', 'current_state', 'last_state', 'user__profile'
- ).filter(
- current_state=state_obj, created__lt=end_date, created__gte=start_date
- ).order_by(
- 'id'
+ retirements = (
+ UserRetirementStatus.objects.select_related("user", "current_state", "last_state", "user__profile")
+ .filter(current_state=state_obj, created__lt=end_date, created__gte=start_date)
+ .order_by("id")
)
serializer = UserRetirementStatusSerializer(retirements, many=True)
return Response(serializer.data)
# This should only occur on the datetime conversion of the start / end dates.
except ValueError as exc:
- return Response(f'Invalid start or end date: {str(exc)}', status=status.HTTP_400_BAD_REQUEST)
+ return Response(f"Invalid start or end date: {str(exc)}", status=status.HTTP_400_BAD_REQUEST)
except KeyError as exc:
- return Response(f'Missing required parameter: {str(exc)}',
- status=status.HTTP_400_BAD_REQUEST)
+ return Response(f"Missing required parameter: {str(exc)}", status=status.HTTP_400_BAD_REQUEST)
except RetirementState.DoesNotExist:
- return Response('Unknown retirement state.', status=status.HTTP_400_BAD_REQUEST)
+ return Response("Unknown retirement state.", status=status.HTTP_400_BAD_REQUEST)
except RetirementStateError as exc:
return Response(str(exc), status=status.HTTP_400_BAD_REQUEST)
@@ -980,9 +966,9 @@ def retrieve(self, request, username): # pylint: disable=unused-argument
"""
try:
user = get_potentially_retired_user_by_username(username)
- retirement = UserRetirementStatus.objects.select_related(
- 'user', 'current_state', 'last_state'
- ).get(user=user)
+ retirement = UserRetirementStatus.objects.select_related("user", "current_state", "last_state").get(
+ user=user
+ )
serializer = UserRetirementStatusSerializer(instance=retirement)
return Response(serializer.data)
except (UserRetirementStatus.DoesNotExist, User.DoesNotExist):
@@ -1008,7 +994,7 @@ def partial_update(self, request):
The content type for this request is 'application/json'.
"""
try:
- username = request.data['username']
+ username = request.data["username"]
retirements = UserRetirementStatus.objects.filter(original_username=username)
# During a narrow window learners were able to re-use a username that had been retired if
@@ -1049,20 +1035,19 @@ def cleanup(self, request):
Deletes a batch of retirement requests by username.
"""
try:
- usernames = request.data['usernames']
+ usernames = request.data["usernames"]
if not isinstance(usernames, list):
- raise TypeError('Usernames should be an array.')
+ raise TypeError("Usernames should be an array.")
- complete_state = RetirementState.objects.get(state_name='COMPLETE')
+ complete_state = RetirementState.objects.get(state_name="COMPLETE")
retirements = UserRetirementStatus.objects.filter(
- original_username__in=usernames,
- current_state=complete_state
+ original_username__in=usernames, current_state=complete_state
)
# Sanity check that they're all valid usernames in the right state
if len(usernames) != len(retirements):
- raise UserRetirementStatus.DoesNotExist('Not all usernames exist in the COMPLETE state.')
+ raise UserRetirementStatus.DoesNotExist("Not all usernames exist in the COMPLETE state.")
retirements.delete()
return Response(status=status.HTTP_204_NO_CONTENT)
@@ -1076,7 +1061,11 @@ class LMSAccountRetirementView(ViewSet):
"""
Provides an API endpoint for retiring a user in the LMS.
"""
- permission_classes = (permissions.IsAuthenticated, CanRetireUser,)
+
+ permission_classes = (
+ permissions.IsAuthenticated,
+ CanRetireUser,
+ )
parser_classes = (JSONParser,)
@request_requires_username
@@ -1093,13 +1082,13 @@ def post(self, request):
Retires the user with the given username in the LMS.
"""
- username = request.data['username']
+ username = request.data["username"]
try:
retirement = UserRetirementStatus.get_retirement_for_retirement_action(username)
RevisionPluginRevision.retire_user(retirement.user)
ArticleRevision.retire_user(retirement.user)
- PendingNameChange.delete_by_user_value(retirement.user, field='user')
+ PendingNameChange.delete_by_user_value(retirement.user, field="user")
ManualEnrollmentAudit.retire_manual_enrollments(retirement.user, retirement.retired_email)
CreditRequest.retire_user(retirement)
@@ -1115,7 +1104,7 @@ def post(self, request):
sender=self.__class__,
email=retirement.original_email,
new_email=retirement.retired_email,
- user=retirement.user
+ user=retirement.user,
)
except UserRetirementStatus.DoesNotExist:
return Response(status=status.HTTP_404_NOT_FOUND)
@@ -1131,7 +1120,11 @@ class AccountRetirementView(ViewSet):
"""
Provides API endpoint for retiring a user.
"""
- permission_classes = (permissions.IsAuthenticated, CanRetireUser,)
+
+ permission_classes = (
+ permissions.IsAuthenticated,
+ CanRetireUser,
+ )
parser_classes = (JSONParser,)
@request_requires_username
@@ -1148,7 +1141,7 @@ def post(self, request):
Retires the user with the given username. This includes retiring this username, the associated email address,
and any other PII associated with this user.
"""
- username = request.data['username']
+ username = request.data["username"]
try:
retirement_status = UserRetirementStatus.get_retirement_for_retirement_action(username)
@@ -1173,18 +1166,18 @@ def post(self, request):
self.retire_entitlement_support_detail(user)
# Retire misc. models that may contain PII of this user
- PendingEmailChange.delete_by_user_value(user, field='user')
- UserOrgTag.delete_by_user_value(user, field='user')
+ PendingEmailChange.delete_by_user_value(user, field="user")
+ UserOrgTag.delete_by_user_value(user, field="user")
# Retire any objects linked to the user via their original email
- CourseEnrollmentAllowed.delete_by_user_value(original_email, field='email')
- UnregisteredLearnerCohortAssignments.delete_by_user_value(original_email, field='email')
+ CourseEnrollmentAllowed.delete_by_user_value(original_email, field="email")
+ UnregisteredLearnerCohortAssignments.delete_by_user_value(original_email, field="email")
# This signal allows code in higher points of LMS to retire the user as necessary
USER_RETIRE_LMS_CRITICAL.send(sender=self.__class__, user=user)
- user.first_name = ''
- user.last_name = ''
+ user.first_name = ""
+ user.last_name = ""
user.is_active = False
user.username = retired_username
user.save()
@@ -1227,24 +1220,20 @@ def retire_users_data_sharing_consent(username, retired_username):
@staticmethod
def retire_sapsf_data_transmission(user): # lint-amnesty, pylint: disable=missing-function-docstring
for ent_user in EnterpriseCustomerUser.objects.filter(user_id=user.id):
- for enrollment in EnterpriseCourseEnrollment.objects.filter(
- enterprise_customer_user=ent_user
- ):
+ for enrollment in EnterpriseCourseEnrollment.objects.filter(enterprise_customer_user=ent_user):
audits = SapSuccessFactorsLearnerDataTransmissionAudit.objects.filter(
enterprise_course_enrollment_id=enrollment.id
)
- audits.update(sapsf_user_id='')
+ audits.update(sapsf_user_id="")
@staticmethod
def retire_degreed_data_transmission(user): # lint-amnesty, pylint: disable=missing-function-docstring
for ent_user in EnterpriseCustomerUser.objects.filter(user_id=user.id):
- for enrollment in EnterpriseCourseEnrollment.objects.filter(
- enterprise_customer_user=ent_user
- ):
+ for enrollment in EnterpriseCourseEnrollment.objects.filter(enterprise_customer_user=ent_user):
audits = DegreedLearnerDataTransmissionAudit.objects.filter(
enterprise_course_enrollment_id=enrollment.id
)
- audits.update(degreed_user_email='')
+ audits.update(degreed_user_email="")
@staticmethod
def retire_user_from_pending_enterprise_customer_user(user, retired_email):
@@ -1256,7 +1245,7 @@ def retire_entitlement_support_detail(user):
Updates all CourseEntitleSupportDetail records for the given user to have an empty ``comments`` field.
"""
for entitlement in CourseEntitlement.objects.filter(user_id=user.id):
- entitlement.courseentitlementsupportdetail_set.all().update(comments='')
+ entitlement.courseentitlementsupportdetail_set.all().update(comments="")
@staticmethod
def clear_pii_from_certificate_records(user):
@@ -1279,6 +1268,7 @@ class UsernameReplacementView(APIView):
This API will be called first, before calling the APIs in other services as this
one handles the checks on the usernames provided.
"""
+
permission_classes = (permissions.IsAuthenticated, CanReplaceUsername)
def post(self, request):
@@ -1320,16 +1310,16 @@ def post(self, request):
# (model_name, column_name)
MODELS_WITH_USERNAME = (
- ('auth.user', 'username'),
- ('consent.DataSharingConsent', 'username'),
- ('consent.HistoricalDataSharingConsent', 'username'),
- ('credit.CreditEligibility', 'username'),
- ('credit.CreditRequest', 'username'),
- ('credit.CreditRequirementStatus', 'username'),
- ('user_api.UserRetirementPartnerReportingStatus', 'original_username'),
- ('user_api.UserRetirementStatus', 'original_username')
+ ("auth.user", "username"),
+ ("consent.DataSharingConsent", "username"),
+ ("consent.HistoricalDataSharingConsent", "username"),
+ ("credit.CreditEligibility", "username"),
+ ("credit.CreditRequest", "username"),
+ ("credit.CreditRequirementStatus", "username"),
+ ("user_api.UserRetirementPartnerReportingStatus", "original_username"),
+ ("user_api.UserRetirementStatus", "original_username"),
)
- UNIQUE_SUFFIX_LENGTH = getattr(settings, 'SOCIAL_AUTH_UUID_LENGTH', 4)
+ UNIQUE_SUFFIX_LENGTH = getattr(settings, "SOCIAL_AUTH_UUID_LENGTH", 4)
username_mappings = request.data.get("username_mappings")
replacement_locations = self._load_models(MODELS_WITH_USERNAME)
@@ -1344,9 +1334,7 @@ def post(self, request):
desired_username = list(username_pair.values())[0]
new_username = self._generate_unique_username(desired_username, suffix_length=UNIQUE_SUFFIX_LENGTH)
successfully_replaced = self._replace_username_for_all_models(
- current_username,
- new_username,
- replacement_locations
+ current_username, new_username, replacement_locations
)
if successfully_replaced:
successful_replacements.append({current_username: new_username})
@@ -1354,14 +1342,11 @@ def post(self, request):
failed_replacements.append({current_username: new_username})
return Response(
status=status.HTTP_200_OK,
- data={
- "successful_replacements": successful_replacements,
- "failed_replacements": failed_replacements
- }
+ data={"successful_replacements": successful_replacements, "failed_replacements": failed_replacements},
)
def _load_models(self, models_with_fields):
- """ Takes tuples that contain a model path and returns the list with a loaded version of the model """
+ """Takes tuples that contain a model path and returns the list with a loaded version of the model"""
try:
replacement_locations = [(apps.get_model(model), column) for (model, column) in models_with_fields]
except LookupError:
@@ -1370,7 +1355,7 @@ def _load_models(self, models_with_fields):
return replacement_locations
def _has_valid_schema(self, post_data):
- """ Verifies the data is a list of objects with a single key:value pair """
+ """Verifies the data is a list of objects with a single key:value pair"""
if not isinstance(post_data, list):
return False
for obj in post_data:
@@ -1389,7 +1374,7 @@ def _generate_unique_username(self, desired_username, suffix_length=4):
while True:
if User.objects.filter(username=new_username).exists():
# adding a dash between user-supplied and system-generated values to avoid weird combinations
- new_username = desired_username + '-' + username_suffix_generator(suffix_length)
+ new_username = desired_username + "-" + username_suffix_generator(suffix_length)
else:
break
return new_username
@@ -1404,10 +1389,8 @@ def _replace_username_for_all_models(self, current_username, new_username, repla
try:
with transaction.atomic():
num_rows_changed = 0
- for (model, column) in replacement_locations:
- num_rows_changed += model.objects.filter(
- **{column: current_username}
- ).update(
+ for model, column in replacement_locations:
+ num_rows_changed += model.objects.filter(**{column: current_username}).update(
**{column: new_username}
)
except Exception as exc: # pylint: disable=broad-except
@@ -1416,7 +1399,7 @@ def _replace_username_for_all_models(self, current_username, new_username, repla
current_username,
new_username,
model.__class__.__name__, # Retrieves the model name that it failed on
- exc
+ exc,
)
return False
if num_rows_changed == 0:
diff --git a/openedx/core/djangoapps/user_api/views.py b/openedx/core/djangoapps/user_api/views.py
index b4fcc68db649..d52493556a19 100644
--- a/openedx/core/djangoapps/user_api/views.py
+++ b/openedx/core/djangoapps/user_api/views.py
@@ -1,6 +1,5 @@
"""HTTP end-points for the User API. """
-
from django.contrib.auth.models import User # lint-amnesty, pylint: disable=imported-auth-user
from django.http import HttpResponse
from django.utils.decorators import method_decorator
@@ -16,21 +15,22 @@
from rest_framework.views import APIView
from openedx.core.djangoapps.django_comment_common.models import Role
-from openedx.core.lib.api.view_utils import require_post_params
from openedx.core.djangoapps.user_api.models import UserPreference
from openedx.core.djangoapps.user_api.preferences.api import get_country_time_zones, update_email_opt_in
from openedx.core.djangoapps.user_api.serializers import (
CountryTimeZoneSerializer,
UserPreferenceSerializer,
- UserSerializer
+ UserSerializer,
)
from openedx.core.lib.api.permissions import ApiKeyHeaderPermission
+from openedx.core.lib.api.view_utils import require_post_params
class UserViewSet(viewsets.ReadOnlyModelViewSet):
"""
DRF class for interacting with the User ORM object
"""
+
permission_classes = (ApiKeyHeaderPermission,)
queryset = User.objects.all().prefetch_related("preferences").select_related("profile")
serializer_class = UserSerializer
@@ -42,6 +42,7 @@ class ForumRoleUsersListView(generics.ListAPIView):
"""
Forum roles are represented by a list of user dicts
"""
+
permission_classes = (ApiKeyHeaderPermission,)
serializer_class = UserSerializer
paginate_by = 10
@@ -51,10 +52,10 @@ def get_queryset(self):
"""
Return a list of users with the specified role/course pair
"""
- name = self.kwargs['name']
- course_id_string = self.request.query_params.get('course_id')
+ name = self.kwargs["name"]
+ course_id_string = self.request.query_params.get("course_id")
if not course_id_string:
- raise ParseError('course_id must be specified')
+ raise ParseError("course_id must be specified")
course_id = CourseKey.from_string(course_id_string)
role = Role.objects.get_or_create(course_id=course_id, name=name)[0]
users = role.users.prefetch_related("preferences").select_related("profile").all()
@@ -65,6 +66,7 @@ class UserPreferenceViewSet(viewsets.ReadOnlyModelViewSet):
"""
DRF class for interacting with the UserPreference ORM
"""
+
permission_classes = (ApiKeyHeaderPermission,)
queryset = UserPreference.objects.all()
filter_backends = (DjangoFilterBackend,)
@@ -78,26 +80,30 @@ class PreferenceUsersListView(generics.ListAPIView):
"""
DRF class for listing a user's preferences
"""
+
permission_classes = (ApiKeyHeaderPermission,)
serializer_class = UserSerializer
paginate_by = 10
paginate_by_param = "page_size"
def get_queryset(self):
- return User.objects.filter(
- preferences__key=self.kwargs["pref_key"]
- ).prefetch_related("preferences").select_related("profile")
+ return (
+ User.objects.filter(preferences__key=self.kwargs["pref_key"])
+ .prefetch_related("preferences")
+ .select_related("profile")
+ )
class UpdateEmailOptInPreference(APIView):
- """View for updating the email opt in preference. """
+ """View for updating the email opt in preference."""
+
authentication_classes = (SessionAuthenticationAllowInactiveUser,)
permission_classes = (IsAuthenticated,)
@method_decorator(require_post_params(["course_id", "email_opt_in"]))
@method_decorator(ensure_csrf_cookie)
def post(self, request):
- """ Post function for updating the email opt in preference.
+ """Post function for updating the email opt in preference.
Allows the modification or creation of the email opt in preference at an
organizational level.
@@ -111,17 +117,13 @@ def post(self, request):
assume False.
"""
- course_id = request.data['course_id']
+ course_id = request.data["course_id"]
try:
org = locator.CourseLocator.from_string(course_id).org
except InvalidKeyError:
- return HttpResponse(
- status=400,
- content=f"No course '{course_id}' found",
- content_type="text/plain"
- )
+ return HttpResponse(status=400, content=f"No course '{course_id}' found", content_type="text/plain")
# Only check for true. All other values are False.
- email_opt_in = request.data['email_opt_in'].lower() == 'true'
+ email_opt_in = request.data["email_opt_in"].lower() == "true"
update_email_opt_in(request.user, org, email_opt_in)
return HttpResponse(status=status.HTTP_200_OK)
@@ -152,9 +154,10 @@ class CountryTimeZoneListView(generics.ListAPIView):
* time_zone: The name of the time zone.
* description: The display version of the time zone
"""
+
serializer_class = CountryTimeZoneSerializer
paginator = None
def get_queryset(self):
- country_code = self.request.GET.get('country_code', None)
+ country_code = self.request.GET.get("country_code", None)
return get_country_time_zones(country_code)
diff --git a/openedx/core/djangoapps/user_authn/tests/test_cookies.py b/openedx/core/djangoapps/user_authn/tests/test_cookies.py
index a90f20f19469..8a7841b3b980 100644
--- a/openedx/core/djangoapps/user_authn/tests/test_cookies.py
+++ b/openedx/core/djangoapps/user_authn/tests/test_cookies.py
@@ -74,9 +74,6 @@ def _copy_cookies_to_request(self, response, request):
for key, val in response.cookies.items()
}
- def _set_use_jwt_cookie_header(self, request):
- request.META['HTTP_USE_JWT_COOKIE'] = 'true'
-
def _assert_recreate_jwt_from_cookies(self, response, can_recreate):
"""
If can_recreate is True, verifies that a JWT can be properly recreated
@@ -133,7 +130,6 @@ def test_set_logged_in_deprecated_cookies(self):
@patch.dict("django.conf.settings.FEATURES", {"DISABLE_SET_JWT_COOKIES_FOR_TESTS": False})
def test_set_logged_in_jwt_cookies(self):
setup_login_oauth_client()
- self._set_use_jwt_cookie_header(self.request)
response = cookies_api.set_logged_in_cookies(self.request, HttpResponse(), self.user)
self._assert_cookies_present(response, cookies_api.ALL_LOGGED_IN_COOKIE_NAMES)
self._assert_consistent_expires(response, num_of_unique_expires=2)
@@ -153,7 +149,6 @@ def test_delete_and_are_logged_in_cookies_set(self):
@patch.dict("django.conf.settings.FEATURES", {"DISABLE_SET_JWT_COOKIES_FOR_TESTS": False})
def test_refresh_jwt_cookies(self):
setup_login_oauth_client()
- self._set_use_jwt_cookie_header(self.request)
response = cookies_api.get_response_with_refreshed_jwt_cookies(self.request, self.user)
data = json.loads(response.content.decode('utf8').replace("'", '"'))
assert data['success'] is True
diff --git a/openedx/core/djangoapps/user_authn/tests/test_tasks.py b/openedx/core/djangoapps/user_authn/tests/test_tasks.py
index 80516f20a39c..5103343a0879 100644
--- a/openedx/core/djangoapps/user_authn/tests/test_tasks.py
+++ b/openedx/core/djangoapps/user_authn/tests/test_tasks.py
@@ -19,6 +19,7 @@ class SendActivationEmailTestCase(TestCase):
"""
Test for send activation email to user
"""
+
def setUp(self):
""" Setup components used by each test."""
super().setUp()
@@ -44,6 +45,7 @@ def test_ComposeEmail(self):
assert self.msg.context['routed_profile_name'] == ''
assert self.msg.context['registration_flow'] is False
assert self.msg.context['is_enterprise_learner'] is False
+ assert self.msg.context['is_first_purchase_discount_overridden'] is False
@mock.patch('time.sleep', mock.Mock(return_value=None))
@mock.patch('openedx.core.djangoapps.user_authn.tasks.log')
diff --git a/openedx/core/djangoapps/user_authn/views/login.py b/openedx/core/djangoapps/user_authn/views/login.py
index 3873f13a7d49..2ad3c0ff18a0 100644
--- a/openedx/core/djangoapps/user_authn/views/login.py
+++ b/openedx/core/djangoapps/user_authn/views/login.py
@@ -363,15 +363,13 @@ def _track_user_login(user, request):
'MailChimp': False
}
)
- register_intent = request.POST.get('register_intent') == 'true'
segment.track(
user.id,
"edx.bi.user.account.authenticated",
{
'category': "conversion",
'label': request.POST.get('course_id'),
- 'provider': None,
- 'register_intent': register_intent,
+ 'provider': None
},
)
diff --git a/openedx/core/djangoapps/user_authn/views/register.py b/openedx/core/djangoapps/user_authn/views/register.py
index 659c90b3d2c4..ab57687d2cd4 100644
--- a/openedx/core/djangoapps/user_authn/views/register.py
+++ b/openedx/core/djangoapps/user_authn/views/register.py
@@ -390,9 +390,12 @@ def _track_user_registration(user, profile, params, third_party_provider, regist
'is_year_of_birth_selected': bool(profile.year_of_birth),
'is_education_selected': bool(profile.level_of_education_display),
'is_goal_set': bool(profile.goals),
- 'total_registration_time': round(float(params.get('totalRegistrationTime', '0'))),
+ 'total_registration_time': round(
+ float(params.get('total_registration_time') or params.get('totalRegistrationTime') or 0)
+ ),
'activation_key': registration.activation_key if registration else None,
'host': params.get('host', ''),
+ 'app_name': params.get('app_name', ''),
'utm_campaign': params.get('utm_campaign', ''),
}
# VAN-738 - added below properties to experiment marketing emails opt in/out events on Braze.
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 f72d609848ff..1e8a4c3ed510 100644
--- a/openedx/core/djangoapps/user_authn/views/tests/test_login.py
+++ b/openedx/core/djangoapps/user_authn/views/tests/test_login.py
@@ -1041,7 +1041,6 @@ class LoginSessionViewTest(ApiTestCase, OpenEdxEventsTestMixin):
USERNAME = "bob"
EMAIL = "bob@example.com"
PASSWORD = "password"
- REGISTER_INTENT = 'true'
@classmethod
def setUpClass(cls):
@@ -1118,7 +1117,6 @@ def test_login(self, include_analytics, mock_segment):
data = {
"email": self.EMAIL,
"password": self.PASSWORD,
- "register_intent": self.REGISTER_INTENT,
}
if include_analytics:
track_label = "edX/DemoX/Fall"
@@ -1147,7 +1145,7 @@ def test_login(self, include_analytics, mock_segment):
mock_segment.track.assert_called_once_with(
expected_user_id,
'edx.bi.user.account.authenticated',
- {'category': 'conversion', 'provider': None, 'label': track_label, 'register_intent': True}
+ {'category': 'conversion', 'provider': None, 'label': track_label}
)
def test_login_with_username(self):
diff --git a/openedx/core/djangoapps/user_authn/views/tests/test_utils.py b/openedx/core/djangoapps/user_authn/views/tests/test_utils.py
index c931fe339901..397448005f85 100644
--- a/openedx/core/djangoapps/user_authn/views/tests/test_utils.py
+++ b/openedx/core/djangoapps/user_authn/views/tests/test_utils.py
@@ -45,6 +45,8 @@ def test_generate_username_from_data(self, data, expected_initials):
({}, None),
({'first_name': '', 'last_name': ''}, None),
({'name': ''}, None),
+ ({'name': '='}, None),
+ ({'name': '@'}, None),
({'first_name': '阿提亚', 'last_name': '阿提亚'}, "AT"),
({'first_name': 'أحمد', 'last_name': 'محمد'}, "HM"),
({'name': 'أحمد محمد'}, "HM"),
diff --git a/openedx/core/djangoapps/user_authn/views/utils.py b/openedx/core/djangoapps/user_authn/views/utils.py
index c6107923a3f1..9b4054bd4037 100644
--- a/openedx/core/djangoapps/user_authn/views/utils.py
+++ b/openedx/core/djangoapps/user_authn/views/utils.py
@@ -128,16 +128,25 @@ def _get_username_prefix(data):
- str: Name initials or None.
"""
username_regex_partial = settings.USERNAME_REGEX_PARTIAL
+ valid_username_regex = r'^[A-Za-z0-9_\-]+$'
full_name = ''
- if data.get('first_name', '').strip() and data.get('last_name', '').strip():
- full_name = f"{unidecode(data.get('first_name', ''))} {unidecode(data.get('last_name', ''))}"
- elif data.get('name', '').strip():
- full_name = unidecode(data['name'])
-
- if full_name.strip():
- full_name = re.findall(username_regex_partial, full_name)[0]
- name_initials = "".join([name_part[0] for name_part in full_name.split()[:2]])
- return name_initials.upper() if name_initials else None
+ try:
+ if data.get('first_name', '').strip() and data.get('last_name', '').strip():
+ full_name = f"{unidecode(data.get('first_name', ''))} {unidecode(data.get('last_name', ''))}"
+ elif data.get('name', '').strip():
+ full_name = unidecode(data['name'])
+
+ if full_name.strip():
+ matched_name = re.findall(username_regex_partial, full_name)
+ if matched_name:
+ full_name = " ".join(matched_name)
+ name_initials = "".join([name_part[0] for name_part in full_name.split()[:2]])
+ if re.match(valid_username_regex, name_initials):
+ return name_initials.upper() if name_initials else None
+
+ except Exception as e: # pylint: disable=broad-except
+ logging.info(f"Error in _get_username_prefix: {e}")
+ return None
return None
diff --git a/openedx/core/djangoapps/xblock/learning_context/learning_context.py b/openedx/core/djangoapps/xblock/learning_context/learning_context.py
index 1ac621ef244f..2dc5155dc4e2 100644
--- a/openedx/core/djangoapps/xblock/learning_context/learning_context.py
+++ b/openedx/core/djangoapps/xblock/learning_context/learning_context.py
@@ -58,3 +58,10 @@ def definition_for_usage(self, usage_key, **kwargs):
Retuns None if the usage key doesn't exist in this context.
"""
raise NotImplementedError
+
+ def send_block_updated_event(self, usage_key):
+ """
+ Send a "block updated" event for the block with the given usage_key in this context.
+
+ usage_key: the UsageKeyV2 subclass used for this learning context
+ """
diff --git a/openedx/core/djangoapps/xblock/rest_api/views.py b/openedx/core/djangoapps/xblock/rest_api/views.py
index 3722d9d8ab15..9972e7463b23 100644
--- a/openedx/core/djangoapps/xblock/rest_api/views.py
+++ b/openedx/core/djangoapps/xblock/rest_api/views.py
@@ -21,6 +21,7 @@
from opaque_keys import InvalidKeyError
from opaque_keys.edx.keys import UsageKey
+from openedx.core.djangoapps.xblock.learning_context.manager import get_learning_context_impl
from openedx.core.lib.api.view_utils import view_auth_classes
from ..api import (
get_block_metadata,
@@ -225,7 +226,9 @@ def post(self, request, usage_key_str):
old_metadata = block.get_explicitly_set_fields_by_scope(Scope.settings)
old_content = block.get_explicitly_set_fields_by_scope(Scope.content)
- block.data = data
+ # only update data if it was passed
+ if data is not None:
+ block.data = data
# update existing metadata with submitted metadata (which can be partial)
# IMPORTANT NOTE: if the client passed 'null' (None) for a piece of metadata that means 'remove it'.
@@ -254,6 +257,10 @@ def post(self, request, usage_key_str):
# Save after the callback so any changes made in the callback will get persisted.
block.save()
+ # Signal that we've modified this block
+ context_impl = get_learning_context_impl(usage_key)
+ context_impl.send_block_updated_event(usage_key)
+
return Response({
"id": str(block.location),
"data": data,
diff --git a/openedx/features/course_duration_limits/access.py b/openedx/features/course_duration_limits/access.py
index ff817a315054..8f73ae2266ea 100644
--- a/openedx/features/course_duration_limits/access.py
+++ b/openedx/features/course_duration_limits/access.py
@@ -68,7 +68,7 @@ def get_user_course_duration(user, course):
return get_expected_duration(course.id)
-def get_user_course_expiration_date(user, course):
+def get_user_course_expiration_date(user, course, enrollment=None):
"""
Return expiration date for given user course pair.
Return None if the course does not expire.
@@ -81,7 +81,7 @@ def get_user_course_expiration_date(user, course):
if access_duration is None:
return None
- enrollment = CourseEnrollment.get_enrollment(user, course.id)
+ enrollment = enrollment or CourseEnrollment.get_enrollment(user, course.id)
if enrollment is None or enrollment.mode != CourseMode.AUDIT:
return None
diff --git a/openedx/features/discounts/applicability.py b/openedx/features/discounts/applicability.py
index 1bb895f7f90f..97d6f74403bd 100644
--- a/openedx/features/discounts/applicability.py
+++ b/openedx/features/discounts/applicability.py
@@ -13,6 +13,7 @@
import pytz
from crum import get_current_request, impersonate
+from django.conf import settings
from django.utils import timezone
from django.utils.dateparse import parse_datetime
from edx_toggles.toggles import WaffleFlag
@@ -28,6 +29,18 @@
from common.djangoapps.track import segment
+# .. toggle_name: discounts.enable_first_purchase_discount_override
+# .. toggle_implementation: WaffleFlag
+# .. toggle_default: False
+# .. toggle_description: Waffle flag to enable the First Purchase Discount to be overridden from
+# EDXWELCOME/BIENVENIDOAEDX 15% discount to a new code.
+# .. toggle_use_cases: opt_in
+# .. toggle_creation_date: 2024-07-18
+# .. toggle_target_removal_date: None
+# .. toggle_tickets: REV-4097
+# .. toggle_warning: This feature toggle does not have a target removal date.
+FIRST_PURCHASE_DISCOUNT_OVERRIDE_FLAG = WaffleFlag('discounts.enable_first_purchase_discount_override', __name__)
+
# .. toggle_name: discounts.enable_discounting
# .. toggle_implementation: WaffleFlag
# .. toggle_default: False
@@ -215,6 +228,13 @@ def discount_percentage(course):
"""
Get the configured discount amount.
"""
+ if FIRST_PURCHASE_DISCOUNT_OVERRIDE_FLAG.is_enabled():
+ return getattr(
+ settings,
+ 'FIRST_PURCHASE_DISCOUNT_OVERRIDE_PERCENTAGE',
+ 15
+ )
+
configured_percentage = DiscountPercentageConfig.current(course_key=course.id).percentage
if configured_percentage:
return configured_percentage
diff --git a/openedx/features/discounts/tests/test_utils.py b/openedx/features/discounts/tests/test_utils.py
index 1ff305831d74..6bd9d1cd1593 100644
--- a/openedx/features/discounts/tests/test_utils.py
+++ b/openedx/features/discounts/tests/test_utils.py
@@ -5,7 +5,7 @@
import ddt
from django.contrib.auth.models import AnonymousUser
-from django.test import TestCase
+from django.test import TestCase, override_settings
from django.utils.translation import override as override_lang
from edx_toggles.toggles.testutils import override_waffle_flag
@@ -14,7 +14,11 @@
from common.djangoapps.student.models import CourseEnrollment
from common.djangoapps.student.tests.factories import UserFactory
from openedx.core.djangoapps.content.course_overviews.tests.factories import CourseOverviewFactory
-from openedx.features.discounts.applicability import DISCOUNT_APPLICABILITY_FLAG, get_discount_expiration_date
+from openedx.features.discounts.applicability import (
+ DISCOUNT_APPLICABILITY_FLAG,
+ FIRST_PURCHASE_DISCOUNT_OVERRIDE_FLAG,
+ get_discount_expiration_date
+)
from .. import utils
@@ -84,6 +88,14 @@ def test_spanish_code(self):
with override_lang('es-419'):
assert utils.generate_offer_data(self.user, self.overview)['code'] == 'BIENVENIDOAEDX'
+ def test_override(self):
+ with override_settings(FIRST_PURCHASE_DISCOUNT_OVERRIDE_CODE='NOTEDXWELCOME'):
+ with override_settings(FIRST_PURCHASE_DISCOUNT_OVERRIDE_PERCENTAGE=30):
+ with override_waffle_flag(DISCOUNT_APPLICABILITY_FLAG, active=True):
+ with override_waffle_flag(FIRST_PURCHASE_DISCOUNT_OVERRIDE_FLAG, active=True):
+ assert utils.generate_offer_data(self.user, self.overview)['code'] == 'NOTEDXWELCOME'
+ assert utils.generate_offer_data(self.user, self.overview)['percentage'] == 30
+
def test_anonymous(self):
assert utils.generate_offer_data(AnonymousUser(), self.overview) is None
diff --git a/openedx/features/discounts/utils.py b/openedx/features/discounts/utils.py
index f52f821d9a9d..e97524d4381a 100644
--- a/openedx/features/discounts/utils.py
+++ b/openedx/features/discounts/utils.py
@@ -5,6 +5,7 @@
from datetime import datetime
import pytz
+from django.conf import settings
from django.utils.translation import get_language
from django.utils.translation import gettext as _
@@ -13,6 +14,7 @@
from lms.djangoapps.courseware.utils import verified_upgrade_deadline_link
from openedx.core.djangolib.markup import HTML
from openedx.features.discounts.applicability import (
+ FIRST_PURCHASE_DISCOUNT_OVERRIDE_FLAG,
REV1008_EXPERIMENT_ID,
can_receive_discount,
discount_percentage,
@@ -98,8 +100,17 @@ def generate_offer_data(user, course):
original, discounted, percentage = _get_discount_prices(user, course, assume_discount=True)
+ # Override the First Purchase Discount to another code only if flag is enabled
+ first_purchase_discount_code = 'BIENVENIDOAEDX' if get_language() == 'es-419' else 'EDXWELCOME'
+ if FIRST_PURCHASE_DISCOUNT_OVERRIDE_FLAG.is_enabled():
+ first_purchase_discount_code = getattr(
+ settings,
+ 'FIRST_PURCHASE_DISCOUNT_OVERRIDE_CODE',
+ first_purchase_discount_code
+ )
+
return {
- 'code': 'BIENVENIDOAEDX' if get_language() == 'es-419' else 'EDXWELCOME',
+ 'code': first_purchase_discount_code,
'expiration_date': expiration_date,
'original_price': original,
'discounted_price': discounted,
diff --git a/openedx/features/enterprise_support/api.py b/openedx/features/enterprise_support/api.py
index 5b62815f06fe..32c493d1e35e 100644
--- a/openedx/features/enterprise_support/api.py
+++ b/openedx/features/enterprise_support/api.py
@@ -422,8 +422,13 @@ def enterprise_customer_from_session(request):
"""
Retrieve enterprise_customer data from the request's session,
returning a ``__CACHE_MISS__`` if absent.
+
+ Now checks for session existence before attempting to access it.
"""
- return request.session.get(ENTERPRISE_CUSTOMER_KEY_NAME, _CACHE_MISS)
+ if not request or not hasattr(request, 'session'):
+ return _CACHE_MISS
+ else:
+ return request.session.get(ENTERPRISE_CUSTOMER_KEY_NAME, _CACHE_MISS)
def enterprise_customer_uuid_from_session(request):
diff --git a/openedx/features/enterprise_support/tests/test_api.py b/openedx/features/enterprise_support/tests/test_api.py
index 8629caae9067..40f2c834e6b0 100644
--- a/openedx/features/enterprise_support/tests/test_api.py
+++ b/openedx/features/enterprise_support/tests/test_api.py
@@ -36,6 +36,7 @@
data_sharing_consent_required,
enterprise_customer_for_request,
enterprise_customer_from_api,
+ enterprise_customer_from_session,
enterprise_customer_from_session_or_learner_data,
enterprise_customer_uuid_for_request,
enterprise_enabled,
@@ -1331,6 +1332,10 @@ def test_enterprise_customer_from_session(self):
# verify that existing session value should not be updated for un-authenticate user
assert mock_request.session[ENTERPRISE_CUSTOMER_KEY_NAME] == enterprise_customer
+ @ddt.data(None, object())
+ def test_enterprise_customer_from_session_no_session_CACHE_MISS(self, request):
+ assert enterprise_customer_from_session(request) == _CACHE_MISS
+
def test_get_consent_notification_data_no_overrides(self):
enterprise_customer = {
'name': 'abc',
diff --git a/package-lock.json b/package-lock.json
index 729d399bb79f..87c75af69044 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -9,22 +9,21 @@
"version": "0.1.0",
"hasInstallScript": true,
"dependencies": {
- "@babel/core": "7.19.0",
+ "@babel/core": "7.25.2",
"@babel/plugin-proposal-object-rest-spread": "^7.18.9",
"@babel/plugin-transform-object-assign": "^7.18.6",
"@babel/preset-env": "^7.19.0",
- "@babel/preset-react": "7.18.6",
+ "@babel/preset-react": "7.24.7",
"@edx/brand-edx.org": "^2.0.7",
"@edx/edx-bootstrap": "1.0.4",
"@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",
- "axios": "^0.28.0",
"babel-loader": "^9.1.3",
"babel-plugin-transform-class-properties": "6.24.1",
"babel-polyfill": "6.26.0",
- "backbone": "1.4.1",
+ "backbone": "1.6.0",
"backbone-associations": "0.6.2",
"backbone.paginator": "2.0.8",
"bootstrap": "4.0.0",
@@ -45,12 +44,11 @@
"jquery-migrate": "1.4.1",
"jquery.scrollto": "2.1.3",
"js-cookie": "3.0.5",
- "jwt-decode": "^3.1.2",
"moment": "2.30.1",
"moment-timezone": "0.5.45",
"node-gyp": "10.0.1",
"picturefill": "3.0.3",
- "popper.js": "1.12.9",
+ "popper.js": "1.16.1",
"prop-types": "15.6.0",
"raw-loader": "0.5.1",
"react": "16.14.0",
@@ -61,8 +59,8 @@
"react-slick": "0.29.0",
"redux": "3.7.2",
"redux-thunk": "2.2.0",
- "requirejs": "2.3.6",
- "rtlcss": "2.2.1",
+ "requirejs": "2.3.7",
+ "rtlcss": "2.6.2",
"sass": "^1.54.8",
"sass-loader": "^14.1.1",
"scriptjs": "2.5.9",
@@ -71,7 +69,6 @@
"uglify-js": "2.7.0",
"underscore": "1.12.1",
"underscore.string": "3.3.6",
- "universal-cookie": "^4.0.4",
"webpack": "^5.90.3",
"webpack-bundle-tracker": "0.4.3",
"webpack-merge": "4.1.1",
@@ -82,33 +79,33 @@
"@edx/eslint-config": "^3.1.1",
"@edx/mockprock": "github:openedx/mockprock#3ad18c6888e6521e9bf7a4df0db6f8579b928235",
"@edx/stylelint-config-edx": "2.3.3",
- "babel-jest": "26.0.0",
+ "babel-jest": "26.6.3",
"enzyme": "3.11.0",
"enzyme-adapter-react-16": "1.15.8",
"eslint-import-resolver-webpack": "0.13.8",
"jasmine-core": "2.6.4",
"jasmine-jquery": "git+https://git@github.com/velesin/jasmine-jquery.git#ebad463d592d3fea00c69f26ea18a930e09c7b58",
- "jest": "26.0.0",
- "jest-enzyme": "6.0.2",
+ "jest": "26.6.3",
+ "jest-enzyme": "6.1.2",
"karma": "0.13.22",
"karma-chrome-launcher": "0.2.3",
"karma-coverage": "0.5.5",
"karma-firefox-launcher": "0.1.7",
"karma-jasmine": "0.3.8",
"karma-jasmine-html-reporter": "0.2.2",
- "karma-junit-reporter": "1.1.0",
+ "karma-junit-reporter": "1.2.0",
"karma-requirejs": "0.2.6",
"karma-selenium-webdriver-launcher": "github:openedx/karma-selenium-webdriver-launcher#0.0.4-openedx.0",
- "karma-sourcemap-loader": "0.3.7",
+ "karma-sourcemap-loader": "0.4.0",
"karma-spec-reporter": "0.0.36",
"karma-webpack": "^5.0.1",
"plato": "1.7.0",
- "react-test-renderer": "16.4.0",
- "selenium-webdriver": "3.4.0",
+ "react-test-renderer": "16.14.0",
+ "selenium-webdriver": "3.6.0",
"sinon": "2.4.1",
"squirejs": "0.1.0",
"string-replace-loader": "^3.1.0",
- "stylelint-formatter-pretty": "1.0.3",
+ "stylelint-formatter-pretty": "1.1.4",
"webpack-cli": "^5.1.4"
}
},
@@ -135,11 +132,12 @@
}
},
"node_modules/@babel/code-frame": {
- "version": "7.24.2",
- "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.2.tgz",
- "integrity": "sha512-y5+tLQyV8pg3fsiln67BVLD1P13Eg4lh5RW9mF0zUuvLrv9uIQ4MCL+CRT+FTsBlBjcIan6PGsLcBN0m3ClUyQ==",
+ "version": "7.24.7",
+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.7.tgz",
+ "integrity": "sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA==",
+ "license": "MIT",
"dependencies": {
- "@babel/highlight": "^7.24.2",
+ "@babel/highlight": "^7.24.7",
"picocolors": "^1.0.0"
},
"engines": {
@@ -147,33 +145,35 @@
}
},
"node_modules/@babel/compat-data": {
- "version": "7.24.1",
- "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.24.1.tgz",
- "integrity": "sha512-Pc65opHDliVpRHuKfzI+gSA4zcgr65O4cl64fFJIWEEh8JoHIHh0Oez1Eo8Arz8zq/JhgKodQaxEwUPRtZylVA==",
+ "version": "7.25.2",
+ "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.25.2.tgz",
+ "integrity": "sha512-bYcppcpKBvX4znYaPEeFau03bp89ShqNMLs+rmdptMw+heSZh9+z84d2YG+K7cYLbWwzdjtDoW/uqZmPjulClQ==",
+ "license": "MIT",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/core": {
- "version": "7.19.0",
- "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.19.0.tgz",
- "integrity": "sha512-reM4+U7B9ss148rh2n1Qs9ASS+w94irYXga7c2jaQv9RVzpS7Mv1a9rnYYwuDa45G+DkORt9g6An2k/V4d9LbQ==",
- "dependencies": {
- "@ampproject/remapping": "^2.1.0",
- "@babel/code-frame": "^7.18.6",
- "@babel/generator": "^7.19.0",
- "@babel/helper-compilation-targets": "^7.19.0",
- "@babel/helper-module-transforms": "^7.19.0",
- "@babel/helpers": "^7.19.0",
- "@babel/parser": "^7.19.0",
- "@babel/template": "^7.18.10",
- "@babel/traverse": "^7.19.0",
- "@babel/types": "^7.19.0",
- "convert-source-map": "^1.7.0",
+ "version": "7.25.2",
+ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.25.2.tgz",
+ "integrity": "sha512-BBt3opiCOxUr9euZ5/ro/Xv8/V7yJ5bjYMqG/C1YAo8MIKAnumZalCN+msbci3Pigy4lIQfPUpfMM27HMGaYEA==",
+ "license": "MIT",
+ "dependencies": {
+ "@ampproject/remapping": "^2.2.0",
+ "@babel/code-frame": "^7.24.7",
+ "@babel/generator": "^7.25.0",
+ "@babel/helper-compilation-targets": "^7.25.2",
+ "@babel/helper-module-transforms": "^7.25.2",
+ "@babel/helpers": "^7.25.0",
+ "@babel/parser": "^7.25.0",
+ "@babel/template": "^7.25.0",
+ "@babel/traverse": "^7.25.2",
+ "@babel/types": "^7.25.2",
+ "convert-source-map": "^2.0.0",
"debug": "^4.1.0",
"gensync": "^1.0.0-beta.2",
- "json5": "^2.2.1",
- "semver": "^6.3.0"
+ "json5": "^2.2.3",
+ "semver": "^6.3.1"
},
"engines": {
"node": ">=6.9.0"
@@ -183,12 +183,19 @@
"url": "https://opencollective.com/babel"
}
},
+ "node_modules/@babel/core/node_modules/convert-source-map": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
+ "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
+ "license": "MIT"
+ },
"node_modules/@babel/generator": {
- "version": "7.24.1",
- "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.24.1.tgz",
- "integrity": "sha512-DfCRfZsBcrPEHUfuBMgbJ1Ut01Y/itOs+hY2nFLgqsqXd52/iSiVq5TITtUasIUgm+IIKdY2/1I7auiQOEeC9A==",
+ "version": "7.25.0",
+ "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.25.0.tgz",
+ "integrity": "sha512-3LEEcj3PVW8pW2R1SR1M89g/qrYk/m/mB/tLqn7dn4sbBUQyTqnlod+II2U4dqiGtUmkcnAmkMDralTFZttRiw==",
+ "license": "MIT",
"dependencies": {
- "@babel/types": "^7.24.0",
+ "@babel/types": "^7.25.0",
"@jridgewell/gen-mapping": "^0.3.5",
"@jridgewell/trace-mapping": "^0.3.25",
"jsesc": "^2.5.1"
@@ -198,35 +205,39 @@
}
},
"node_modules/@babel/helper-annotate-as-pure": {
- "version": "7.22.5",
- "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.22.5.tgz",
- "integrity": "sha512-LvBTxu8bQSQkcyKOU+a1btnNFQ1dMAd0R6PyW3arXes06F6QLWLIrd681bxRPIXlrMGR3XYnW9JyML7dP3qgxg==",
+ "version": "7.24.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.24.7.tgz",
+ "integrity": "sha512-BaDeOonYvhdKw+JoMVkAixAAJzG2jVPIwWoKBPdYuY9b452e2rPuI9QPYh3KpofZ3pW2akOmwZLOiOsHMiqRAg==",
+ "license": "MIT",
"dependencies": {
- "@babel/types": "^7.22.5"
+ "@babel/types": "^7.24.7"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-builder-binary-assignment-operator-visitor": {
- "version": "7.22.15",
- "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.22.15.tgz",
- "integrity": "sha512-QkBXwGgaoC2GtGZRoma6kv7Szfv06khvhFav67ZExau2RaXzy8MpHSMO2PNoP2XtmQphJQRHFfg77Bq731Yizw==",
+ "version": "7.24.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.24.7.tgz",
+ "integrity": "sha512-xZeCVVdwb4MsDBkkyZ64tReWYrLRHlMN72vP7Bdm3OUOuyFZExhsHUUnuWnm2/XOlAJzR0LfPpB56WXZn0X/lA==",
+ "license": "MIT",
"dependencies": {
- "@babel/types": "^7.22.15"
+ "@babel/traverse": "^7.24.7",
+ "@babel/types": "^7.24.7"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-compilation-targets": {
- "version": "7.23.6",
- "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.23.6.tgz",
- "integrity": "sha512-9JB548GZoQVmzrFgp8o7KxdgkTGm6xs9DW0o/Pim72UDjzr5ObUQ6ZzYPqA+g9OTS2bBQoctLJrky0RDCAWRgQ==",
+ "version": "7.25.2",
+ "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.25.2.tgz",
+ "integrity": "sha512-U2U5LsSaZ7TAt3cfaymQ8WHh0pxvdHoEk6HVpaexxixjyEquMh0L0YNJNM6CTGKMXV1iksi0iZkGw4AcFkPaaw==",
+ "license": "MIT",
"dependencies": {
- "@babel/compat-data": "^7.23.5",
- "@babel/helper-validator-option": "^7.23.5",
- "browserslist": "^4.22.2",
+ "@babel/compat-data": "^7.25.2",
+ "@babel/helper-validator-option": "^7.24.8",
+ "browserslist": "^4.23.1",
"lru-cache": "^5.1.1",
"semver": "^6.3.1"
},
@@ -235,18 +246,17 @@
}
},
"node_modules/@babel/helper-create-class-features-plugin": {
- "version": "7.24.1",
- "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.24.1.tgz",
- "integrity": "sha512-1yJa9dX9g//V6fDebXoEfEsxkZHk3Hcbm+zLhyu6qVgYFLvmTALTeV+jNU9e5RnYtioBrGEOdoI2joMSNQ/+aA==",
- "dependencies": {
- "@babel/helper-annotate-as-pure": "^7.22.5",
- "@babel/helper-environment-visitor": "^7.22.20",
- "@babel/helper-function-name": "^7.23.0",
- "@babel/helper-member-expression-to-functions": "^7.23.0",
- "@babel/helper-optimise-call-expression": "^7.22.5",
- "@babel/helper-replace-supers": "^7.24.1",
- "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5",
- "@babel/helper-split-export-declaration": "^7.22.6",
+ "version": "7.25.0",
+ "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.25.0.tgz",
+ "integrity": "sha512-GYM6BxeQsETc9mnct+nIIpf63SAyzvyYN7UB/IlTyd+MBg06afFGp0mIeUqGyWgS2mxad6vqbMrHVlaL3m70sQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-annotate-as-pure": "^7.24.7",
+ "@babel/helper-member-expression-to-functions": "^7.24.8",
+ "@babel/helper-optimise-call-expression": "^7.24.7",
+ "@babel/helper-replace-supers": "^7.25.0",
+ "@babel/helper-skip-transparent-expression-wrappers": "^7.24.7",
+ "@babel/traverse": "^7.25.0",
"semver": "^6.3.1"
},
"engines": {
@@ -257,11 +267,12 @@
}
},
"node_modules/@babel/helper-create-regexp-features-plugin": {
- "version": "7.22.15",
- "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.22.15.tgz",
- "integrity": "sha512-29FkPLFjn4TPEa3RE7GpW+qbE8tlsu3jntNYNfcGsc49LphF1PQIiD+vMZ1z1xVOKt+93khA9tc2JBs3kBjA7w==",
+ "version": "7.25.2",
+ "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.25.2.tgz",
+ "integrity": "sha512-+wqVGP+DFmqwFD3EH6TMTfUNeqDehV3E/dl+Sd54eaXqm17tEUNbEIn4sVivVowbvUpOtIGxdo3GoXyDH9N/9g==",
+ "license": "MIT",
"dependencies": {
- "@babel/helper-annotate-as-pure": "^7.22.5",
+ "@babel/helper-annotate-as-pure": "^7.24.7",
"regexpu-core": "^5.3.1",
"semver": "^6.3.1"
},
@@ -287,69 +298,42 @@
"@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0"
}
},
- "node_modules/@babel/helper-environment-visitor": {
- "version": "7.22.20",
- "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz",
- "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==",
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/@babel/helper-function-name": {
- "version": "7.23.0",
- "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz",
- "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==",
- "dependencies": {
- "@babel/template": "^7.22.15",
- "@babel/types": "^7.23.0"
- },
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/@babel/helper-hoist-variables": {
- "version": "7.22.5",
- "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz",
- "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==",
- "dependencies": {
- "@babel/types": "^7.22.5"
- },
- "engines": {
- "node": ">=6.9.0"
- }
- },
"node_modules/@babel/helper-member-expression-to-functions": {
- "version": "7.23.0",
- "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.23.0.tgz",
- "integrity": "sha512-6gfrPwh7OuT6gZyJZvd6WbTfrqAo7vm4xCzAXOusKqq/vWdKXphTpj5klHKNmRUU6/QRGlBsyU9mAIPaWHlqJA==",
+ "version": "7.24.8",
+ "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.24.8.tgz",
+ "integrity": "sha512-LABppdt+Lp/RlBxqrh4qgf1oEH/WxdzQNDJIu5gC/W1GyvPVrOBiItmmM8wan2fm4oYqFuFfkXmlGpLQhPY8CA==",
+ "license": "MIT",
"dependencies": {
- "@babel/types": "^7.23.0"
+ "@babel/traverse": "^7.24.8",
+ "@babel/types": "^7.24.8"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-module-imports": {
- "version": "7.24.3",
- "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.24.3.tgz",
- "integrity": "sha512-viKb0F9f2s0BCS22QSF308z/+1YWKV/76mwt61NBzS5izMzDPwdq1pTrzf+Li3npBWX9KdQbkeCt1jSAM7lZqg==",
+ "version": "7.24.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.24.7.tgz",
+ "integrity": "sha512-8AyH3C+74cgCVVXow/myrynrAGv+nTVg5vKu2nZph9x7RcRwzmh0VFallJuFTZ9mx6u4eSdXZfcOzSqTUm0HCA==",
+ "license": "MIT",
"dependencies": {
- "@babel/types": "^7.24.0"
+ "@babel/traverse": "^7.24.7",
+ "@babel/types": "^7.24.7"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-module-transforms": {
- "version": "7.23.3",
- "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.23.3.tgz",
- "integrity": "sha512-7bBs4ED9OmswdfDzpz4MpWgSrV7FXlc3zIagvLFjS5H+Mk7Snr21vQ6QwrsoCGMfNC4e4LQPdoULEt4ykz0SRQ==",
+ "version": "7.25.2",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.25.2.tgz",
+ "integrity": "sha512-BjyRAbix6j/wv83ftcVJmBt72QtHI56C7JXZoG2xATiLpmoC7dpd8WnkikExHDVPpi/3qCmO6WY1EaXOluiecQ==",
+ "license": "MIT",
"dependencies": {
- "@babel/helper-environment-visitor": "^7.22.20",
- "@babel/helper-module-imports": "^7.22.15",
- "@babel/helper-simple-access": "^7.22.5",
- "@babel/helper-split-export-declaration": "^7.22.6",
- "@babel/helper-validator-identifier": "^7.22.20"
+ "@babel/helper-module-imports": "^7.24.7",
+ "@babel/helper-simple-access": "^7.24.7",
+ "@babel/helper-validator-identifier": "^7.24.7",
+ "@babel/traverse": "^7.25.2"
},
"engines": {
"node": ">=6.9.0"
@@ -359,32 +343,35 @@
}
},
"node_modules/@babel/helper-optimise-call-expression": {
- "version": "7.22.5",
- "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.22.5.tgz",
- "integrity": "sha512-HBwaojN0xFRx4yIvpwGqxiV2tUfl7401jlok564NgB9EHS1y6QT17FmKWm4ztqjeVdXLuC4fSvHc5ePpQjoTbw==",
+ "version": "7.24.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.24.7.tgz",
+ "integrity": "sha512-jKiTsW2xmWwxT1ixIdfXUZp+P5yURx2suzLZr5Hi64rURpDYdMW0pv+Uf17EYk2Rd428Lx4tLsnjGJzYKDM/6A==",
+ "license": "MIT",
"dependencies": {
- "@babel/types": "^7.22.5"
+ "@babel/types": "^7.24.7"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-plugin-utils": {
- "version": "7.24.0",
- "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.0.tgz",
- "integrity": "sha512-9cUznXMG0+FxRuJfvL82QlTqIzhVW9sL0KjMPHhAOOvpQGL8QtdxnBKILjBqxlHyliz0yCa1G903ZXI/FuHy2w==",
+ "version": "7.24.8",
+ "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.8.tgz",
+ "integrity": "sha512-FFWx5142D8h2Mgr/iPVGH5G7w6jDn4jUSpZTyDnQO0Yn7Ks2Kuz6Pci8H6MPCoUJegd/UZQ3tAvfLCxQSnWWwg==",
+ "license": "MIT",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-remap-async-to-generator": {
- "version": "7.22.20",
- "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.22.20.tgz",
- "integrity": "sha512-pBGyV4uBqOns+0UvhsTO8qgl8hO89PmiDYv+/COyp1aeMcmfrfruz+/nCMFiYyFF/Knn0yfrC85ZzNFjembFTw==",
+ "version": "7.25.0",
+ "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.25.0.tgz",
+ "integrity": "sha512-NhavI2eWEIz/H9dbrG0TuOicDhNexze43i5z7lEqwYm0WEZVTwnPpA0EafUTP7+6/W79HWIP2cTe3Z5NiSTVpw==",
+ "license": "MIT",
"dependencies": {
- "@babel/helper-annotate-as-pure": "^7.22.5",
- "@babel/helper-environment-visitor": "^7.22.20",
- "@babel/helper-wrap-function": "^7.22.20"
+ "@babel/helper-annotate-as-pure": "^7.24.7",
+ "@babel/helper-wrap-function": "^7.25.0",
+ "@babel/traverse": "^7.25.0"
},
"engines": {
"node": ">=6.9.0"
@@ -394,13 +381,14 @@
}
},
"node_modules/@babel/helper-replace-supers": {
- "version": "7.24.1",
- "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.24.1.tgz",
- "integrity": "sha512-QCR1UqC9BzG5vZl8BMicmZ28RuUBnHhAMddD8yHFHDRH9lLTZ9uUPehX8ctVPT8l0TKblJidqcgUUKGVrePleQ==",
+ "version": "7.25.0",
+ "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.25.0.tgz",
+ "integrity": "sha512-q688zIvQVYtZu+i2PsdIu/uWGRpfxzr5WESsfpShfZECkO+d2o+WROWezCi/Q6kJ0tfPa5+pUGUlfx2HhrA3Bg==",
+ "license": "MIT",
"dependencies": {
- "@babel/helper-environment-visitor": "^7.22.20",
- "@babel/helper-member-expression-to-functions": "^7.23.0",
- "@babel/helper-optimise-call-expression": "^7.22.5"
+ "@babel/helper-member-expression-to-functions": "^7.24.8",
+ "@babel/helper-optimise-call-expression": "^7.24.7",
+ "@babel/traverse": "^7.25.0"
},
"engines": {
"node": ">=6.9.0"
@@ -410,94 +398,92 @@
}
},
"node_modules/@babel/helper-simple-access": {
- "version": "7.22.5",
- "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.22.5.tgz",
- "integrity": "sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w==",
+ "version": "7.24.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.24.7.tgz",
+ "integrity": "sha512-zBAIvbCMh5Ts+b86r/CjU+4XGYIs+R1j951gxI3KmmxBMhCg4oQMsv6ZXQ64XOm/cvzfU1FmoCyt6+owc5QMYg==",
+ "license": "MIT",
"dependencies": {
- "@babel/types": "^7.22.5"
+ "@babel/traverse": "^7.24.7",
+ "@babel/types": "^7.24.7"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-skip-transparent-expression-wrappers": {
- "version": "7.22.5",
- "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.22.5.tgz",
- "integrity": "sha512-tK14r66JZKiC43p8Ki33yLBVJKlQDFoA8GYN67lWCDCqoL6EMMSuM9b+Iff2jHaM/RRFYl7K+iiru7hbRqNx8Q==",
- "dependencies": {
- "@babel/types": "^7.22.5"
- },
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/@babel/helper-split-export-declaration": {
- "version": "7.22.6",
- "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz",
- "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==",
+ "version": "7.24.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.24.7.tgz",
+ "integrity": "sha512-IO+DLT3LQUElMbpzlatRASEyQtfhSE0+m465v++3jyyXeBTBUjtVZg28/gHeV5mrTJqvEKhKroBGAvhW+qPHiQ==",
+ "license": "MIT",
"dependencies": {
- "@babel/types": "^7.22.5"
+ "@babel/traverse": "^7.24.7",
+ "@babel/types": "^7.24.7"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-string-parser": {
- "version": "7.24.1",
- "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.1.tgz",
- "integrity": "sha512-2ofRCjnnA9y+wk8b9IAREroeUP02KHp431N2mhKniy2yKIDKpbrHv9eXwm8cBeWQYcJmzv5qKCu65P47eCF7CQ==",
+ "version": "7.24.8",
+ "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.8.tgz",
+ "integrity": "sha512-pO9KhhRcuUyGnJWwyEgnRJTSIZHiT+vMD0kPeD+so0l7mxkMT19g3pjY9GTnHySck/hDzq+dtW/4VgnMkippsQ==",
+ "license": "MIT",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-validator-identifier": {
- "version": "7.22.20",
- "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz",
- "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==",
+ "version": "7.24.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz",
+ "integrity": "sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==",
+ "license": "MIT",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-validator-option": {
- "version": "7.23.5",
- "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.23.5.tgz",
- "integrity": "sha512-85ttAOMLsr53VgXkTbkx8oA6YTfT4q7/HzXSLEYmjcSTJPMPQtvq1BD79Byep5xMUYbGRzEpDsjUf3dyp54IKw==",
+ "version": "7.24.8",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.24.8.tgz",
+ "integrity": "sha512-xb8t9tD1MHLungh/AIoWYN+gVHaB9kwlu8gffXGSt3FFEIT7RjS+xWbc2vUD1UTZdIpKj/ab3rdqJ7ufngyi2Q==",
+ "license": "MIT",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-wrap-function": {
- "version": "7.22.20",
- "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.22.20.tgz",
- "integrity": "sha512-pms/UwkOpnQe/PDAEdV/d7dVCoBbB+R4FvYoHGZz+4VPcg7RtYy2KP7S2lbuWM6FCSgob5wshfGESbC/hzNXZw==",
+ "version": "7.25.0",
+ "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.25.0.tgz",
+ "integrity": "sha512-s6Q1ebqutSiZnEjaofc/UKDyC4SbzV5n5SrA2Gq8UawLycr3i04f1dX4OzoQVnexm6aOCh37SQNYlJ/8Ku+PMQ==",
+ "license": "MIT",
"dependencies": {
- "@babel/helper-function-name": "^7.22.5",
- "@babel/template": "^7.22.15",
- "@babel/types": "^7.22.19"
+ "@babel/template": "^7.25.0",
+ "@babel/traverse": "^7.25.0",
+ "@babel/types": "^7.25.0"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helpers": {
- "version": "7.24.1",
- "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.24.1.tgz",
- "integrity": "sha512-BpU09QqEe6ZCHuIHFphEFgvNSrubve1FtyMton26ekZ85gRGi6LrTF7zArARp2YvyFxloeiRmtSCq5sjh1WqIg==",
+ "version": "7.25.0",
+ "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.25.0.tgz",
+ "integrity": "sha512-MjgLZ42aCm0oGjJj8CtSM3DB8NOOf8h2l7DCTePJs29u+v7yO/RBX9nShlKMgFnRks/Q4tBAe7Hxnov9VkGwLw==",
+ "license": "MIT",
"dependencies": {
- "@babel/template": "^7.24.0",
- "@babel/traverse": "^7.24.1",
- "@babel/types": "^7.24.0"
+ "@babel/template": "^7.25.0",
+ "@babel/types": "^7.25.0"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/highlight": {
- "version": "7.24.2",
- "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.2.tgz",
- "integrity": "sha512-Yac1ao4flkTxTteCDZLEvdxg2fZfz1v8M4QpaGypq/WPDqg3ijHYbDfs+LG5hvzSoqaSZ9/Z9lKSP3CjZjv+pA==",
+ "version": "7.24.7",
+ "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.7.tgz",
+ "integrity": "sha512-EStJpq4OuY8xYfhGVXngigBJRWxftKX9ksiGDnmlY3o7B/V7KIAc9X4oiK87uPJSc/vs5L869bem5fhZa8caZw==",
+ "license": "MIT",
"dependencies": {
- "@babel/helper-validator-identifier": "^7.22.20",
+ "@babel/helper-validator-identifier": "^7.24.7",
"chalk": "^2.4.2",
"js-tokens": "^4.0.0",
"picocolors": "^1.0.0"
@@ -507,9 +493,13 @@
}
},
"node_modules/@babel/parser": {
- "version": "7.24.1",
- "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.1.tgz",
- "integrity": "sha512-Zo9c7N3xdOIQrNip7Lc9wvRPzlRtovHVE4lkz8WEDr7uYh/GMQhSiIgFxGIArRHYdJE5kxtZjAf8rT0xhdLCzg==",
+ "version": "7.25.3",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.25.3.tgz",
+ "integrity": "sha512-iLTJKDbJ4hMvFPgQwwsVoxtHyWpKKPBrxkANrSYewDPaPpT5py5yeVkgPIJ7XYXhndxJpaA3PyALSXQ7u8e/Dw==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.25.2"
+ },
"bin": {
"parser": "bin/babel-parser.js"
},
@@ -517,12 +507,44 @@
"node": ">=6.0.0"
}
},
+ "node_modules/@babel/plugin-bugfix-firefox-class-in-computed-class-key": {
+ "version": "7.25.3",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.25.3.tgz",
+ "integrity": "sha512-wUrcsxZg6rqBXG05HG1FPYgsP6EvwF4WpBbxIpWIIYnH8wG0gzx3yZY3dtEHas4sTAOGkbTsc9EGPxwff8lRoA==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.24.8",
+ "@babel/traverse": "^7.25.3"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/plugin-bugfix-safari-class-field-initializer-scope": {
+ "version": "7.25.0",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-class-field-initializer-scope/-/plugin-bugfix-safari-class-field-initializer-scope-7.25.0.tgz",
+ "integrity": "sha512-Bm4bH2qsX880b/3ziJ8KD711LT7z4u8CFudmjqle65AZj/HNUFhEf90dqYv6O86buWvSBmeQDjv0Tn2aF/bIBA==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.24.8"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
"node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": {
- "version": "7.24.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.24.1.tgz",
- "integrity": "sha512-y4HqEnkelJIOQGd+3g1bTeKsA5c6qM7eOn7VggGVbBc0y8MLSKHacwcIE2PplNlQSj0PqS9rrXL/nkPVK+kUNg==",
+ "version": "7.25.0",
+ "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.0.tgz",
+ "integrity": "sha512-lXwdNZtTmeVOOFtwM/WDe7yg1PL8sYhRk/XH0FzbR2HDQ0xC+EnQ/JHeoMYSavtU115tnUk0q9CDyq8si+LMAA==",
+ "license": "MIT",
"dependencies": {
- "@babel/helper-plugin-utils": "^7.24.0"
+ "@babel/helper-plugin-utils": "^7.24.8"
},
"engines": {
"node": ">=6.9.0"
@@ -532,13 +554,14 @@
}
},
"node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": {
- "version": "7.24.1",
- "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.24.1.tgz",
- "integrity": "sha512-Hj791Ii4ci8HqnaKHAlLNs+zaLXb0EzSDhiAWp5VNlyvCNymYfacs64pxTxbH1znW/NcArSmwpmG9IKE/TUVVQ==",
+ "version": "7.24.7",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.24.7.tgz",
+ "integrity": "sha512-+izXIbke1T33mY4MSNnrqhPXDz01WYhEf3yF5NbnUtkiNnm+XBZJl3kNfoK6NKmYlz/D07+l2GWVK/QfDkNCuQ==",
+ "license": "MIT",
"dependencies": {
- "@babel/helper-plugin-utils": "^7.24.0",
- "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5",
- "@babel/plugin-transform-optional-chaining": "^7.24.1"
+ "@babel/helper-plugin-utils": "^7.24.7",
+ "@babel/helper-skip-transparent-expression-wrappers": "^7.24.7",
+ "@babel/plugin-transform-optional-chaining": "^7.24.7"
},
"engines": {
"node": ">=6.9.0"
@@ -548,12 +571,13 @@
}
},
"node_modules/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": {
- "version": "7.24.1",
- "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.24.1.tgz",
- "integrity": "sha512-m9m/fXsXLiHfwdgydIFnpk+7jlVbnvlK5B2EKiPdLUb6WX654ZaaEWJUjk8TftRbZpK0XibovlLWX4KIZhV6jw==",
+ "version": "7.25.0",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.25.0.tgz",
+ "integrity": "sha512-tggFrk1AIShG/RUQbEwt2Tr/E+ObkfwrPjR6BjbRvsx24+PSjK8zrq0GWPNCjo8qpRx4DuJzlcvWJqlm+0h3kw==",
+ "license": "MIT",
"dependencies": {
- "@babel/helper-environment-visitor": "^7.22.20",
- "@babel/helper-plugin-utils": "^7.24.0"
+ "@babel/helper-plugin-utils": "^7.24.8",
+ "@babel/traverse": "^7.25.0"
},
"engines": {
"node": ">=6.9.0"
@@ -663,11 +687,12 @@
}
},
"node_modules/@babel/plugin-syntax-import-assertions": {
- "version": "7.24.1",
- "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.24.1.tgz",
- "integrity": "sha512-IuwnI5XnuF189t91XbxmXeCDz3qs6iDRO7GJ++wcfgeXNs/8FmIlKcpDSXNVyuLQxlwvskmI3Ct73wUODkJBlQ==",
+ "version": "7.24.7",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.24.7.tgz",
+ "integrity": "sha512-Ec3NRUMoi8gskrkBe3fNmEQfxDvY8bgfQpz6jlk/41kX9eUjvpyqWU7PBP/pLAvMaSQjbMNKJmvX57jP+M6bPg==",
+ "license": "MIT",
"dependencies": {
- "@babel/helper-plugin-utils": "^7.24.0"
+ "@babel/helper-plugin-utils": "^7.24.7"
},
"engines": {
"node": ">=6.9.0"
@@ -677,11 +702,12 @@
}
},
"node_modules/@babel/plugin-syntax-import-attributes": {
- "version": "7.24.1",
- "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.24.1.tgz",
- "integrity": "sha512-zhQTMH0X2nVLnb04tz+s7AMuasX8U0FnpE+nHTOhSOINjWMnopoZTxtIKsd45n4GQ/HIZLyfIpoul8e2m0DnRA==",
+ "version": "7.24.7",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.24.7.tgz",
+ "integrity": "sha512-hbX+lKKeUMGihnK8nvKqmXBInriT3GVjzXKFriV3YC6APGxMbP8RZNFwy91+hocLXq90Mta+HshoB31802bb8A==",
+ "license": "MIT",
"dependencies": {
- "@babel/helper-plugin-utils": "^7.24.0"
+ "@babel/helper-plugin-utils": "^7.24.7"
},
"engines": {
"node": ">=6.9.0"
@@ -713,11 +739,12 @@
}
},
"node_modules/@babel/plugin-syntax-jsx": {
- "version": "7.24.1",
- "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.24.1.tgz",
- "integrity": "sha512-2eCtxZXf+kbkMIsXS4poTvT4Yu5rXiRa+9xGVT56raghjmBTKMpFNc9R4IDiB4emao9eO22Ox7CxuJG7BgExqA==",
+ "version": "7.24.7",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.24.7.tgz",
+ "integrity": "sha512-6ddciUPe/mpMnOKv/U+RSd2vvVy+Yw/JfBB0ZHYjEZt9NLHmCUylNYlsbqCCS1Bffjlb0fCwC9Vqz+sBz6PsiQ==",
+ "license": "MIT",
"dependencies": {
- "@babel/helper-plugin-utils": "^7.24.0"
+ "@babel/helper-plugin-utils": "^7.24.7"
},
"engines": {
"node": ">=6.9.0"
@@ -836,11 +863,12 @@
}
},
"node_modules/@babel/plugin-transform-arrow-functions": {
- "version": "7.24.1",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.24.1.tgz",
- "integrity": "sha512-ngT/3NkRhsaep9ck9uj2Xhv9+xB1zShY3tM3g6om4xxCELwCDN4g4Aq5dRn48+0hasAql7s2hdBOysCfNpr4fw==",
+ "version": "7.24.7",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.24.7.tgz",
+ "integrity": "sha512-Dt9LQs6iEY++gXUwY03DNFat5C2NbO48jj+j/bSAz6b3HgPs39qcPiYt77fDObIcFwj3/C2ICX9YMwGflUoSHQ==",
+ "license": "MIT",
"dependencies": {
- "@babel/helper-plugin-utils": "^7.24.0"
+ "@babel/helper-plugin-utils": "^7.24.7"
},
"engines": {
"node": ">=6.9.0"
@@ -850,14 +878,15 @@
}
},
"node_modules/@babel/plugin-transform-async-generator-functions": {
- "version": "7.24.3",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.24.3.tgz",
- "integrity": "sha512-Qe26CMYVjpQxJ8zxM1340JFNjZaF+ISWpr1Kt/jGo+ZTUzKkfw/pphEWbRCb+lmSM6k/TOgfYLvmbHkUQ0asIg==",
+ "version": "7.25.0",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.25.0.tgz",
+ "integrity": "sha512-uaIi2FdqzjpAMvVqvB51S42oC2JEVgh0LDsGfZVDysWE8LrJtQC2jvKmOqEYThKyB7bDEb7BP1GYWDm7tABA0Q==",
+ "license": "MIT",
"dependencies": {
- "@babel/helper-environment-visitor": "^7.22.20",
- "@babel/helper-plugin-utils": "^7.24.0",
- "@babel/helper-remap-async-to-generator": "^7.22.20",
- "@babel/plugin-syntax-async-generators": "^7.8.4"
+ "@babel/helper-plugin-utils": "^7.24.8",
+ "@babel/helper-remap-async-to-generator": "^7.25.0",
+ "@babel/plugin-syntax-async-generators": "^7.8.4",
+ "@babel/traverse": "^7.25.0"
},
"engines": {
"node": ">=6.9.0"
@@ -867,13 +896,14 @@
}
},
"node_modules/@babel/plugin-transform-async-to-generator": {
- "version": "7.24.1",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.24.1.tgz",
- "integrity": "sha512-AawPptitRXp1y0n4ilKcGbRYWfbbzFWz2NqNu7dacYDtFtz0CMjG64b3LQsb3KIgnf4/obcUL78hfaOS7iCUfw==",
+ "version": "7.24.7",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.24.7.tgz",
+ "integrity": "sha512-SQY01PcJfmQ+4Ash7NE+rpbLFbmqA2GPIgqzxfFTL4t1FKRq4zTms/7htKpoCUI9OcFYgzqfmCdH53s6/jn5fA==",
+ "license": "MIT",
"dependencies": {
- "@babel/helper-module-imports": "^7.24.1",
- "@babel/helper-plugin-utils": "^7.24.0",
- "@babel/helper-remap-async-to-generator": "^7.22.20"
+ "@babel/helper-module-imports": "^7.24.7",
+ "@babel/helper-plugin-utils": "^7.24.7",
+ "@babel/helper-remap-async-to-generator": "^7.24.7"
},
"engines": {
"node": ">=6.9.0"
@@ -883,11 +913,12 @@
}
},
"node_modules/@babel/plugin-transform-block-scoped-functions": {
- "version": "7.24.1",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.24.1.tgz",
- "integrity": "sha512-TWWC18OShZutrv9C6mye1xwtam+uNi2bnTOCBUd5sZxyHOiWbU6ztSROofIMrK84uweEZC219POICK/sTYwfgg==",
+ "version": "7.24.7",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.24.7.tgz",
+ "integrity": "sha512-yO7RAz6EsVQDaBH18IDJcMB1HnrUn2FJ/Jslc/WtPPWcjhpUJXU/rjbwmluzp7v/ZzWcEhTMXELnnsz8djWDwQ==",
+ "license": "MIT",
"dependencies": {
- "@babel/helper-plugin-utils": "^7.24.0"
+ "@babel/helper-plugin-utils": "^7.24.7"
},
"engines": {
"node": ">=6.9.0"
@@ -897,11 +928,12 @@
}
},
"node_modules/@babel/plugin-transform-block-scoping": {
- "version": "7.24.1",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.24.1.tgz",
- "integrity": "sha512-h71T2QQvDgM2SmT29UYU6ozjMlAt7s7CSs5Hvy8f8cf/GM/Z4a2zMfN+fjVGaieeCrXR3EdQl6C4gQG+OgmbKw==",
+ "version": "7.25.0",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.25.0.tgz",
+ "integrity": "sha512-yBQjYoOjXlFv9nlXb3f1casSHOZkWr29NX+zChVanLg5Nc157CrbEX9D7hxxtTpuFy7Q0YzmmWfJxzvps4kXrQ==",
+ "license": "MIT",
"dependencies": {
- "@babel/helper-plugin-utils": "^7.24.0"
+ "@babel/helper-plugin-utils": "^7.24.8"
},
"engines": {
"node": ">=6.9.0"
@@ -911,12 +943,13 @@
}
},
"node_modules/@babel/plugin-transform-class-properties": {
- "version": "7.24.1",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.24.1.tgz",
- "integrity": "sha512-OMLCXi0NqvJfORTaPQBwqLXHhb93wkBKZ4aNwMl6WtehO7ar+cmp+89iPEQPqxAnxsOKTaMcs3POz3rKayJ72g==",
+ "version": "7.24.7",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.24.7.tgz",
+ "integrity": "sha512-vKbfawVYayKcSeSR5YYzzyXvsDFWU2mD8U5TFeXtbCPLFUqe7GyCgvO6XDHzje862ODrOwy6WCPmKeWHbCFJ4w==",
+ "license": "MIT",
"dependencies": {
- "@babel/helper-create-class-features-plugin": "^7.24.1",
- "@babel/helper-plugin-utils": "^7.24.0"
+ "@babel/helper-create-class-features-plugin": "^7.24.7",
+ "@babel/helper-plugin-utils": "^7.24.7"
},
"engines": {
"node": ">=6.9.0"
@@ -926,12 +959,13 @@
}
},
"node_modules/@babel/plugin-transform-class-static-block": {
- "version": "7.24.1",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.24.1.tgz",
- "integrity": "sha512-FUHlKCn6J3ERiu8Dv+4eoz7w8+kFLSyeVG4vDAikwADGjUCoHw/JHokyGtr8OR4UjpwPVivyF+h8Q5iv/JmrtA==",
+ "version": "7.24.7",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.24.7.tgz",
+ "integrity": "sha512-HMXK3WbBPpZQufbMG4B46A90PkuuhN9vBCb5T8+VAHqvAqvcLi+2cKoukcpmUYkszLhScU3l1iudhrks3DggRQ==",
+ "license": "MIT",
"dependencies": {
- "@babel/helper-create-class-features-plugin": "^7.24.1",
- "@babel/helper-plugin-utils": "^7.24.0",
+ "@babel/helper-create-class-features-plugin": "^7.24.7",
+ "@babel/helper-plugin-utils": "^7.24.7",
"@babel/plugin-syntax-class-static-block": "^7.14.5"
},
"engines": {
@@ -942,17 +976,16 @@
}
},
"node_modules/@babel/plugin-transform-classes": {
- "version": "7.24.1",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.24.1.tgz",
- "integrity": "sha512-ZTIe3W7UejJd3/3R4p7ScyyOoafetUShSf4kCqV0O7F/RiHxVj/wRaRnQlrGwflvcehNA8M42HkAiEDYZu2F1Q==",
- "dependencies": {
- "@babel/helper-annotate-as-pure": "^7.22.5",
- "@babel/helper-compilation-targets": "^7.23.6",
- "@babel/helper-environment-visitor": "^7.22.20",
- "@babel/helper-function-name": "^7.23.0",
- "@babel/helper-plugin-utils": "^7.24.0",
- "@babel/helper-replace-supers": "^7.24.1",
- "@babel/helper-split-export-declaration": "^7.22.6",
+ "version": "7.25.0",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.25.0.tgz",
+ "integrity": "sha512-xyi6qjr/fYU304fiRwFbekzkqVJZ6A7hOjWZd+89FVcBqPV3S9Wuozz82xdpLspckeaafntbzglaW4pqpzvtSw==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-annotate-as-pure": "^7.24.7",
+ "@babel/helper-compilation-targets": "^7.24.8",
+ "@babel/helper-plugin-utils": "^7.24.8",
+ "@babel/helper-replace-supers": "^7.25.0",
+ "@babel/traverse": "^7.25.0",
"globals": "^11.1.0"
},
"engines": {
@@ -963,12 +996,13 @@
}
},
"node_modules/@babel/plugin-transform-computed-properties": {
- "version": "7.24.1",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.24.1.tgz",
- "integrity": "sha512-5pJGVIUfJpOS+pAqBQd+QMaTD2vCL/HcePooON6pDpHgRp4gNRmzyHTPIkXntwKsq3ayUFVfJaIKPw2pOkOcTw==",
+ "version": "7.24.7",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.24.7.tgz",
+ "integrity": "sha512-25cS7v+707Gu6Ds2oY6tCkUwsJ9YIDbggd9+cu9jzzDgiNq7hR/8dkzxWfKWnTic26vsI3EsCXNd4iEB6e8esQ==",
+ "license": "MIT",
"dependencies": {
- "@babel/helper-plugin-utils": "^7.24.0",
- "@babel/template": "^7.24.0"
+ "@babel/helper-plugin-utils": "^7.24.7",
+ "@babel/template": "^7.24.7"
},
"engines": {
"node": ">=6.9.0"
@@ -978,11 +1012,12 @@
}
},
"node_modules/@babel/plugin-transform-destructuring": {
- "version": "7.24.1",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.24.1.tgz",
- "integrity": "sha512-ow8jciWqNxR3RYbSNVuF4U2Jx130nwnBnhRw6N6h1bOejNkABmcI5X5oz29K4alWX7vf1C+o6gtKXikzRKkVdw==",
+ "version": "7.24.8",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.24.8.tgz",
+ "integrity": "sha512-36e87mfY8TnRxc7yc6M9g9gOB7rKgSahqkIKwLpz4Ppk2+zC2Cy1is0uwtuSG6AE4zlTOUa+7JGz9jCJGLqQFQ==",
+ "license": "MIT",
"dependencies": {
- "@babel/helper-plugin-utils": "^7.24.0"
+ "@babel/helper-plugin-utils": "^7.24.8"
},
"engines": {
"node": ">=6.9.0"
@@ -992,12 +1027,13 @@
}
},
"node_modules/@babel/plugin-transform-dotall-regex": {
- "version": "7.24.1",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.24.1.tgz",
- "integrity": "sha512-p7uUxgSoZwZ2lPNMzUkqCts3xlp8n+o05ikjy7gbtFJSt9gdU88jAmtfmOxHM14noQXBxfgzf2yRWECiNVhTCw==",
+ "version": "7.24.7",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.24.7.tgz",
+ "integrity": "sha512-ZOA3W+1RRTSWvyqcMJDLqbchh7U4NRGqwRfFSVbOLS/ePIP4vHB5e8T8eXcuqyN1QkgKyj5wuW0lcS85v4CrSw==",
+ "license": "MIT",
"dependencies": {
- "@babel/helper-create-regexp-features-plugin": "^7.22.15",
- "@babel/helper-plugin-utils": "^7.24.0"
+ "@babel/helper-create-regexp-features-plugin": "^7.24.7",
+ "@babel/helper-plugin-utils": "^7.24.7"
},
"engines": {
"node": ">=6.9.0"
@@ -1007,11 +1043,12 @@
}
},
"node_modules/@babel/plugin-transform-duplicate-keys": {
- "version": "7.24.1",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.24.1.tgz",
- "integrity": "sha512-msyzuUnvsjsaSaocV6L7ErfNsa5nDWL1XKNnDePLgmz+WdU4w/J8+AxBMrWfi9m4IxfL5sZQKUPQKDQeeAT6lA==",
+ "version": "7.24.7",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.24.7.tgz",
+ "integrity": "sha512-JdYfXyCRihAe46jUIliuL2/s0x0wObgwwiGxw/UbgJBr20gQBThrokO4nYKgWkD7uBaqM7+9x5TU7NkExZJyzw==",
+ "license": "MIT",
"dependencies": {
- "@babel/helper-plugin-utils": "^7.24.0"
+ "@babel/helper-plugin-utils": "^7.24.7"
},
"engines": {
"node": ">=6.9.0"
@@ -1020,12 +1057,29 @@
"@babel/core": "^7.0.0-0"
}
},
+ "node_modules/@babel/plugin-transform-duplicate-named-capturing-groups-regex": {
+ "version": "7.25.0",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-named-capturing-groups-regex/-/plugin-transform-duplicate-named-capturing-groups-regex-7.25.0.tgz",
+ "integrity": "sha512-YLpb4LlYSc3sCUa35un84poXoraOiQucUTTu8X1j18JV+gNa8E0nyUf/CjZ171IRGr4jEguF+vzJU66QZhn29g==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-create-regexp-features-plugin": "^7.25.0",
+ "@babel/helper-plugin-utils": "^7.24.8"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
"node_modules/@babel/plugin-transform-dynamic-import": {
- "version": "7.24.1",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.24.1.tgz",
- "integrity": "sha512-av2gdSTyXcJVdI+8aFZsCAtR29xJt0S5tas+Ef8NvBNmD1a+N/3ecMLeMBgfcK+xzsjdLDT6oHt+DFPyeqUbDA==",
+ "version": "7.24.7",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.24.7.tgz",
+ "integrity": "sha512-sc3X26PhZQDb3JhORmakcbvkeInvxz+A8oda99lj7J60QRuPZvNAk9wQlTBS1ZynelDrDmTU4pw1tyc5d5ZMUg==",
+ "license": "MIT",
"dependencies": {
- "@babel/helper-plugin-utils": "^7.24.0",
+ "@babel/helper-plugin-utils": "^7.24.7",
"@babel/plugin-syntax-dynamic-import": "^7.8.3"
},
"engines": {
@@ -1036,12 +1090,13 @@
}
},
"node_modules/@babel/plugin-transform-exponentiation-operator": {
- "version": "7.24.1",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.24.1.tgz",
- "integrity": "sha512-U1yX13dVBSwS23DEAqU+Z/PkwE9/m7QQy8Y9/+Tdb8UWYaGNDYwTLi19wqIAiROr8sXVum9A/rtiH5H0boUcTw==",
+ "version": "7.24.7",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.24.7.tgz",
+ "integrity": "sha512-Rqe/vSc9OYgDajNIK35u7ot+KeCoetqQYFXM4Epf7M7ez3lWlOjrDjrwMei6caCVhfdw+mIKD4cgdGNy5JQotQ==",
+ "license": "MIT",
"dependencies": {
- "@babel/helper-builder-binary-assignment-operator-visitor": "^7.22.15",
- "@babel/helper-plugin-utils": "^7.24.0"
+ "@babel/helper-builder-binary-assignment-operator-visitor": "^7.24.7",
+ "@babel/helper-plugin-utils": "^7.24.7"
},
"engines": {
"node": ">=6.9.0"
@@ -1051,11 +1106,12 @@
}
},
"node_modules/@babel/plugin-transform-export-namespace-from": {
- "version": "7.24.1",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.24.1.tgz",
- "integrity": "sha512-Ft38m/KFOyzKw2UaJFkWG9QnHPG/Q/2SkOrRk4pNBPg5IPZ+dOxcmkK5IyuBcxiNPyyYowPGUReyBvrvZs7IlQ==",
+ "version": "7.24.7",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.24.7.tgz",
+ "integrity": "sha512-v0K9uNYsPL3oXZ/7F9NNIbAj2jv1whUEtyA6aujhekLs56R++JDQuzRcP2/z4WX5Vg/c5lE9uWZA0/iUoFhLTA==",
+ "license": "MIT",
"dependencies": {
- "@babel/helper-plugin-utils": "^7.24.0",
+ "@babel/helper-plugin-utils": "^7.24.7",
"@babel/plugin-syntax-export-namespace-from": "^7.8.3"
},
"engines": {
@@ -1066,12 +1122,13 @@
}
},
"node_modules/@babel/plugin-transform-for-of": {
- "version": "7.24.1",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.24.1.tgz",
- "integrity": "sha512-OxBdcnF04bpdQdR3i4giHZNZQn7cm8RQKcSwA17wAAqEELo1ZOwp5FFgeptWUQXFyT9kwHo10aqqauYkRZPCAg==",
+ "version": "7.24.7",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.24.7.tgz",
+ "integrity": "sha512-wo9ogrDG1ITTTBsy46oGiN1dS9A7MROBTcYsfS8DtsImMkHk9JXJ3EWQM6X2SUw4x80uGPlwj0o00Uoc6nEE3g==",
+ "license": "MIT",
"dependencies": {
- "@babel/helper-plugin-utils": "^7.24.0",
- "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5"
+ "@babel/helper-plugin-utils": "^7.24.7",
+ "@babel/helper-skip-transparent-expression-wrappers": "^7.24.7"
},
"engines": {
"node": ">=6.9.0"
@@ -1081,13 +1138,14 @@
}
},
"node_modules/@babel/plugin-transform-function-name": {
- "version": "7.24.1",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.24.1.tgz",
- "integrity": "sha512-BXmDZpPlh7jwicKArQASrj8n22/w6iymRnvHYYd2zO30DbE277JO20/7yXJT3QxDPtiQiOxQBbZH4TpivNXIxA==",
+ "version": "7.25.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.25.1.tgz",
+ "integrity": "sha512-TVVJVdW9RKMNgJJlLtHsKDTydjZAbwIsn6ySBPQaEAUU5+gVvlJt/9nRmqVbsV/IBanRjzWoaAQKLoamWVOUuA==",
+ "license": "MIT",
"dependencies": {
- "@babel/helper-compilation-targets": "^7.23.6",
- "@babel/helper-function-name": "^7.23.0",
- "@babel/helper-plugin-utils": "^7.24.0"
+ "@babel/helper-compilation-targets": "^7.24.8",
+ "@babel/helper-plugin-utils": "^7.24.8",
+ "@babel/traverse": "^7.25.1"
},
"engines": {
"node": ">=6.9.0"
@@ -1097,11 +1155,12 @@
}
},
"node_modules/@babel/plugin-transform-json-strings": {
- "version": "7.24.1",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.24.1.tgz",
- "integrity": "sha512-U7RMFmRvoasscrIFy5xA4gIp8iWnWubnKkKuUGJjsuOH7GfbMkB+XZzeslx2kLdEGdOJDamEmCqOks6e8nv8DQ==",
+ "version": "7.24.7",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.24.7.tgz",
+ "integrity": "sha512-2yFnBGDvRuxAaE/f0vfBKvtnvvqU8tGpMHqMNpTN2oWMKIR3NqFkjaAgGwawhqK/pIN2T3XdjGPdaG0vDhOBGw==",
+ "license": "MIT",
"dependencies": {
- "@babel/helper-plugin-utils": "^7.24.0",
+ "@babel/helper-plugin-utils": "^7.24.7",
"@babel/plugin-syntax-json-strings": "^7.8.3"
},
"engines": {
@@ -1112,11 +1171,12 @@
}
},
"node_modules/@babel/plugin-transform-literals": {
- "version": "7.24.1",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.24.1.tgz",
- "integrity": "sha512-zn9pwz8U7nCqOYIiBaOxoQOtYmMODXTJnkxG4AtX8fPmnCRYWBOHD0qcpwS9e2VDSp1zNJYpdnFMIKb8jmwu6g==",
+ "version": "7.25.2",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.25.2.tgz",
+ "integrity": "sha512-HQI+HcTbm9ur3Z2DkO+jgESMAMcYLuN/A7NRw9juzxAezN9AvqvUTnpKP/9kkYANz6u7dFlAyOu44ejuGySlfw==",
+ "license": "MIT",
"dependencies": {
- "@babel/helper-plugin-utils": "^7.24.0"
+ "@babel/helper-plugin-utils": "^7.24.8"
},
"engines": {
"node": ">=6.9.0"
@@ -1126,11 +1186,12 @@
}
},
"node_modules/@babel/plugin-transform-logical-assignment-operators": {
- "version": "7.24.1",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.24.1.tgz",
- "integrity": "sha512-OhN6J4Bpz+hIBqItTeWJujDOfNP+unqv/NJgyhlpSqgBTPm37KkMmZV6SYcOj+pnDbdcl1qRGV/ZiIjX9Iy34w==",
+ "version": "7.24.7",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.24.7.tgz",
+ "integrity": "sha512-4D2tpwlQ1odXmTEIFWy9ELJcZHqrStlzK/dAOWYyxX3zT0iXQB6banjgeOJQXzEc4S0E0a5A+hahxPaEFYftsw==",
+ "license": "MIT",
"dependencies": {
- "@babel/helper-plugin-utils": "^7.24.0",
+ "@babel/helper-plugin-utils": "^7.24.7",
"@babel/plugin-syntax-logical-assignment-operators": "^7.10.4"
},
"engines": {
@@ -1141,11 +1202,12 @@
}
},
"node_modules/@babel/plugin-transform-member-expression-literals": {
- "version": "7.24.1",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.24.1.tgz",
- "integrity": "sha512-4ojai0KysTWXzHseJKa1XPNXKRbuUrhkOPY4rEGeR+7ChlJVKxFa3H3Bz+7tWaGKgJAXUWKOGmltN+u9B3+CVg==",
+ "version": "7.24.7",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.24.7.tgz",
+ "integrity": "sha512-T/hRC1uqrzXMKLQ6UCwMT85S3EvqaBXDGf0FaMf4446Qx9vKwlghvee0+uuZcDUCZU5RuNi4781UQ7R308zzBw==",
+ "license": "MIT",
"dependencies": {
- "@babel/helper-plugin-utils": "^7.24.0"
+ "@babel/helper-plugin-utils": "^7.24.7"
},
"engines": {
"node": ">=6.9.0"
@@ -1155,12 +1217,13 @@
}
},
"node_modules/@babel/plugin-transform-modules-amd": {
- "version": "7.24.1",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.24.1.tgz",
- "integrity": "sha512-lAxNHi4HVtjnHd5Rxg3D5t99Xm6H7b04hUS7EHIXcUl2EV4yl1gWdqZrNzXnSrHveL9qMdbODlLF55mvgjAfaQ==",
+ "version": "7.24.7",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.24.7.tgz",
+ "integrity": "sha512-9+pB1qxV3vs/8Hdmz/CulFB8w2tuu6EB94JZFsjdqxQokwGa9Unap7Bo2gGBGIvPmDIVvQrom7r5m/TCDMURhg==",
+ "license": "MIT",
"dependencies": {
- "@babel/helper-module-transforms": "^7.23.3",
- "@babel/helper-plugin-utils": "^7.24.0"
+ "@babel/helper-module-transforms": "^7.24.7",
+ "@babel/helper-plugin-utils": "^7.24.7"
},
"engines": {
"node": ">=6.9.0"
@@ -1170,13 +1233,14 @@
}
},
"node_modules/@babel/plugin-transform-modules-commonjs": {
- "version": "7.24.1",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.24.1.tgz",
- "integrity": "sha512-szog8fFTUxBfw0b98gEWPaEqF42ZUD/T3bkynW/wtgx2p/XCP55WEsb+VosKceRSd6njipdZvNogqdtI4Q0chw==",
+ "version": "7.24.8",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.24.8.tgz",
+ "integrity": "sha512-WHsk9H8XxRs3JXKWFiqtQebdh9b/pTk4EgueygFzYlTKAg0Ud985mSevdNjdXdFBATSKVJGQXP1tv6aGbssLKA==",
+ "license": "MIT",
"dependencies": {
- "@babel/helper-module-transforms": "^7.23.3",
- "@babel/helper-plugin-utils": "^7.24.0",
- "@babel/helper-simple-access": "^7.22.5"
+ "@babel/helper-module-transforms": "^7.24.8",
+ "@babel/helper-plugin-utils": "^7.24.8",
+ "@babel/helper-simple-access": "^7.24.7"
},
"engines": {
"node": ">=6.9.0"
@@ -1186,14 +1250,15 @@
}
},
"node_modules/@babel/plugin-transform-modules-systemjs": {
- "version": "7.24.1",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.24.1.tgz",
- "integrity": "sha512-mqQ3Zh9vFO1Tpmlt8QPnbwGHzNz3lpNEMxQb1kAemn/erstyqw1r9KeOlOfo3y6xAnFEcOv2tSyrXfmMk+/YZA==",
+ "version": "7.25.0",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.25.0.tgz",
+ "integrity": "sha512-YPJfjQPDXxyQWg/0+jHKj1llnY5f/R6a0p/vP4lPymxLu7Lvl4k2WMitqi08yxwQcCVUUdG9LCUj4TNEgAp3Jw==",
+ "license": "MIT",
"dependencies": {
- "@babel/helper-hoist-variables": "^7.22.5",
- "@babel/helper-module-transforms": "^7.23.3",
- "@babel/helper-plugin-utils": "^7.24.0",
- "@babel/helper-validator-identifier": "^7.22.20"
+ "@babel/helper-module-transforms": "^7.25.0",
+ "@babel/helper-plugin-utils": "^7.24.8",
+ "@babel/helper-validator-identifier": "^7.24.7",
+ "@babel/traverse": "^7.25.0"
},
"engines": {
"node": ">=6.9.0"
@@ -1203,12 +1268,13 @@
}
},
"node_modules/@babel/plugin-transform-modules-umd": {
- "version": "7.24.1",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.24.1.tgz",
- "integrity": "sha512-tuA3lpPj+5ITfcCluy6nWonSL7RvaG0AOTeAuvXqEKS34lnLzXpDb0dcP6K8jD0zWZFNDVly90AGFJPnm4fOYg==",
+ "version": "7.24.7",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.24.7.tgz",
+ "integrity": "sha512-3aytQvqJ/h9z4g8AsKPLvD4Zqi2qT+L3j7XoFFu1XBlZWEl2/1kWnhmAbxpLgPrHSY0M6UA02jyTiwUVtiKR6A==",
+ "license": "MIT",
"dependencies": {
- "@babel/helper-module-transforms": "^7.23.3",
- "@babel/helper-plugin-utils": "^7.24.0"
+ "@babel/helper-module-transforms": "^7.24.7",
+ "@babel/helper-plugin-utils": "^7.24.7"
},
"engines": {
"node": ">=6.9.0"
@@ -1218,12 +1284,13 @@
}
},
"node_modules/@babel/plugin-transform-named-capturing-groups-regex": {
- "version": "7.22.5",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.22.5.tgz",
- "integrity": "sha512-YgLLKmS3aUBhHaxp5hi1WJTgOUb/NCuDHzGT9z9WTt3YG+CPRhJs6nprbStx6DnWM4dh6gt7SU3sZodbZ08adQ==",
+ "version": "7.24.7",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.24.7.tgz",
+ "integrity": "sha512-/jr7h/EWeJtk1U/uz2jlsCioHkZk1JJZVcc8oQsJ1dUlaJD83f4/6Zeh2aHt9BIFokHIsSeDfhUmju0+1GPd6g==",
+ "license": "MIT",
"dependencies": {
- "@babel/helper-create-regexp-features-plugin": "^7.22.5",
- "@babel/helper-plugin-utils": "^7.22.5"
+ "@babel/helper-create-regexp-features-plugin": "^7.24.7",
+ "@babel/helper-plugin-utils": "^7.24.7"
},
"engines": {
"node": ">=6.9.0"
@@ -1233,11 +1300,12 @@
}
},
"node_modules/@babel/plugin-transform-new-target": {
- "version": "7.24.1",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.24.1.tgz",
- "integrity": "sha512-/rurytBM34hYy0HKZQyA0nHbQgQNFm4Q/BOc9Hflxi2X3twRof7NaE5W46j4kQitm7SvACVRXsa6N/tSZxvPug==",
+ "version": "7.24.7",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.24.7.tgz",
+ "integrity": "sha512-RNKwfRIXg4Ls/8mMTza5oPF5RkOW8Wy/WgMAp1/F1yZ8mMbtwXW+HDoJiOsagWrAhI5f57Vncrmr9XeT4CVapA==",
+ "license": "MIT",
"dependencies": {
- "@babel/helper-plugin-utils": "^7.24.0"
+ "@babel/helper-plugin-utils": "^7.24.7"
},
"engines": {
"node": ">=6.9.0"
@@ -1247,11 +1315,12 @@
}
},
"node_modules/@babel/plugin-transform-nullish-coalescing-operator": {
- "version": "7.24.1",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.24.1.tgz",
- "integrity": "sha512-iQ+caew8wRrhCikO5DrUYx0mrmdhkaELgFa+7baMcVuhxIkN7oxt06CZ51D65ugIb1UWRQ8oQe+HXAVM6qHFjw==",
+ "version": "7.24.7",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.24.7.tgz",
+ "integrity": "sha512-Ts7xQVk1OEocqzm8rHMXHlxvsfZ0cEF2yomUqpKENHWMF4zKk175Y4q8H5knJes6PgYad50uuRmt3UJuhBw8pQ==",
+ "license": "MIT",
"dependencies": {
- "@babel/helper-plugin-utils": "^7.24.0",
+ "@babel/helper-plugin-utils": "^7.24.7",
"@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3"
},
"engines": {
@@ -1262,11 +1331,12 @@
}
},
"node_modules/@babel/plugin-transform-numeric-separator": {
- "version": "7.24.1",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.24.1.tgz",
- "integrity": "sha512-7GAsGlK4cNL2OExJH1DzmDeKnRv/LXq0eLUSvudrehVA5Rgg4bIrqEUW29FbKMBRT0ztSqisv7kjP+XIC4ZMNw==",
+ "version": "7.24.7",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.24.7.tgz",
+ "integrity": "sha512-e6q1TiVUzvH9KRvicuxdBTUj4AdKSRwzIyFFnfnezpCfP2/7Qmbb8qbU2j7GODbl4JMkblitCQjKYUaX/qkkwA==",
+ "license": "MIT",
"dependencies": {
- "@babel/helper-plugin-utils": "^7.24.0",
+ "@babel/helper-plugin-utils": "^7.24.7",
"@babel/plugin-syntax-numeric-separator": "^7.10.4"
},
"engines": {
@@ -1277,11 +1347,12 @@
}
},
"node_modules/@babel/plugin-transform-object-assign": {
- "version": "7.24.1",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-assign/-/plugin-transform-object-assign-7.24.1.tgz",
- "integrity": "sha512-I1kctor9iKtupb7jv7FyjApHCuKLBKCblVAeHVK9PB6FW7GI0ac6RtobC3MwwJy8CZ1JxuhQmnbrsqI5G8hAIg==",
+ "version": "7.24.7",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-assign/-/plugin-transform-object-assign-7.24.7.tgz",
+ "integrity": "sha512-DOzAi77P9jSyPijHS7Z8vH0wLRcZH6wWxuIZgLAiy8FWOkcKMJmnyHjy2JM94k6A0QxlA/hlLh+R9T3GEryjNQ==",
+ "license": "MIT",
"dependencies": {
- "@babel/helper-plugin-utils": "^7.24.0"
+ "@babel/helper-plugin-utils": "^7.24.7"
},
"engines": {
"node": ">=6.9.0"
@@ -1291,14 +1362,15 @@
}
},
"node_modules/@babel/plugin-transform-object-rest-spread": {
- "version": "7.24.1",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.24.1.tgz",
- "integrity": "sha512-XjD5f0YqOtebto4HGISLNfiNMTTs6tbkFf2TOqJlYKYmbo+mN9Dnpl4SRoofiziuOWMIyq3sZEUqLo3hLITFEA==",
+ "version": "7.24.7",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.24.7.tgz",
+ "integrity": "sha512-4QrHAr0aXQCEFni2q4DqKLD31n2DL+RxcwnNjDFkSG0eNQ/xCavnRkfCUjsyqGC2OviNJvZOF/mQqZBw7i2C5Q==",
+ "license": "MIT",
"dependencies": {
- "@babel/helper-compilation-targets": "^7.23.6",
- "@babel/helper-plugin-utils": "^7.24.0",
+ "@babel/helper-compilation-targets": "^7.24.7",
+ "@babel/helper-plugin-utils": "^7.24.7",
"@babel/plugin-syntax-object-rest-spread": "^7.8.3",
- "@babel/plugin-transform-parameters": "^7.24.1"
+ "@babel/plugin-transform-parameters": "^7.24.7"
},
"engines": {
"node": ">=6.9.0"
@@ -1308,12 +1380,13 @@
}
},
"node_modules/@babel/plugin-transform-object-super": {
- "version": "7.24.1",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.24.1.tgz",
- "integrity": "sha512-oKJqR3TeI5hSLRxudMjFQ9re9fBVUU0GICqM3J1mi8MqlhVr6hC/ZN4ttAyMuQR6EZZIY6h/exe5swqGNNIkWQ==",
+ "version": "7.24.7",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.24.7.tgz",
+ "integrity": "sha512-A/vVLwN6lBrMFmMDmPPz0jnE6ZGx7Jq7d6sT/Ev4H65RER6pZ+kczlf1DthF5N0qaPHBsI7UXiE8Zy66nmAovg==",
+ "license": "MIT",
"dependencies": {
- "@babel/helper-plugin-utils": "^7.24.0",
- "@babel/helper-replace-supers": "^7.24.1"
+ "@babel/helper-plugin-utils": "^7.24.7",
+ "@babel/helper-replace-supers": "^7.24.7"
},
"engines": {
"node": ">=6.9.0"
@@ -1323,11 +1396,12 @@
}
},
"node_modules/@babel/plugin-transform-optional-catch-binding": {
- "version": "7.24.1",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.24.1.tgz",
- "integrity": "sha512-oBTH7oURV4Y+3EUrf6cWn1OHio3qG/PVwO5J03iSJmBg6m2EhKjkAu/xuaXaYwWW9miYtvbWv4LNf0AmR43LUA==",
+ "version": "7.24.7",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.24.7.tgz",
+ "integrity": "sha512-uLEndKqP5BfBbC/5jTwPxLh9kqPWWgzN/f8w6UwAIirAEqiIVJWWY312X72Eub09g5KF9+Zn7+hT7sDxmhRuKA==",
+ "license": "MIT",
"dependencies": {
- "@babel/helper-plugin-utils": "^7.24.0",
+ "@babel/helper-plugin-utils": "^7.24.7",
"@babel/plugin-syntax-optional-catch-binding": "^7.8.3"
},
"engines": {
@@ -1338,12 +1412,13 @@
}
},
"node_modules/@babel/plugin-transform-optional-chaining": {
- "version": "7.24.1",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.24.1.tgz",
- "integrity": "sha512-n03wmDt+987qXwAgcBlnUUivrZBPZ8z1plL0YvgQalLm+ZE5BMhGm94jhxXtA1wzv1Cu2aaOv1BM9vbVttrzSg==",
+ "version": "7.24.8",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.24.8.tgz",
+ "integrity": "sha512-5cTOLSMs9eypEy8JUVvIKOu6NgvbJMnpG62VpIHrTmROdQ+L5mDAaI40g25k5vXti55JWNX5jCkq3HZxXBQANw==",
+ "license": "MIT",
"dependencies": {
- "@babel/helper-plugin-utils": "^7.24.0",
- "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5",
+ "@babel/helper-plugin-utils": "^7.24.8",
+ "@babel/helper-skip-transparent-expression-wrappers": "^7.24.7",
"@babel/plugin-syntax-optional-chaining": "^7.8.3"
},
"engines": {
@@ -1354,11 +1429,12 @@
}
},
"node_modules/@babel/plugin-transform-parameters": {
- "version": "7.24.1",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.24.1.tgz",
- "integrity": "sha512-8Jl6V24g+Uw5OGPeWNKrKqXPDw2YDjLc53ojwfMcKwlEoETKU9rU0mHUtcg9JntWI/QYzGAXNWEcVHZ+fR+XXg==",
+ "version": "7.24.7",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.24.7.tgz",
+ "integrity": "sha512-yGWW5Rr+sQOhK0Ot8hjDJuxU3XLRQGflvT4lhlSY0DFvdb3TwKaY26CJzHtYllU0vT9j58hc37ndFPsqT1SrzA==",
+ "license": "MIT",
"dependencies": {
- "@babel/helper-plugin-utils": "^7.24.0"
+ "@babel/helper-plugin-utils": "^7.24.7"
},
"engines": {
"node": ">=6.9.0"
@@ -1368,12 +1444,13 @@
}
},
"node_modules/@babel/plugin-transform-private-methods": {
- "version": "7.24.1",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.24.1.tgz",
- "integrity": "sha512-tGvisebwBO5em4PaYNqt4fkw56K2VALsAbAakY0FjTYqJp7gfdrgr7YX76Or8/cpik0W6+tj3rZ0uHU9Oil4tw==",
+ "version": "7.24.7",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.24.7.tgz",
+ "integrity": "sha512-COTCOkG2hn4JKGEKBADkA8WNb35TGkkRbI5iT845dB+NyqgO8Hn+ajPbSnIQznneJTa3d30scb6iz/DhH8GsJQ==",
+ "license": "MIT",
"dependencies": {
- "@babel/helper-create-class-features-plugin": "^7.24.1",
- "@babel/helper-plugin-utils": "^7.24.0"
+ "@babel/helper-create-class-features-plugin": "^7.24.7",
+ "@babel/helper-plugin-utils": "^7.24.7"
},
"engines": {
"node": ">=6.9.0"
@@ -1383,13 +1460,14 @@
}
},
"node_modules/@babel/plugin-transform-private-property-in-object": {
- "version": "7.24.1",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.24.1.tgz",
- "integrity": "sha512-pTHxDVa0BpUbvAgX3Gat+7cSciXqUcY9j2VZKTbSB6+VQGpNgNO9ailxTGHSXlqOnX1Hcx1Enme2+yv7VqP9bg==",
+ "version": "7.24.7",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.24.7.tgz",
+ "integrity": "sha512-9z76mxwnwFxMyxZWEgdgECQglF2Q7cFLm0kMf8pGwt+GSJsY0cONKj/UuO4bOH0w/uAel3ekS4ra5CEAyJRmDA==",
+ "license": "MIT",
"dependencies": {
- "@babel/helper-annotate-as-pure": "^7.22.5",
- "@babel/helper-create-class-features-plugin": "^7.24.1",
- "@babel/helper-plugin-utils": "^7.24.0",
+ "@babel/helper-annotate-as-pure": "^7.24.7",
+ "@babel/helper-create-class-features-plugin": "^7.24.7",
+ "@babel/helper-plugin-utils": "^7.24.7",
"@babel/plugin-syntax-private-property-in-object": "^7.14.5"
},
"engines": {
@@ -1400,11 +1478,12 @@
}
},
"node_modules/@babel/plugin-transform-property-literals": {
- "version": "7.24.1",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.24.1.tgz",
- "integrity": "sha512-LetvD7CrHmEx0G442gOomRr66d7q8HzzGGr4PMHGr+5YIm6++Yke+jxj246rpvsbyhJwCLxcTn6zW1P1BSenqA==",
+ "version": "7.24.7",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.24.7.tgz",
+ "integrity": "sha512-EMi4MLQSHfd2nrCqQEWxFdha2gBCqU4ZcCng4WBGZ5CJL4bBRW0ptdqqDdeirGZcpALazVVNJqRmsO8/+oNCBA==",
+ "license": "MIT",
"dependencies": {
- "@babel/helper-plugin-utils": "^7.24.0"
+ "@babel/helper-plugin-utils": "^7.24.7"
},
"engines": {
"node": ">=6.9.0"
@@ -1414,11 +1493,12 @@
}
},
"node_modules/@babel/plugin-transform-react-display-name": {
- "version": "7.24.1",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.24.1.tgz",
- "integrity": "sha512-mvoQg2f9p2qlpDQRBC7M3c3XTr0k7cp/0+kFKKO/7Gtu0LSw16eKB+Fabe2bDT/UpsyasTBBkAnbdsLrkD5XMw==",
+ "version": "7.24.7",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.24.7.tgz",
+ "integrity": "sha512-H/Snz9PFxKsS1JLI4dJLtnJgCJRoo0AUm3chP6NYr+9En1JMKloheEiLIhlp5MDVznWo+H3AAC1Mc8lmUEpsgg==",
+ "license": "MIT",
"dependencies": {
- "@babel/helper-plugin-utils": "^7.24.0"
+ "@babel/helper-plugin-utils": "^7.24.7"
},
"engines": {
"node": ">=6.9.0"
@@ -1428,15 +1508,16 @@
}
},
"node_modules/@babel/plugin-transform-react-jsx": {
- "version": "7.23.4",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.23.4.tgz",
- "integrity": "sha512-5xOpoPguCZCRbo/JeHlloSkTA8Bld1J/E1/kLfD1nsuiW1m8tduTA1ERCgIZokDflX/IBzKcqR3l7VlRgiIfHA==",
+ "version": "7.25.2",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.25.2.tgz",
+ "integrity": "sha512-KQsqEAVBpU82NM/B/N9j9WOdphom1SZH3R+2V7INrQUH+V9EBFwZsEJl8eBIVeQE62FxJCc70jzEZwqU7RcVqA==",
+ "license": "MIT",
"dependencies": {
- "@babel/helper-annotate-as-pure": "^7.22.5",
- "@babel/helper-module-imports": "^7.22.15",
- "@babel/helper-plugin-utils": "^7.22.5",
- "@babel/plugin-syntax-jsx": "^7.23.3",
- "@babel/types": "^7.23.4"
+ "@babel/helper-annotate-as-pure": "^7.24.7",
+ "@babel/helper-module-imports": "^7.24.7",
+ "@babel/helper-plugin-utils": "^7.24.8",
+ "@babel/plugin-syntax-jsx": "^7.24.7",
+ "@babel/types": "^7.25.2"
},
"engines": {
"node": ">=6.9.0"
@@ -1446,11 +1527,12 @@
}
},
"node_modules/@babel/plugin-transform-react-jsx-development": {
- "version": "7.22.5",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.22.5.tgz",
- "integrity": "sha512-bDhuzwWMuInwCYeDeMzyi7TaBgRQei6DqxhbyniL7/VG4RSS7HtSL2QbY4eESy1KJqlWt8g3xeEBGPuo+XqC8A==",
+ "version": "7.24.7",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.24.7.tgz",
+ "integrity": "sha512-QG9EnzoGn+Qar7rxuW+ZOsbWOt56FvvI93xInqsZDC5fsekx1AlIO4KIJ5M+D0p0SqSH156EpmZyXq630B8OlQ==",
+ "license": "MIT",
"dependencies": {
- "@babel/plugin-transform-react-jsx": "^7.22.5"
+ "@babel/plugin-transform-react-jsx": "^7.24.7"
},
"engines": {
"node": ">=6.9.0"
@@ -1460,12 +1542,13 @@
}
},
"node_modules/@babel/plugin-transform-react-pure-annotations": {
- "version": "7.24.1",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.24.1.tgz",
- "integrity": "sha512-+pWEAaDJvSm9aFvJNpLiM2+ktl2Sn2U5DdyiWdZBxmLc6+xGt88dvFqsHiAiDS+8WqUwbDfkKz9jRxK3M0k+kA==",
+ "version": "7.24.7",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.24.7.tgz",
+ "integrity": "sha512-PLgBVk3fzbmEjBJ/u8kFzOqS9tUeDjiaWud/rRym/yjCo/M9cASPlnrd2ZmmZpQT40fOOrvR8jh+n8jikrOhNA==",
+ "license": "MIT",
"dependencies": {
- "@babel/helper-annotate-as-pure": "^7.22.5",
- "@babel/helper-plugin-utils": "^7.24.0"
+ "@babel/helper-annotate-as-pure": "^7.24.7",
+ "@babel/helper-plugin-utils": "^7.24.7"
},
"engines": {
"node": ">=6.9.0"
@@ -1475,11 +1558,12 @@
}
},
"node_modules/@babel/plugin-transform-regenerator": {
- "version": "7.24.1",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.24.1.tgz",
- "integrity": "sha512-sJwZBCzIBE4t+5Q4IGLaaun5ExVMRY0lYwos/jNecjMrVCygCdph3IKv0tkP5Fc87e/1+bebAmEAGBfnRD+cnw==",
+ "version": "7.24.7",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.24.7.tgz",
+ "integrity": "sha512-lq3fvXPdimDrlg6LWBoqj+r/DEWgONuwjuOuQCSYgRroXDH/IdM1C0IZf59fL5cHLpjEH/O6opIRBbqv7ELnuA==",
+ "license": "MIT",
"dependencies": {
- "@babel/helper-plugin-utils": "^7.24.0",
+ "@babel/helper-plugin-utils": "^7.24.7",
"regenerator-transform": "^0.15.2"
},
"engines": {
@@ -1490,11 +1574,12 @@
}
},
"node_modules/@babel/plugin-transform-reserved-words": {
- "version": "7.24.1",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.24.1.tgz",
- "integrity": "sha512-JAclqStUfIwKN15HrsQADFgeZt+wexNQ0uLhuqvqAUFoqPMjEcFCYZBhq0LUdz6dZK/mD+rErhW71fbx8RYElg==",
+ "version": "7.24.7",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.24.7.tgz",
+ "integrity": "sha512-0DUq0pHcPKbjFZCfTss/pGkYMfy3vFWydkUBd9r0GHpIyfs2eCDENvqadMycRS9wZCXR41wucAfJHJmwA0UmoQ==",
+ "license": "MIT",
"dependencies": {
- "@babel/helper-plugin-utils": "^7.24.0"
+ "@babel/helper-plugin-utils": "^7.24.7"
},
"engines": {
"node": ">=6.9.0"
@@ -1504,11 +1589,12 @@
}
},
"node_modules/@babel/plugin-transform-shorthand-properties": {
- "version": "7.24.1",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.24.1.tgz",
- "integrity": "sha512-LyjVB1nsJ6gTTUKRjRWx9C1s9hE7dLfP/knKdrfeH9UPtAGjYGgxIbFfx7xyLIEWs7Xe1Gnf8EWiUqfjLhInZA==",
+ "version": "7.24.7",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.24.7.tgz",
+ "integrity": "sha512-KsDsevZMDsigzbA09+vacnLpmPH4aWjcZjXdyFKGzpplxhbeB4wYtury3vglQkg6KM/xEPKt73eCjPPf1PgXBA==",
+ "license": "MIT",
"dependencies": {
- "@babel/helper-plugin-utils": "^7.24.0"
+ "@babel/helper-plugin-utils": "^7.24.7"
},
"engines": {
"node": ">=6.9.0"
@@ -1518,12 +1604,13 @@
}
},
"node_modules/@babel/plugin-transform-spread": {
- "version": "7.24.1",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.24.1.tgz",
- "integrity": "sha512-KjmcIM+fxgY+KxPVbjelJC6hrH1CgtPmTvdXAfn3/a9CnWGSTY7nH4zm5+cjmWJybdcPSsD0++QssDsjcpe47g==",
+ "version": "7.24.7",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.24.7.tgz",
+ "integrity": "sha512-x96oO0I09dgMDxJaANcRyD4ellXFLLiWhuwDxKZX5g2rWP1bTPkBSwCYv96VDXVT1bD9aPj8tppr5ITIh8hBng==",
+ "license": "MIT",
"dependencies": {
- "@babel/helper-plugin-utils": "^7.24.0",
- "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5"
+ "@babel/helper-plugin-utils": "^7.24.7",
+ "@babel/helper-skip-transparent-expression-wrappers": "^7.24.7"
},
"engines": {
"node": ">=6.9.0"
@@ -1533,11 +1620,12 @@
}
},
"node_modules/@babel/plugin-transform-sticky-regex": {
- "version": "7.24.1",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.24.1.tgz",
- "integrity": "sha512-9v0f1bRXgPVcPrngOQvLXeGNNVLc8UjMVfebo9ka0WF3/7+aVUHmaJVT3sa0XCzEFioPfPHZiOcYG9qOsH63cw==",
+ "version": "7.24.7",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.24.7.tgz",
+ "integrity": "sha512-kHPSIJc9v24zEml5geKg9Mjx5ULpfncj0wRpYtxbvKyTtHCYDkVE3aHQ03FrpEo4gEe2vrJJS1Y9CJTaThA52g==",
+ "license": "MIT",
"dependencies": {
- "@babel/helper-plugin-utils": "^7.24.0"
+ "@babel/helper-plugin-utils": "^7.24.7"
},
"engines": {
"node": ">=6.9.0"
@@ -1547,11 +1635,12 @@
}
},
"node_modules/@babel/plugin-transform-template-literals": {
- "version": "7.24.1",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.24.1.tgz",
- "integrity": "sha512-WRkhROsNzriarqECASCNu/nojeXCDTE/F2HmRgOzi7NGvyfYGq1NEjKBK3ckLfRgGc6/lPAqP0vDOSw3YtG34g==",
+ "version": "7.24.7",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.24.7.tgz",
+ "integrity": "sha512-AfDTQmClklHCOLxtGoP7HkeMw56k1/bTQjwsfhL6pppo/M4TOBSq+jjBUBLmV/4oeFg4GWMavIl44ZeCtmmZTw==",
+ "license": "MIT",
"dependencies": {
- "@babel/helper-plugin-utils": "^7.24.0"
+ "@babel/helper-plugin-utils": "^7.24.7"
},
"engines": {
"node": ">=6.9.0"
@@ -1561,11 +1650,12 @@
}
},
"node_modules/@babel/plugin-transform-typeof-symbol": {
- "version": "7.24.1",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.24.1.tgz",
- "integrity": "sha512-CBfU4l/A+KruSUoW+vTQthwcAdwuqbpRNB8HQKlZABwHRhsdHZ9fezp4Sn18PeAlYxTNiLMlx4xUBV3AWfg1BA==",
+ "version": "7.24.8",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.24.8.tgz",
+ "integrity": "sha512-adNTUpDCVnmAE58VEqKlAA6ZBlNkMnWD0ZcW76lyNFN3MJniyGFZfNwERVk8Ap56MCnXztmDr19T4mPTztcuaw==",
+ "license": "MIT",
"dependencies": {
- "@babel/helper-plugin-utils": "^7.24.0"
+ "@babel/helper-plugin-utils": "^7.24.8"
},
"engines": {
"node": ">=6.9.0"
@@ -1575,11 +1665,12 @@
}
},
"node_modules/@babel/plugin-transform-unicode-escapes": {
- "version": "7.24.1",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.24.1.tgz",
- "integrity": "sha512-RlkVIcWT4TLI96zM660S877E7beKlQw7Ig+wqkKBiWfj0zH5Q4h50q6er4wzZKRNSYpfo6ILJ+hrJAGSX2qcNw==",
+ "version": "7.24.7",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.24.7.tgz",
+ "integrity": "sha512-U3ap1gm5+4edc2Q/P+9VrBNhGkfnf+8ZqppY71Bo/pzZmXhhLdqgaUl6cuB07O1+AQJtCLfaOmswiNbSQ9ivhw==",
+ "license": "MIT",
"dependencies": {
- "@babel/helper-plugin-utils": "^7.24.0"
+ "@babel/helper-plugin-utils": "^7.24.7"
},
"engines": {
"node": ">=6.9.0"
@@ -1589,12 +1680,13 @@
}
},
"node_modules/@babel/plugin-transform-unicode-property-regex": {
- "version": "7.24.1",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.24.1.tgz",
- "integrity": "sha512-Ss4VvlfYV5huWApFsF8/Sq0oXnGO+jB+rijFEFugTd3cwSObUSnUi88djgR5528Csl0uKlrI331kRqe56Ov2Ng==",
+ "version": "7.24.7",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.24.7.tgz",
+ "integrity": "sha512-uH2O4OV5M9FZYQrwc7NdVmMxQJOCCzFeYudlZSzUAHRFeOujQefa92E74TQDVskNHCzOXoigEuoyzHDhaEaK5w==",
+ "license": "MIT",
"dependencies": {
- "@babel/helper-create-regexp-features-plugin": "^7.22.15",
- "@babel/helper-plugin-utils": "^7.24.0"
+ "@babel/helper-create-regexp-features-plugin": "^7.24.7",
+ "@babel/helper-plugin-utils": "^7.24.7"
},
"engines": {
"node": ">=6.9.0"
@@ -1604,12 +1696,13 @@
}
},
"node_modules/@babel/plugin-transform-unicode-regex": {
- "version": "7.24.1",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.24.1.tgz",
- "integrity": "sha512-2A/94wgZgxfTsiLaQ2E36XAOdcZmGAaEEgVmxQWwZXWkGhvoHbaqXcKnU8zny4ycpu3vNqg0L/PcCiYtHtA13g==",
+ "version": "7.24.7",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.24.7.tgz",
+ "integrity": "sha512-hlQ96MBZSAXUq7ltkjtu3FJCCSMx/j629ns3hA3pXnBXjanNP0LHi+JpPeA81zaWgVK1VGH95Xuy7u0RyQ8kMg==",
+ "license": "MIT",
"dependencies": {
- "@babel/helper-create-regexp-features-plugin": "^7.22.15",
- "@babel/helper-plugin-utils": "^7.24.0"
+ "@babel/helper-create-regexp-features-plugin": "^7.24.7",
+ "@babel/helper-plugin-utils": "^7.24.7"
},
"engines": {
"node": ">=6.9.0"
@@ -1619,12 +1712,13 @@
}
},
"node_modules/@babel/plugin-transform-unicode-sets-regex": {
- "version": "7.24.1",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.24.1.tgz",
- "integrity": "sha512-fqj4WuzzS+ukpgerpAoOnMfQXwUHFxXUZUE84oL2Kao2N8uSlvcpnAidKASgsNgzZHBsHWvcm8s9FPWUhAb8fA==",
+ "version": "7.24.7",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.24.7.tgz",
+ "integrity": "sha512-2G8aAvF4wy1w/AGZkemprdGMRg5o6zPNhbHVImRz3lss55TYCBd6xStN19rt8XJHq20sqV0JbyWjOWwQRwV/wg==",
+ "license": "MIT",
"dependencies": {
- "@babel/helper-create-regexp-features-plugin": "^7.22.15",
- "@babel/helper-plugin-utils": "^7.24.0"
+ "@babel/helper-create-regexp-features-plugin": "^7.24.7",
+ "@babel/helper-plugin-utils": "^7.24.7"
},
"engines": {
"node": ">=6.9.0"
@@ -1644,25 +1738,28 @@
}
},
"node_modules/@babel/preset-env": {
- "version": "7.24.3",
- "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.24.3.tgz",
- "integrity": "sha512-fSk430k5c2ff8536JcPvPWK4tZDwehWLGlBp0wrsBUjZVdeQV6lePbwKWZaZfK2vnh/1kQX1PzAJWsnBmVgGJA==",
- "dependencies": {
- "@babel/compat-data": "^7.24.1",
- "@babel/helper-compilation-targets": "^7.23.6",
- "@babel/helper-plugin-utils": "^7.24.0",
- "@babel/helper-validator-option": "^7.23.5",
- "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.24.1",
- "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.24.1",
- "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.24.1",
+ "version": "7.25.3",
+ "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.25.3.tgz",
+ "integrity": "sha512-QsYW7UeAaXvLPX9tdVliMJE7MD7M6MLYVTovRTIwhoYQVFHR1rM4wO8wqAezYi3/BpSD+NzVCZ69R6smWiIi8g==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/compat-data": "^7.25.2",
+ "@babel/helper-compilation-targets": "^7.25.2",
+ "@babel/helper-plugin-utils": "^7.24.8",
+ "@babel/helper-validator-option": "^7.24.8",
+ "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.25.3",
+ "@babel/plugin-bugfix-safari-class-field-initializer-scope": "^7.25.0",
+ "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.25.0",
+ "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.24.7",
+ "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.25.0",
"@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2",
"@babel/plugin-syntax-async-generators": "^7.8.4",
"@babel/plugin-syntax-class-properties": "^7.12.13",
"@babel/plugin-syntax-class-static-block": "^7.14.5",
"@babel/plugin-syntax-dynamic-import": "^7.8.3",
"@babel/plugin-syntax-export-namespace-from": "^7.8.3",
- "@babel/plugin-syntax-import-assertions": "^7.24.1",
- "@babel/plugin-syntax-import-attributes": "^7.24.1",
+ "@babel/plugin-syntax-import-assertions": "^7.24.7",
+ "@babel/plugin-syntax-import-attributes": "^7.24.7",
"@babel/plugin-syntax-import-meta": "^7.10.4",
"@babel/plugin-syntax-json-strings": "^7.8.3",
"@babel/plugin-syntax-logical-assignment-operators": "^7.10.4",
@@ -1674,59 +1771,60 @@
"@babel/plugin-syntax-private-property-in-object": "^7.14.5",
"@babel/plugin-syntax-top-level-await": "^7.14.5",
"@babel/plugin-syntax-unicode-sets-regex": "^7.18.6",
- "@babel/plugin-transform-arrow-functions": "^7.24.1",
- "@babel/plugin-transform-async-generator-functions": "^7.24.3",
- "@babel/plugin-transform-async-to-generator": "^7.24.1",
- "@babel/plugin-transform-block-scoped-functions": "^7.24.1",
- "@babel/plugin-transform-block-scoping": "^7.24.1",
- "@babel/plugin-transform-class-properties": "^7.24.1",
- "@babel/plugin-transform-class-static-block": "^7.24.1",
- "@babel/plugin-transform-classes": "^7.24.1",
- "@babel/plugin-transform-computed-properties": "^7.24.1",
- "@babel/plugin-transform-destructuring": "^7.24.1",
- "@babel/plugin-transform-dotall-regex": "^7.24.1",
- "@babel/plugin-transform-duplicate-keys": "^7.24.1",
- "@babel/plugin-transform-dynamic-import": "^7.24.1",
- "@babel/plugin-transform-exponentiation-operator": "^7.24.1",
- "@babel/plugin-transform-export-namespace-from": "^7.24.1",
- "@babel/plugin-transform-for-of": "^7.24.1",
- "@babel/plugin-transform-function-name": "^7.24.1",
- "@babel/plugin-transform-json-strings": "^7.24.1",
- "@babel/plugin-transform-literals": "^7.24.1",
- "@babel/plugin-transform-logical-assignment-operators": "^7.24.1",
- "@babel/plugin-transform-member-expression-literals": "^7.24.1",
- "@babel/plugin-transform-modules-amd": "^7.24.1",
- "@babel/plugin-transform-modules-commonjs": "^7.24.1",
- "@babel/plugin-transform-modules-systemjs": "^7.24.1",
- "@babel/plugin-transform-modules-umd": "^7.24.1",
- "@babel/plugin-transform-named-capturing-groups-regex": "^7.22.5",
- "@babel/plugin-transform-new-target": "^7.24.1",
- "@babel/plugin-transform-nullish-coalescing-operator": "^7.24.1",
- "@babel/plugin-transform-numeric-separator": "^7.24.1",
- "@babel/plugin-transform-object-rest-spread": "^7.24.1",
- "@babel/plugin-transform-object-super": "^7.24.1",
- "@babel/plugin-transform-optional-catch-binding": "^7.24.1",
- "@babel/plugin-transform-optional-chaining": "^7.24.1",
- "@babel/plugin-transform-parameters": "^7.24.1",
- "@babel/plugin-transform-private-methods": "^7.24.1",
- "@babel/plugin-transform-private-property-in-object": "^7.24.1",
- "@babel/plugin-transform-property-literals": "^7.24.1",
- "@babel/plugin-transform-regenerator": "^7.24.1",
- "@babel/plugin-transform-reserved-words": "^7.24.1",
- "@babel/plugin-transform-shorthand-properties": "^7.24.1",
- "@babel/plugin-transform-spread": "^7.24.1",
- "@babel/plugin-transform-sticky-regex": "^7.24.1",
- "@babel/plugin-transform-template-literals": "^7.24.1",
- "@babel/plugin-transform-typeof-symbol": "^7.24.1",
- "@babel/plugin-transform-unicode-escapes": "^7.24.1",
- "@babel/plugin-transform-unicode-property-regex": "^7.24.1",
- "@babel/plugin-transform-unicode-regex": "^7.24.1",
- "@babel/plugin-transform-unicode-sets-regex": "^7.24.1",
+ "@babel/plugin-transform-arrow-functions": "^7.24.7",
+ "@babel/plugin-transform-async-generator-functions": "^7.25.0",
+ "@babel/plugin-transform-async-to-generator": "^7.24.7",
+ "@babel/plugin-transform-block-scoped-functions": "^7.24.7",
+ "@babel/plugin-transform-block-scoping": "^7.25.0",
+ "@babel/plugin-transform-class-properties": "^7.24.7",
+ "@babel/plugin-transform-class-static-block": "^7.24.7",
+ "@babel/plugin-transform-classes": "^7.25.0",
+ "@babel/plugin-transform-computed-properties": "^7.24.7",
+ "@babel/plugin-transform-destructuring": "^7.24.8",
+ "@babel/plugin-transform-dotall-regex": "^7.24.7",
+ "@babel/plugin-transform-duplicate-keys": "^7.24.7",
+ "@babel/plugin-transform-duplicate-named-capturing-groups-regex": "^7.25.0",
+ "@babel/plugin-transform-dynamic-import": "^7.24.7",
+ "@babel/plugin-transform-exponentiation-operator": "^7.24.7",
+ "@babel/plugin-transform-export-namespace-from": "^7.24.7",
+ "@babel/plugin-transform-for-of": "^7.24.7",
+ "@babel/plugin-transform-function-name": "^7.25.1",
+ "@babel/plugin-transform-json-strings": "^7.24.7",
+ "@babel/plugin-transform-literals": "^7.25.2",
+ "@babel/plugin-transform-logical-assignment-operators": "^7.24.7",
+ "@babel/plugin-transform-member-expression-literals": "^7.24.7",
+ "@babel/plugin-transform-modules-amd": "^7.24.7",
+ "@babel/plugin-transform-modules-commonjs": "^7.24.8",
+ "@babel/plugin-transform-modules-systemjs": "^7.25.0",
+ "@babel/plugin-transform-modules-umd": "^7.24.7",
+ "@babel/plugin-transform-named-capturing-groups-regex": "^7.24.7",
+ "@babel/plugin-transform-new-target": "^7.24.7",
+ "@babel/plugin-transform-nullish-coalescing-operator": "^7.24.7",
+ "@babel/plugin-transform-numeric-separator": "^7.24.7",
+ "@babel/plugin-transform-object-rest-spread": "^7.24.7",
+ "@babel/plugin-transform-object-super": "^7.24.7",
+ "@babel/plugin-transform-optional-catch-binding": "^7.24.7",
+ "@babel/plugin-transform-optional-chaining": "^7.24.8",
+ "@babel/plugin-transform-parameters": "^7.24.7",
+ "@babel/plugin-transform-private-methods": "^7.24.7",
+ "@babel/plugin-transform-private-property-in-object": "^7.24.7",
+ "@babel/plugin-transform-property-literals": "^7.24.7",
+ "@babel/plugin-transform-regenerator": "^7.24.7",
+ "@babel/plugin-transform-reserved-words": "^7.24.7",
+ "@babel/plugin-transform-shorthand-properties": "^7.24.7",
+ "@babel/plugin-transform-spread": "^7.24.7",
+ "@babel/plugin-transform-sticky-regex": "^7.24.7",
+ "@babel/plugin-transform-template-literals": "^7.24.7",
+ "@babel/plugin-transform-typeof-symbol": "^7.24.8",
+ "@babel/plugin-transform-unicode-escapes": "^7.24.7",
+ "@babel/plugin-transform-unicode-property-regex": "^7.24.7",
+ "@babel/plugin-transform-unicode-regex": "^7.24.7",
+ "@babel/plugin-transform-unicode-sets-regex": "^7.24.7",
"@babel/preset-modules": "0.1.6-no-external-plugins",
"babel-plugin-polyfill-corejs2": "^0.4.10",
"babel-plugin-polyfill-corejs3": "^0.10.4",
"babel-plugin-polyfill-regenerator": "^0.6.1",
- "core-js-compat": "^3.31.0",
+ "core-js-compat": "^3.37.1",
"semver": "^6.3.1"
},
"engines": {
@@ -1750,16 +1848,17 @@
}
},
"node_modules/@babel/preset-react": {
- "version": "7.18.6",
- "resolved": "https://registry.npmjs.org/@babel/preset-react/-/preset-react-7.18.6.tgz",
- "integrity": "sha512-zXr6atUmyYdiWRVLOZahakYmOBHtWc2WGCkP8PYTgZi0iJXDY2CN180TdrIW4OGOAdLc7TifzDIvtx6izaRIzg==",
+ "version": "7.24.7",
+ "resolved": "https://registry.npmjs.org/@babel/preset-react/-/preset-react-7.24.7.tgz",
+ "integrity": "sha512-AAH4lEkpmzFWrGVlHaxJB7RLH21uPQ9+He+eFLWHmF9IuFQVugz8eAsamaW0DXRrTfco5zj1wWtpdcXJUOfsag==",
+ "license": "MIT",
"dependencies": {
- "@babel/helper-plugin-utils": "^7.18.6",
- "@babel/helper-validator-option": "^7.18.6",
- "@babel/plugin-transform-react-display-name": "^7.18.6",
- "@babel/plugin-transform-react-jsx": "^7.18.6",
- "@babel/plugin-transform-react-jsx-development": "^7.18.6",
- "@babel/plugin-transform-react-pure-annotations": "^7.18.6"
+ "@babel/helper-plugin-utils": "^7.24.7",
+ "@babel/helper-validator-option": "^7.24.7",
+ "@babel/plugin-transform-react-display-name": "^7.24.7",
+ "@babel/plugin-transform-react-jsx": "^7.24.7",
+ "@babel/plugin-transform-react-jsx-development": "^7.24.7",
+ "@babel/plugin-transform-react-pure-annotations": "^7.24.7"
},
"engines": {
"node": ">=6.9.0"
@@ -1790,31 +1889,30 @@
"integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw=="
},
"node_modules/@babel/template": {
- "version": "7.24.0",
- "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.24.0.tgz",
- "integrity": "sha512-Bkf2q8lMB0AFpX0NFEqSbx1OkTHf0f+0j82mkw+ZpzBnkk7e9Ql0891vlfgi+kHwOk8tQjiQHpqh4LaSa0fKEA==",
+ "version": "7.25.0",
+ "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.0.tgz",
+ "integrity": "sha512-aOOgh1/5XzKvg1jvVz7AVrx2piJ2XBi227DHmbY6y+bM9H2FlN+IfecYu4Xl0cNiiVejlsCri89LUsbj8vJD9Q==",
+ "license": "MIT",
"dependencies": {
- "@babel/code-frame": "^7.23.5",
- "@babel/parser": "^7.24.0",
- "@babel/types": "^7.24.0"
+ "@babel/code-frame": "^7.24.7",
+ "@babel/parser": "^7.25.0",
+ "@babel/types": "^7.25.0"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/traverse": {
- "version": "7.24.1",
- "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.24.1.tgz",
- "integrity": "sha512-xuU6o9m68KeqZbQuDt2TcKSxUw/mrsvavlEqQ1leZ/B+C9tk6E4sRWy97WaXgvq5E+nU3cXMxv3WKOCanVMCmQ==",
- "dependencies": {
- "@babel/code-frame": "^7.24.1",
- "@babel/generator": "^7.24.1",
- "@babel/helper-environment-visitor": "^7.22.20",
- "@babel/helper-function-name": "^7.23.0",
- "@babel/helper-hoist-variables": "^7.22.5",
- "@babel/helper-split-export-declaration": "^7.22.6",
- "@babel/parser": "^7.24.1",
- "@babel/types": "^7.24.0",
+ "version": "7.25.3",
+ "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.25.3.tgz",
+ "integrity": "sha512-HefgyP1x754oGCsKmV5reSmtV7IXj/kpaE1XYY+D9G5PvKKoFfSbiS4M77MdjuwlZKDIKFCffq9rPU+H/s3ZdQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.24.7",
+ "@babel/generator": "^7.25.0",
+ "@babel/parser": "^7.25.3",
+ "@babel/template": "^7.25.0",
+ "@babel/types": "^7.25.2",
"debug": "^4.3.1",
"globals": "^11.1.0"
},
@@ -1823,12 +1921,13 @@
}
},
"node_modules/@babel/types": {
- "version": "7.24.0",
- "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.0.tgz",
- "integrity": "sha512-+j7a5c253RfKh8iABBhywc8NSfP5LURe7Uh4qpsh6jc+aLJguvmIUBdjSdEMQv2bENrCR5MfRdjGo7vzS/ob7w==",
+ "version": "7.25.2",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.25.2.tgz",
+ "integrity": "sha512-YTnYtra7W9e6/oAZEHj0bJehPRUlLH9/fbpT5LfB0NhQXyALCRkRs3zH9v07IYhkgpqX6Z78FnuccZr/l4Fs4Q==",
+ "license": "MIT",
"dependencies": {
- "@babel/helper-string-parser": "^7.23.4",
- "@babel/helper-validator-identifier": "^7.22.20",
+ "@babel/helper-string-parser": "^7.24.8",
+ "@babel/helper-validator-identifier": "^7.24.7",
"to-fast-properties": "^2.0.0"
},
"engines": {
@@ -1841,6 +1940,18 @@
"integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==",
"dev": true
},
+ "node_modules/@choojs/findup": {
+ "version": "0.2.1",
+ "resolved": "https://registry.npmjs.org/@choojs/findup/-/findup-0.2.1.tgz",
+ "integrity": "sha512-YstAqNb0MCN8PjdLCDfRsBcGVRN41f3vgLvaI0IrIcBp4AqILRSS0DeWNGkicC+f/zRIPJLc+9RURVSepwvfBw==",
+ "license": "MIT",
+ "dependencies": {
+ "commander": "^2.15.1"
+ },
+ "bin": {
+ "findup": "bin/findup.js"
+ }
+ },
"node_modules/@cnakazawa/watch": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@cnakazawa/watch/-/watch-1.0.4.tgz",
@@ -1973,16 +2084,6 @@
"resolved": "https://registry.npmjs.org/jquery/-/jquery-3.7.1.tgz",
"integrity": "sha512-m4avr8yL8kmFN8psrbFFFmB/If14iN5o9nw/NgnnM+kybDJpRsAynV2BsfpTYrTRysYUdADVD7CkUUizgkpLfg=="
},
- "node_modules/@edx/edx-bootstrap/node_modules/popper.js": {
- "version": "1.16.1",
- "resolved": "https://registry.npmjs.org/popper.js/-/popper.js-1.16.1.tgz",
- "integrity": "sha512-Wb4p1J4zyFTbM+u6WuO4XstYx4Ky9Cewe4DWrel7B0w6VVICvPwdOpotjzcf6eD8TsckVnIMNONQyPIUFOUbCQ==",
- "deprecated": "You can find the new Popper v2 at @popperjs/core, this package is dedicated to the legacy v1",
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/popperjs"
- }
- },
"node_modules/@edx/edx-proctoring": {
"version": "4.18.1",
"resolved": "https://registry.npmjs.org/@edx/edx-proctoring/-/edx-proctoring-4.18.1.tgz",
@@ -2093,17 +2194,6 @@
"email-validator": "^2.0.4"
}
},
- "node_modules/@edx/frontend-component-cookie-policy-banner/node_modules/popper.js": {
- "version": "1.16.1",
- "resolved": "https://registry.npmjs.org/popper.js/-/popper.js-1.16.1.tgz",
- "integrity": "sha512-Wb4p1J4zyFTbM+u6WuO4XstYx4Ky9Cewe4DWrel7B0w6VVICvPwdOpotjzcf6eD8TsckVnIMNONQyPIUFOUbCQ==",
- "deprecated": "You can find the new Popper v2 at @popperjs/core, this package is dedicated to the legacy v1",
- "peer": true,
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/popperjs"
- }
- },
"node_modules/@edx/frontend-component-cookie-policy-banner/node_modules/prop-types": {
"version": "15.8.1",
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
@@ -4133,24 +4223,6 @@
"resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.3.3.tgz",
"integrity": "sha512-LKVP3cgXBT9RYj+t+9FDKwS5tdI+rPBXaNSkma7hvqy35lc7mAokC2zsqWJH0LaqIt3B962nuYI77hsJoT1gow=="
},
- "node_modules/@types/eslint": {
- "version": "8.56.6",
- "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.56.6.tgz",
- "integrity": "sha512-ymwc+qb1XkjT/gfoQwxIeHZ6ixH23A+tCT2ADSA/DPVKzAjwYkTXBMCQ/f6fe4wEa85Lhp26VPeUxI7wMhAi7A==",
- "dependencies": {
- "@types/estree": "*",
- "@types/json-schema": "*"
- }
- },
- "node_modules/@types/eslint-scope": {
- "version": "3.7.7",
- "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz",
- "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==",
- "dependencies": {
- "@types/eslint": "*",
- "@types/estree": "*"
- }
- },
"node_modules/@types/estree": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz",
@@ -4538,10 +4610,11 @@
"node": ">=0.4.0"
}
},
- "node_modules/acorn-import-assertions": {
- "version": "1.9.0",
- "resolved": "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.9.0.tgz",
- "integrity": "sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA==",
+ "node_modules/acorn-import-attributes": {
+ "version": "1.9.5",
+ "resolved": "https://registry.npmjs.org/acorn-import-attributes/-/acorn-import-attributes-1.9.5.tgz",
+ "integrity": "sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==",
+ "license": "MIT",
"peerDependencies": {
"acorn": "^8"
}
@@ -4564,15 +4637,6 @@
"node": ">=0.4.0"
}
},
- "node_modules/adm-zip": {
- "version": "0.4.16",
- "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.4.16.tgz",
- "integrity": "sha512-TFi4HBKSGfIKsK5YCkKaaFG2m4PEDyViZmEwof3MTIgzimHLto6muaHVpbrljdIvIrFZzEq/p4nafOeLcYegrg==",
- "dev": true,
- "engines": {
- "node": ">=0.3.0"
- }
- },
"node_modules/after": {
"version": "0.8.2",
"resolved": "https://registry.npmjs.org/after/-/after-0.8.2.tgz",
@@ -5319,17 +5383,6 @@
"node": ">=4"
}
},
- "node_modules/axios": {
- "version": "0.28.1",
- "resolved": "https://registry.npmjs.org/axios/-/axios-0.28.1.tgz",
- "integrity": "sha512-iUcGA5a7p0mVb4Gm/sy+FSECNkPFT4y7wt6OM/CDpO/OnNCvSs3PoMG8ibrC9jRoGYU0gUK5pXVC4NPXq6lHRQ==",
- "license": "MIT",
- "dependencies": {
- "follow-redirects": "^1.15.0",
- "form-data": "^4.0.0",
- "proxy-from-env": "^1.1.0"
- }
- },
"node_modules/axobject-query": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-3.2.1.tgz",
@@ -5462,16 +5515,17 @@
"integrity": "sha512-m2CvfDW4+1qfDdsrtf4dwOslQC3yhbgyBFptncp4wvtdrDHqueW7slsYv4gArie056phvQFhT2nRcGS4bnm6mA=="
},
"node_modules/babel-jest": {
- "version": "26.0.0",
- "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-26.0.0.tgz",
- "integrity": "sha512-2AtcYOP4xhFn6TkvGmbEArNBcYLm/cTOIXB1a5j2juPOIC2U0nHEouMqYzgnPXgWC+CBK5RmYoGnwRt6eV4E8A==",
+ "version": "26.6.3",
+ "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-26.6.3.tgz",
+ "integrity": "sha512-pl4Q+GAVOHwvjrck6jKjvmGhnO3jHX/xuB9d27f+EJZ/6k+6nMuPjorrYp7s++bKKdANwzElBWnLWaObvTnaZA==",
"dev": true,
+ "license": "MIT",
"dependencies": {
- "@jest/transform": "^26.0.0",
- "@jest/types": "^26.0.0",
+ "@jest/transform": "^26.6.2",
+ "@jest/types": "^26.6.2",
"@types/babel__core": "^7.1.7",
"babel-plugin-istanbul": "^6.0.0",
- "babel-preset-jest": "^26.0.0",
+ "babel-preset-jest": "^26.6.2",
"chalk": "^4.0.0",
"graceful-fs": "^4.2.4",
"slash": "^3.0.0"
@@ -5488,6 +5542,7 @@
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"color-convert": "^2.0.1"
},
@@ -5503,6 +5558,7 @@
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"ansi-styles": "^4.1.0",
"supports-color": "^7.1.0"
@@ -5519,6 +5575,7 @@
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"color-name": "~1.1.4"
},
@@ -5530,13 +5587,15 @@
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
- "dev": true
+ "dev": true,
+ "license": "MIT"
},
"node_modules/babel-jest/node_modules/has-flag": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
"dev": true,
+ "license": "MIT",
"engines": {
"node": ">=8"
}
@@ -5546,6 +5605,7 @@
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"has-flag": "^4.0.0"
},
@@ -5979,9 +6039,10 @@
}
},
"node_modules/backbone": {
- "version": "1.4.1",
- "resolved": "https://registry.npmjs.org/backbone/-/backbone-1.4.1.tgz",
- "integrity": "sha512-ADy1ztN074YkWbHi8ojJVFe3vAanO/lrzMGZWUClIP7oDD/Pjy2vrASraUP+2EVCfIiTtCW4FChVow01XneivA==",
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/backbone/-/backbone-1.6.0.tgz",
+ "integrity": "sha512-13PUjmsgw/49EowNcQvfG4gmczz1ximTMhUktj0Jfrjth0MVaTxehpU+qYYX4MxnuIuhmvBLC6/ayxuAGnOhbA==",
+ "license": "MIT",
"dependencies": {
"underscore": ">=1.8.3"
}
@@ -6222,9 +6283,9 @@
"integrity": "sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow=="
},
"node_modules/browserslist": {
- "version": "4.23.0",
- "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.0.tgz",
- "integrity": "sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ==",
+ "version": "4.23.3",
+ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.3.tgz",
+ "integrity": "sha512-btwCFJVjI4YWDNfau8RhZ+B1Q/VLoUITrm3RlP6y1tYGWIOa+InuYiRGXUBXo8nA1qKmHMyLB/iVQg5TT4eFoA==",
"funding": [
{
"type": "opencollective",
@@ -6239,11 +6300,12 @@
"url": "https://github.com/sponsors/ai"
}
],
+ "license": "MIT",
"dependencies": {
- "caniuse-lite": "^1.0.30001587",
- "electron-to-chromium": "^1.4.668",
- "node-releases": "^2.0.14",
- "update-browserslist-db": "^1.0.13"
+ "caniuse-lite": "^1.0.30001646",
+ "electron-to-chromium": "^1.5.4",
+ "node-releases": "^2.0.18",
+ "update-browserslist-db": "^1.1.0"
},
"bin": {
"browserslist": "cli.js"
@@ -6522,9 +6584,9 @@
"integrity": "sha512-BS+RAD1DggiDlE2KaBUWKsMDuVmmh3hCM5LI0OW25mGlPttGLeOjDUa1DmZvJVFCXvtshY4BTyFgv31eFTLg8g=="
},
"node_modules/caniuse-lite": {
- "version": "1.0.30001600",
- "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001600.tgz",
- "integrity": "sha512-+2S9/2JFhYmYaDpZvo0lKkfvuKIglrx68MwOBqMGHhQsNkLjB5xtc/TGoEPs+MxjSyN/72qer2g97nzR641mOQ==",
+ "version": "1.0.30001651",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001651.tgz",
+ "integrity": "sha512-9Cf+Xv1jJNe1xPZLGuUXLNkE1BoDkqRqYyFJ9TDYSqhduqA4hu4oR9HluGoWYQC/aj8WHjsGVV+bwkh0+tegRg==",
"funding": [
{
"type": "opencollective",
@@ -6538,7 +6600,8 @@
"type": "github",
"url": "https://github.com/sponsors/ai"
}
- ]
+ ],
+ "license": "CC-BY-4.0"
},
"node_modules/capture-exit": {
"version": "2.0.0",
@@ -7311,7 +7374,8 @@
"node_modules/convert-source-map": {
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz",
- "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A=="
+ "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==",
+ "dev": true
},
"node_modules/cookie": {
"version": "0.3.1",
@@ -7347,11 +7411,12 @@
"hasInstallScript": true
},
"node_modules/core-js-compat": {
- "version": "3.36.1",
- "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.36.1.tgz",
- "integrity": "sha512-Dk997v9ZCt3X/npqzyGdTlq6t7lDBhZwGvV94PKzDArjp7BTRm7WlDAXYd/OWdeFHO8OChQYRJNJvUCqCbrtKA==",
+ "version": "3.38.0",
+ "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.38.0.tgz",
+ "integrity": "sha512-75LAicdLa4OJVwFxFbQR3NdnZjNgX6ILpVcVzcC4T2smerB5lELMrJQQQoWV6TiuC/vlaFqgU2tKQx9w5s0e0A==",
+ "license": "MIT",
"dependencies": {
- "browserslist": "^4.23.0"
+ "browserslist": "^4.23.3"
},
"funding": {
"type": "opencollective",
@@ -8153,15 +8218,6 @@
"urijs": "1.19.11"
}
},
- "node_modules/edx-ui-toolkit/node_modules/backbone": {
- "version": "1.6.0",
- "resolved": "https://registry.npmjs.org/backbone/-/backbone-1.6.0.tgz",
- "integrity": "sha512-13PUjmsgw/49EowNcQvfG4gmczz1ximTMhUktj0Jfrjth0MVaTxehpU+qYYX4MxnuIuhmvBLC6/ayxuAGnOhbA==",
- "license": "MIT",
- "dependencies": {
- "underscore": ">=1.8.3"
- }
- },
"node_modules/edx-ui-toolkit/node_modules/formatio": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/formatio/-/formatio-1.1.1.tgz",
@@ -8227,9 +8283,10 @@
"dev": true
},
"node_modules/electron-to-chromium": {
- "version": "1.4.717",
- "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.717.tgz",
- "integrity": "sha512-6Fmg8QkkumNOwuZ/5mIbMU9WI3H2fmn5ajcVya64I5Yr5CcNmO7vcLt0Y7c96DCiMO5/9G+4sI2r6eEvdg1F7A=="
+ "version": "1.5.6",
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.6.tgz",
+ "integrity": "sha512-jwXWsM5RPf6j9dPYzaorcBSUg6AiqocPEyMpkchkvntaH9HGfOOMZwxMJjDY/XEs3T5dM7uyH1VhRMkqUU9qVw==",
+ "license": "ISC"
},
"node_modules/email-prop-type": {
"version": "1.1.7",
@@ -8884,6 +8941,7 @@
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz",
"integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==",
+ "license": "MIT",
"engines": {
"node": ">=6"
}
@@ -10288,37 +10346,6 @@
"node": ">=8"
}
},
- "node_modules/findup": {
- "version": "0.1.5",
- "resolved": "https://registry.npmjs.org/findup/-/findup-0.1.5.tgz",
- "integrity": "sha512-Udxo3C9A6alt2GZ2MNsgnIvX7De0V3VGxeP/x98NSVgSlizcDHdmJza61LI7zJy4OEtSiJyE72s0/+tBl5/ZxA==",
- "dependencies": {
- "colors": "~0.6.0-1",
- "commander": "~2.1.0"
- },
- "bin": {
- "findup": "bin/findup.js"
- },
- "engines": {
- "node": ">=0.6"
- }
- },
- "node_modules/findup/node_modules/colors": {
- "version": "0.6.2",
- "resolved": "https://registry.npmjs.org/colors/-/colors-0.6.2.tgz",
- "integrity": "sha512-OsSVtHK8Ir8r3+Fxw/b4jS1ZLPXkV6ZxDRJQzeD7qo0SqMXWrHDM71DgYzPMHY8SFJ0Ao+nNU2p1MmwdzKqPrw==",
- "engines": {
- "node": ">=0.1.90"
- }
- },
- "node_modules/findup/node_modules/commander": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/commander/-/commander-2.1.0.tgz",
- "integrity": "sha512-J2wnb6TKniXNOtoHS8TSrG9IOQluPrsmyAJ8oCUJOBmv+uLBCyPYAZkD2jFvw2DCzIXNnISIM01NIvr35TkBMQ==",
- "engines": {
- "node": ">= 0.6.x"
- }
- },
"node_modules/flat": {
"version": "5.0.2",
"resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz",
@@ -10363,6 +10390,7 @@
"version": "1.15.6",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz",
"integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==",
+ "dev": true,
"funding": [
{
"type": "individual",
@@ -10450,19 +10478,6 @@
"node": "*"
}
},
- "node_modules/form-data": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
- "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
- "dependencies": {
- "asynckit": "^0.4.0",
- "combined-stream": "^1.0.8",
- "mime-types": "^2.1.12"
- },
- "engines": {
- "node": ">= 6"
- }
- },
"node_modules/formatio": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/formatio/-/formatio-1.2.0.tgz",
@@ -11485,6 +11500,13 @@
"node": ">= 4"
}
},
+ "node_modules/immediate": {
+ "version": "3.0.6",
+ "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz",
+ "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/immutable": {
"version": "4.3.5",
"resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.5.tgz",
@@ -11806,12 +11828,13 @@
"integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA=="
},
"node_modules/irregular-plurals": {
- "version": "1.4.0",
- "resolved": "https://registry.npmjs.org/irregular-plurals/-/irregular-plurals-1.4.0.tgz",
- "integrity": "sha512-kniTIJmaZYiwa17eTtWIfm0K342seyugl6vuC8DiiyiRAJWAVlLkqGCI0Im0neo0TkXw+pRcKaBPRdcKHnQJ6Q==",
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/irregular-plurals/-/irregular-plurals-2.0.0.tgz",
+ "integrity": "sha512-Y75zBYLkh0lJ9qxeHlMjQ7bSbyiSqNW/UOPWDmzC7cXskL1hekSITh1Oc6JV0XCWWZ9DE8VYSB71xocLk3gmGw==",
"dev": true,
+ "license": "MIT",
"engines": {
- "node": ">=0.10.0"
+ "node": ">=6"
}
},
"node_modules/is-absolute-url": {
@@ -12811,14 +12834,15 @@
"license": "MIT"
},
"node_modules/jest": {
- "version": "26.0.0",
- "resolved": "https://registry.npmjs.org/jest/-/jest-26.0.0.tgz",
- "integrity": "sha512-OtoG+cpcP+UXx+pQ7rzoQ11Pfb5+OUkrsNn5YPc0GU2HeBktgTANonUZEgT6cCgUHX7jUiuDIusDNTL4iNcWGQ==",
+ "version": "26.6.3",
+ "resolved": "https://registry.npmjs.org/jest/-/jest-26.6.3.tgz",
+ "integrity": "sha512-lGS5PXGAzR4RF7V5+XObhqz2KZIDUA1yD0DG6pBVmy10eh0ZIXQImRuzocsI/N2XZ1GrLFwTS27In2i2jlpq1Q==",
"dev": true,
+ "license": "MIT",
"dependencies": {
- "@jest/core": "^26.0.0",
+ "@jest/core": "^26.6.3",
"import-local": "^3.0.2",
- "jest-cli": "^26.0.0"
+ "jest-cli": "^26.6.3"
},
"bin": {
"jest": "bin/jest.js"
@@ -12990,28 +13014,6 @@
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
- "node_modules/jest-config/node_modules/babel-jest": {
- "version": "26.6.3",
- "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-26.6.3.tgz",
- "integrity": "sha512-pl4Q+GAVOHwvjrck6jKjvmGhnO3jHX/xuB9d27f+EJZ/6k+6nMuPjorrYp7s++bKKdANwzElBWnLWaObvTnaZA==",
- "dev": true,
- "dependencies": {
- "@jest/transform": "^26.6.2",
- "@jest/types": "^26.6.2",
- "@types/babel__core": "^7.1.7",
- "babel-plugin-istanbul": "^6.0.0",
- "babel-preset-jest": "^26.6.2",
- "chalk": "^4.0.0",
- "graceful-fs": "^4.2.4",
- "slash": "^3.0.0"
- },
- "engines": {
- "node": ">= 10.14.2"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0"
- }
- },
"node_modules/jest-config/node_modules/chalk": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
@@ -13774,14 +13776,15 @@
}
},
"node_modules/jest-enzyme": {
- "version": "6.0.2",
- "resolved": "https://registry.npmjs.org/jest-enzyme/-/jest-enzyme-6.0.2.tgz",
- "integrity": "sha512-lf6tjFA5Lthcazqx1acwo/rTjIKIsOp2jbZCZSi1WmRX0JHOJXa5NfioaaJ4wJeIWoIShU/9Go0OjKCXiAzCAg==",
+ "version": "6.1.2",
+ "resolved": "https://registry.npmjs.org/jest-enzyme/-/jest-enzyme-6.1.2.tgz",
+ "integrity": "sha512-+ds7r2ru3QkNJxelQ2tnC6d33pjUSsZHPD3v4TlnHlNMuGX3UKdxm5C46yZBvJICYBvIF+RFKBhLMM4evNM95Q==",
"dev": true,
+ "license": "MIT",
"dependencies": {
- "enzyme-matchers": "^6.0.2",
+ "enzyme-matchers": "^6.1.2",
"enzyme-to-json": "^3.3.0",
- "jest-environment-enzyme": "^6.0.2"
+ "jest-environment-enzyme": "^6.1.2"
},
"peerDependencies": {
"enzyme": "3.x",
@@ -15305,22 +15308,70 @@
"resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz",
"integrity": "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==",
"dev": true,
- "peer": true,
+ "peer": true,
+ "dependencies": {
+ "array-includes": "^3.1.6",
+ "array.prototype.flat": "^1.3.1",
+ "object.assign": "^4.1.4",
+ "object.values": "^1.1.6"
+ },
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/jszip": {
+ "version": "3.10.1",
+ "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz",
+ "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==",
+ "dev": true,
+ "license": "(MIT OR GPL-3.0-or-later)",
+ "dependencies": {
+ "lie": "~3.3.0",
+ "pako": "~1.0.2",
+ "readable-stream": "~2.3.6",
+ "setimmediate": "^1.0.5"
+ }
+ },
+ "node_modules/jszip/node_modules/isarray": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
+ "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/jszip/node_modules/readable-stream": {
+ "version": "2.3.8",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
+ "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "core-util-is": "~1.0.0",
+ "inherits": "~2.0.3",
+ "isarray": "~1.0.0",
+ "process-nextick-args": "~2.0.0",
+ "safe-buffer": "~5.1.1",
+ "string_decoder": "~1.1.1",
+ "util-deprecate": "~1.0.1"
+ }
+ },
+ "node_modules/jszip/node_modules/safe-buffer": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
+ "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/jszip/node_modules/string_decoder": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
+ "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
+ "dev": true,
+ "license": "MIT",
"dependencies": {
- "array-includes": "^3.1.6",
- "array.prototype.flat": "^1.3.1",
- "object.assign": "^4.1.4",
- "object.values": "^1.1.6"
- },
- "engines": {
- "node": ">=4.0"
+ "safe-buffer": "~5.1.0"
}
},
- "node_modules/jwt-decode": {
- "version": "3.1.2",
- "resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-3.1.2.tgz",
- "integrity": "sha512-UfpWE/VZn0iP50d8cz9NrZLM9lSWhcJ+0Gt/nm4by88UL+J1SiKN8/5dkjMmbEzwL2CAe+67GsegCbIKtbp75A=="
- },
"node_modules/karma": {
"version": "0.13.22",
"resolved": "https://registry.npmjs.org/karma/-/karma-0.13.22.tgz",
@@ -15625,10 +15676,11 @@
}
},
"node_modules/karma-junit-reporter": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/karma-junit-reporter/-/karma-junit-reporter-1.1.0.tgz",
- "integrity": "sha512-Iucb7SsKqYxn7azHltDHSI2wNzZ7MEzCQjZPNStEAlxyJMkX5OBWd0D6Sesy/mMKh9FltLDqe23hsZFyNvIKxQ==",
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/karma-junit-reporter/-/karma-junit-reporter-1.2.0.tgz",
+ "integrity": "sha512-FeuLOKlXNtJhIQK3oQASbO5QOib762CEHV8+L9wwTQpiZJgp7xKg3sNno66rL5bQPV2soG6fJdAFWqqnMJuh2w==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"path-is-absolute": "^1.0.0",
"xmlbuilder": "8.2.2"
@@ -15661,12 +15713,13 @@
}
},
"node_modules/karma-sourcemap-loader": {
- "version": "0.3.7",
- "resolved": "https://registry.npmjs.org/karma-sourcemap-loader/-/karma-sourcemap-loader-0.3.7.tgz",
- "integrity": "sha512-zu99gTgKf6KhLg14c+e+SHb297A8MX9TRxjW+USEmk/xq2ULnyvkfZuOZOLHU6522QfJai1SZznP/brNwivtDg==",
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/karma-sourcemap-loader/-/karma-sourcemap-loader-0.4.0.tgz",
+ "integrity": "sha512-xCRL3/pmhAYF3I6qOrcn0uhbQevitc2DERMPH82FMnG+4WReoGcGFZb1pURf2a5apyrOHRdvD+O6K7NljqKHyA==",
"dev": true,
+ "license": "MIT",
"dependencies": {
- "graceful-fs": "^4.1.2"
+ "graceful-fs": "^4.2.10"
}
},
"node_modules/karma-spec-reporter": {
@@ -15860,6 +15913,16 @@
"node": ">= 0.8.0"
}
},
+ "node_modules/lie": {
+ "version": "3.3.0",
+ "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz",
+ "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "immediate": "~3.0.5"
+ }
+ },
"node_modules/lines-and-columns": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
@@ -16095,70 +16158,16 @@
"integrity": "sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ=="
},
"node_modules/log-symbols": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-1.0.2.tgz",
- "integrity": "sha512-mmPrW0Fh2fxOzdBbFv4g1m6pR72haFLPJ2G5SJEELf1y+iaQrDG6cWCPjy54RHYbZAt7X+ls690Kw62AdWXBzQ==",
- "dev": true,
- "dependencies": {
- "chalk": "^1.0.0"
- },
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/log-symbols/node_modules/ansi-regex": {
- "version": "2.1.1",
- "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz",
- "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==",
- "dev": true,
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/log-symbols/node_modules/ansi-styles": {
- "version": "2.2.1",
- "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz",
- "integrity": "sha512-kmCevFghRiWM7HB5zTPULl4r9bVFSWjz62MhqizDGUrq2NWuNMQyuv4tHHoKJHs69M/MF64lEcHdYIocrdWQYA==",
- "dev": true,
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/log-symbols/node_modules/chalk": {
- "version": "1.1.3",
- "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz",
- "integrity": "sha512-U3lRVLMSlsCfjqYPbLyVv11M9CPW4I728d6TCKMAOJueEeB9/8o+eSsMnxPJD+Q+K909sdESg7C+tIkoH6on1A==",
- "dev": true,
- "dependencies": {
- "ansi-styles": "^2.2.1",
- "escape-string-regexp": "^1.0.2",
- "has-ansi": "^2.0.0",
- "strip-ansi": "^3.0.0",
- "supports-color": "^2.0.0"
- },
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/log-symbols/node_modules/strip-ansi": {
- "version": "3.0.1",
- "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
- "integrity": "sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==",
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-3.0.0.tgz",
+ "integrity": "sha512-dSkNGuI7iG3mfvDzUuYZyvk5dD9ocYCYzNU6CYDE6+Xqd+gwme6Z00NS3dUh8mq/73HaEtT7m6W+yUPtU6BZnQ==",
"dev": true,
+ "license": "MIT",
"dependencies": {
- "ansi-regex": "^2.0.0"
+ "chalk": "^2.4.2"
},
"engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/log-symbols/node_modules/supports-color": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz",
- "integrity": "sha512-KKNVtd6pCYgPIKU4cp2733HWYCpplQhddZLBUryaAHou723x+FRzQ5Df824Fj+IyyuiQTRoub4SnIFfIcrp70g==",
- "dev": true,
- "engines": {
- "node": ">=0.8.0"
+ "node": ">=8"
}
},
"node_modules/log4js": {
@@ -17267,9 +17276,10 @@
"optional": true
},
"node_modules/node-releases": {
- "version": "2.0.14",
- "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz",
- "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw=="
+ "version": "2.0.18",
+ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz",
+ "integrity": "sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==",
+ "license": "MIT"
},
"node_modules/nopt": {
"version": "3.0.6",
@@ -17837,6 +17847,13 @@
"node": ">=6"
}
},
+ "node_modules/pako": {
+ "version": "1.0.11",
+ "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
+ "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==",
+ "dev": true,
+ "license": "(MIT AND Zlib)"
+ },
"node_modules/parent-module": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
@@ -18070,9 +18087,10 @@
"dev": true
},
"node_modules/picocolors": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz",
- "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ=="
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz",
+ "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==",
+ "license": "ISC"
},
"node_modules/picomatch": {
"version": "2.3.1",
@@ -18727,15 +18745,16 @@
}
},
"node_modules/plur": {
- "version": "2.1.2",
- "resolved": "https://registry.npmjs.org/plur/-/plur-2.1.2.tgz",
- "integrity": "sha512-WhcHk576xg9y/iv6RWOuroZgsqvCbJN+XGvAypCJwLAYs2iWDp5LUmvaCdV6JR2O0SMBf8l6p7A94AyLCFVMlQ==",
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/plur/-/plur-3.1.1.tgz",
+ "integrity": "sha512-t1Ax8KUvV3FFII8ltczPn2tJdjqbd1sIzu6t4JL7nQ3EyeL/lTrj5PWKb06ic5/6XYDr65rQ4uzQEGN70/6X5w==",
"dev": true,
+ "license": "MIT",
"dependencies": {
- "irregular-plurals": "^1.0.0"
+ "irregular-plurals": "^2.0.0"
},
"engines": {
- "node": ">=0.10.0"
+ "node": ">=6"
}
},
"node_modules/pluralize": {
@@ -18759,10 +18778,15 @@
}
},
"node_modules/popper.js": {
- "version": "1.12.9",
- "resolved": "https://registry.npmjs.org/popper.js/-/popper.js-1.12.9.tgz",
- "integrity": "sha512-s0EEi9xiSCdqz9lV4IiVH5yIZnDX/U1t0tCjB+0jXx9jW8lrR7kOpVupiUg175ogqVoudpDXHF1p6dKATCHMeg==",
- "deprecated": "You can find the new Popper v2 at @popperjs/core, this package is dedicated to the legacy v1"
+ "version": "1.16.1",
+ "resolved": "https://registry.npmjs.org/popper.js/-/popper.js-1.16.1.tgz",
+ "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",
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/popperjs"
+ }
},
"node_modules/posix-character-classes": {
"version": "0.1.1",
@@ -19412,12 +19436,6 @@
"react": ">=0.14.0"
}
},
- "node_modules/proxy-from-env": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
- "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
- "license": "MIT"
- },
"node_modules/pseudomap": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz",
@@ -20095,18 +20113,31 @@
}
},
"node_modules/react-test-renderer": {
- "version": "16.4.0",
- "resolved": "https://registry.npmjs.org/react-test-renderer/-/react-test-renderer-16.4.0.tgz",
- "integrity": "sha512-Seh1t9xFY6TKiV/hRlPzUkqX1xHOiKIMsctfU0cggo1ajsLjoIJFL520LlrxV+4/VIj+clrCeH6s/aVv/vTStg==",
+ "version": "16.14.0",
+ "resolved": "https://registry.npmjs.org/react-test-renderer/-/react-test-renderer-16.14.0.tgz",
+ "integrity": "sha512-L8yPjqPE5CZO6rKsKXRO/rVPiaCOy0tQQJbC+UjPNlobl5mad59lvPjwFsQHTvL03caVDIVr9x9/OSgDe6I5Eg==",
"dev": true,
+ "license": "MIT",
"dependencies": {
- "fbjs": "^0.8.16",
"object-assign": "^4.1.1",
- "prop-types": "^15.6.0",
- "react-is": "^16.4.0"
+ "prop-types": "^15.6.2",
+ "react-is": "^16.8.6",
+ "scheduler": "^0.19.1"
},
"peerDependencies": {
- "react": "^16.0.0"
+ "react": "^16.14.0"
+ }
+ },
+ "node_modules/react-test-renderer/node_modules/prop-types": {
+ "version": "15.8.1",
+ "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
+ "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "loose-envify": "^1.4.0",
+ "object-assign": "^4.1.1",
+ "react-is": "^16.13.1"
}
},
"node_modules/react-transition-group": {
@@ -20986,9 +21017,10 @@
}
},
"node_modules/requirejs": {
- "version": "2.3.6",
- "resolved": "https://registry.npmjs.org/requirejs/-/requirejs-2.3.6.tgz",
- "integrity": "sha512-ipEzlWQe6RK3jkzikgCupiTbTvm4S0/CAU5GlgptkN5SO6F3u0UD0K18wy6ErDqiCyP4J4YYe1HuAShvsxePLg==",
+ "version": "2.3.7",
+ "resolved": "https://registry.npmjs.org/requirejs/-/requirejs-2.3.7.tgz",
+ "integrity": "sha512-DouTG8T1WanGok6Qjg2SXuCMzszOo0eHeH9hDZ5Y4x8Je+9JB38HdTLT4/VA8OaUhBa0JPVHJ0pyBkM1z+pDsw==",
+ "license": "MIT",
"bin": {
"r_js": "bin/r.js",
"r.js": "bin/r.js"
@@ -21161,14 +21193,15 @@
}
},
"node_modules/rtlcss": {
- "version": "2.2.1",
- "resolved": "https://registry.npmjs.org/rtlcss/-/rtlcss-2.2.1.tgz",
- "integrity": "sha512-JjQ5DlrmwiItAjlmhoxrJq5ihgZcE0wMFxt7S17bIrt4Lw0WwKKFk+viRhvodB/0falyG/5fiO043ZDh6/aqTw==",
+ "version": "2.6.2",
+ "resolved": "https://registry.npmjs.org/rtlcss/-/rtlcss-2.6.2.tgz",
+ "integrity": "sha512-06LFAr+GAPo+BvaynsXRfoYTJvSaWRyOhURCQ7aeI1MKph9meM222F+Zkt3bDamyHHJuGi3VPtiRkpyswmQbGA==",
+ "license": "MIT",
"dependencies": {
- "chalk": "^2.3.0",
- "findup": "^0.1.5",
+ "@choojs/findup": "^0.2.1",
+ "chalk": "^2.4.2",
"mkdirp": "^0.5.1",
- "postcss": "^6.0.14",
+ "postcss": "^6.0.23",
"strip-json-comments": "^2.0.0"
},
"bin": {
@@ -21179,6 +21212,7 @@
"version": "6.0.23",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.23.tgz",
"integrity": "sha512-soOk1h6J3VMTZtVeVpv15/Hpdl2cBLX3CAw4TAbkpTJiNPk9YP/zWcD1ND+xEtvyuuvKzbxliTOIyvkSeSJ6ag==",
+ "license": "MIT",
"dependencies": {
"chalk": "^2.4.1",
"source-map": "^0.6.1",
@@ -21192,6 +21226,7 @@
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz",
"integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==",
+ "license": "MIT",
"engines": {
"node": ">=0.10.0"
}
@@ -22089,12 +22124,13 @@
"integrity": "sha512-qGVDoreyYiP1pkQnbnFAUIS5AjenNwwQBdl7zeos9etl+hYKWahjRTfzAZZYBv5xNHx7vNKCmaLDQZ6Fr2AEXg=="
},
"node_modules/selenium-webdriver": {
- "version": "3.4.0",
- "resolved": "https://registry.npmjs.org/selenium-webdriver/-/selenium-webdriver-3.4.0.tgz",
- "integrity": "sha512-Bfq9FX33fFSj0F26BaaZllQBZl3d9FykVaH4CEJfwtN43+V8jrTpuhfiR5Zm9nkXjMbcEQz2EKWAjNzV8Hkqbw==",
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/selenium-webdriver/-/selenium-webdriver-3.6.0.tgz",
+ "integrity": "sha512-WH7Aldse+2P5bbFBO4Gle/nuQOdVwpHMTL6raL3uuBj/vPG07k6uzt3aiahu352ONBr5xXh0hDlM3LhtXPOC4Q==",
"dev": true,
+ "license": "Apache-2.0",
"dependencies": {
- "adm-zip": "^0.4.7",
+ "jszip": "^3.1.3",
"rimraf": "^2.5.4",
"tmp": "0.0.30",
"xml2js": "^0.4.17"
@@ -22107,7 +22143,9 @@
"version": "2.7.1",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz",
"integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==",
+ "deprecated": "Rimraf versions prior to v4 are no longer supported",
"dev": true,
+ "license": "ISC",
"dependencies": {
"glob": "^7.1.3"
},
@@ -23313,126 +23351,93 @@
}
},
"node_modules/stylelint-formatter-pretty": {
- "version": "1.0.3",
- "resolved": "https://registry.npmjs.org/stylelint-formatter-pretty/-/stylelint-formatter-pretty-1.0.3.tgz",
- "integrity": "sha512-Jg39kL6kkjUrdKIiHwwz/fbElcF5dOS48ZhvGrEJeWijUbmY1yudclfXv9H61eBqKKu0E33nfez2r0G4EvPtFA==",
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/stylelint-formatter-pretty/-/stylelint-formatter-pretty-1.1.4.tgz",
+ "integrity": "sha512-wgPu7UaWJO8ISiSXelp46jhtM8J/CIsrndgpUOow80v0GvyrmLrRURCFV16oKddkH0NcjlO91C7c+ef2U+siNw==",
"dev": true,
+ "license": "MIT",
"dependencies": {
- "ansi-escapes": "^2.0.0",
- "chalk": "^1.1.3",
- "log-symbols": "^1.0.2",
- "plur": "^2.1.2",
- "string-width": "^2.0.0"
+ "ansi-escapes": "^4.3.0",
+ "chalk": "^3.0.0",
+ "log-symbols": "^3.0.0",
+ "plur": "^3.1.1",
+ "string-width": "^4.2.0"
},
"engines": {
- "node": ">=4"
- }
- },
- "node_modules/stylelint-formatter-pretty/node_modules/ansi-escapes": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-2.0.0.tgz",
- "integrity": "sha512-tH/fSoQp4DrEodDK3QpdiWiZTSe7sBJ9eOqcQBZ0o9HTM+5M/viSEn+sPMoTuPjQQ8n++w3QJoPEjt8LVPcrCg==",
- "dev": true,
- "engines": {
- "node": ">=4"
- }
- },
- "node_modules/stylelint-formatter-pretty/node_modules/ansi-regex": {
- "version": "2.1.1",
- "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz",
- "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==",
- "dev": true,
- "engines": {
- "node": ">=0.10.0"
+ "node": ">=8"
}
},
"node_modules/stylelint-formatter-pretty/node_modules/ansi-styles": {
- "version": "2.2.1",
- "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz",
- "integrity": "sha512-kmCevFghRiWM7HB5zTPULl4r9bVFSWjz62MhqizDGUrq2NWuNMQyuv4tHHoKJHs69M/MF64lEcHdYIocrdWQYA==",
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "color-convert": "^2.0.1"
+ },
"engines": {
- "node": ">=0.10.0"
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/stylelint-formatter-pretty/node_modules/chalk": {
- "version": "1.1.3",
- "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz",
- "integrity": "sha512-U3lRVLMSlsCfjqYPbLyVv11M9CPW4I728d6TCKMAOJueEeB9/8o+eSsMnxPJD+Q+K909sdESg7C+tIkoH6on1A==",
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz",
+ "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==",
"dev": true,
+ "license": "MIT",
"dependencies": {
- "ansi-styles": "^2.2.1",
- "escape-string-regexp": "^1.0.2",
- "has-ansi": "^2.0.0",
- "strip-ansi": "^3.0.0",
- "supports-color": "^2.0.0"
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
},
"engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/stylelint-formatter-pretty/node_modules/is-fullwidth-code-point": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz",
- "integrity": "sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w==",
- "dev": true,
- "engines": {
- "node": ">=4"
+ "node": ">=8"
}
},
- "node_modules/stylelint-formatter-pretty/node_modules/string-width": {
- "version": "2.1.1",
- "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz",
- "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==",
+ "node_modules/stylelint-formatter-pretty/node_modules/color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"dev": true,
+ "license": "MIT",
"dependencies": {
- "is-fullwidth-code-point": "^2.0.0",
- "strip-ansi": "^4.0.0"
+ "color-name": "~1.1.4"
},
"engines": {
- "node": ">=4"
+ "node": ">=7.0.0"
}
},
- "node_modules/stylelint-formatter-pretty/node_modules/string-width/node_modules/ansi-regex": {
- "version": "3.0.1",
- "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.1.tgz",
- "integrity": "sha512-+O9Jct8wf++lXxxFc4hc8LsjaSq0HFzzL7cVsw8pRDIPdjKD2mT4ytDZlLuSBZ4cLKZFXIrMGO7DbQCtMJJMKw==",
+ "node_modules/stylelint-formatter-pretty/node_modules/color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"dev": true,
- "engines": {
- "node": ">=4"
- }
+ "license": "MIT"
},
- "node_modules/stylelint-formatter-pretty/node_modules/string-width/node_modules/strip-ansi": {
+ "node_modules/stylelint-formatter-pretty/node_modules/has-flag": {
"version": "4.0.0",
- "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz",
- "integrity": "sha512-4XaJ2zQdCzROZDivEVIDPkcQn8LMFSa8kj8Gxb/Lnwzv9A8VctNZ+lfivC/sV3ivW8ElJTERXZoPBRrZKkNKow==",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
"dev": true,
- "dependencies": {
- "ansi-regex": "^3.0.0"
- },
+ "license": "MIT",
"engines": {
- "node": ">=4"
+ "node": ">=8"
}
},
- "node_modules/stylelint-formatter-pretty/node_modules/strip-ansi": {
- "version": "3.0.1",
- "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
- "integrity": "sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==",
+ "node_modules/stylelint-formatter-pretty/node_modules/supports-color": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+ "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
"dev": true,
+ "license": "MIT",
"dependencies": {
- "ansi-regex": "^2.0.0"
+ "has-flag": "^4.0.0"
},
"engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/stylelint-formatter-pretty/node_modules/supports-color": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz",
- "integrity": "sha512-KKNVtd6pCYgPIKU4cp2733HWYCpplQhddZLBUryaAHou723x+FRzQ5Df824Fj+IyyuiQTRoub4SnIFfIcrp70g==",
- "dev": true,
- "engines": {
- "node": ">=0.8.0"
+ "node": ">=8"
}
},
"node_modules/stylelint-scss": {
@@ -24767,9 +24772,9 @@
}
},
"node_modules/update-browserslist-db": {
- "version": "1.0.13",
- "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz",
- "integrity": "sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==",
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.0.tgz",
+ "integrity": "sha512-EdRAaAyk2cUE1wOf2DkEhzxqOQvFOoRJFNS6NeyJ01Gp2beMRpBAINjM2iDXE3KCuKhwnvHIQCJm6ThL2Z+HzQ==",
"funding": [
{
"type": "opencollective",
@@ -24784,9 +24789,10 @@
"url": "https://github.com/sponsors/ai"
}
],
+ "license": "MIT",
"dependencies": {
- "escalade": "^3.1.1",
- "picocolors": "^1.0.0"
+ "escalade": "^3.1.2",
+ "picocolors": "^1.0.1"
},
"bin": {
"update-browserslist-db": "cli.js"
@@ -25104,20 +25110,20 @@
}
},
"node_modules/webpack": {
- "version": "5.91.0",
- "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.91.0.tgz",
- "integrity": "sha512-rzVwlLeBWHJbmgTC/8TvAcu5vpJNII+MelQpylD4jNERPwpBJOE2lEcko1zJX3QJeLjTTAnQxn/OJ8bjDzVQaw==",
+ "version": "5.94.0",
+ "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.94.0.tgz",
+ "integrity": "sha512-KcsGn50VT+06JH/iunZJedYGUJS5FGjow8wb9c0v5n1Om8O1g4L6LjtfxwlXIATopoQu+vOXXa7gYisWxCoPyg==",
+ "license": "MIT",
"dependencies": {
- "@types/eslint-scope": "^3.7.3",
"@types/estree": "^1.0.5",
"@webassemblyjs/ast": "^1.12.1",
"@webassemblyjs/wasm-edit": "^1.12.1",
"@webassemblyjs/wasm-parser": "^1.12.1",
"acorn": "^8.7.1",
- "acorn-import-assertions": "^1.9.0",
+ "acorn-import-attributes": "^1.9.5",
"browserslist": "^4.21.10",
"chrome-trace-event": "^1.0.2",
- "enhanced-resolve": "^5.16.0",
+ "enhanced-resolve": "^5.17.1",
"es-module-lexer": "^1.2.1",
"eslint-scope": "5.1.1",
"events": "^3.2.0",
@@ -25272,9 +25278,10 @@
}
},
"node_modules/webpack/node_modules/enhanced-resolve": {
- "version": "5.16.0",
- "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.16.0.tgz",
- "integrity": "sha512-O+QWCviPNSSLAD9Ucn8Awv+poAkqn3T1XY5/N7kR7rQO9yfSGWkYZDwpJ+iKF7B8rxaQKWngSqACpgzeapSyoA==",
+ "version": "5.17.1",
+ "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.17.1.tgz",
+ "integrity": "sha512-LMHl3dXhTcfv8gM4kEzIUeTQ+7fpdA0l2tUf34BddXPkz2A5xJ5L/Pchd5BL6rdccM9QGvu0sWZzK1Z1t4wwyg==",
+ "license": "MIT",
"dependencies": {
"graceful-fs": "^4.2.4",
"tapable": "^2.2.0"
@@ -25287,6 +25294,7 @@
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz",
"integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==",
+ "license": "BSD-2-Clause",
"dependencies": {
"esrecurse": "^4.3.0",
"estraverse": "^4.1.1"
@@ -25299,6 +25307,7 @@
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz",
"integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==",
+ "license": "BSD-2-Clause",
"engines": {
"node": ">=4.0"
}
@@ -25307,6 +25316,7 @@
"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==",
+ "license": "MIT",
"dependencies": {
"@types/json-schema": "^7.0.8",
"ajv": "^6.12.5",
@@ -25324,6 +25334,7 @@
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz",
"integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==",
+ "license": "MIT",
"engines": {
"node": ">=6"
}
diff --git a/package.json b/package.json
index 47a4f9275360..3bd526c3b481 100644
--- a/package.json
+++ b/package.json
@@ -15,22 +15,21 @@
"watch-sass": "scripts/watch_sass.sh"
},
"dependencies": {
- "@babel/core": "7.19.0",
+ "@babel/core": "7.25.2",
"@babel/plugin-proposal-object-rest-spread": "^7.18.9",
"@babel/plugin-transform-object-assign": "^7.18.6",
"@babel/preset-env": "^7.19.0",
- "@babel/preset-react": "7.18.6",
+ "@babel/preset-react": "7.24.7",
"@edx/brand-edx.org": "^2.0.7",
"@edx/edx-bootstrap": "1.0.4",
"@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",
- "axios": "^0.28.0",
"babel-loader": "^9.1.3",
"babel-plugin-transform-class-properties": "6.24.1",
"babel-polyfill": "6.26.0",
- "backbone": "1.4.1",
+ "backbone": "1.6.0",
"backbone-associations": "0.6.2",
"backbone.paginator": "2.0.8",
"bootstrap": "4.0.0",
@@ -51,12 +50,11 @@
"jquery-migrate": "1.4.1",
"jquery.scrollto": "2.1.3",
"js-cookie": "3.0.5",
- "jwt-decode": "^3.1.2",
"moment": "2.30.1",
"moment-timezone": "0.5.45",
"node-gyp": "10.0.1",
"picturefill": "3.0.3",
- "popper.js": "1.12.9",
+ "popper.js": "1.16.1",
"prop-types": "15.6.0",
"raw-loader": "0.5.1",
"react": "16.14.0",
@@ -67,8 +65,8 @@
"react-slick": "0.29.0",
"redux": "3.7.2",
"redux-thunk": "2.2.0",
- "requirejs": "2.3.6",
- "rtlcss": "2.2.1",
+ "requirejs": "2.3.7",
+ "rtlcss": "2.6.2",
"sass": "^1.54.8",
"sass-loader": "^14.1.1",
"scriptjs": "2.5.9",
@@ -77,7 +75,6 @@
"uglify-js": "2.7.0",
"underscore": "1.12.1",
"underscore.string": "3.3.6",
- "universal-cookie": "^4.0.4",
"webpack": "^5.90.3",
"webpack-bundle-tracker": "0.4.3",
"webpack-merge": "4.1.1",
@@ -88,33 +85,33 @@
"@edx/eslint-config": "^3.1.1",
"@edx/mockprock": "github:openedx/mockprock#3ad18c6888e6521e9bf7a4df0db6f8579b928235",
"@edx/stylelint-config-edx": "2.3.3",
- "babel-jest": "26.0.0",
+ "babel-jest": "26.6.3",
"enzyme": "3.11.0",
"enzyme-adapter-react-16": "1.15.8",
"eslint-import-resolver-webpack": "0.13.8",
"jasmine-core": "2.6.4",
"jasmine-jquery": "git+https://git@github.com/velesin/jasmine-jquery.git#ebad463d592d3fea00c69f26ea18a930e09c7b58",
- "jest": "26.0.0",
- "jest-enzyme": "6.0.2",
+ "jest": "26.6.3",
+ "jest-enzyme": "6.1.2",
"karma": "0.13.22",
"karma-chrome-launcher": "0.2.3",
"karma-coverage": "0.5.5",
"karma-firefox-launcher": "0.1.7",
"karma-jasmine": "0.3.8",
"karma-jasmine-html-reporter": "0.2.2",
- "karma-junit-reporter": "1.1.0",
+ "karma-junit-reporter": "1.2.0",
"karma-requirejs": "0.2.6",
"karma-selenium-webdriver-launcher": "github:openedx/karma-selenium-webdriver-launcher#0.0.4-openedx.0",
- "karma-sourcemap-loader": "0.3.7",
+ "karma-sourcemap-loader": "0.4.0",
"karma-spec-reporter": "0.0.36",
"karma-webpack": "^5.0.1",
"plato": "1.7.0",
- "react-test-renderer": "16.4.0",
- "selenium-webdriver": "3.4.0",
+ "react-test-renderer": "16.14.0",
+ "selenium-webdriver": "3.6.0",
"sinon": "2.4.1",
"squirejs": "0.1.0",
"string-replace-loader": "^3.1.0",
- "stylelint-formatter-pretty": "1.0.3",
+ "stylelint-formatter-pretty": "1.1.4",
"webpack-cli": "^5.1.4"
}
}
diff --git a/requirements/common_constraints.txt b/requirements/common_constraints.txt
index 4abc9ae22cb3..f2ef0216ca07 100644
--- a/requirements/common_constraints.txt
+++ b/requirements/common_constraints.txt
@@ -21,15 +21,14 @@ Django<5.0
# elasticsearch>=7.14.0 includes breaking changes in it which caused issues in discovery upgrade process.
# 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
# django-simple-history>3.0.0 adds indexing and causes a lot of migrations to be affected
-# opentelemetry requires version 6.x at the moment:
-# https://github.com/open-telemetry/opentelemetry-python/issues/3570
-# Normally this could be added as a constraint in edx-django-utils, where we're
-# adding the opentelemetry dependency. However, when we compile pip-tools.txt,
-# that uses version 7.x, and then there's no undoing that when compiling base.txt.
-# So we need to pin it globally, for now.
-# Ticket for unpinning: https://github.com/openedx/edx-lint/issues/407
-importlib-metadata<7
+# Cause: https://github.com/openedx/event-tracking/pull/290
+# event-tracking 2.4.1 upgrades to pymongo 4.4.0 which is not supported on edx-platform.
+# We will pin event-tracking to do not break existing installations
+# This can be unpinned once https://github.com/openedx/edx-platform/issues/34586
+# has been resolved and edx-platform is running with pymongo>=4.4.0
+
diff --git a/requirements/constraints.txt b/requirements/constraints.txt
index 250b46bbba17..8c95f7fcc200 100644
--- a/requirements/constraints.txt
+++ b/requirements/constraints.txt
@@ -12,6 +12,12 @@
# This file contains all common constraints for edx-repos
-c common_constraints.txt
+# Date: 2024-08-21
+# Description: This is the major upgrade of algoliasearch python client and it will
+# break one of the edX' platform plugin, so we need to make that compatible first.
+# Ticket: https://github.com/openedx/edx-platform/issues/35334
+algoliasearch<4.0.0
+
# 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.
celery>=5.2.2,<6.0.0
@@ -20,7 +26,7 @@ celery>=5.2.2,<6.0.0
# The team that owns this package will manually bump this package rather than having it pulled in automatically.
# This is to allow them to better control its deployment and to do it in a process that works better
# for them.
-edx-enterprise==4.20.5
+edx-enterprise==4.25.10
# Stay on LTS version, remove once this is added to common constraint
Django<5.0
@@ -32,9 +38,13 @@ django-oauth-toolkit==1.7.1
# incremental upgrade
django-simple-history==3.4.0
-# constrained in opaque_keys. migration guide here: https://pymongo.readthedocs.io/en/4.0/migrate-to-pymongo4.html
-# Major upgrade will be done in separate ticket.
-pymongo<4.0.0
+# Adding pin to avoid any major upgrade
+pymongo<4.4.1
+
+# To override the constraint of edx-lint
+# This can be removed once https://github.com/openedx/edx-platform/issues/34586 is resolved
+# and the upstream constraint in edx-lint has been removed.
+event-tracking==3.0.0
# greater version has breaking changes and requires some migration steps.
django-webpack-loader==0.7.0
@@ -59,14 +69,6 @@ pycodestyle<2.9.0
pylint<2.16.0 # greater version failing quality test. Fix them in seperate ticket.
-# adding these constraints to minimize boto3 and botocore changeset
-social-auth-core==4.3.0
-
-# social-auth-app-django versions after 5.2.0 has a problematic migration that will cause issues deployments with large
-# `social_auth_usersocialauth` tables. 5.1.0 has missing migration and 5.2.0 has that problematic migration.
-social-auth-app-django==5.0.0
-
-
# urllib3>=2.0.0 conflicts with elastic search && snowflake-connector-python packages
# which require urllib3<2 for now.
# Issue for unpinning: https://github.com/openedx/edx-platform/issues/32222
@@ -91,7 +93,7 @@ libsass==0.10.0
click==8.1.6
# pinning this version to avoid updates while the library is being developed
-openedx-learning==0.10.0
+openedx-learning==0.11.4
# Open AI version 1.0.0 dropped support for openai.ChatCompletion which is currently in use in enterprise.
openai<=0.28.1
@@ -123,3 +125,20 @@ path<16.12.0
# Temporary to Support the python 3.11 Upgrade
backports.zoneinfo;python_version<"3.9" # Newer versions have zoneinfo available in the standard library
+
+# Relevant GitHub Issue: https://github.com/openedx/edx-platform/issues/35126
+# We need to upgrade the version of elasticsearch to atleast 7.15 before we can upgrade to Numpy 2.0.0
+# Otherwise we see a failure while running the following command:
+# export DJANGO_SETTINGS_MODULE=cms.envs.test; python manage.py cms check_reserved_keywords --override_file db_keyword_overrides.yml --report_path reports/reserved_keywords --report_file cms_reserved_keyword_report.csv
+numpy<2.0.0
+
+# django-storages==1.14.4 breaks course imports
+# Two lines were added in 1.14.4 that make file_exists_in_storage function always return False,
+# as the default value of AWS_S3_FILE_OVERWRITE is True
+django-storages<1.14.4
+
+# social-auth-app-django 5.4.2 introduces a new migration that will not play nicely with large installations. This will touch
+# user tables, which are quite large, especially on instances like edx.org.
+# We are pinning this until after all the smaller migrations get handled and then we can migrate this all at once.
+# Ticket to unpin: https://github.com/edx/edx-arch-experiments/issues/760
+social-auth-app-django<=5.4.1
diff --git a/requirements/edx-sandbox/base.txt b/requirements/edx-sandbox/base.txt
index bf5148b100de..4a7b0c0a7d35 100644
--- a/requirements/edx-sandbox/base.txt
+++ b/requirements/edx-sandbox/base.txt
@@ -4,7 +4,7 @@
#
# make upgrade
#
-cffi==1.16.0
+cffi==1.17.0
# via cryptography
chem==1.3.0
# via -r requirements/edx-sandbox/base.in
@@ -16,11 +16,11 @@ codejail-includes==1.0.0
# via -r requirements/edx-sandbox/base.in
contourpy==1.2.1
# via matplotlib
-cryptography==42.0.8
+cryptography==43.0.0
# via -r requirements/edx-sandbox/base.in
cycler==0.12.1
# via matplotlib
-fonttools==4.53.0
+fonttools==4.53.1
# via matplotlib
joblib==1.4.2
# via nltk
@@ -35,18 +35,19 @@ markupsafe==2.1.5
# via
# chem
# openedx-calc
-matplotlib==3.9.0
+matplotlib==3.9.2
# via -r requirements/edx-sandbox/base.in
mpmath==1.3.0
# via sympy
networkx==3.3
# via -r requirements/edx-sandbox/base.in
-nltk==3.8.1
+nltk==3.9.1
# via
# -r requirements/edx-sandbox/base.in
# chem
numpy==1.26.4
# via
+ # -c requirements/edx-sandbox/../constraints.txt
# chem
# contourpy
# matplotlib
@@ -56,7 +57,7 @@ openedx-calc==3.1.0
# via -r requirements/edx-sandbox/base.in
packaging==24.1
# via matplotlib
-pillow==10.3.0
+pillow==10.4.0
# via matplotlib
pycparser==2.22
# via cffi
@@ -70,9 +71,9 @@ python-dateutil==2.9.0.post0
# via matplotlib
random2==1.0.2
# via -r requirements/edx-sandbox/base.in
-regex==2024.5.15
+regex==2024.7.24
# via nltk
-scipy==1.13.1
+scipy==1.14.0
# via
# -r requirements/edx-sandbox/base.in
# chem
@@ -81,9 +82,9 @@ six==1.16.0
# via
# codejail-includes
# python-dateutil
-sympy==1.12.1
+sympy==1.13.2
# via
# -r requirements/edx-sandbox/base.in
# openedx-calc
-tqdm==4.66.4
+tqdm==4.66.5
# via nltk
diff --git a/requirements/edx/base.txt b/requirements/edx/base.txt
index 317cdcca1fbb..9c85f60fc3a8 100644
--- a/requirements/edx/base.txt
+++ b/requirements/edx/base.txt
@@ -8,14 +8,18 @@
# via -r requirements/edx/github.in
acid-xblock==0.3.1
# via -r requirements/edx/kernel.in
-aiohttp==3.9.5
+aiohappyeyeballs==2.4.0
+ # via aiohttp
+aiohttp==3.10.5
# via
# geoip2
# openai
aiosignal==1.3.1
# via aiohttp
algoliasearch==3.0.0
- # via -r requirements/edx/bundled.in
+ # via
+ # -c requirements/edx/../constraints.txt
+ # -r requirements/edx/bundled.in
amqp==5.2.0
# via kombu
analytics-python==1.4.post1
@@ -33,7 +37,7 @@ asgiref==3.8.1
# django-countries
asn1crypto==1.5.1
# via snowflake-connector-python
-attrs==23.2.0
+attrs==24.2.0
# via
# -r requirements/edx/kernel.in
# aiohttp
@@ -43,13 +47,15 @@ attrs==23.2.0
# openedx-events
# openedx-learning
# referencing
-babel==2.15.0
+babel==2.16.0
# via
# -r requirements/edx/kernel.in
# enmerkar
# enmerkar-underscore
backoff==1.10.0
# via analytics-python
+bcrypt==4.2.0
+ # via paramiko
beautifulsoup4==4.12.3
# via pynliner
billiard==4.2.0
@@ -64,19 +70,23 @@ bleach[css]==6.1.0
# xblock-poll
boto==2.49.0
# via -r requirements/edx/kernel.in
-boto3==1.34.123
+boto3==1.35.1
# via
# -r requirements/edx/kernel.in
# django-ses
# fs-s3fs
# ora2
-botocore==1.34.123
+botocore==1.35.1
# via
# -r requirements/edx/kernel.in
# boto3
# s3transfer
bridgekeeper==0.9
# via -r requirements/edx/kernel.in
+cachecontrol==0.14.0
+ # via firebase-admin
+cachetools==5.5.0
+ # via google-auth
camel-converter[pydantic]==3.1.2
# via meilisearch
celery==5.4.0
@@ -89,14 +99,14 @@ celery==5.4.0
# edx-enterprise
# event-tracking
# openedx-learning
-certifi==2024.6.2
+certifi==2024.7.4
# via
# -r requirements/edx/paver.txt
# elasticsearch
# py2neo
# requests
# snowflake-connector-python
-cffi==1.16.0
+cffi==1.17.0
# via
# cryptography
# pynacl
@@ -143,6 +153,7 @@ cryptography==42.0.8
# edx-enterprise
# jwcrypto
# optimizely-sdk
+ # paramiko
# pgpy
# pyjwt
# pyopenssl
@@ -157,7 +168,7 @@ defusedxml==0.7.1
# ora2
# python3-openid
# social-auth-core
-django==4.2.13
+django==4.2.15
# via
# -c requirements/edx/../common_constraints.txt
# -c requirements/edx/../constraints.txt
@@ -176,6 +187,7 @@ django==4.2.13
# django-multi-email-field
# django-mysql
# django-oauth-toolkit
+ # django-push-notifications
# django-sekizai
# django-ses
# django-statici18n
@@ -224,6 +236,7 @@ django==4.2.13
# openedx-filters
# openedx-learning
# ora2
+ # social-auth-app-django
# super-csv
# xblock-google-drive
# xss-utils
@@ -241,7 +254,7 @@ django-config-models==2.7.0
# edx-enterprise
# edx-name-affirmation
# lti-consumer-xblock
-django-cors-headers==4.3.1
+django-cors-headers==4.4.0
# via -r requirements/edx/kernel.in
django-countries==7.6.1
# via
@@ -258,7 +271,7 @@ django-crum==0.7.9
# super-csv
django-fernet-fields-v2==0.9
# via edx-enterprise
-django-filter==24.2
+django-filter==24.3
# via
# -r requirements/edx/kernel.in
# edx-enterprise
@@ -296,7 +309,7 @@ django-mptt==0.16.0
# openedx-django-wiki
django-multi-email-field==0.7.0
# via edx-enterprise
-django-mysql==4.13.0
+django-mysql==4.14.0
# via -r requirements/edx/kernel.in
django-oauth-toolkit==1.7.1
# via
@@ -307,6 +320,8 @@ django-object-actions==4.2.0
# via edx-enterprise
django-pipeline==3.1.0
# via -r requirements/edx/kernel.in
+django-push-notifications==3.1.0
+ # via edx-ace
django-ratelimit==4.1.0
# via -r requirements/edx/kernel.in
django-sekizai==4.1.0
@@ -332,6 +347,7 @@ django-statici18n==2.5.0
# xblock-poll
django-storages==1.14.3
# via
+ # -c requirements/edx/../constraints.txt
# -r requirements/edx/kernel.in
# edxval
django-user-tasks==3.2.0
@@ -371,6 +387,10 @@ djangorestframework==3.14.0
# super-csv
djangorestframework-xml==2.0.0
# via edx-enterprise
+dnspython==2.6.1
+ # via
+ # -r requirements/edx/paver.txt
+ # pymongo
done-xblock==2.3.0
# via -r requirements/edx/bundled.in
drf-jwt==1.19.2
@@ -381,7 +401,7 @@ drf-yasg==1.21.7
# via
# django-user-tasks
# edx-api-doc-tools
-edx-ace==1.8.0
+edx-ace==1.11.1
# via -r requirements/edx/kernel.in
edx-api-doc-tools==1.8.0
# via
@@ -409,7 +429,7 @@ edx-celeryutils==1.3.0
# super-csv
edx-codejail==3.4.1
# via -r requirements/edx/kernel.in
-edx-completion==4.6.2
+edx-completion==4.6.7
# via -r requirements/edx/kernel.in
edx-django-release-util==1.4.0
# via
@@ -418,10 +438,11 @@ edx-django-release-util==1.4.0
# edxval
edx-django-sites-extensions==4.2.0
# via -r requirements/edx/kernel.in
-edx-django-utils==5.14.2
+edx-django-utils==5.15.0
# via
# -r requirements/edx/kernel.in
# django-config-models
+ # edx-ace
# edx-drf-extensions
# edx-enterprise
# edx-event-bus-kafka
@@ -446,11 +467,11 @@ edx-drf-extensions==10.3.0
# edx-when
# edxval
# openedx-learning
-edx-enterprise==4.20.5
+edx-enterprise==4.25.10
# via
# -c requirements/edx/../constraints.txt
# -r requirements/edx/kernel.in
-edx-event-bus-kafka==5.7.0
+edx-event-bus-kafka==5.8.1
# via -r requirements/edx/kernel.in
edx-event-bus-redis==0.5.0
# via -r requirements/edx/kernel.in
@@ -461,7 +482,7 @@ edx-i18n-tools==1.5.0
# ora2
edx-milestones==0.6.0
# via -r requirements/edx/kernel.in
-edx-name-affirmation==2.3.7
+edx-name-affirmation==2.4.0
# via -r requirements/edx/kernel.in
edx-opaque-keys[django]==2.10.0
# via
@@ -487,16 +508,16 @@ edx-proctoring==4.18.1
# edx-proctoring-proctortrack
edx-rbac==1.9.0
# via edx-enterprise
-edx-rest-api-client==5.6.1
+edx-rest-api-client==5.7.1
# via
# -r requirements/edx/kernel.in
# edx-enterprise
# edx-proctoring
-edx-search==3.9.1
+edx-search==4.0.0
# via -r requirements/edx/kernel.in
edx-sga==0.25.0
# via -r requirements/edx/bundled.in
-edx-submissions==3.7.1
+edx-submissions==3.7.7
# via
# -r requirements/edx/kernel.in
# ora2
@@ -528,18 +549,21 @@ elasticsearch==7.13.4
# edx-search
enmerkar==0.7.1
# via enmerkar-underscore
-enmerkar-underscore==2.3.0
+enmerkar-underscore==2.3.1
# via -r requirements/edx/kernel.in
-event-tracking==2.4.0
+event-tracking==3.0.0
# via
+ # -c requirements/edx/../constraints.txt
# -r requirements/edx/kernel.in
# edx-completion
# edx-proctoring
# edx-search
-fastavro==1.9.4
+fastavro==1.9.5
# via openedx-events
-filelock==3.14.0
+filelock==3.15.4
# via snowflake-connector-python
+firebase-admin==6.5.0
+ # via edx-ace
frozenlist==1.4.1
# via
# aiohttp
@@ -560,7 +584,50 @@ geoip2==4.8.0
# via -r requirements/edx/kernel.in
glob2==0.7
# via -r requirements/edx/kernel.in
-gunicorn==22.0.0
+google-api-core[grpc]==2.19.1
+ # via
+ # firebase-admin
+ # google-api-python-client
+ # google-cloud-core
+ # google-cloud-firestore
+ # google-cloud-storage
+google-api-python-client==2.141.0
+ # via firebase-admin
+google-auth==2.34.0
+ # via
+ # google-api-core
+ # google-api-python-client
+ # google-auth-httplib2
+ # google-cloud-core
+ # google-cloud-firestore
+ # google-cloud-storage
+google-auth-httplib2==0.2.0
+ # via google-api-python-client
+google-cloud-core==2.4.1
+ # via
+ # google-cloud-firestore
+ # google-cloud-storage
+google-cloud-firestore==2.17.2
+ # via firebase-admin
+google-cloud-storage==2.18.2
+ # via firebase-admin
+google-crc32c==1.5.0
+ # via
+ # google-cloud-storage
+ # google-resumable-media
+google-resumable-media==2.7.2
+ # via google-cloud-storage
+googleapis-common-protos==1.63.2
+ # via
+ # google-api-core
+ # grpcio-status
+grpcio==1.65.5
+ # via
+ # google-api-core
+ # grpcio-status
+grpcio-status==1.65.5
+ # via google-api-core
+gunicorn==23.0.0
# via -r requirements/edx/kernel.in
help-tokens==2.4.0
# via -r requirements/edx/kernel.in
@@ -568,7 +635,11 @@ html5lib==1.1
# via
# -r requirements/edx/kernel.in
# ora2
-icalendar==5.0.12
+httplib2==0.22.0
+ # via
+ # google-api-python-client
+ # google-auth-httplib2
+icalendar==5.0.13
# via -r requirements/edx/kernel.in
idna==3.7
# via
@@ -577,10 +648,8 @@ idna==3.7
# requests
# snowflake-connector-python
# yarl
-importlib-metadata==6.11.0
- # via
- # -c requirements/edx/../common_constraints.txt
- # -r requirements/edx/kernel.in
+importlib-metadata==8.3.0
+ # via -r requirements/edx/kernel.in
inflection==0.5.1
# via
# drf-spectacular
@@ -599,7 +668,7 @@ jmespath==1.0.1
# botocore
joblib==1.4.2
# via nltk
-jsondiff==2.0.0
+jsondiff==2.2.0
# via edx-enterprise
jsonfield==3.1.0
# via
@@ -610,7 +679,7 @@ jsonfield==3.1.0
# edx-submissions
# lti-consumer-xblock
# ora2
-jsonschema==4.22.0
+jsonschema==4.23.0
# via
# drf-spectacular
# optimizely-sdk
@@ -620,7 +689,7 @@ jwcrypto==1.5.6
# via
# django-oauth-toolkit
# pylti1p3
-kombu==5.3.7
+kombu==5.4.0
# via celery
laboratory==1.0.2
# via -r requirements/edx/kernel.in
@@ -678,7 +747,7 @@ markupsafe==2.1.5
# xblock
maxminddb==2.6.2
# via geoip2
-meilisearch==0.31.3
+meilisearch==0.31.4
# via -r requirements/edx/kernel.in
mock==5.1.0
# via -r requirements/edx/paver.txt
@@ -688,28 +757,31 @@ monotonic==1.6
# via
# analytics-python
# py2neo
-more-itertools==10.3.0
+more-itertools==10.4.0
# via cssutils
mpmath==1.3.0
# via sympy
+msgpack==1.0.8
+ # via cachecontrol
multidict==6.0.5
# via
# aiohttp
# yarl
mysqlclient==2.2.4
# via -r requirements/edx/kernel.in
-newrelic==9.10.0
+newrelic==9.13.0
# via
# -r requirements/edx/bundled.in
# edx-django-utils
-nh3==0.2.17
+nh3==0.2.18
# via -r requirements/edx/kernel.in
-nltk==3.8.1
+nltk==3.9.1
# via chem
nodeenv==1.9.1
# via -r requirements/edx/kernel.in
numpy==1.26.4
# via
+ # -c requirements/edx/../constraints.txt
# chem
# openedx-calc
# scipy
@@ -739,29 +811,30 @@ openedx-django-require==2.1.0
# via -r requirements/edx/kernel.in
openedx-django-wiki==2.1.0
# via -r requirements/edx/kernel.in
-openedx-events==9.10.0
+openedx-events==9.14.0
# via
# -r requirements/edx/kernel.in
+ # edx-enterprise
# edx-event-bus-kafka
# edx-event-bus-redis
# event-tracking
# ora2
-openedx-filters==1.8.1
+openedx-filters==1.9.0
# via
# -r requirements/edx/kernel.in
# lti-consumer-xblock
# ora2
-openedx-learning==0.10.0
+openedx-learning==0.11.4
# via
# -c requirements/edx/../constraints.txt
# -r requirements/edx/kernel.in
-openedx-mongodbproxy==0.2.0
+openedx-mongodbproxy==0.2.1
# via -r requirements/edx/kernel.in
optimizely-sdk==4.1.1
# via
# -c requirements/edx/../constraints.txt
# -r requirements/edx/bundled.in
-ora2==6.11.1
+ora2==6.11.2
# via -r requirements/edx/bundled.in
packaging==24.1
# via
@@ -771,6 +844,8 @@ packaging==24.1
# snowflake-connector-python
pansi==2020.7.3
# via py2neo
+paramiko==3.4.1
+ # via edx-enterprise
path==16.11.0
# via
# -c requirements/edx/../constraints.txt
@@ -793,7 +868,7 @@ pgpy==0.6.0
# via edx-enterprise
piexif==1.1.3
# via -r requirements/edx/kernel.in
-pillow==10.3.0
+pillow==10.4.0
# via
# -r requirements/edx/kernel.in
# edx-enterprise
@@ -805,7 +880,18 @@ polib==1.2.0
# via edx-i18n-tools
prompt-toolkit==3.0.47
# via click-repl
-psutil==5.9.8
+proto-plus==1.24.0
+ # via
+ # google-api-core
+ # google-cloud-firestore
+protobuf==5.27.3
+ # via
+ # google-api-core
+ # google-cloud-firestore
+ # googleapis-common-protos
+ # grpcio-status
+ # proto-plus
+psutil==6.0.0
# via
# -r requirements/edx/paver.txt
# edx-django-utils
@@ -814,7 +900,12 @@ py2neo @ https://github.com/overhangio/py2neo/releases/download/2021.2.3/py2neo-
# -c requirements/edx/../constraints.txt
# -r requirements/edx/bundled.in
pyasn1==0.6.0
- # via pgpy
+ # via
+ # pgpy
+ # pyasn1-modules
+ # rsa
+pyasn1-modules==0.4.0
+ # via google-auth
pycountry==24.6.1
# via -r requirements/edx/kernel.in
pycparser==2.22
@@ -825,9 +916,9 @@ pycryptodomex==3.20.0
# edx-proctoring
# lti-consumer-xblock
# pyjwkest
-pydantic==2.7.3
+pydantic==2.8.2
# via camel-converter
-pydantic-core==2.18.4
+pydantic-core==2.20.1
# via pydantic
pygments==2.18.0
# via
@@ -838,7 +929,7 @@ pyjwkest==1.4.2
# -r requirements/edx/kernel.in
# edx-token-utils
# lti-consumer-xblock
-pyjwt[crypto]==2.8.0
+pyjwt[crypto]==2.9.0
# via
# -r requirements/edx/kernel.in
# drf-jwt
@@ -846,6 +937,7 @@ pyjwt[crypto]==2.8.0
# edx-drf-extensions
# edx-proctoring
# edx-rest-api-client
+ # firebase-admin
# pylti1p3
# snowflake-connector-python
# social-auth-core
@@ -855,7 +947,7 @@ pylti1p3==2.0.0
# via -r requirements/edx/kernel.in
pymemcache==4.0.0
# via -r requirements/edx/paver.txt
-pymongo==3.13.0
+pymongo==4.4.0
# via
# -c requirements/edx/../constraints.txt
# -r requirements/edx/kernel.in
@@ -865,16 +957,19 @@ pymongo==3.13.0
# mongoengine
# openedx-mongodbproxy
pynacl==1.5.0
- # via edx-django-utils
+ # via
+ # edx-django-utils
+ # paramiko
pynliner==0.8.0
# via -r requirements/edx/kernel.in
-pyopenssl==24.1.0
+pyopenssl==24.2.1
# via
# optimizely-sdk
# snowflake-connector-python
pyparsing==3.1.2
# via
# chem
+ # httplib2
# openedx-calc
pyrsistent==0.20.0
# via optimizely-sdk
@@ -930,7 +1025,7 @@ pytz==2024.1
# xblock
pyuca==1.2
# via -r requirements/edx/kernel.in
-pyyaml==6.0.1
+pyyaml==6.0.2
# via
# -r requirements/edx/kernel.in
# code-annotations
@@ -938,12 +1033,13 @@ pyyaml==6.0.1
# drf-yasg
# edx-django-release-util
# edx-i18n-tools
+ # jsondiff
# xblock
random2==1.0.2
# via -r requirements/edx/kernel.in
recommender-xblock==2.2.0
# via -r requirements/edx/bundled.in
-redis==5.0.5
+redis==5.0.8
# via
# -r requirements/edx/kernel.in
# walrus
@@ -951,19 +1047,22 @@ referencing==0.35.1
# via
# jsonschema
# jsonschema-specifications
-regex==2024.5.15
+regex==2024.7.24
# via nltk
requests==2.32.3
# via
# -r requirements/edx/paver.txt
# algoliasearch
# analytics-python
+ # cachecontrol
# django-oauth-toolkit
# edx-bulk-grades
# edx-drf-extensions
# edx-enterprise
# edx-rest-api-client
# geoip2
+ # google-api-core
+ # google-cloud-storage
# mailsnake
# meilisearch
# openai
@@ -981,29 +1080,31 @@ requests-oauthlib==2.0.0
# via
# -r requirements/edx/kernel.in
# social-auth-core
-rpds-py==0.18.1
+rpds-py==0.20.0
# via
# jsonschema
# referencing
+rsa==4.9
+ # via google-auth
rules==3.4
# via
# -r requirements/edx/kernel.in
# edx-enterprise
# edx-proctoring
# openedx-learning
-s3transfer==0.10.1
+s3transfer==0.10.2
# via boto3
sailthru-client==2.2.3
# via edx-ace
-scipy==1.13.1
+scipy==1.14.0
# via
# chem
# openedx-calc
semantic-version==2.10.0
# via edx-drf-extensions
-shapely==2.0.4
+shapely==2.0.6
# via -r requirements/edx/kernel.in
-simplejson==3.19.2
+simplejson==3.19.3
# via
# -r requirements/edx/kernel.in
# sailthru-client
@@ -1044,16 +1145,15 @@ slumber==0.7.1
# edx-bulk-grades
# edx-enterprise
# edx-rest-api-client
-snowflake-connector-python==3.10.1
+snowflake-connector-python==3.12.0
# via edx-enterprise
-social-auth-app-django==5.0.0
+social-auth-app-django==5.4.1
# via
# -c requirements/edx/../constraints.txt
# -r requirements/edx/kernel.in
# edx-auth-backends
-social-auth-core==4.3.0
+social-auth-core==4.5.4
# via
- # -c requirements/edx/../constraints.txt
# -r requirements/edx/kernel.in
# edx-auth-backends
# social-auth-app-django
@@ -1065,9 +1165,9 @@ sortedcontainers==2.4.0
# via
# -r requirements/edx/kernel.in
# snowflake-connector-python
-soupsieve==2.5
+soupsieve==2.6
# via beautifulsoup4
-sqlparse==0.5.0
+sqlparse==0.5.1
# via django
staff-graded-xblock==2.3.0
# via -r requirements/edx/bundled.in
@@ -1082,7 +1182,7 @@ stevedore==5.2.0
# edx-opaque-keys
super-csv==3.2.0
# via edx-bulk-grades
-sympy==1.12.1
+sympy==1.13.2
# via openedx-calc
testfixtures==8.3.0
# via edx-enterprise
@@ -1090,9 +1190,9 @@ text-unidecode==1.3
# via python-slugify
tinycss2==1.2.1
# via bleach
-tomlkit==0.12.5
+tomlkit==0.13.2
# via snowflake-connector-python
-tqdm==4.66.4
+tqdm==4.66.5
# via
# nltk
# openai
@@ -1116,7 +1216,8 @@ uritemplate==4.1.1
# via
# drf-spectacular
# drf-yasg
-urllib3==1.26.18
+ # google-api-python-client
+urllib3==1.26.19
# via
# -c requirements/edx/../constraints.txt
# -r requirements/edx/paver.txt
@@ -1131,11 +1232,11 @@ vine==5.1.0
# amqp
# celery
# kombu
-voluptuous==0.14.2
+voluptuous==0.15.2
# via ora2
-walrus==0.9.3
+walrus==0.9.4
# via edx-event-bus-redis
-watchdog==4.0.1
+watchdog==4.0.2
# via -r requirements/edx/paver.txt
wcwidth==0.2.13
# via prompt-toolkit
@@ -1152,13 +1253,13 @@ webencodings==0.5.1
# bleach
# html5lib
# tinycss2
-webob==1.8.7
+webob==1.8.8
# via
# -r requirements/edx/kernel.in
# xblock
wrapt==1.16.0
# via -r requirements/edx/paver.txt
-xblock[django]==4.0.1
+xblock[django]==5.1.0
# via
# -r requirements/edx/kernel.in
# acid-xblock
@@ -1173,7 +1274,7 @@ xblock[django]==4.0.1
# xblock-drag-and-drop-v2
# xblock-google-drive
# xblock-utils
-xblock-drag-and-drop-v2==4.0.2
+xblock-drag-and-drop-v2==4.0.3
# via -r requirements/edx/bundled.in
xblock-google-drive==0.7.0
# via -r requirements/edx/bundled.in
@@ -1191,7 +1292,7 @@ xss-utils==0.6.0
# via -r requirements/edx/kernel.in
yarl==1.9.4
# via aiohttp
-zipp==3.19.2
+zipp==3.20.0
# via importlib-metadata
# The following packages are considered to be unsafe in a requirements file:
diff --git a/requirements/edx/coverage.txt b/requirements/edx/coverage.txt
index 73c03573cca8..a004eeeb9ffa 100644
--- a/requirements/edx/coverage.txt
+++ b/requirements/edx/coverage.txt
@@ -6,9 +6,9 @@
#
chardet==5.2.0
# via diff-cover
-coverage==7.5.3
+coverage==7.6.1
# via -r requirements/edx/coverage.in
-diff-cover==9.0.0
+diff-cover==9.1.1
# via -r requirements/edx/coverage.in
jinja2==3.1.4
# via diff-cover
diff --git a/requirements/edx/development.txt b/requirements/edx/development.txt
index 53d574da8db0..15e078ae0dd3 100644
--- a/requirements/edx/development.txt
+++ b/requirements/edx/development.txt
@@ -16,7 +16,12 @@ acid-xblock==0.3.1
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
-aiohttp==3.9.5
+aiohappyeyeballs==2.4.0
+ # via
+ # -r requirements/edx/doc.txt
+ # -r requirements/edx/testing.txt
+ # aiohttp
+aiohttp==3.10.5
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
@@ -27,12 +32,13 @@ aiosignal==1.3.1
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
# aiohttp
-alabaster==0.7.16
+alabaster==1.0.0
# via
# -r requirements/edx/doc.txt
# sphinx
algoliasearch==3.0.0
# via
+ # -c requirements/edx/../constraints.txt
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
amqp==5.2.0
@@ -57,9 +63,7 @@ annotated-types==0.7.0
anyio==4.4.0
# via
# -r requirements/edx/testing.txt
- # httpx
# starlette
- # watchfiles
appdirs==1.4.4
# via
# -r requirements/edx/doc.txt
@@ -82,7 +86,7 @@ astroid==2.13.5
# -r requirements/edx/testing.txt
# pylint
# pylint-celery
-attrs==23.2.0
+attrs==24.2.0
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
@@ -93,7 +97,7 @@ attrs==23.2.0
# openedx-events
# openedx-learning
# referencing
-babel==2.15.0
+babel==2.16.0
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
@@ -106,6 +110,11 @@ backoff==1.10.0
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
# analytics-python
+bcrypt==4.2.0
+ # via
+ # -r requirements/edx/doc.txt
+ # -r requirements/edx/testing.txt
+ # paramiko
beautifulsoup4==4.12.3
# via
# -r requirements/edx/doc.txt
@@ -131,14 +140,14 @@ boto==2.49.0
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
-boto3==1.34.123
+boto3==1.35.1
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
# django-ses
# fs-s3fs
# ora2
-botocore==1.34.123
+botocore==1.35.1
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
@@ -152,9 +161,16 @@ build==1.2.1
# via
# -r requirements/edx/../pip-tools.txt
# pip-tools
-cachetools==5.3.3
+cachecontrol==0.14.0
# via
+ # -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
+ # firebase-admin
+cachetools==5.5.0
+ # via
+ # -r requirements/edx/doc.txt
+ # -r requirements/edx/testing.txt
+ # google-auth
# tox
camel-converter[pydantic]==3.1.2
# via
@@ -172,17 +188,15 @@ celery==5.4.0
# edx-enterprise
# event-tracking
# openedx-learning
-certifi==2024.6.2
+certifi==2024.7.4
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
# elasticsearch
- # httpcore
- # httpx
# py2neo
# requests
# snowflake-connector-python
-cffi==1.16.0
+cffi==1.17.0
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
@@ -228,7 +242,6 @@ click==8.1.6
# nltk
# pact-python
# pip-tools
- # typer
# user-util
# uvicorn
click-didyoumean==0.3.1
@@ -265,7 +278,7 @@ colorama==0.4.6
# via
# -r requirements/edx/testing.txt
# tox
-coverage[toml]==7.5.3
+coverage[toml]==7.6.1
# via
# -r requirements/edx/testing.txt
# pytest-cov
@@ -281,6 +294,7 @@ cryptography==42.0.8
# edx-enterprise
# jwcrypto
# optimizely-sdk
+ # paramiko
# pgpy
# pyjwt
# pyopenssl
@@ -309,7 +323,7 @@ defusedxml==0.7.1
# ora2
# python3-openid
# social-auth-core
-diff-cover==9.0.0
+diff-cover==9.1.1
# via -r requirements/edx/testing.txt
dill==0.3.8
# via
@@ -319,7 +333,7 @@ distlib==0.3.8
# via
# -r requirements/edx/testing.txt
# virtualenv
-django==4.2.13
+django==4.2.15
# via
# -c requirements/edx/../common_constraints.txt
# -c requirements/edx/../constraints.txt
@@ -340,6 +354,7 @@ django==4.2.13
# django-multi-email-field
# django-mysql
# django-oauth-toolkit
+ # django-push-notifications
# django-sekizai
# django-ses
# django-statici18n
@@ -390,6 +405,7 @@ django==4.2.13
# openedx-filters
# openedx-learning
# ora2
+ # social-auth-app-django
# super-csv
# xblock-google-drive
# xss-utils
@@ -419,7 +435,7 @@ django-config-models==2.7.0
# edx-enterprise
# edx-name-affirmation
# lti-consumer-xblock
-django-cors-headers==4.3.1
+django-cors-headers==4.4.0
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
@@ -438,14 +454,14 @@ django-crum==0.7.9
# edx-rbac
# edx-toggles
# super-csv
-django-debug-toolbar==4.4.2
+django-debug-toolbar==4.4.6
# via -r requirements/edx/development.in
django-fernet-fields-v2==0.9
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
# edx-enterprise
-django-filter==24.2
+django-filter==24.3
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
@@ -495,7 +511,7 @@ django-multi-email-field==0.7.0
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
# edx-enterprise
-django-mysql==4.13.0
+django-mysql==4.14.0
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
@@ -514,6 +530,11 @@ django-pipeline==3.1.0
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
+django-push-notifications==3.1.0
+ # via
+ # -r requirements/edx/doc.txt
+ # -r requirements/edx/testing.txt
+ # edx-ace
django-ratelimit==4.1.0
# via
# -r requirements/edx/doc.txt
@@ -546,6 +567,7 @@ django-statici18n==2.5.0
# xblock-poll
django-storages==1.14.3
# via
+ # -c requirements/edx/../constraints.txt
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
# edxval
@@ -554,7 +576,7 @@ django-stubs==1.16.0
# -c requirements/edx/../constraints.txt
# -r requirements/edx/development.in
# djangorestframework-stubs
-django-stubs-ext==5.0.2
+django-stubs-ext==5.0.4
# via django-stubs
django-user-tasks==3.2.0
# via
@@ -607,8 +629,9 @@ djangorestframework-xml==2.0.0
# edx-enterprise
dnspython==2.6.1
# via
+ # -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
- # email-validator
+ # pymongo
docutils==0.21.2
# via
# -r requirements/edx/doc.txt
@@ -634,7 +657,7 @@ drf-yasg==1.21.7
# -r requirements/edx/testing.txt
# django-user-tasks
# edx-api-doc-tools
-edx-ace==1.8.0
+edx-ace==1.11.1
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
@@ -673,7 +696,7 @@ edx-codejail==3.4.1
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
-edx-completion==4.6.2
+edx-completion==4.6.7
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
@@ -687,11 +710,12 @@ edx-django-sites-extensions==4.2.0
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
-edx-django-utils==5.14.2
+edx-django-utils==5.15.0
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
# django-config-models
+ # edx-ace
# edx-drf-extensions
# edx-enterprise
# edx-event-bus-kafka
@@ -717,12 +741,12 @@ edx-drf-extensions==10.3.0
# edx-when
# edxval
# openedx-learning
-edx-enterprise==4.20.5
+edx-enterprise==4.25.10
# via
# -c requirements/edx/../constraints.txt
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
-edx-event-bus-kafka==5.7.0
+edx-event-bus-kafka==5.8.1
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
@@ -736,13 +760,13 @@ edx-i18n-tools==1.5.0
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
# ora2
-edx-lint==5.3.6
+edx-lint==5.3.7
# via -r requirements/edx/testing.txt
edx-milestones==0.6.0
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
-edx-name-affirmation==2.3.7
+edx-name-affirmation==2.4.0
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
@@ -776,13 +800,13 @@ edx-rbac==1.9.0
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
# edx-enterprise
-edx-rest-api-client==5.6.1
+edx-rest-api-client==5.7.1
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
# edx-enterprise
# edx-proctoring
-edx-search==3.9.1
+edx-search==4.0.0
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
@@ -790,7 +814,7 @@ edx-sga==0.25.0
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
-edx-submissions==3.7.1
+edx-submissions==3.7.7
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
@@ -832,21 +856,18 @@ elasticsearch==7.13.4
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
# edx-search
-email-validator==2.1.1
- # via
- # -r requirements/edx/testing.txt
- # fastapi
enmerkar==0.7.1
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
# enmerkar-underscore
-enmerkar-underscore==2.3.0
+enmerkar-underscore==2.3.1
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
-event-tracking==2.4.0
+event-tracking==3.0.0
# via
+ # -c requirements/edx/../constraints.txt
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
# edx-completion
@@ -856,32 +877,33 @@ execnet==2.1.1
# via
# -r requirements/edx/testing.txt
# pytest-xdist
-factory-boy==3.3.0
+factory-boy==3.3.1
# via -r requirements/edx/testing.txt
-faker==25.8.0
+faker==27.0.0
# via
# -r requirements/edx/testing.txt
# factory-boy
-fastapi==0.111.0
+fastapi==0.112.1
# via
# -r requirements/edx/testing.txt
# pact-python
-fastapi-cli==0.0.4
- # via
- # -r requirements/edx/testing.txt
- # fastapi
-fastavro==1.9.4
+fastavro==1.9.5
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
# openedx-events
-filelock==3.14.0
+filelock==3.15.4
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
# snowflake-connector-python
# tox
# virtualenv
+firebase-admin==6.5.0
+ # via
+ # -r requirements/edx/doc.txt
+ # -r requirements/edx/testing.txt
+ # edx-ace
freezegun==1.5.1
# via -r requirements/edx/testing.txt
frozenlist==1.4.1
@@ -921,18 +943,90 @@ glob2==0.7
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
-grimp==3.2
+google-api-core[grpc]==2.19.1
+ # via
+ # -r requirements/edx/doc.txt
+ # -r requirements/edx/testing.txt
+ # firebase-admin
+ # google-api-python-client
+ # google-cloud-core
+ # google-cloud-firestore
+ # google-cloud-storage
+google-api-python-client==2.141.0
+ # via
+ # -r requirements/edx/doc.txt
+ # -r requirements/edx/testing.txt
+ # firebase-admin
+google-auth==2.34.0
+ # via
+ # -r requirements/edx/doc.txt
+ # -r requirements/edx/testing.txt
+ # google-api-core
+ # google-api-python-client
+ # google-auth-httplib2
+ # google-cloud-core
+ # google-cloud-firestore
+ # google-cloud-storage
+google-auth-httplib2==0.2.0
+ # via
+ # -r requirements/edx/doc.txt
+ # -r requirements/edx/testing.txt
+ # google-api-python-client
+google-cloud-core==2.4.1
+ # via
+ # -r requirements/edx/doc.txt
+ # -r requirements/edx/testing.txt
+ # google-cloud-firestore
+ # google-cloud-storage
+google-cloud-firestore==2.17.2
+ # via
+ # -r requirements/edx/doc.txt
+ # -r requirements/edx/testing.txt
+ # firebase-admin
+google-cloud-storage==2.18.2
+ # via
+ # -r requirements/edx/doc.txt
+ # -r requirements/edx/testing.txt
+ # firebase-admin
+google-crc32c==1.5.0
+ # via
+ # -r requirements/edx/doc.txt
+ # -r requirements/edx/testing.txt
+ # google-cloud-storage
+ # google-resumable-media
+google-resumable-media==2.7.2
+ # via
+ # -r requirements/edx/doc.txt
+ # -r requirements/edx/testing.txt
+ # google-cloud-storage
+googleapis-common-protos==1.63.2
+ # via
+ # -r requirements/edx/doc.txt
+ # -r requirements/edx/testing.txt
+ # google-api-core
+ # grpcio-status
+grimp==3.4.1
# via
# -r requirements/edx/testing.txt
# import-linter
-gunicorn==22.0.0
+grpcio==1.65.5
+ # via
+ # -r requirements/edx/doc.txt
+ # -r requirements/edx/testing.txt
+ # google-api-core
+ # grpcio-status
+grpcio-status==1.65.5
+ # via
+ # -r requirements/edx/doc.txt
+ # -r requirements/edx/testing.txt
+ # google-api-core
+gunicorn==23.0.0
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
h11==0.14.0
# via
# -r requirements/edx/testing.txt
- # httpcore
# uvicorn
help-tokens==2.4.0
# via
@@ -943,21 +1037,15 @@ html5lib==1.1
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
# ora2
-httpcore==1.0.5
+httplib2==0.22.0
# via
+ # -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
- # httpx
+ # google-api-python-client
+ # google-auth-httplib2
httpretty==1.1.4
# via -r requirements/edx/testing.txt
-httptools==0.6.1
- # via
- # -r requirements/edx/testing.txt
- # uvicorn
-httpx==0.27.0
- # via
- # -r requirements/edx/testing.txt
- # fastapi
-icalendar==5.0.12
+icalendar==5.0.13
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
@@ -966,8 +1054,6 @@ idna==3.7
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
# anyio
- # email-validator
- # httpx
# optimizely-sdk
# requests
# snowflake-connector-python
@@ -978,9 +1064,8 @@ imagesize==1.4.1
# sphinx
import-linter==2.0
# via -r requirements/edx/testing.txt
-importlib-metadata==6.11.0
+importlib-metadata==8.3.0
# via
- # -c requirements/edx/../common_constraints.txt
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
inflection==0.5.1
@@ -1017,7 +1102,6 @@ jinja2==3.1.4
# -r requirements/edx/testing.txt
# code-annotations
# diff-cover
- # fastapi
# sphinx
jmespath==1.0.1
# via
@@ -1030,7 +1114,7 @@ joblib==1.4.2
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
# nltk
-jsondiff==2.0.0
+jsondiff==2.2.0
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
@@ -1045,7 +1129,7 @@ jsonfield==3.1.0
# edx-submissions
# lti-consumer-xblock
# ora2
-jsonschema==4.22.0
+jsonschema==4.23.0
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
@@ -1063,7 +1147,7 @@ jwcrypto==1.5.6
# -r requirements/edx/testing.txt
# django-oauth-toolkit
# pylti1p3
-kombu==5.3.7
+kombu==5.4.0
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
@@ -1134,10 +1218,6 @@ markdown==3.3.7
# openedx-django-wiki
# staff-graded-xblock
# xblock-poll
-markdown-it-py==3.0.0
- # via
- # -r requirements/edx/testing.txt
- # rich
markupsafe==2.1.5
# via
# -r requirements/edx/doc.txt
@@ -1156,11 +1236,7 @@ mccabe==0.7.0
# via
# -r requirements/edx/testing.txt
# pylint
-mdurl==0.1.2
- # via
- # -r requirements/edx/testing.txt
- # markdown-it-py
-meilisearch==0.31.3
+meilisearch==0.31.4
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
@@ -1182,7 +1258,7 @@ monotonic==1.6
# -r requirements/edx/testing.txt
# analytics-python
# py2neo
-more-itertools==10.3.0
+more-itertools==10.4.0
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
@@ -1192,13 +1268,18 @@ mpmath==1.3.0
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
# sympy
+msgpack==1.0.8
+ # via
+ # -r requirements/edx/doc.txt
+ # -r requirements/edx/testing.txt
+ # cachecontrol
multidict==6.0.5
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
# aiohttp
# yarl
-mypy==1.10.0
+mypy==1.11.1
# via
# -r requirements/edx/development.in
# django-stubs
@@ -1209,16 +1290,16 @@ mysqlclient==2.2.4
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
-newrelic==9.10.0
+newrelic==9.13.0
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
# edx-django-utils
-nh3==0.2.17
+nh3==0.2.18
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
-nltk==3.8.1
+nltk==3.9.1
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
@@ -1230,6 +1311,7 @@ nodeenv==1.9.1
# -r requirements/edx/testing.txt
numpy==1.26.4
# via
+ # -c requirements/edx/../constraints.txt
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
# chem
@@ -1276,26 +1358,27 @@ openedx-django-wiki==2.1.0
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
-openedx-events==9.10.0
+openedx-events==9.14.0
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
+ # edx-enterprise
# edx-event-bus-kafka
# edx-event-bus-redis
# event-tracking
# ora2
-openedx-filters==1.8.1
+openedx-filters==1.9.0
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
# lti-consumer-xblock
# ora2
-openedx-learning==0.10.0
+openedx-learning==0.11.4
# via
# -c requirements/edx/../constraints.txt
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
-openedx-mongodbproxy==0.2.0
+openedx-mongodbproxy==0.2.1
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
@@ -1304,14 +1387,10 @@ optimizely-sdk==4.1.1
# -c requirements/edx/../constraints.txt
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
-ora2==6.11.1
+ora2==6.11.2
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
-orjson==3.10.4
- # via
- # -r requirements/edx/testing.txt
- # fastapi
packaging==24.1
# via
# -r requirements/edx/../pip-tools.txt
@@ -1327,13 +1406,18 @@ packaging==24.1
# snowflake-connector-python
# sphinx
# tox
-pact-python==2.2.0
+pact-python==2.2.1
# via -r requirements/edx/testing.txt
pansi==2020.7.3
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
# py2neo
+paramiko==3.4.1
+ # via
+ # -r requirements/edx/doc.txt
+ # -r requirements/edx/testing.txt
+ # edx-enterprise
path==16.11.0
# via
# -c requirements/edx/../constraints.txt
@@ -1370,7 +1454,7 @@ piexif==1.1.3
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
-pillow==10.3.0
+pillow==10.4.0
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
@@ -1403,7 +1487,22 @@ prompt-toolkit==3.0.47
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
# click-repl
-psutil==5.9.8
+proto-plus==1.24.0
+ # via
+ # -r requirements/edx/doc.txt
+ # -r requirements/edx/testing.txt
+ # google-api-core
+ # google-cloud-firestore
+protobuf==5.27.3
+ # via
+ # -r requirements/edx/doc.txt
+ # -r requirements/edx/testing.txt
+ # google-api-core
+ # google-cloud-firestore
+ # googleapis-common-protos
+ # grpcio-status
+ # proto-plus
+psutil==6.0.0
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
@@ -1422,6 +1521,13 @@ pyasn1==0.6.0
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
# pgpy
+ # pyasn1-modules
+ # rsa
+pyasn1-modules==0.4.0
+ # via
+ # -r requirements/edx/doc.txt
+ # -r requirements/edx/testing.txt
+ # google-auth
pycodestyle==2.8.0
# via
# -c requirements/edx/../constraints.txt
@@ -1442,18 +1548,18 @@ pycryptodomex==3.20.0
# edx-proctoring
# lti-consumer-xblock
# pyjwkest
-pydantic==2.7.3
+pydantic==2.8.2
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
# camel-converter
# fastapi
-pydantic-core==2.18.4
+pydantic-core==2.20.1
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
# pydantic
-pydata-sphinx-theme==0.15.3
+pydata-sphinx-theme==0.15.4
# via
# -r requirements/edx/doc.txt
# sphinx-book-theme
@@ -1465,7 +1571,6 @@ pygments==2.18.0
# diff-cover
# py2neo
# pydata-sphinx-theme
- # rich
# sphinx
# sphinx-mdinclude
pyjwkest==1.4.2
@@ -1474,7 +1579,7 @@ pyjwkest==1.4.2
# -r requirements/edx/testing.txt
# edx-token-utils
# lti-consumer-xblock
-pyjwt[crypto]==2.8.0
+pyjwt[crypto]==2.9.0
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
@@ -1483,6 +1588,7 @@ pyjwt[crypto]==2.8.0
# edx-drf-extensions
# edx-proctoring
# edx-rest-api-client
+ # firebase-admin
# pylti1p3
# snowflake-connector-python
# social-auth-core
@@ -1523,7 +1629,7 @@ pymemcache==4.0.0
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
-pymongo==3.13.0
+pymongo==4.4.0
# via
# -c requirements/edx/../constraints.txt
# -r requirements/edx/doc.txt
@@ -1537,11 +1643,12 @@ pynacl==1.5.0
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
# edx-django-utils
+ # paramiko
pynliner==0.8.0
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
-pyopenssl==24.1.0
+pyopenssl==24.2.1
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
@@ -1552,8 +1659,9 @@ pyparsing==3.1.2
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
# chem
+ # httplib2
# openedx-calc
-pyproject-api==1.6.1
+pyproject-api==1.7.1
# via
# -r requirements/edx/testing.txt
# tox
@@ -1574,7 +1682,7 @@ pysrt==1.1.2
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
# edxval
-pytest==8.2.2
+pytest==8.3.2
# via
# -r requirements/edx/testing.txt
# pylint-pytest
@@ -1617,10 +1725,6 @@ python-dateutil==2.9.0.post0
# olxcleaner
# ora2
# xblock
-python-dotenv==1.0.1
- # via
- # -r requirements/edx/testing.txt
- # uvicorn
python-ipware==3.0.0
# via
# -r requirements/edx/doc.txt
@@ -1630,10 +1734,6 @@ python-memcached==1.62
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
-python-multipart==0.0.9
- # via
- # -r requirements/edx/testing.txt
- # fastapi
python-slugify==8.0.4
# via
# -r requirements/edx/doc.txt
@@ -1679,7 +1779,7 @@ pyuca==1.2
# -r requirements/edx/testing.txt
pywatchman==2.0.0
# via -r requirements/edx/development.in
-pyyaml==6.0.1
+pyyaml==6.0.2
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
@@ -1688,8 +1788,8 @@ pyyaml==6.0.1
# drf-yasg
# edx-django-release-util
# edx-i18n-tools
+ # jsondiff
# sphinxcontrib-openapi
- # uvicorn
# xblock
random2==1.0.2
# via
@@ -1699,7 +1799,7 @@ recommender-xblock==2.2.0
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
-redis==5.0.5
+redis==5.0.8
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
@@ -1710,7 +1810,7 @@ referencing==0.35.1
# -r requirements/edx/testing.txt
# jsonschema
# jsonschema-specifications
-regex==2024.5.15
+regex==2024.7.24
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
@@ -1721,6 +1821,7 @@ requests==2.32.3
# -r requirements/edx/testing.txt
# algoliasearch
# analytics-python
+ # cachecontrol
# django-oauth-toolkit
# djangorestframework-stubs
# edx-bulk-grades
@@ -1728,6 +1829,8 @@ requests==2.32.3
# edx-enterprise
# edx-rest-api-client
# geoip2
+ # google-api-core
+ # google-cloud-storage
# mailsnake
# meilisearch
# openai
@@ -1748,16 +1851,17 @@ requests-oauthlib==2.0.0
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
# social-auth-core
-rich==13.7.1
- # via
- # -r requirements/edx/testing.txt
- # typer
-rpds-py==0.18.1
+rpds-py==0.20.0
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
# jsonschema
# referencing
+rsa==4.9
+ # via
+ # -r requirements/edx/doc.txt
+ # -r requirements/edx/testing.txt
+ # google-auth
rules==3.4
# via
# -r requirements/edx/doc.txt
@@ -1765,7 +1869,7 @@ rules==3.4
# edx-enterprise
# edx-proctoring
# openedx-learning
-s3transfer==0.10.1
+s3transfer==0.10.2
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
@@ -1775,7 +1879,7 @@ sailthru-client==2.2.3
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
# edx-ace
-scipy==1.13.1
+scipy==1.14.0
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
@@ -1786,15 +1890,11 @@ semantic-version==2.10.0
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
# edx-drf-extensions
-shapely==2.0.4
+shapely==2.0.6
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
-shellingham==1.5.4
- # via
- # -r requirements/edx/testing.txt
- # typer
-simplejson==3.19.2
+simplejson==3.19.3
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
@@ -1851,25 +1951,23 @@ sniffio==1.3.1
# via
# -r requirements/edx/testing.txt
# anyio
- # httpx
snowballstemmer==2.2.0
# via
# -r requirements/edx/doc.txt
# sphinx
-snowflake-connector-python==3.10.1
+snowflake-connector-python==3.12.0
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
# edx-enterprise
-social-auth-app-django==5.0.0
+social-auth-app-django==5.4.1
# via
# -c requirements/edx/../constraints.txt
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
# edx-auth-backends
-social-auth-core==4.3.0
+social-auth-core==4.5.4
# via
- # -c requirements/edx/../constraints.txt
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
# edx-auth-backends
@@ -1884,12 +1982,12 @@ sortedcontainers==2.4.0
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
# snowflake-connector-python
-soupsieve==2.5
+soupsieve==2.6
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
# beautifulsoup4
-sphinx==7.3.7
+sphinx==8.0.2
# via
# -r requirements/edx/doc.txt
# pydata-sphinx-theme
@@ -1900,25 +1998,25 @@ sphinx==7.3.7
# sphinxcontrib-httpdomain
# sphinxcontrib-openapi
# sphinxext-rediraffe
-sphinx-book-theme==1.1.2
+sphinx-book-theme==1.1.3
# via -r requirements/edx/doc.txt
-sphinx-design==0.6.0
+sphinx-design==0.6.1
# via -r requirements/edx/doc.txt
-sphinx-mdinclude==0.6.1
+sphinx-mdinclude==0.6.2
# via
# -r requirements/edx/doc.txt
# sphinxcontrib-openapi
-sphinx-reredirects==0.1.3
+sphinx-reredirects==0.1.5
# via -r requirements/edx/doc.txt
-sphinxcontrib-applehelp==1.0.8
+sphinxcontrib-applehelp==2.0.0
# via
# -r requirements/edx/doc.txt
# sphinx
-sphinxcontrib-devhelp==1.0.6
+sphinxcontrib-devhelp==2.0.0
# via
# -r requirements/edx/doc.txt
# sphinx
-sphinxcontrib-htmlhelp==2.0.5
+sphinxcontrib-htmlhelp==2.1.0
# via
# -r requirements/edx/doc.txt
# sphinx
@@ -1932,17 +2030,17 @@ sphinxcontrib-jsmath==1.0.1
# sphinx
sphinxcontrib-openapi[markdown]==0.8.4
# via -r requirements/edx/doc.txt
-sphinxcontrib-qthelp==1.0.7
+sphinxcontrib-qthelp==2.0.0
# via
# -r requirements/edx/doc.txt
# sphinx
-sphinxcontrib-serializinghtml==1.1.10
+sphinxcontrib-serializinghtml==2.0.0
# via
# -r requirements/edx/doc.txt
# sphinx
sphinxext-rediraffe==0.2.7
# via -r requirements/edx/doc.txt
-sqlparse==0.5.0
+sqlparse==0.5.1
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
@@ -1952,7 +2050,7 @@ staff-graded-xblock==2.3.0
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
-starlette==0.37.2
+starlette==0.38.2
# via
# -r requirements/edx/testing.txt
# fastapi
@@ -1970,7 +2068,7 @@ super-csv==3.2.0
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
# edx-bulk-grades
-sympy==1.12.1
+sympy==1.13.2
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
@@ -1992,27 +2090,23 @@ tinycss2==1.2.1
# bleach
tomli==2.0.1
# via django-stubs
-tomlkit==0.12.5
+tomlkit==0.13.2
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
# pylint
# snowflake-connector-python
-tox==4.15.1
+tox==4.18.0
# via -r requirements/edx/testing.txt
-tqdm==4.66.4
+tqdm==4.66.5
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
# nltk
# openai
-typer==0.12.3
- # via
- # -r requirements/edx/testing.txt
- # fastapi-cli
types-pytz==2024.1.0.20240417
# via django-stubs
-types-pyyaml==6.0.12.20240311
+types-pyyaml==6.0.12.20240808
# via
# django-stubs
# djangorestframework-stubs
@@ -2039,16 +2133,11 @@ typing-extensions==4.12.2
# pydata-sphinx-theme
# pylti1p3
# snowflake-connector-python
- # typer
tzdata==2024.1
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
# celery
-ujson==5.10.0
- # via
- # -r requirements/edx/testing.txt
- # fastapi
unicodecsv==0.14.1
# via
# -r requirements/edx/doc.txt
@@ -2062,7 +2151,8 @@ uritemplate==4.1.1
# -r requirements/edx/testing.txt
# drf-spectacular
# drf-yasg
-urllib3==1.26.18
+ # google-api-python-client
+urllib3==1.26.19
# via
# -c requirements/edx/../constraints.txt
# -r requirements/edx/doc.txt
@@ -2075,15 +2165,10 @@ user-util==1.1.0
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
-uvicorn[standard]==0.30.1
+uvicorn==0.30.6
# via
# -r requirements/edx/testing.txt
- # fastapi
# pact-python
-uvloop==0.19.0
- # via
- # -r requirements/edx/testing.txt
- # uvicorn
vine==5.1.0
# via
# -r requirements/edx/doc.txt
@@ -2091,31 +2176,27 @@ vine==5.1.0
# amqp
# celery
# kombu
-virtualenv==20.26.2
+virtualenv==20.26.3
# via
# -r requirements/edx/testing.txt
# tox
-voluptuous==0.14.2
+voluptuous==0.15.2
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
# ora2
vulture==2.11
# via -r requirements/edx/development.in
-walrus==0.9.3
+walrus==0.9.4
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
# edx-event-bus-redis
-watchdog==4.0.1
+watchdog==4.0.2
# via
# -r requirements/edx/development.in
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
-watchfiles==0.22.0
- # via
- # -r requirements/edx/testing.txt
- # uvicorn
wcwidth==0.2.13
# via
# -r requirements/edx/doc.txt
@@ -2137,16 +2218,12 @@ webencodings==0.5.1
# bleach
# html5lib
# tinycss2
-webob==1.8.7
+webob==1.8.8
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
# xblock
-websockets==12.0
- # via
- # -r requirements/edx/testing.txt
- # uvicorn
-wheel==0.43.0
+wheel==0.44.0
# via
# -r requirements/edx/../pip-tools.txt
# pip-tools
@@ -2155,7 +2232,7 @@ wrapt==1.16.0
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
# astroid
-xblock[django]==4.0.1
+xblock[django]==5.1.0
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
@@ -2171,7 +2248,7 @@ xblock[django]==4.0.1
# xblock-drag-and-drop-v2
# xblock-google-drive
# xblock-utils
-xblock-drag-and-drop-v2==4.0.2
+xblock-drag-and-drop-v2==4.0.3
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
@@ -2205,7 +2282,7 @@ yarl==1.9.4
# -r requirements/edx/testing.txt
# aiohttp
# pact-python
-zipp==3.19.2
+zipp==3.20.0
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
diff --git a/requirements/edx/doc.txt b/requirements/edx/doc.txt
index d16d139f6234..defc09a23deb 100644
--- a/requirements/edx/doc.txt
+++ b/requirements/edx/doc.txt
@@ -10,7 +10,11 @@ accessible-pygments==0.0.5
# via pydata-sphinx-theme
acid-xblock==0.3.1
# via -r requirements/edx/base.txt
-aiohttp==3.9.5
+aiohappyeyeballs==2.4.0
+ # via
+ # -r requirements/edx/base.txt
+ # aiohttp
+aiohttp==3.10.5
# via
# -r requirements/edx/base.txt
# geoip2
@@ -19,10 +23,12 @@ aiosignal==1.3.1
# via
# -r requirements/edx/base.txt
# aiohttp
-alabaster==0.7.16
+alabaster==1.0.0
# via sphinx
algoliasearch==3.0.0
- # via -r requirements/edx/base.txt
+ # via
+ # -c requirements/edx/../constraints.txt
+ # -r requirements/edx/base.txt
amqp==5.2.0
# via
# -r requirements/edx/base.txt
@@ -51,7 +57,7 @@ asn1crypto==1.5.1
# via
# -r requirements/edx/base.txt
# snowflake-connector-python
-attrs==23.2.0
+attrs==24.2.0
# via
# -r requirements/edx/base.txt
# aiohttp
@@ -61,7 +67,7 @@ attrs==23.2.0
# openedx-events
# openedx-learning
# referencing
-babel==2.15.0
+babel==2.16.0
# via
# -r requirements/edx/base.txt
# enmerkar
@@ -72,6 +78,10 @@ backoff==1.10.0
# via
# -r requirements/edx/base.txt
# analytics-python
+bcrypt==4.2.0
+ # via
+ # -r requirements/edx/base.txt
+ # paramiko
beautifulsoup4==4.12.3
# via
# -r requirements/edx/base.txt
@@ -92,19 +102,27 @@ bleach[css]==6.1.0
# xblock-poll
boto==2.49.0
# via -r requirements/edx/base.txt
-boto3==1.34.123
+boto3==1.35.1
# via
# -r requirements/edx/base.txt
# django-ses
# fs-s3fs
# ora2
-botocore==1.34.123
+botocore==1.35.1
# via
# -r requirements/edx/base.txt
# boto3
# s3transfer
bridgekeeper==0.9
# via -r requirements/edx/base.txt
+cachecontrol==0.14.0
+ # via
+ # -r requirements/edx/base.txt
+ # firebase-admin
+cachetools==5.5.0
+ # via
+ # -r requirements/edx/base.txt
+ # google-auth
camel-converter[pydantic]==3.1.2
# via
# -r requirements/edx/base.txt
@@ -119,14 +137,14 @@ celery==5.4.0
# edx-enterprise
# event-tracking
# openedx-learning
-certifi==2024.6.2
+certifi==2024.7.4
# via
# -r requirements/edx/base.txt
# elasticsearch
# py2neo
# requests
# snowflake-connector-python
-cffi==1.16.0
+cffi==1.17.0
# via
# -r requirements/edx/base.txt
# cryptography
@@ -185,6 +203,7 @@ cryptography==42.0.8
# edx-enterprise
# jwcrypto
# optimizely-sdk
+ # paramiko
# pgpy
# pyjwt
# pyopenssl
@@ -203,7 +222,7 @@ defusedxml==0.7.1
# ora2
# python3-openid
# social-auth-core
-django==4.2.13
+django==4.2.15
# via
# -c requirements/edx/../common_constraints.txt
# -c requirements/edx/../constraints.txt
@@ -222,6 +241,7 @@ django==4.2.13
# django-multi-email-field
# django-mysql
# django-oauth-toolkit
+ # django-push-notifications
# django-sekizai
# django-ses
# django-statici18n
@@ -270,6 +290,7 @@ django==4.2.13
# openedx-filters
# openedx-learning
# ora2
+ # social-auth-app-django
# super-csv
# xblock-google-drive
# xss-utils
@@ -293,7 +314,7 @@ django-config-models==2.7.0
# edx-enterprise
# edx-name-affirmation
# lti-consumer-xblock
-django-cors-headers==4.3.1
+django-cors-headers==4.4.0
# via -r requirements/edx/base.txt
django-countries==7.6.1
# via
@@ -312,7 +333,7 @@ django-fernet-fields-v2==0.9
# via
# -r requirements/edx/base.txt
# edx-enterprise
-django-filter==24.2
+django-filter==24.3
# via
# -r requirements/edx/base.txt
# edx-enterprise
@@ -354,7 +375,7 @@ django-multi-email-field==0.7.0
# via
# -r requirements/edx/base.txt
# edx-enterprise
-django-mysql==4.13.0
+django-mysql==4.14.0
# via -r requirements/edx/base.txt
django-oauth-toolkit==1.7.1
# via
@@ -367,6 +388,10 @@ django-object-actions==4.2.0
# edx-enterprise
django-pipeline==3.1.0
# via -r requirements/edx/base.txt
+django-push-notifications==3.1.0
+ # via
+ # -r requirements/edx/base.txt
+ # edx-ace
django-ratelimit==4.1.0
# via -r requirements/edx/base.txt
django-sekizai==4.1.0
@@ -392,6 +417,7 @@ django-statici18n==2.5.0
# xblock-poll
django-storages==1.14.3
# via
+ # -c requirements/edx/../constraints.txt
# -r requirements/edx/base.txt
# edxval
django-user-tasks==3.2.0
@@ -433,6 +459,10 @@ djangorestframework-xml==2.0.0
# via
# -r requirements/edx/base.txt
# edx-enterprise
+dnspython==2.6.1
+ # via
+ # -r requirements/edx/base.txt
+ # pymongo
docutils==0.21.2
# via
# pydata-sphinx-theme
@@ -451,7 +481,7 @@ drf-yasg==1.21.7
# -r requirements/edx/base.txt
# django-user-tasks
# edx-api-doc-tools
-edx-ace==1.8.0
+edx-ace==1.11.1
# via -r requirements/edx/base.txt
edx-api-doc-tools==1.8.0
# via
@@ -479,7 +509,7 @@ edx-celeryutils==1.3.0
# super-csv
edx-codejail==3.4.1
# via -r requirements/edx/base.txt
-edx-completion==4.6.2
+edx-completion==4.6.7
# via -r requirements/edx/base.txt
edx-django-release-util==1.4.0
# via
@@ -488,10 +518,11 @@ edx-django-release-util==1.4.0
# edxval
edx-django-sites-extensions==4.2.0
# via -r requirements/edx/base.txt
-edx-django-utils==5.14.2
+edx-django-utils==5.15.0
# via
# -r requirements/edx/base.txt
# django-config-models
+ # edx-ace
# edx-drf-extensions
# edx-enterprise
# edx-event-bus-kafka
@@ -516,11 +547,11 @@ edx-drf-extensions==10.3.0
# edx-when
# edxval
# openedx-learning
-edx-enterprise==4.20.5
+edx-enterprise==4.25.10
# via
# -c requirements/edx/../constraints.txt
# -r requirements/edx/base.txt
-edx-event-bus-kafka==5.7.0
+edx-event-bus-kafka==5.8.1
# via -r requirements/edx/base.txt
edx-event-bus-redis==0.5.0
# via -r requirements/edx/base.txt
@@ -531,7 +562,7 @@ edx-i18n-tools==1.5.0
# ora2
edx-milestones==0.6.0
# via -r requirements/edx/base.txt
-edx-name-affirmation==2.3.7
+edx-name-affirmation==2.4.0
# via -r requirements/edx/base.txt
edx-opaque-keys[django]==2.10.0
# via
@@ -558,16 +589,16 @@ edx-rbac==1.9.0
# via
# -r requirements/edx/base.txt
# edx-enterprise
-edx-rest-api-client==5.6.1
+edx-rest-api-client==5.7.1
# via
# -r requirements/edx/base.txt
# edx-enterprise
# edx-proctoring
-edx-search==3.9.1
+edx-search==4.0.0
# via -r requirements/edx/base.txt
edx-sga==0.25.0
# via -r requirements/edx/base.txt
-edx-submissions==3.7.1
+edx-submissions==3.7.7
# via
# -r requirements/edx/base.txt
# ora2
@@ -604,22 +635,27 @@ enmerkar==0.7.1
# via
# -r requirements/edx/base.txt
# enmerkar-underscore
-enmerkar-underscore==2.3.0
+enmerkar-underscore==2.3.1
# via -r requirements/edx/base.txt
-event-tracking==2.4.0
+event-tracking==3.0.0
# via
+ # -c requirements/edx/../constraints.txt
# -r requirements/edx/base.txt
# edx-completion
# edx-proctoring
# edx-search
-fastavro==1.9.4
+fastavro==1.9.5
# via
# -r requirements/edx/base.txt
# openedx-events
-filelock==3.14.0
+filelock==3.15.4
# via
# -r requirements/edx/base.txt
# snowflake-connector-python
+firebase-admin==6.5.0
+ # via
+ # -r requirements/edx/base.txt
+ # edx-ace
frozenlist==1.4.1
# via
# -r requirements/edx/base.txt
@@ -647,7 +683,68 @@ gitpython==3.1.43
# via -r requirements/edx/doc.in
glob2==0.7
# via -r requirements/edx/base.txt
-gunicorn==22.0.0
+google-api-core[grpc]==2.19.1
+ # via
+ # -r requirements/edx/base.txt
+ # firebase-admin
+ # google-api-python-client
+ # google-cloud-core
+ # google-cloud-firestore
+ # google-cloud-storage
+google-api-python-client==2.141.0
+ # via
+ # -r requirements/edx/base.txt
+ # firebase-admin
+google-auth==2.34.0
+ # via
+ # -r requirements/edx/base.txt
+ # google-api-core
+ # google-api-python-client
+ # google-auth-httplib2
+ # google-cloud-core
+ # google-cloud-firestore
+ # google-cloud-storage
+google-auth-httplib2==0.2.0
+ # via
+ # -r requirements/edx/base.txt
+ # google-api-python-client
+google-cloud-core==2.4.1
+ # via
+ # -r requirements/edx/base.txt
+ # google-cloud-firestore
+ # google-cloud-storage
+google-cloud-firestore==2.17.2
+ # via
+ # -r requirements/edx/base.txt
+ # firebase-admin
+google-cloud-storage==2.18.2
+ # via
+ # -r requirements/edx/base.txt
+ # firebase-admin
+google-crc32c==1.5.0
+ # via
+ # -r requirements/edx/base.txt
+ # google-cloud-storage
+ # google-resumable-media
+google-resumable-media==2.7.2
+ # via
+ # -r requirements/edx/base.txt
+ # google-cloud-storage
+googleapis-common-protos==1.63.2
+ # via
+ # -r requirements/edx/base.txt
+ # google-api-core
+ # grpcio-status
+grpcio==1.65.5
+ # via
+ # -r requirements/edx/base.txt
+ # google-api-core
+ # grpcio-status
+grpcio-status==1.65.5
+ # via
+ # -r requirements/edx/base.txt
+ # google-api-core
+gunicorn==23.0.0
# via -r requirements/edx/base.txt
help-tokens==2.4.0
# via -r requirements/edx/base.txt
@@ -655,7 +752,12 @@ html5lib==1.1
# via
# -r requirements/edx/base.txt
# ora2
-icalendar==5.0.12
+httplib2==0.22.0
+ # via
+ # -r requirements/edx/base.txt
+ # google-api-python-client
+ # google-auth-httplib2
+icalendar==5.0.13
# via -r requirements/edx/base.txt
idna==3.7
# via
@@ -666,10 +768,8 @@ idna==3.7
# yarl
imagesize==1.4.1
# via sphinx
-importlib-metadata==6.11.0
- # via
- # -c requirements/edx/../common_constraints.txt
- # -r requirements/edx/base.txt
+importlib-metadata==8.3.0
+ # via -r requirements/edx/base.txt
inflection==0.5.1
# via
# -r requirements/edx/base.txt
@@ -699,7 +799,7 @@ joblib==1.4.2
# via
# -r requirements/edx/base.txt
# nltk
-jsondiff==2.0.0
+jsondiff==2.2.0
# via
# -r requirements/edx/base.txt
# edx-enterprise
@@ -712,7 +812,7 @@ jsonfield==3.1.0
# edx-submissions
# lti-consumer-xblock
# ora2
-jsonschema==4.22.0
+jsonschema==4.23.0
# via
# -r requirements/edx/base.txt
# drf-spectacular
@@ -727,7 +827,7 @@ jwcrypto==1.5.6
# -r requirements/edx/base.txt
# django-oauth-toolkit
# pylti1p3
-kombu==5.3.7
+kombu==5.4.0
# via
# -r requirements/edx/base.txt
# celery
@@ -791,7 +891,7 @@ maxminddb==2.6.2
# via
# -r requirements/edx/base.txt
# geoip2
-meilisearch==0.31.3
+meilisearch==0.31.4
# via -r requirements/edx/base.txt
mistune==3.0.2
# via sphinx-mdinclude
@@ -804,7 +904,7 @@ monotonic==1.6
# -r requirements/edx/base.txt
# analytics-python
# py2neo
-more-itertools==10.3.0
+more-itertools==10.4.0
# via
# -r requirements/edx/base.txt
# cssutils
@@ -812,6 +912,10 @@ mpmath==1.3.0
# via
# -r requirements/edx/base.txt
# sympy
+msgpack==1.0.8
+ # via
+ # -r requirements/edx/base.txt
+ # cachecontrol
multidict==6.0.5
# via
# -r requirements/edx/base.txt
@@ -819,13 +923,13 @@ multidict==6.0.5
# yarl
mysqlclient==2.2.4
# via -r requirements/edx/base.txt
-newrelic==9.10.0
+newrelic==9.13.0
# via
# -r requirements/edx/base.txt
# edx-django-utils
-nh3==0.2.17
+nh3==0.2.18
# via -r requirements/edx/base.txt
-nltk==3.8.1
+nltk==3.9.1
# via
# -r requirements/edx/base.txt
# chem
@@ -833,6 +937,7 @@ nodeenv==1.9.1
# via -r requirements/edx/base.txt
numpy==1.26.4
# via
+ # -c requirements/edx/../constraints.txt
# -r requirements/edx/base.txt
# chem
# openedx-calc
@@ -865,29 +970,30 @@ openedx-django-require==2.1.0
# via -r requirements/edx/base.txt
openedx-django-wiki==2.1.0
# via -r requirements/edx/base.txt
-openedx-events==9.10.0
+openedx-events==9.14.0
# via
# -r requirements/edx/base.txt
+ # edx-enterprise
# edx-event-bus-kafka
# edx-event-bus-redis
# event-tracking
# ora2
-openedx-filters==1.8.1
+openedx-filters==1.9.0
# via
# -r requirements/edx/base.txt
# lti-consumer-xblock
# ora2
-openedx-learning==0.10.0
+openedx-learning==0.11.4
# via
# -c requirements/edx/../constraints.txt
# -r requirements/edx/base.txt
-openedx-mongodbproxy==0.2.0
+openedx-mongodbproxy==0.2.1
# via -r requirements/edx/base.txt
optimizely-sdk==4.1.1
# via
# -c requirements/edx/../constraints.txt
# -r requirements/edx/base.txt
-ora2==6.11.1
+ora2==6.11.2
# via -r requirements/edx/base.txt
packaging==24.1
# via
@@ -902,6 +1008,10 @@ pansi==2020.7.3
# via
# -r requirements/edx/base.txt
# py2neo
+paramiko==3.4.1
+ # via
+ # -r requirements/edx/base.txt
+ # edx-enterprise
path==16.11.0
# via
# -c requirements/edx/../constraints.txt
@@ -928,7 +1038,7 @@ picobox==4.0.0
# via sphinxcontrib-openapi
piexif==1.1.3
# via -r requirements/edx/base.txt
-pillow==10.3.0
+pillow==10.4.0
# via
# -r requirements/edx/base.txt
# edx-enterprise
@@ -946,7 +1056,20 @@ prompt-toolkit==3.0.47
# via
# -r requirements/edx/base.txt
# click-repl
-psutil==5.9.8
+proto-plus==1.24.0
+ # via
+ # -r requirements/edx/base.txt
+ # google-api-core
+ # google-cloud-firestore
+protobuf==5.27.3
+ # via
+ # -r requirements/edx/base.txt
+ # google-api-core
+ # google-cloud-firestore
+ # googleapis-common-protos
+ # grpcio-status
+ # proto-plus
+psutil==6.0.0
# via
# -r requirements/edx/base.txt
# edx-django-utils
@@ -958,6 +1081,12 @@ pyasn1==0.6.0
# via
# -r requirements/edx/base.txt
# pgpy
+ # pyasn1-modules
+ # rsa
+pyasn1-modules==0.4.0
+ # via
+ # -r requirements/edx/base.txt
+ # google-auth
pycountry==24.6.1
# via -r requirements/edx/base.txt
pycparser==2.22
@@ -970,15 +1099,15 @@ pycryptodomex==3.20.0
# edx-proctoring
# lti-consumer-xblock
# pyjwkest
-pydantic==2.7.3
+pydantic==2.8.2
# via
# -r requirements/edx/base.txt
# camel-converter
-pydantic-core==2.18.4
+pydantic-core==2.20.1
# via
# -r requirements/edx/base.txt
# pydantic
-pydata-sphinx-theme==0.15.3
+pydata-sphinx-theme==0.15.4
# via sphinx-book-theme
pygments==2.18.0
# via
@@ -993,7 +1122,7 @@ pyjwkest==1.4.2
# -r requirements/edx/base.txt
# edx-token-utils
# lti-consumer-xblock
-pyjwt[crypto]==2.8.0
+pyjwt[crypto]==2.9.0
# via
# -r requirements/edx/base.txt
# drf-jwt
@@ -1001,6 +1130,7 @@ pyjwt[crypto]==2.8.0
# edx-drf-extensions
# edx-proctoring
# edx-rest-api-client
+ # firebase-admin
# pylti1p3
# snowflake-connector-python
# social-auth-core
@@ -1012,7 +1142,7 @@ pylti1p3==2.0.0
# via -r requirements/edx/base.txt
pymemcache==4.0.0
# via -r requirements/edx/base.txt
-pymongo==3.13.0
+pymongo==4.4.0
# via
# -c requirements/edx/../constraints.txt
# -r requirements/edx/base.txt
@@ -1024,9 +1154,10 @@ pynacl==1.5.0
# via
# -r requirements/edx/base.txt
# edx-django-utils
+ # paramiko
pynliner==0.8.0
# via -r requirements/edx/base.txt
-pyopenssl==24.1.0
+pyopenssl==24.2.1
# via
# -r requirements/edx/base.txt
# optimizely-sdk
@@ -1035,6 +1166,7 @@ pyparsing==3.1.2
# via
# -r requirements/edx/base.txt
# chem
+ # httplib2
# openedx-calc
pyrsistent==0.20.0
# via
@@ -1098,7 +1230,7 @@ pytz==2024.1
# xblock
pyuca==1.2
# via -r requirements/edx/base.txt
-pyyaml==6.0.1
+pyyaml==6.0.2
# via
# -r requirements/edx/base.txt
# code-annotations
@@ -1106,13 +1238,14 @@ pyyaml==6.0.1
# drf-yasg
# edx-django-release-util
# edx-i18n-tools
+ # jsondiff
# sphinxcontrib-openapi
# xblock
random2==1.0.2
# via -r requirements/edx/base.txt
recommender-xblock==2.2.0
# via -r requirements/edx/base.txt
-redis==5.0.5
+redis==5.0.8
# via
# -r requirements/edx/base.txt
# walrus
@@ -1121,7 +1254,7 @@ referencing==0.35.1
# -r requirements/edx/base.txt
# jsonschema
# jsonschema-specifications
-regex==2024.5.15
+regex==2024.7.24
# via
# -r requirements/edx/base.txt
# nltk
@@ -1130,12 +1263,15 @@ requests==2.32.3
# -r requirements/edx/base.txt
# algoliasearch
# analytics-python
+ # cachecontrol
# django-oauth-toolkit
# edx-bulk-grades
# edx-drf-extensions
# edx-enterprise
# edx-rest-api-client
# geoip2
+ # google-api-core
+ # google-cloud-storage
# mailsnake
# meilisearch
# openai
@@ -1154,18 +1290,22 @@ requests-oauthlib==2.0.0
# via
# -r requirements/edx/base.txt
# social-auth-core
-rpds-py==0.18.1
+rpds-py==0.20.0
# via
# -r requirements/edx/base.txt
# jsonschema
# referencing
+rsa==4.9
+ # via
+ # -r requirements/edx/base.txt
+ # google-auth
rules==3.4
# via
# -r requirements/edx/base.txt
# edx-enterprise
# edx-proctoring
# openedx-learning
-s3transfer==0.10.1
+s3transfer==0.10.2
# via
# -r requirements/edx/base.txt
# boto3
@@ -1173,7 +1313,7 @@ sailthru-client==2.2.3
# via
# -r requirements/edx/base.txt
# edx-ace
-scipy==1.13.1
+scipy==1.14.0
# via
# -r requirements/edx/base.txt
# chem
@@ -1182,9 +1322,9 @@ semantic-version==2.10.0
# via
# -r requirements/edx/base.txt
# edx-drf-extensions
-shapely==2.0.4
+shapely==2.0.6
# via -r requirements/edx/base.txt
-simplejson==3.19.2
+simplejson==3.19.3
# via
# -r requirements/edx/base.txt
# sailthru-client
@@ -1229,18 +1369,17 @@ smmap==5.0.1
# via gitdb
snowballstemmer==2.2.0
# via sphinx
-snowflake-connector-python==3.10.1
+snowflake-connector-python==3.12.0
# via
# -r requirements/edx/base.txt
# edx-enterprise
-social-auth-app-django==5.0.0
+social-auth-app-django==5.4.1
# via
# -c requirements/edx/../constraints.txt
# -r requirements/edx/base.txt
# edx-auth-backends
-social-auth-core==4.3.0
+social-auth-core==4.5.4
# via
- # -c requirements/edx/../constraints.txt
# -r requirements/edx/base.txt
# edx-auth-backends
# social-auth-app-django
@@ -1252,11 +1391,11 @@ sortedcontainers==2.4.0
# via
# -r requirements/edx/base.txt
# snowflake-connector-python
-soupsieve==2.5
+soupsieve==2.6
# via
# -r requirements/edx/base.txt
# beautifulsoup4
-sphinx==7.3.7
+sphinx==8.0.2
# via
# -r requirements/edx/doc.in
# pydata-sphinx-theme
@@ -1267,19 +1406,19 @@ sphinx==7.3.7
# sphinxcontrib-httpdomain
# sphinxcontrib-openapi
# sphinxext-rediraffe
-sphinx-book-theme==1.1.2
+sphinx-book-theme==1.1.3
# via -r requirements/edx/doc.in
-sphinx-design==0.6.0
+sphinx-design==0.6.1
# via -r requirements/edx/doc.in
-sphinx-mdinclude==0.6.1
+sphinx-mdinclude==0.6.2
# via sphinxcontrib-openapi
-sphinx-reredirects==0.1.3
+sphinx-reredirects==0.1.5
# via -r requirements/edx/doc.in
-sphinxcontrib-applehelp==1.0.8
+sphinxcontrib-applehelp==2.0.0
# via sphinx
-sphinxcontrib-devhelp==1.0.6
+sphinxcontrib-devhelp==2.0.0
# via sphinx
-sphinxcontrib-htmlhelp==2.0.5
+sphinxcontrib-htmlhelp==2.1.0
# via sphinx
sphinxcontrib-httpdomain==1.8.1
# via sphinxcontrib-openapi
@@ -1287,13 +1426,13 @@ sphinxcontrib-jsmath==1.0.1
# via sphinx
sphinxcontrib-openapi[markdown]==0.8.4
# via -r requirements/edx/doc.in
-sphinxcontrib-qthelp==1.0.7
+sphinxcontrib-qthelp==2.0.0
# via sphinx
-sphinxcontrib-serializinghtml==1.1.10
+sphinxcontrib-serializinghtml==2.0.0
# via sphinx
sphinxext-rediraffe==0.2.7
# via -r requirements/edx/doc.in
-sqlparse==0.5.0
+sqlparse==0.5.1
# via
# -r requirements/edx/base.txt
# django
@@ -1311,7 +1450,7 @@ super-csv==3.2.0
# via
# -r requirements/edx/base.txt
# edx-bulk-grades
-sympy==1.12.1
+sympy==1.13.2
# via
# -r requirements/edx/base.txt
# openedx-calc
@@ -1327,11 +1466,11 @@ tinycss2==1.2.1
# via
# -r requirements/edx/base.txt
# bleach
-tomlkit==0.12.5
+tomlkit==0.13.2
# via
# -r requirements/edx/base.txt
# snowflake-connector-python
-tqdm==4.66.4
+tqdm==4.66.5
# via
# -r requirements/edx/base.txt
# nltk
@@ -1360,7 +1499,8 @@ uritemplate==4.1.1
# -r requirements/edx/base.txt
# drf-spectacular
# drf-yasg
-urllib3==1.26.18
+ # google-api-python-client
+urllib3==1.26.19
# via
# -c requirements/edx/../constraints.txt
# -r requirements/edx/base.txt
@@ -1376,15 +1516,15 @@ vine==5.1.0
# amqp
# celery
# kombu
-voluptuous==0.14.2
+voluptuous==0.15.2
# via
# -r requirements/edx/base.txt
# ora2
-walrus==0.9.3
+walrus==0.9.4
# via
# -r requirements/edx/base.txt
# edx-event-bus-redis
-watchdog==4.0.1
+watchdog==4.0.2
# via -r requirements/edx/base.txt
wcwidth==0.2.13
# via
@@ -1404,13 +1544,13 @@ webencodings==0.5.1
# bleach
# html5lib
# tinycss2
-webob==1.8.7
+webob==1.8.8
# via
# -r requirements/edx/base.txt
# xblock
wrapt==1.16.0
# via -r requirements/edx/base.txt
-xblock[django]==4.0.1
+xblock[django]==5.1.0
# via
# -r requirements/edx/base.txt
# acid-xblock
@@ -1425,7 +1565,7 @@ xblock[django]==4.0.1
# xblock-drag-and-drop-v2
# xblock-google-drive
# xblock-utils
-xblock-drag-and-drop-v2==4.0.2
+xblock-drag-and-drop-v2==4.0.3
# via -r requirements/edx/base.txt
xblock-google-drive==0.7.0
# via -r requirements/edx/base.txt
@@ -1447,7 +1587,7 @@ yarl==1.9.4
# via
# -r requirements/edx/base.txt
# aiohttp
-zipp==3.19.2
+zipp==3.20.0
# via
# -r requirements/edx/base.txt
# importlib-metadata
diff --git a/requirements/edx/paver.txt b/requirements/edx/paver.txt
index 7699847940c9..faa0085f1631 100644
--- a/requirements/edx/paver.txt
+++ b/requirements/edx/paver.txt
@@ -4,12 +4,14 @@
#
# make upgrade
#
-certifi==2024.6.2
+certifi==2024.7.4
# via requests
charset-normalizer==2.0.12
# via
# -c requirements/edx/../constraints.txt
# requests
+dnspython==2.6.1
+ # via pymongo
edx-opaque-keys==2.10.0
# via -r requirements/edx/paver.in
idna==3.7
@@ -32,11 +34,11 @@ paver==1.3.4
# via -r requirements/edx/paver.in
pbr==6.0.0
# via stevedore
-psutil==5.9.8
+psutil==6.0.0
# via -r requirements/edx/paver.in
pymemcache==4.0.0
# via -r requirements/edx/paver.in
-pymongo==3.13.0
+pymongo==4.4.0
# via
# -c requirements/edx/../constraints.txt
# -r requirements/edx/paver.in
@@ -55,11 +57,11 @@ stevedore==5.2.0
# edx-opaque-keys
typing-extensions==4.12.2
# via edx-opaque-keys
-urllib3==1.26.18
+urllib3==1.26.19
# via
# -c requirements/edx/../constraints.txt
# requests
-watchdog==4.0.1
+watchdog==4.0.2
# via -r requirements/edx/paver.in
wrapt==1.16.0
# via -r requirements/edx/paver.in
diff --git a/requirements/edx/semgrep.txt b/requirements/edx/semgrep.txt
index 98c261bbd777..292f1319048d 100644
--- a/requirements/edx/semgrep.txt
+++ b/requirements/edx/semgrep.txt
@@ -4,7 +4,7 @@
#
# make upgrade
#
-attrs==23.2.0
+attrs==24.2.0
# via
# glom
# jsonschema
@@ -15,9 +15,9 @@ boltons==21.0.0
# face
# glom
# semgrep
-bracex==2.4
+bracex==2.5
# via wcmatch
-certifi==2024.6.2
+certifi==2024.7.4
# via requests
charset-normalizer==2.0.12
# via
@@ -40,7 +40,7 @@ glom==22.1.0
# via semgrep
idna==3.7
# via requests
-jsonschema==4.22.0
+jsonschema==4.23.0
# via semgrep
jsonschema-specifications==2023.12.1
# via jsonschema
@@ -50,7 +50,7 @@ mdurl==0.1.2
# via markdown-it-py
packaging==24.1
# via semgrep
-peewee==3.17.5
+peewee==3.17.6
# via semgrep
pygments==2.18.0
# via rich
@@ -62,7 +62,7 @@ requests==2.32.3
# via semgrep
rich==13.7.1
# via semgrep
-rpds-py==0.18.1
+rpds-py==0.20.0
# via
# jsonschema
# referencing
@@ -76,7 +76,7 @@ tomli==2.0.1
# via semgrep
typing-extensions==4.12.2
# via semgrep
-urllib3==1.26.18
+urllib3==1.26.19
# via
# -c requirements/edx/../constraints.txt
# requests
diff --git a/requirements/edx/testing.txt b/requirements/edx/testing.txt
index da6e15a2feac..49be07ecec71 100644
--- a/requirements/edx/testing.txt
+++ b/requirements/edx/testing.txt
@@ -8,7 +8,11 @@
# via -r requirements/edx/base.txt
acid-xblock==0.3.1
# via -r requirements/edx/base.txt
-aiohttp==3.9.5
+aiohappyeyeballs==2.4.0
+ # via
+ # -r requirements/edx/base.txt
+ # aiohttp
+aiohttp==3.10.5
# via
# -r requirements/edx/base.txt
# geoip2
@@ -18,7 +22,9 @@ aiosignal==1.3.1
# -r requirements/edx/base.txt
# aiohttp
algoliasearch==3.0.0
- # via -r requirements/edx/base.txt
+ # via
+ # -c requirements/edx/../constraints.txt
+ # -r requirements/edx/base.txt
amqp==5.2.0
# via
# -r requirements/edx/base.txt
@@ -34,10 +40,7 @@ annotated-types==0.7.0
# -r requirements/edx/base.txt
# pydantic
anyio==4.4.0
- # via
- # httpx
- # starlette
- # watchfiles
+ # via starlette
appdirs==1.4.4
# via
# -r requirements/edx/base.txt
@@ -56,7 +59,7 @@ astroid==2.13.5
# via
# pylint
# pylint-celery
-attrs==23.2.0
+attrs==24.2.0
# via
# -r requirements/edx/base.txt
# aiohttp
@@ -66,7 +69,7 @@ attrs==23.2.0
# openedx-events
# openedx-learning
# referencing
-babel==2.15.0
+babel==2.16.0
# via
# -r requirements/edx/base.txt
# enmerkar
@@ -75,6 +78,10 @@ backoff==1.10.0
# via
# -r requirements/edx/base.txt
# analytics-python
+bcrypt==4.2.0
+ # via
+ # -r requirements/edx/base.txt
+ # paramiko
beautifulsoup4==4.12.3
# via
# -r requirements/edx/base.txt
@@ -95,21 +102,28 @@ bleach[css]==6.1.0
# xblock-poll
boto==2.49.0
# via -r requirements/edx/base.txt
-boto3==1.34.123
+boto3==1.35.1
# via
# -r requirements/edx/base.txt
# django-ses
# fs-s3fs
# ora2
-botocore==1.34.123
+botocore==1.35.1
# via
# -r requirements/edx/base.txt
# boto3
# s3transfer
bridgekeeper==0.9
# via -r requirements/edx/base.txt
-cachetools==5.3.3
- # via tox
+cachecontrol==0.14.0
+ # via
+ # -r requirements/edx/base.txt
+ # firebase-admin
+cachetools==5.5.0
+ # via
+ # -r requirements/edx/base.txt
+ # google-auth
+ # tox
camel-converter[pydantic]==3.1.2
# via
# -r requirements/edx/base.txt
@@ -124,16 +138,14 @@ celery==5.4.0
# edx-enterprise
# event-tracking
# openedx-learning
-certifi==2024.6.2
+certifi==2024.7.4
# via
# -r requirements/edx/base.txt
# elasticsearch
- # httpcore
- # httpx
# py2neo
# requests
# snowflake-connector-python
-cffi==1.16.0
+cffi==1.17.0
# via
# -r requirements/edx/base.txt
# cryptography
@@ -170,7 +182,6 @@ click==8.1.6
# import-linter
# nltk
# pact-python
- # typer
# user-util
# uvicorn
click-didyoumean==0.3.1
@@ -198,7 +209,7 @@ codejail-includes==1.0.0
# via -r requirements/edx/base.txt
colorama==0.4.6
# via tox
-coverage[toml]==7.5.3
+coverage[toml]==7.6.1
# via
# -r requirements/edx/coverage.txt
# pytest-cov
@@ -211,6 +222,7 @@ cryptography==42.0.8
# edx-enterprise
# jwcrypto
# optimizely-sdk
+ # paramiko
# pgpy
# pyjwt
# pyopenssl
@@ -233,13 +245,13 @@ defusedxml==0.7.1
# ora2
# python3-openid
# social-auth-core
-diff-cover==9.0.0
+diff-cover==9.1.1
# via -r requirements/edx/coverage.txt
dill==0.3.8
# via pylint
distlib==0.3.8
# via virtualenv
-django==4.2.13
+django==4.2.15
# via
# -c requirements/edx/../common_constraints.txt
# -c requirements/edx/../constraints.txt
@@ -258,6 +270,7 @@ django==4.2.13
# django-multi-email-field
# django-mysql
# django-oauth-toolkit
+ # django-push-notifications
# django-sekizai
# django-ses
# django-statici18n
@@ -306,6 +319,7 @@ django==4.2.13
# openedx-filters
# openedx-learning
# ora2
+ # social-auth-app-django
# super-csv
# xblock-google-drive
# xss-utils
@@ -329,7 +343,7 @@ django-config-models==2.7.0
# edx-enterprise
# edx-name-affirmation
# lti-consumer-xblock
-django-cors-headers==4.3.1
+django-cors-headers==4.4.0
# via -r requirements/edx/base.txt
django-countries==7.6.1
# via
@@ -348,7 +362,7 @@ django-fernet-fields-v2==0.9
# via
# -r requirements/edx/base.txt
# edx-enterprise
-django-filter==24.2
+django-filter==24.3
# via
# -r requirements/edx/base.txt
# edx-enterprise
@@ -390,7 +404,7 @@ django-multi-email-field==0.7.0
# via
# -r requirements/edx/base.txt
# edx-enterprise
-django-mysql==4.13.0
+django-mysql==4.14.0
# via -r requirements/edx/base.txt
django-oauth-toolkit==1.7.1
# via
@@ -403,6 +417,10 @@ django-object-actions==4.2.0
# edx-enterprise
django-pipeline==3.1.0
# via -r requirements/edx/base.txt
+django-push-notifications==3.1.0
+ # via
+ # -r requirements/edx/base.txt
+ # edx-ace
django-ratelimit==4.1.0
# via -r requirements/edx/base.txt
django-sekizai==4.1.0
@@ -428,6 +446,7 @@ django-statici18n==2.5.0
# xblock-poll
django-storages==1.14.3
# via
+ # -c requirements/edx/../constraints.txt
# -r requirements/edx/base.txt
# edxval
django-user-tasks==3.2.0
@@ -470,7 +489,9 @@ djangorestframework-xml==2.0.0
# -r requirements/edx/base.txt
# edx-enterprise
dnspython==2.6.1
- # via email-validator
+ # via
+ # -r requirements/edx/base.txt
+ # pymongo
done-xblock==2.3.0
# via -r requirements/edx/base.txt
drf-jwt==1.19.2
@@ -484,7 +505,7 @@ drf-yasg==1.21.7
# -r requirements/edx/base.txt
# django-user-tasks
# edx-api-doc-tools
-edx-ace==1.8.0
+edx-ace==1.11.1
# via -r requirements/edx/base.txt
edx-api-doc-tools==1.8.0
# via
@@ -512,7 +533,7 @@ edx-celeryutils==1.3.0
# super-csv
edx-codejail==3.4.1
# via -r requirements/edx/base.txt
-edx-completion==4.6.2
+edx-completion==4.6.7
# via -r requirements/edx/base.txt
edx-django-release-util==1.4.0
# via
@@ -521,10 +542,11 @@ edx-django-release-util==1.4.0
# edxval
edx-django-sites-extensions==4.2.0
# via -r requirements/edx/base.txt
-edx-django-utils==5.14.2
+edx-django-utils==5.15.0
# via
# -r requirements/edx/base.txt
# django-config-models
+ # edx-ace
# edx-drf-extensions
# edx-enterprise
# edx-event-bus-kafka
@@ -549,11 +571,11 @@ edx-drf-extensions==10.3.0
# edx-when
# edxval
# openedx-learning
-edx-enterprise==4.20.5
+edx-enterprise==4.25.10
# via
# -c requirements/edx/../constraints.txt
# -r requirements/edx/base.txt
-edx-event-bus-kafka==5.7.0
+edx-event-bus-kafka==5.8.1
# via -r requirements/edx/base.txt
edx-event-bus-redis==0.5.0
# via -r requirements/edx/base.txt
@@ -562,11 +584,11 @@ edx-i18n-tools==1.5.0
# -c requirements/edx/../constraints.txt
# -r requirements/edx/base.txt
# ora2
-edx-lint==5.3.6
+edx-lint==5.3.7
# via -r requirements/edx/testing.in
edx-milestones==0.6.0
# via -r requirements/edx/base.txt
-edx-name-affirmation==2.3.7
+edx-name-affirmation==2.4.0
# via -r requirements/edx/base.txt
edx-opaque-keys[django]==2.10.0
# via
@@ -593,16 +615,16 @@ edx-rbac==1.9.0
# via
# -r requirements/edx/base.txt
# edx-enterprise
-edx-rest-api-client==5.6.1
+edx-rest-api-client==5.7.1
# via
# -r requirements/edx/base.txt
# edx-enterprise
# edx-proctoring
-edx-search==3.9.1
+edx-search==4.0.0
# via -r requirements/edx/base.txt
edx-sga==0.25.0
# via -r requirements/edx/base.txt
-edx-submissions==3.7.1
+edx-submissions==3.7.7
# via
# -r requirements/edx/base.txt
# ora2
@@ -635,40 +657,41 @@ elasticsearch==7.13.4
# -c requirements/edx/../common_constraints.txt
# -r requirements/edx/base.txt
# edx-search
-email-validator==2.1.1
- # via fastapi
enmerkar==0.7.1
# via
# -r requirements/edx/base.txt
# enmerkar-underscore
-enmerkar-underscore==2.3.0
+enmerkar-underscore==2.3.1
# via -r requirements/edx/base.txt
-event-tracking==2.4.0
+event-tracking==3.0.0
# via
+ # -c requirements/edx/../constraints.txt
# -r requirements/edx/base.txt
# edx-completion
# edx-proctoring
# edx-search
execnet==2.1.1
# via pytest-xdist
-factory-boy==3.3.0
+factory-boy==3.3.1
# via -r requirements/edx/testing.in
-faker==25.8.0
+faker==27.0.0
# via factory-boy
-fastapi==0.111.0
+fastapi==0.112.1
# via pact-python
-fastapi-cli==0.0.4
- # via fastapi
-fastavro==1.9.4
+fastavro==1.9.5
# via
# -r requirements/edx/base.txt
# openedx-events
-filelock==3.14.0
+filelock==3.15.4
# via
# -r requirements/edx/base.txt
# snowflake-connector-python
# tox
# virtualenv
+firebase-admin==6.5.0
+ # via
+ # -r requirements/edx/base.txt
+ # edx-ace
freezegun==1.5.1
# via -r requirements/edx/testing.in
frozenlist==1.4.1
@@ -694,46 +717,100 @@ geoip2==4.8.0
# via -r requirements/edx/base.txt
glob2==0.7
# via -r requirements/edx/base.txt
-grimp==3.2
+google-api-core[grpc]==2.19.1
+ # via
+ # -r requirements/edx/base.txt
+ # firebase-admin
+ # google-api-python-client
+ # google-cloud-core
+ # google-cloud-firestore
+ # google-cloud-storage
+google-api-python-client==2.141.0
+ # via
+ # -r requirements/edx/base.txt
+ # firebase-admin
+google-auth==2.34.0
+ # via
+ # -r requirements/edx/base.txt
+ # google-api-core
+ # google-api-python-client
+ # google-auth-httplib2
+ # google-cloud-core
+ # google-cloud-firestore
+ # google-cloud-storage
+google-auth-httplib2==0.2.0
+ # via
+ # -r requirements/edx/base.txt
+ # google-api-python-client
+google-cloud-core==2.4.1
+ # via
+ # -r requirements/edx/base.txt
+ # google-cloud-firestore
+ # google-cloud-storage
+google-cloud-firestore==2.17.2
+ # via
+ # -r requirements/edx/base.txt
+ # firebase-admin
+google-cloud-storage==2.18.2
+ # via
+ # -r requirements/edx/base.txt
+ # firebase-admin
+google-crc32c==1.5.0
+ # via
+ # -r requirements/edx/base.txt
+ # google-cloud-storage
+ # google-resumable-media
+google-resumable-media==2.7.2
+ # via
+ # -r requirements/edx/base.txt
+ # google-cloud-storage
+googleapis-common-protos==1.63.2
+ # via
+ # -r requirements/edx/base.txt
+ # google-api-core
+ # grpcio-status
+grimp==3.4.1
# via import-linter
-gunicorn==22.0.0
+grpcio==1.65.5
+ # via
+ # -r requirements/edx/base.txt
+ # google-api-core
+ # grpcio-status
+grpcio-status==1.65.5
+ # via
+ # -r requirements/edx/base.txt
+ # google-api-core
+gunicorn==23.0.0
# via -r requirements/edx/base.txt
h11==0.14.0
- # via
- # httpcore
- # uvicorn
+ # via uvicorn
help-tokens==2.4.0
# via -r requirements/edx/base.txt
html5lib==1.1
# via
# -r requirements/edx/base.txt
# ora2
-httpcore==1.0.5
- # via httpx
+httplib2==0.22.0
+ # via
+ # -r requirements/edx/base.txt
+ # google-api-python-client
+ # google-auth-httplib2
httpretty==1.1.4
# via -r requirements/edx/testing.in
-httptools==0.6.1
- # via uvicorn
-httpx==0.27.0
- # via fastapi
-icalendar==5.0.12
+icalendar==5.0.13
# via -r requirements/edx/base.txt
idna==3.7
# via
# -r requirements/edx/base.txt
# anyio
- # email-validator
- # httpx
# optimizely-sdk
# requests
# snowflake-connector-python
# yarl
import-linter==2.0
# via -r requirements/edx/testing.in
-importlib-metadata==6.11.0
- # via
- # -c requirements/edx/../common_constraints.txt
- # -r requirements/edx/base.txt
+importlib-metadata==8.3.0
+ # via -r requirements/edx/base.txt
inflection==0.5.1
# via
# -r requirements/edx/base.txt
@@ -761,7 +838,6 @@ jinja2==3.1.4
# -r requirements/edx/coverage.txt
# code-annotations
# diff-cover
- # fastapi
jmespath==1.0.1
# via
# -r requirements/edx/base.txt
@@ -771,7 +847,7 @@ joblib==1.4.2
# via
# -r requirements/edx/base.txt
# nltk
-jsondiff==2.0.0
+jsondiff==2.2.0
# via
# -r requirements/edx/base.txt
# edx-enterprise
@@ -784,7 +860,7 @@ jsonfield==3.1.0
# edx-submissions
# lti-consumer-xblock
# ora2
-jsonschema==4.22.0
+jsonschema==4.23.0
# via
# -r requirements/edx/base.txt
# drf-spectacular
@@ -798,7 +874,7 @@ jwcrypto==1.5.6
# -r requirements/edx/base.txt
# django-oauth-toolkit
# pylti1p3
-kombu==5.3.7
+kombu==5.4.0
# via
# -r requirements/edx/base.txt
# celery
@@ -853,8 +929,6 @@ markdown==3.3.7
# openedx-django-wiki
# staff-graded-xblock
# xblock-poll
-markdown-it-py==3.0.0
- # via rich
markupsafe==2.1.5
# via
# -r requirements/edx/base.txt
@@ -870,9 +944,7 @@ maxminddb==2.6.2
# geoip2
mccabe==0.7.0
# via pylint
-mdurl==0.1.2
- # via markdown-it-py
-meilisearch==0.31.3
+meilisearch==0.31.4
# via -r requirements/edx/base.txt
mock==5.1.0
# via -r requirements/edx/base.txt
@@ -883,7 +955,7 @@ monotonic==1.6
# -r requirements/edx/base.txt
# analytics-python
# py2neo
-more-itertools==10.3.0
+more-itertools==10.4.0
# via
# -r requirements/edx/base.txt
# cssutils
@@ -891,6 +963,10 @@ mpmath==1.3.0
# via
# -r requirements/edx/base.txt
# sympy
+msgpack==1.0.8
+ # via
+ # -r requirements/edx/base.txt
+ # cachecontrol
multidict==6.0.5
# via
# -r requirements/edx/base.txt
@@ -898,13 +974,13 @@ multidict==6.0.5
# yarl
mysqlclient==2.2.4
# via -r requirements/edx/base.txt
-newrelic==9.10.0
+newrelic==9.13.0
# via
# -r requirements/edx/base.txt
# edx-django-utils
-nh3==0.2.17
+nh3==0.2.18
# via -r requirements/edx/base.txt
-nltk==3.8.1
+nltk==3.9.1
# via
# -r requirements/edx/base.txt
# chem
@@ -912,6 +988,7 @@ nodeenv==1.9.1
# via -r requirements/edx/base.txt
numpy==1.26.4
# via
+ # -c requirements/edx/../constraints.txt
# -r requirements/edx/base.txt
# chem
# openedx-calc
@@ -944,32 +1021,31 @@ openedx-django-require==2.1.0
# via -r requirements/edx/base.txt
openedx-django-wiki==2.1.0
# via -r requirements/edx/base.txt
-openedx-events==9.10.0
+openedx-events==9.14.0
# via
# -r requirements/edx/base.txt
+ # edx-enterprise
# edx-event-bus-kafka
# edx-event-bus-redis
# event-tracking
# ora2
-openedx-filters==1.8.1
+openedx-filters==1.9.0
# via
# -r requirements/edx/base.txt
# lti-consumer-xblock
# ora2
-openedx-learning==0.10.0
+openedx-learning==0.11.4
# via
# -c requirements/edx/../constraints.txt
# -r requirements/edx/base.txt
-openedx-mongodbproxy==0.2.0
+openedx-mongodbproxy==0.2.1
# via -r requirements/edx/base.txt
optimizely-sdk==4.1.1
# via
# -c requirements/edx/../constraints.txt
# -r requirements/edx/base.txt
-ora2==6.11.1
+ora2==6.11.2
# via -r requirements/edx/base.txt
-orjson==3.10.4
- # via fastapi
packaging==24.1
# via
# -r requirements/edx/base.txt
@@ -980,12 +1056,16 @@ packaging==24.1
# pytest
# snowflake-connector-python
# tox
-pact-python==2.2.0
+pact-python==2.2.1
# via -r requirements/edx/testing.in
pansi==2020.7.3
# via
# -r requirements/edx/base.txt
# py2neo
+paramiko==3.4.1
+ # via
+ # -r requirements/edx/base.txt
+ # edx-enterprise
path==16.11.0
# via
# -c requirements/edx/../constraints.txt
@@ -1010,7 +1090,7 @@ pgpy==0.6.0
# edx-enterprise
piexif==1.1.3
# via -r requirements/edx/base.txt
-pillow==10.3.0
+pillow==10.4.0
# via
# -r requirements/edx/base.txt
# edx-enterprise
@@ -1038,7 +1118,20 @@ prompt-toolkit==3.0.47
# via
# -r requirements/edx/base.txt
# click-repl
-psutil==5.9.8
+proto-plus==1.24.0
+ # via
+ # -r requirements/edx/base.txt
+ # google-api-core
+ # google-cloud-firestore
+protobuf==5.27.3
+ # via
+ # -r requirements/edx/base.txt
+ # google-api-core
+ # google-cloud-firestore
+ # googleapis-common-protos
+ # grpcio-status
+ # proto-plus
+psutil==6.0.0
# via
# -r requirements/edx/base.txt
# edx-django-utils
@@ -1054,6 +1147,12 @@ pyasn1==0.6.0
# via
# -r requirements/edx/base.txt
# pgpy
+ # pyasn1-modules
+ # rsa
+pyasn1-modules==0.4.0
+ # via
+ # -r requirements/edx/base.txt
+ # google-auth
pycodestyle==2.8.0
# via
# -c requirements/edx/../constraints.txt
@@ -1070,12 +1169,12 @@ pycryptodomex==3.20.0
# edx-proctoring
# lti-consumer-xblock
# pyjwkest
-pydantic==2.7.3
+pydantic==2.8.2
# via
# -r requirements/edx/base.txt
# camel-converter
# fastapi
-pydantic-core==2.18.4
+pydantic-core==2.20.1
# via
# -r requirements/edx/base.txt
# pydantic
@@ -1085,13 +1184,12 @@ pygments==2.18.0
# -r requirements/edx/coverage.txt
# diff-cover
# py2neo
- # rich
pyjwkest==1.4.2
# via
# -r requirements/edx/base.txt
# edx-token-utils
# lti-consumer-xblock
-pyjwt[crypto]==2.8.0
+pyjwt[crypto]==2.9.0
# via
# -r requirements/edx/base.txt
# drf-jwt
@@ -1099,6 +1197,7 @@ pyjwt[crypto]==2.8.0
# edx-drf-extensions
# edx-proctoring
# edx-rest-api-client
+ # firebase-admin
# pylti1p3
# snowflake-connector-python
# social-auth-core
@@ -1128,7 +1227,7 @@ pylti1p3==2.0.0
# via -r requirements/edx/base.txt
pymemcache==4.0.0
# via -r requirements/edx/base.txt
-pymongo==3.13.0
+pymongo==4.4.0
# via
# -c requirements/edx/../constraints.txt
# -r requirements/edx/base.txt
@@ -1140,9 +1239,10 @@ pynacl==1.5.0
# via
# -r requirements/edx/base.txt
# edx-django-utils
+ # paramiko
pynliner==0.8.0
# via -r requirements/edx/base.txt
-pyopenssl==24.1.0
+pyopenssl==24.2.1
# via
# -r requirements/edx/base.txt
# optimizely-sdk
@@ -1151,8 +1251,9 @@ pyparsing==3.1.2
# via
# -r requirements/edx/base.txt
# chem
+ # httplib2
# openedx-calc
-pyproject-api==1.6.1
+pyproject-api==1.7.1
# via tox
pyquery==2.0.0
# via -r requirements/edx/testing.in
@@ -1164,7 +1265,7 @@ pysrt==1.1.2
# via
# -r requirements/edx/base.txt
# edxval
-pytest==8.2.2
+pytest==8.3.2
# via
# -r requirements/edx/testing.in
# pylint-pytest
@@ -1206,16 +1307,12 @@ python-dateutil==2.9.0.post0
# olxcleaner
# ora2
# xblock
-python-dotenv==1.0.1
- # via uvicorn
python-ipware==3.0.0
# via
# -r requirements/edx/base.txt
# django-ipware
python-memcached==1.62
# via -r requirements/edx/base.txt
-python-multipart==0.0.9
- # via fastapi
python-slugify==8.0.4
# via
# -r requirements/edx/base.txt
@@ -1251,7 +1348,7 @@ pytz==2024.1
# xblock
pyuca==1.2
# via -r requirements/edx/base.txt
-pyyaml==6.0.1
+pyyaml==6.0.2
# via
# -r requirements/edx/base.txt
# code-annotations
@@ -1259,13 +1356,13 @@ pyyaml==6.0.1
# drf-yasg
# edx-django-release-util
# edx-i18n-tools
- # uvicorn
+ # jsondiff
# xblock
random2==1.0.2
# via -r requirements/edx/base.txt
recommender-xblock==2.2.0
# via -r requirements/edx/base.txt
-redis==5.0.5
+redis==5.0.8
# via
# -r requirements/edx/base.txt
# walrus
@@ -1274,7 +1371,7 @@ referencing==0.35.1
# -r requirements/edx/base.txt
# jsonschema
# jsonschema-specifications
-regex==2024.5.15
+regex==2024.7.24
# via
# -r requirements/edx/base.txt
# nltk
@@ -1283,12 +1380,15 @@ requests==2.32.3
# -r requirements/edx/base.txt
# algoliasearch
# analytics-python
+ # cachecontrol
# django-oauth-toolkit
# edx-bulk-grades
# edx-drf-extensions
# edx-enterprise
# edx-rest-api-client
# geoip2
+ # google-api-core
+ # google-cloud-storage
# mailsnake
# meilisearch
# openai
@@ -1307,20 +1407,22 @@ requests-oauthlib==2.0.0
# via
# -r requirements/edx/base.txt
# social-auth-core
-rich==13.7.1
- # via typer
-rpds-py==0.18.1
+rpds-py==0.20.0
# via
# -r requirements/edx/base.txt
# jsonschema
# referencing
+rsa==4.9
+ # via
+ # -r requirements/edx/base.txt
+ # google-auth
rules==3.4
# via
# -r requirements/edx/base.txt
# edx-enterprise
# edx-proctoring
# openedx-learning
-s3transfer==0.10.1
+s3transfer==0.10.2
# via
# -r requirements/edx/base.txt
# boto3
@@ -1328,7 +1430,7 @@ sailthru-client==2.2.3
# via
# -r requirements/edx/base.txt
# edx-ace
-scipy==1.13.1
+scipy==1.14.0
# via
# -r requirements/edx/base.txt
# chem
@@ -1337,11 +1439,9 @@ semantic-version==2.10.0
# via
# -r requirements/edx/base.txt
# edx-drf-extensions
-shapely==2.0.4
+shapely==2.0.6
# via -r requirements/edx/base.txt
-shellingham==1.5.4
- # via typer
-simplejson==3.19.2
+simplejson==3.19.3
# via
# -r requirements/edx/base.txt
# sailthru-client
@@ -1386,21 +1486,18 @@ slumber==0.7.1
# edx-enterprise
# edx-rest-api-client
sniffio==1.3.1
- # via
- # anyio
- # httpx
-snowflake-connector-python==3.10.1
+ # via anyio
+snowflake-connector-python==3.12.0
# via
# -r requirements/edx/base.txt
# edx-enterprise
-social-auth-app-django==5.0.0
+social-auth-app-django==5.4.1
# via
# -c requirements/edx/../constraints.txt
# -r requirements/edx/base.txt
# edx-auth-backends
-social-auth-core==4.3.0
+social-auth-core==4.5.4
# via
- # -c requirements/edx/../constraints.txt
# -r requirements/edx/base.txt
# edx-auth-backends
# social-auth-app-django
@@ -1412,17 +1509,17 @@ sortedcontainers==2.4.0
# via
# -r requirements/edx/base.txt
# snowflake-connector-python
-soupsieve==2.5
+soupsieve==2.6
# via
# -r requirements/edx/base.txt
# beautifulsoup4
-sqlparse==0.5.0
+sqlparse==0.5.1
# via
# -r requirements/edx/base.txt
# django
staff-graded-xblock==2.3.0
# via -r requirements/edx/base.txt
-starlette==0.37.2
+starlette==0.38.2
# via fastapi
stevedore==5.2.0
# via
@@ -1436,7 +1533,7 @@ super-csv==3.2.0
# via
# -r requirements/edx/base.txt
# edx-bulk-grades
-sympy==1.12.1
+sympy==1.13.2
# via
# -r requirements/edx/base.txt
# openedx-calc
@@ -1453,20 +1550,18 @@ tinycss2==1.2.1
# via
# -r requirements/edx/base.txt
# bleach
-tomlkit==0.12.5
+tomlkit==0.13.2
# via
# -r requirements/edx/base.txt
# pylint
# snowflake-connector-python
-tox==4.15.1
+tox==4.18.0
# via -r requirements/edx/testing.in
-tqdm==4.66.4
+tqdm==4.66.5
# via
# -r requirements/edx/base.txt
# nltk
# openai
-typer==0.12.3
- # via fastapi-cli
typing-extensions==4.12.2
# via
# -r requirements/edx/base.txt
@@ -1480,13 +1575,10 @@ typing-extensions==4.12.2
# pydantic-core
# pylti1p3
# snowflake-connector-python
- # typer
tzdata==2024.1
# via
# -r requirements/edx/base.txt
# celery
-ujson==5.10.0
- # via fastapi
unicodecsv==0.14.1
# via
# -r requirements/edx/base.txt
@@ -1498,7 +1590,8 @@ uritemplate==4.1.1
# -r requirements/edx/base.txt
# drf-spectacular
# drf-yasg
-urllib3==1.26.18
+ # google-api-python-client
+urllib3==1.26.19
# via
# -c requirements/edx/../constraints.txt
# -r requirements/edx/base.txt
@@ -1508,32 +1601,26 @@ urllib3==1.26.18
# requests
user-util==1.1.0
# via -r requirements/edx/base.txt
-uvicorn[standard]==0.30.1
- # via
- # fastapi
- # pact-python
-uvloop==0.19.0
- # via uvicorn
+uvicorn==0.30.6
+ # via pact-python
vine==5.1.0
# via
# -r requirements/edx/base.txt
# amqp
# celery
# kombu
-virtualenv==20.26.2
+virtualenv==20.26.3
# via tox
-voluptuous==0.14.2
+voluptuous==0.15.2
# via
# -r requirements/edx/base.txt
# ora2
-walrus==0.9.3
+walrus==0.9.4
# via
# -r requirements/edx/base.txt
# edx-event-bus-redis
-watchdog==4.0.1
+watchdog==4.0.2
# via -r requirements/edx/base.txt
-watchfiles==0.22.0
- # via uvicorn
wcwidth==0.2.13
# via
# -r requirements/edx/base.txt
@@ -1552,17 +1639,15 @@ webencodings==0.5.1
# bleach
# html5lib
# tinycss2
-webob==1.8.7
+webob==1.8.8
# via
# -r requirements/edx/base.txt
# xblock
-websockets==12.0
- # via uvicorn
wrapt==1.16.0
# via
# -r requirements/edx/base.txt
# astroid
-xblock[django]==4.0.1
+xblock[django]==5.1.0
# via
# -r requirements/edx/base.txt
# acid-xblock
@@ -1577,7 +1662,7 @@ xblock[django]==4.0.1
# xblock-drag-and-drop-v2
# xblock-google-drive
# xblock-utils
-xblock-drag-and-drop-v2==4.0.2
+xblock-drag-and-drop-v2==4.0.3
# via -r requirements/edx/base.txt
xblock-google-drive==0.7.0
# via -r requirements/edx/base.txt
@@ -1600,7 +1685,7 @@ yarl==1.9.4
# -r requirements/edx/base.txt
# aiohttp
# pact-python
-zipp==3.19.2
+zipp==3.20.0
# via
# -r requirements/edx/base.txt
# importlib-metadata
diff --git a/requirements/pip-tools.txt b/requirements/pip-tools.txt
index 3630835e943d..f7b35489c353 100644
--- a/requirements/pip-tools.txt
+++ b/requirements/pip-tools.txt
@@ -18,7 +18,7 @@ pyproject-hooks==1.1.0
# via
# build
# pip-tools
-wheel==0.43.0
+wheel==0.44.0
# via pip-tools
# The following packages are considered to be unsafe in a requirements file:
diff --git a/requirements/pip.txt b/requirements/pip.txt
index 78af7cdafdb1..f0cf3d109992 100644
--- a/requirements/pip.txt
+++ b/requirements/pip.txt
@@ -4,11 +4,11 @@
#
# make upgrade
#
-wheel==0.43.0
+wheel==0.44.0
# via -r requirements/pip.in
# The following packages are considered to be unsafe in a requirements file:
-pip==24.0
+pip==24.2
# via -r requirements/pip.in
-setuptools==70.0.0
+setuptools==73.0.0
# via -r requirements/pip.in
diff --git a/scripts/structures_pruning/requirements/base.txt b/scripts/structures_pruning/requirements/base.txt
index 87aa858e9f8e..828a81a8d4ed 100644
--- a/scripts/structures_pruning/requirements/base.txt
+++ b/scripts/structures_pruning/requirements/base.txt
@@ -11,11 +11,13 @@ click==8.1.6
# click-log
click-log==0.4.0
# via -r scripts/structures_pruning/requirements/base.in
+dnspython==2.6.1
+ # via pymongo
edx-opaque-keys==2.10.0
# via -r scripts/structures_pruning/requirements/base.in
pbr==6.0.0
# via stevedore
-pymongo==3.13.0
+pymongo==4.4.0
# via
# -c scripts/structures_pruning/requirements/../../../requirements/constraints.txt
# -r scripts/structures_pruning/requirements/base.in
diff --git a/scripts/structures_pruning/requirements/testing.txt b/scripts/structures_pruning/requirements/testing.txt
index 2590ca8ca52b..47648d50fddb 100644
--- a/scripts/structures_pruning/requirements/testing.txt
+++ b/scripts/structures_pruning/requirements/testing.txt
@@ -12,6 +12,10 @@ click-log==0.4.0
# via -r scripts/structures_pruning/requirements/base.txt
ddt==1.7.2
# via -r scripts/structures_pruning/requirements/testing.in
+dnspython==2.6.1
+ # via
+ # -r scripts/structures_pruning/requirements/base.txt
+ # pymongo
edx-opaque-keys==2.10.0
# via -r scripts/structures_pruning/requirements/base.txt
iniconfig==2.0.0
@@ -24,11 +28,11 @@ pbr==6.0.0
# stevedore
pluggy==1.5.0
# via pytest
-pymongo==3.13.0
+pymongo==4.4.0
# via
# -r scripts/structures_pruning/requirements/base.txt
# edx-opaque-keys
-pytest==8.2.2
+pytest==8.3.2
# via -r scripts/structures_pruning/requirements/testing.in
stevedore==5.2.0
# via
diff --git a/scripts/user_retirement/requirements/base.txt b/scripts/user_retirement/requirements/base.txt
index da08ad4989e1..3e5ed4738070 100644
--- a/scripts/user_retirement/requirements/base.txt
+++ b/scripts/user_retirement/requirements/base.txt
@@ -6,21 +6,21 @@
#
asgiref==3.8.1
# via django
-attrs==23.2.0
+attrs==24.2.0
# via zeep
backoff==2.2.1
# via -r scripts/user_retirement/requirements/base.in
-boto3==1.34.123
+boto3==1.35.1
# via -r scripts/user_retirement/requirements/base.in
-botocore==1.34.123
+botocore==1.35.1
# via
# boto3
# s3transfer
-cachetools==5.3.3
+cachetools==5.5.0
# via google-auth
-certifi==2024.6.2
+certifi==2024.7.4
# via requests
-cffi==1.16.0
+cffi==1.17.0
# via
# cryptography
# pynacl
@@ -33,9 +33,9 @@ click==8.1.6
# -c scripts/user_retirement/requirements/../../../requirements/constraints.txt
# -r scripts/user_retirement/requirements/base.in
# edx-django-utils
-cryptography==42.0.8
+cryptography==43.0.0
# via pyjwt
-django==4.2.13
+django==4.2.15
# via
# -c scripts/user_retirement/requirements/../../../requirements/common_constraints.txt
# -c scripts/user_retirement/requirements/../../../requirements/constraints.txt
@@ -46,22 +46,22 @@ django-crum==0.7.9
# via edx-django-utils
django-waffle==4.1.0
# via edx-django-utils
-edx-django-utils==5.14.2
+edx-django-utils==5.15.0
# via edx-rest-api-client
-edx-rest-api-client==5.7.0
+edx-rest-api-client==5.7.1
# via -r scripts/user_retirement/requirements/base.in
-google-api-core==2.19.0
+google-api-core==2.19.1
# via google-api-python-client
-google-api-python-client==2.133.0
+google-api-python-client==2.141.0
# via -r scripts/user_retirement/requirements/base.in
-google-auth==2.30.0
+google-auth==2.34.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.63.1
+googleapis-common-protos==1.63.2
# via google-api-core
httplib2==0.22.0
# via
@@ -81,22 +81,22 @@ lxml==4.9.4
# via
# -c scripts/user_retirement/requirements/../../../requirements/constraints.txt
# zeep
-more-itertools==10.3.0
+more-itertools==10.4.0
# via simple-salesforce
-newrelic==9.10.0
+newrelic==9.13.0
# via edx-django-utils
pbr==6.0.0
# via stevedore
platformdirs==4.2.2
# via zeep
-proto-plus==1.23.0
+proto-plus==1.24.0
# via google-api-core
-protobuf==4.25.3
+protobuf==5.27.3
# via
# google-api-core
# googleapis-common-protos
# proto-plus
-psutil==5.9.8
+psutil==6.0.0
# via edx-django-utils
pyasn1==0.6.0
# via
@@ -106,7 +106,7 @@ pyasn1-modules==0.4.0
# via google-auth
pycparser==2.22
# via cffi
-pyjwt[crypto]==2.8.0
+pyjwt[crypto]==2.9.0
# via
# edx-rest-api-client
# simple-salesforce
@@ -120,9 +120,9 @@ pytz==2024.1
# via
# jenkinsapi
# zeep
-pyyaml==6.0.1
+pyyaml==6.0.2
# via -r scripts/user_retirement/requirements/base.in
-requests==2.31.0
+requests==2.32.3
# via
# -r scripts/user_retirement/requirements/base.in
# edx-rest-api-client
@@ -139,11 +139,11 @@ requests-toolbelt==1.0.0
# via zeep
rsa==4.9
# via google-auth
-s3transfer==0.10.1
+s3transfer==0.10.2
# via boto3
simple-salesforce==1.12.6
# via -r scripts/user_retirement/requirements/base.in
-simplejson==3.19.2
+simplejson==3.19.3
# via -r scripts/user_retirement/requirements/base.in
six==1.16.0
# via
@@ -152,7 +152,7 @@ six==1.16.0
# python-dateutil
slumber==0.7.1
# via edx-rest-api-client
-sqlparse==0.5.0
+sqlparse==0.5.1
# via django
stevedore==5.2.0
# via edx-django-utils
@@ -162,7 +162,7 @@ unicodecsv==0.14.1
# via -r scripts/user_retirement/requirements/base.in
uritemplate==4.1.1
# via google-api-python-client
-urllib3==1.26.18
+urllib3==1.26.19
# via
# -c scripts/user_retirement/requirements/../../../requirements/constraints.txt
# botocore
diff --git a/scripts/user_retirement/requirements/testing.txt b/scripts/user_retirement/requirements/testing.txt
index c009f8ec8ab9..f7464dfa0602 100644
--- a/scripts/user_retirement/requirements/testing.txt
+++ b/scripts/user_retirement/requirements/testing.txt
@@ -8,31 +8,31 @@ asgiref==3.8.1
# via
# -r scripts/user_retirement/requirements/base.txt
# django
-attrs==23.2.0
+attrs==24.2.0
# via
# -r scripts/user_retirement/requirements/base.txt
# zeep
backoff==2.2.1
# via -r scripts/user_retirement/requirements/base.txt
-boto3==1.34.123
+boto3==1.35.1
# via
# -r scripts/user_retirement/requirements/base.txt
# moto
-botocore==1.34.123
+botocore==1.35.1
# via
# -r scripts/user_retirement/requirements/base.txt
# boto3
# moto
# s3transfer
-cachetools==5.3.3
+cachetools==5.5.0
# via
# -r scripts/user_retirement/requirements/base.txt
# google-auth
-certifi==2024.6.2
+certifi==2024.7.4
# via
# -r scripts/user_retirement/requirements/base.txt
# requests
-cffi==1.16.0
+cffi==1.17.0
# via
# -r scripts/user_retirement/requirements/base.txt
# cryptography
@@ -45,14 +45,14 @@ click==8.1.6
# via
# -r scripts/user_retirement/requirements/base.txt
# edx-django-utils
-cryptography==42.0.8
+cryptography==43.0.0
# via
# -r scripts/user_retirement/requirements/base.txt
# moto
# pyjwt
ddt==1.7.2
# via -r scripts/user_retirement/requirements/testing.in
-django==4.2.13
+django==4.2.15
# via
# -r scripts/user_retirement/requirements/base.txt
# django-crum
@@ -66,19 +66,19 @@ django-waffle==4.1.0
# via
# -r scripts/user_retirement/requirements/base.txt
# edx-django-utils
-edx-django-utils==5.14.2
+edx-django-utils==5.15.0
# via
# -r scripts/user_retirement/requirements/base.txt
# edx-rest-api-client
-edx-rest-api-client==5.7.0
+edx-rest-api-client==5.7.1
# via -r scripts/user_retirement/requirements/base.txt
-google-api-core==2.19.0
+google-api-core==2.19.1
# via
# -r scripts/user_retirement/requirements/base.txt
# google-api-python-client
-google-api-python-client==2.133.0
+google-api-python-client==2.141.0
# via -r scripts/user_retirement/requirements/base.txt
-google-auth==2.30.0
+google-auth==2.34.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.63.1
+googleapis-common-protos==1.63.2
# via
# -r scripts/user_retirement/requirements/base.txt
# google-api-core
@@ -126,13 +126,13 @@ markupsafe==2.1.5
# werkzeug
mock==5.1.0
# via -r scripts/user_retirement/requirements/testing.in
-more-itertools==10.3.0
+more-itertools==10.4.0
# via
# -r scripts/user_retirement/requirements/base.txt
# simple-salesforce
moto==4.2.14
# via -r scripts/user_retirement/requirements/testing.in
-newrelic==9.10.0
+newrelic==9.13.0
# via
# -r scripts/user_retirement/requirements/base.txt
# edx-django-utils
@@ -148,17 +148,17 @@ platformdirs==4.2.2
# zeep
pluggy==1.5.0
# via pytest
-proto-plus==1.23.0
+proto-plus==1.24.0
# via
# -r scripts/user_retirement/requirements/base.txt
# google-api-core
-protobuf==4.25.3
+protobuf==5.27.3
# via
# -r scripts/user_retirement/requirements/base.txt
# google-api-core
# googleapis-common-protos
# proto-plus
-psutil==5.9.8
+psutil==6.0.0
# via
# -r scripts/user_retirement/requirements/base.txt
# edx-django-utils
@@ -175,7 +175,7 @@ pycparser==2.22
# via
# -r scripts/user_retirement/requirements/base.txt
# cffi
-pyjwt[crypto]==2.8.0
+pyjwt[crypto]==2.9.0
# via
# -r scripts/user_retirement/requirements/base.txt
# edx-rest-api-client
@@ -188,7 +188,7 @@ pyparsing==3.1.2
# via
# -r scripts/user_retirement/requirements/base.txt
# httplib2
-pytest==8.2.2
+pytest==8.3.2
# via -r scripts/user_retirement/requirements/testing.in
python-dateutil==2.9.0.post0
# via
@@ -200,11 +200,11 @@ pytz==2024.1
# -r scripts/user_retirement/requirements/base.txt
# jenkinsapi
# zeep
-pyyaml==6.0.1
+pyyaml==6.0.2
# via
# -r scripts/user_retirement/requirements/base.txt
# responses
-requests==2.31.0
+requests==2.32.3
# via
# -r scripts/user_retirement/requirements/base.txt
# edx-rest-api-client
@@ -228,7 +228,7 @@ requests-toolbelt==1.0.0
# via
# -r scripts/user_retirement/requirements/base.txt
# zeep
-responses==0.25.2
+responses==0.25.3
# via
# -r scripts/user_retirement/requirements/testing.in
# moto
@@ -236,13 +236,13 @@ rsa==4.9
# via
# -r scripts/user_retirement/requirements/base.txt
# google-auth
-s3transfer==0.10.1
+s3transfer==0.10.2
# via
# -r scripts/user_retirement/requirements/base.txt
# boto3
simple-salesforce==1.12.6
# via -r scripts/user_retirement/requirements/base.txt
-simplejson==3.19.2
+simplejson==3.19.3
# via -r scripts/user_retirement/requirements/base.txt
six==1.16.0
# via
@@ -254,7 +254,7 @@ slumber==0.7.1
# via
# -r scripts/user_retirement/requirements/base.txt
# edx-rest-api-client
-sqlparse==0.5.0
+sqlparse==0.5.1
# via
# -r scripts/user_retirement/requirements/base.txt
# django
@@ -272,7 +272,7 @@ uritemplate==4.1.1
# via
# -r scripts/user_retirement/requirements/base.txt
# google-api-python-client
-urllib3==1.26.18
+urllib3==1.26.19
# via
# -r scripts/user_retirement/requirements/base.txt
# botocore
diff --git a/scripts/user_retirement/retire_one_learner.py b/scripts/user_retirement/retire_one_learner.py
index 2d298c0729b4..b8c40364e643 100755
--- a/scripts/user_retirement/retire_one_learner.py
+++ b/scripts/user_retirement/retire_one_learner.py
@@ -10,11 +10,9 @@
lms: http://localhost:18000/
ecommerce: http://localhost:18130/
credentials: http://localhost:18150/
- demographics: http://localhost:18360/
retirement_pipeline:
- ['RETIRING_CREDENTIALS', 'CREDENTIALS_COMPLETE', 'CREDENTIALS', 'retire_learner']
- ['RETIRING_ECOM', 'ECOM_COMPLETE', 'ECOMMERCE', 'retire_learner']
- - ['RETIRING_DEMOGRAPHICS', 'DEMOGRAPHICS_COMPLETE', 'DEMOGRAPHICS', 'retire_learner']
- ['RETIRING_LICENSE_MANAGER', 'LICENSE_MANAGER_COMPLETE', 'LICENSE_MANAGER', 'retire_learner']
- ['RETIRING_FORUMS', 'FORUMS_COMPLETE', 'LMS', 'retirement_retire_forum']
- ['RETIRING_EMAIL_LISTS', 'EMAIL_LISTS_COMPLETE', 'LMS', 'retirement_retire_mailings']
diff --git a/scripts/user_retirement/tests/utils/test_edx_api.py b/scripts/user_retirement/tests/utils/test_edx_api.py
index 706b1ce6d4a7..eb826cd1a4fd 100644
--- a/scripts/user_retirement/tests/utils/test_edx_api.py
+++ b/scripts/user_retirement/tests/utils/test_edx_api.py
@@ -21,7 +21,7 @@
FAKE_USERNAMES,
TEST_RETIREMENT_QUEUE_STATES,
TEST_RETIREMENT_STATE,
- get_fake_user_retirement
+ get_fake_user_retirement,
)
from scripts.user_retirement.utils import edx_api
@@ -499,46 +499,6 @@ def test_replace_usernames(self, mock_method):
)
-class TestDemographicsApi(OAuth2Mixin, unittest.TestCase):
- """
- Test the edX Demographics API client.
- """
-
- @responses.activate(registry=OrderedRegistry)
- def setUp(self):
- super().setUp()
- self.mock_access_token_response()
- self.lms_base_url = 'http://localhost:18000/'
- self.demographics_base_url = 'http://localhost:18360/'
- self.demographics_api = edx_api.DemographicsApi(
- self.lms_base_url,
- self.demographics_base_url,
- 'the_client_id',
- 'the_client_secret'
- )
-
- def tearDown(self):
- super().tearDown()
- responses.reset()
-
- @patch.object(edx_api.DemographicsApi, 'retire_learner')
- def test_retire_learner(self, mock_method):
- json_data = {
- 'lms_user_id': get_fake_user_retirement()['user']['id']
- }
- responses.add(
- POST,
- urljoin(self.demographics_base_url, 'demographics/api/v1/retire_demographics/'),
- match=[matchers.json_params_matcher(json_data)]
- )
- self.demographics_api.retire_learner(
- learner=get_fake_user_retirement()
- )
- mock_method.assert_called_once_with(
- learner=get_fake_user_retirement()
- )
-
-
class TestLicenseManagerApi(OAuth2Mixin, unittest.TestCase):
"""
Test the edX License Manager API client.
diff --git a/scripts/user_retirement/utils/edx_api.py b/scripts/user_retirement/utils/edx_api.py
index 10903640456a..e891f04019a9 100644
--- a/scripts/user_retirement/utils/edx_api.py
+++ b/scripts/user_retirement/utils/edx_api.py
@@ -1,6 +1,7 @@
"""
edX API classes which call edX service REST API endpoints using the edx-rest-api-client module.
"""
+
import logging
from urllib.parse import urljoin
@@ -28,6 +29,7 @@ class BaseApiClient:
"""
API client base class used to submit API requests to a particular web service.
"""
+
append_slash = True
_access_token = None
@@ -45,15 +47,15 @@ def get_api_url(self, path):
Args:
path (str): API endpoint path.
"""
- path = path.strip('/')
+ path = path.strip("/")
if self.append_slash:
- path += '/'
+ path += "/"
- return urljoin(f'{self.api_base_url}/', path)
+ return urljoin(f"{self.api_base_url}/", path)
def _request(self, method, url, log_404_as_error=True, **kwargs):
- if 'headers' not in kwargs:
- kwargs['headers'] = {'Content-type': 'application/json'}
+ if "headers" not in kwargs:
+ kwargs["headers"] = {"Content-type": "application/json"}
try:
response = requests.request(method, url, auth=SuppliedJwtAuth(self._access_token), **kwargs)
@@ -68,7 +70,7 @@ def _request(self, method, url, log_404_as_error=True, **kwargs):
# Immediately raise the error so that a 404 isn't logged as an API error in this case.
raise HttpDoesNotExistException(str(exc))
- LOG.error(f'API Error: {str(exc)} with status code: {status_code}')
+ LOG.error(f"API Error: {str(exc)} with status code: {status_code}")
if status_code == 504:
# Differentiate gateway errors so different backoff can be used.
@@ -92,31 +94,31 @@ def get_access_token(oauth_base_url, client_id, client_secret):
Returns:
str: JWT access token
"""
- oauth_access_token_url = urljoin(f'{oauth_base_url}/', OAUTH_ACCESS_TOKEN_URL)
+ oauth_access_token_url = urljoin(f"{oauth_base_url}/", OAUTH_ACCESS_TOKEN_URL)
data = {
- 'grant_type': 'client_credentials',
- 'client_id': client_id,
- 'client_secret': client_secret,
- 'token_type': 'jwt',
+ "grant_type": "client_credentials",
+ "client_id": client_id,
+ "client_secret": client_secret,
+ "token_type": "jwt",
}
try:
response = requests.post(
oauth_access_token_url,
data=data,
headers={
- 'User-Agent': 'scripts.user_retirement',
+ "User-Agent": "scripts.user_retirement",
},
- timeout=(REQUEST_CONNECT_TIMEOUT, REQUEST_READ_TIMEOUT)
+ timeout=(REQUEST_CONNECT_TIMEOUT, REQUEST_READ_TIMEOUT),
)
response.raise_for_status()
- return response.json()['access_token']
+ return response.json()["access_token"]
except KeyError as exc:
- LOG.error(f'Failed to get token. {str(exc)} does not exist.')
+ LOG.error(f"Failed to get token. {str(exc)} does not exist.")
raise
except HTTPError as exc:
LOG.error(
- f'API Error: {str(exc)} with status code: {exc.response.status_code} fetching access token: {client_id}'
+ f"API Error: {str(exc)} with status code: {exc.response.status_code} fetching access token: {client_id}"
)
raise
@@ -125,7 +127,7 @@ def _backoff_handler(details):
"""
Simple logging handler for when timeout backoff occurs.
"""
- LOG.info('Trying again in {wait:0.1f} seconds after {tries} tries calling {target}'.format(**details))
+ LOG.info("Trying again in {wait:0.1f} seconds after {tries} tries calling {target}".format(**details))
def _wait_one_minute():
@@ -143,10 +145,7 @@ def _giveup_on_unexpected_exception(exc):
# Treat a ConnectionError as retryable.
isinstance(exc, ConnectionError)
# All 5xx status codes are retryable except for 504 Gateway Timeout.
- or (
- 500 <= exc.response.status_code < 600
- and exc.response.status_code != 504 # Gateway Timeout
- )
+ or (500 <= exc.response.status_code < 600 and exc.response.status_code != 504) # Gateway Timeout
# Status code 104 is unreserved, but we must have added this because we observed retryable 104 responses.
or exc.response.status_code == 104
)
@@ -167,7 +166,7 @@ def inner(func): # pylint: disable=missing-docstring
# Wrap the actual _backoff_handler so that we can patch the real one in unit tests. Otherwise, the func
# will get decorated on import, embedding this handler as a python object reference, precluding our ability
# to patch it in tests.
- on_backoff=lambda details: _backoff_handler(details) # pylint: disable=unnecessary-lambda
+ on_backoff=lambda details: _backoff_handler(details), # pylint: disable=unnecessary-lambda
)
func_with_timeout_backoff = backoff.on_exception(
_wait_one_minute,
@@ -176,7 +175,7 @@ def inner(func): # pylint: disable=missing-docstring
# Wrap the actual _backoff_handler so that we can patch the real one in unit tests. Otherwise, the func
# will get decorated on import, embedding this handler as a python object reference, precluding our ability
# to patch it in tests.
- on_backoff=lambda details: _backoff_handler(details) # pylint: disable=unnecessary-lambda
+ on_backoff=lambda details: _backoff_handler(details), # pylint: disable=unnecessary-lambda
)
return func_with_backoff(func_with_timeout_backoff(func))
@@ -193,14 +192,11 @@ def learners_to_retire(self, states_to_request, cool_off_days=7, limit=None):
"""
Retrieves a list of learners awaiting retirement actions.
"""
- params = {
- 'cool_off_days': cool_off_days,
- 'states': states_to_request
- }
+ params = {"cool_off_days": cool_off_days, "states": states_to_request}
if limit:
- params['limit'] = limit
- api_url = self.get_api_url('api/user/v1/accounts/retirement_queue')
- return self._request('GET', api_url, params=params)
+ params["limit"] = limit
+ api_url = self.get_api_url("api/user/v1/accounts/retirement_queue")
+ return self._request("GET", api_url, params=params)
@_retry_lms_api()
def get_learners_by_date_and_status(self, state_to_request, start_date, end_date):
@@ -214,20 +210,20 @@ def get_learners_by_date_and_status(self, state_to_request, start_date, end_date
:param end_date: Date or Datetime
"""
params = {
- 'start_date': start_date.strftime('%Y-%m-%d'),
- 'end_date': end_date.strftime('%Y-%m-%d'),
- 'state': state_to_request
+ "start_date": start_date.strftime("%Y-%m-%d"),
+ "end_date": end_date.strftime("%Y-%m-%d"),
+ "state": state_to_request,
}
- api_url = self.get_api_url('api/user/v1/accounts/retirements_by_status_and_date')
- return self._request('GET', api_url, params=params)
+ api_url = self.get_api_url("api/user/v1/accounts/retirements_by_status_and_date")
+ return self._request("GET", api_url, params=params)
@_retry_lms_api()
def get_learner_retirement_state(self, username):
"""
Retrieves the given learner's retirement state.
"""
- api_url = self.get_api_url(f'api/user/v1/accounts/{username}/retirement_status')
- return self._request('GET', api_url)
+ api_url = self.get_api_url(f"api/user/v1/accounts/{username}/retirement_status")
+ return self._request("GET", api_url)
@_retry_lms_api()
def update_learner_retirement_state(self, username, new_state_name, message, force=False):
@@ -235,26 +231,22 @@ def update_learner_retirement_state(self, username, new_state_name, message, for
Updates the given learner's retirement state to the retirement state name new_string
with the additional string information in message (for logging purposes).
"""
- data = {
- 'username': username,
- 'new_state': new_state_name,
- 'response': message
- }
+ data = {"username": username, "new_state": new_state_name, "response": message}
if force:
- data['force'] = True
+ data["force"] = True
- api_url = self.get_api_url('api/user/v1/accounts/update_retirement_status')
- return self._request('PATCH', api_url, json=data)
+ api_url = self.get_api_url("api/user/v1/accounts/update_retirement_status")
+ return self._request("PATCH", api_url, json=data)
@_retry_lms_api()
def retirement_deactivate_logout(self, learner):
"""
Performs the user deactivation and forced logout step of learner retirement
"""
- data = {'username': learner['original_username']}
- api_url = self.get_api_url('api/user/v1/accounts/deactivate_logout')
- return self._request('POST', api_url, json=data)
+ data = {"username": learner["original_username"]}
+ api_url = self.get_api_url("api/user/v1/accounts/deactivate_logout")
+ return self._request("POST", api_url, json=data)
@_retry_lms_api()
def retirement_retire_forum(self, learner):
@@ -262,10 +254,10 @@ def retirement_retire_forum(self, learner):
Performs the forum retirement step of learner retirement
"""
# api/discussion/
- data = {'username': learner['original_username']}
+ data = {"username": learner["original_username"]}
try:
- api_url = self.get_api_url('api/discussion/v1/accounts/retire_forum')
- return self._request('POST', api_url, json=data)
+ api_url = self.get_api_url("api/discussion/v1/accounts/retire_forum")
+ return self._request("POST", api_url, json=data)
except HttpDoesNotExistException:
LOG.info("No information about learner retirement")
return True
@@ -275,18 +267,18 @@ def retirement_retire_mailings(self, learner):
"""
Performs the email list retirement step of learner retirement
"""
- data = {'username': learner['original_username']}
- api_url = self.get_api_url('api/user/v1/accounts/retire_mailings')
- return self._request('POST', api_url, json=data)
+ data = {"username": learner["original_username"]}
+ api_url = self.get_api_url("api/user/v1/accounts/retire_mailings")
+ return self._request("POST", api_url, json=data)
@_retry_lms_api()
def retirement_unenroll(self, learner):
"""
Unenrolls the user from all courses
"""
- data = {'username': learner['original_username']}
- api_url = self.get_api_url('api/enrollment/v1/unenroll')
- return self._request('POST', api_url, json=data)
+ data = {"username": learner["original_username"]}
+ api_url = self.get_api_url("api/enrollment/v1/unenroll")
+ return self._request("POST", api_url, json=data)
# This endpoint additionally returns 500 when the EdxNotes backend service is unavailable.
@_retry_lms_api()
@@ -294,9 +286,9 @@ def retirement_retire_notes(self, learner):
"""
Deletes all the user's notes (aka. annotations)
"""
- data = {'username': learner['original_username']}
- api_url = self.get_api_url('api/edxnotes/v1/retire_user')
- return self._request('POST', api_url, json=data)
+ data = {"username": learner["original_username"]}
+ api_url = self.get_api_url("api/edxnotes/v1/retire_user")
+ return self._request("POST", api_url, json=data)
@_retry_lms_api()
def retirement_lms_retire_misc(self, learner):
@@ -304,27 +296,27 @@ def retirement_lms_retire_misc(self, learner):
Deletes, blanks, or one-way hashes personal information in LMS as
defined in EDUCATOR-2802 and sub-tasks.
"""
- data = {'username': learner['original_username']}
- api_url = self.get_api_url('api/user/v1/accounts/retire_misc')
- return self._request('POST', api_url, json=data)
+ data = {"username": learner["original_username"]}
+ api_url = self.get_api_url("api/user/v1/accounts/retire_misc")
+ return self._request("POST", api_url, json=data)
@_retry_lms_api()
def retirement_lms_retire(self, learner):
"""
Deletes, blanks, or one-way hashes all remaining personal information in LMS
"""
- data = {'username': learner['original_username']}
- api_url = self.get_api_url('api/user/v1/accounts/retire')
- return self._request('POST', api_url, json=data)
+ data = {"username": learner["original_username"]}
+ api_url = self.get_api_url("api/user/v1/accounts/retire")
+ return self._request("POST", api_url, json=data)
@_retry_lms_api()
def retirement_partner_queue(self, learner):
"""
Calls LMS to add the given user to the retirement reporting queue
"""
- data = {'username': learner['original_username']}
- api_url = self.get_api_url('api/user/v1/accounts/retirement_partner_report')
- return self._request('PUT', api_url, json=data)
+ data = {"username": learner["original_username"]}
+ api_url = self.get_api_url("api/user/v1/accounts/retirement_partner_report")
+ return self._request("PUT", api_url, json=data)
@_retry_lms_api()
def retirement_partner_report(self):
@@ -332,16 +324,16 @@ def retirement_partner_report(self):
Retrieves the list of users to create partner reports for and set their status to
processing
"""
- api_url = self.get_api_url('api/user/v1/accounts/retirement_partner_report')
- return self._request('POST', api_url)
+ api_url = self.get_api_url("api/user/v1/accounts/retirement_partner_report")
+ return self._request("POST", api_url)
@_retry_lms_api()
def retirement_partner_cleanup(self, usernames):
"""
Removes the given users from the partner reporting queue
"""
- api_url = self.get_api_url('api/user/v1/accounts/retirement_partner_report_cleanup')
- return self._request('POST', api_url, json=usernames)
+ api_url = self.get_api_url("api/user/v1/accounts/retirement_partner_report_cleanup")
+ return self._request("POST", api_url, json=usernames)
@_retry_lms_api()
def retirement_retire_proctoring_data(self, learner):
@@ -349,7 +341,7 @@ def retirement_retire_proctoring_data(self, learner):
Deletes or hashes learner data from edx-proctoring
"""
api_url = self.get_api_url(f"api/edx_proctoring/v1/retire_user/{learner['user']['id']}")
- return self._request('POST', api_url)
+ return self._request("POST", api_url)
@_retry_lms_api()
def retirement_retire_proctoring_backend_data(self, learner):
@@ -357,16 +349,16 @@ def retirement_retire_proctoring_backend_data(self, learner):
Removes the given learner from 3rd party proctoring backends
"""
api_url = self.get_api_url(f"api/edx_proctoring/v1/retire_backend_user/{learner['user']['id']}")
- return self._request('POST', api_url)
+ return self._request("POST", api_url)
@_retry_lms_api()
def bulk_cleanup_retirements(self, usernames):
"""
Deletes the retirements for all given usernames
"""
- data = {'usernames': usernames}
- api_url = self.get_api_url('api/user/v1/accounts/retirement_cleanup')
- return self._request('POST', api_url, json=data)
+ data = {"usernames": usernames}
+ api_url = self.get_api_url("api/user/v1/accounts/retirement_cleanup")
+ return self._request("POST", api_url, json=data)
def replace_lms_usernames(self, username_mappings):
"""
@@ -377,8 +369,8 @@ def replace_lms_usernames(self, username_mappings):
[{current_un_1: desired_un_1}, {current_un_2: desired_un_2}]
"""
data = {"username_mappings": username_mappings}
- api_url = self.get_api_url('api/user/v1/accounts/replace_usernames')
- return self._request('POST', api_url, json=data)
+ api_url = self.get_api_url("api/user/v1/accounts/replace_usernames")
+ return self._request("POST", api_url, json=data)
def replace_forums_usernames(self, username_mappings):
"""
@@ -389,8 +381,8 @@ def replace_forums_usernames(self, username_mappings):
[{current_un_1: new_un_1}, {current_un_2: new_un_2}]
"""
data = {"username_mappings": username_mappings}
- api_url = self.get_api_url('api/discussion/v1/accounts/replace_usernames')
- return self._request('POST', api_url, json=data)
+ api_url = self.get_api_url("api/discussion/v1/accounts/replace_usernames")
+ return self._request("POST", api_url, json=data)
class EcommerceApi(BaseApiClient):
@@ -403,9 +395,9 @@ def retire_learner(self, learner):
"""
Performs the learner retirement step for Ecommerce
"""
- data = {'username': learner['original_username']}
- api_url = self.get_api_url('api/v2/user/retire')
- return self._request('POST', api_url, json=data)
+ data = {"username": learner["original_username"]}
+ api_url = self.get_api_url("api/v2/user/retire")
+ return self._request("POST", api_url, json=data)
@_retry_lms_api()
def get_tracking_key(self, learner):
@@ -414,7 +406,7 @@ def get_tracking_key(self, learner):
ecommerce doesn't have access to the LMS user id.
"""
api_url = self.get_api_url(f"api/v2/retirement/tracking_id/{learner['original_username']}")
- return self._request('GET', api_url)['ecommerce_tracking_id']
+ return self._request("GET", api_url)["ecommerce_tracking_id"]
def replace_usernames(self, username_mappings):
"""
@@ -425,8 +417,8 @@ def replace_usernames(self, username_mappings):
[{current_un_1: new_un_1}, {current_un_2: new_un_2}]
"""
data = {"username_mappings": username_mappings}
- api_url = self.get_api_url('api/v2/user_management/replace_usernames')
- return self._request('POST', api_url, json=data)
+ api_url = self.get_api_url("api/v2/user_management/replace_usernames")
+ return self._request("POST", api_url, json=data)
class CredentialsApi(BaseApiClient):
@@ -439,9 +431,9 @@ def retire_learner(self, learner):
"""
Performs the learner retirement step for Credentials
"""
- data = {'username': learner['original_username']}
- api_url = self.get_api_url('user/retire')
- return self._request('POST', api_url, json=data)
+ data = {"username": learner["original_username"]}
+ api_url = self.get_api_url("user/retire")
+ return self._request("POST", api_url, json=data)
def replace_usernames(self, username_mappings):
"""
@@ -452,8 +444,8 @@ def replace_usernames(self, username_mappings):
[{current_un_1: new_un_1}, {current_un_2: new_un_2}]
"""
data = {"username_mappings": username_mappings}
- api_url = self.get_api_url('api/v2/replace_usernames')
- return self._request('POST', api_url, json=data)
+ api_url = self.get_api_url("api/v2/replace_usernames")
+ return self._request("POST", api_url, json=data)
class DiscoveryApi(BaseApiClient):
@@ -470,30 +462,8 @@ def replace_usernames(self, username_mappings):
[{current_un_1: new_un_1}, {current_un_2: new_un_2}]
"""
data = {"username_mappings": username_mappings}
- api_url = self.get_api_url('api/v1/replace_usernames')
- return self._request('POST', api_url, json=data)
-
-
-class DemographicsApi(BaseApiClient):
- """
- Demographics API client.
- """
-
- @_retry_lms_api()
- def retire_learner(self, learner):
- """
- Performs the learner retirement step for Demographics. Passes the learner's LMS User Id instead of username.
- """
- data = {'lms_user_id': learner['user']['id']}
- # If the user we are retiring has no data in the Demographics DB the request will return a 404. We
- # catch the HTTPError and return True in order to prevent this error getting raised and
- # incorrectly causing the learner to enter an ERROR state during retirement.
- try:
- api_url = self.get_api_url('demographics/api/v1/retire_demographics')
- return self._request('POST', api_url, log_404_as_error=False, json=data)
- except HttpDoesNotExistException:
- LOG.info("No demographics data found for user")
- return True
+ api_url = self.get_api_url("api/v1/replace_usernames")
+ return self._request("POST", api_url, json=data)
class LicenseManagerApi(BaseApiClient):
@@ -508,15 +478,15 @@ def retire_learner(self, learner):
username.
"""
data = {
- 'lms_user_id': learner['user']['id'],
- 'original_username': learner['original_username'],
+ "lms_user_id": learner["user"]["id"],
+ "original_username": learner["original_username"],
}
# If the user we are retiring has no data in the License Manager DB the request will return a 404. We
# catch the HTTPError and return True in order to prevent this error getting raised and
# incorrectly causing the learner to enter an ERROR state during retirement.
try:
- api_url = self.get_api_url('api/v1/retire_user')
- return self._request('POST', api_url, log_404_as_error=False, json=data)
+ api_url = self.get_api_url("api/v1/retire_user")
+ return self._request("POST", api_url, log_404_as_error=False, json=data)
except HttpDoesNotExistException:
LOG.info("No license manager data found for user")
return True
diff --git a/scripts/user_retirement/utils/helpers.py b/scripts/user_retirement/utils/helpers.py
index 8203e363593c..1bcbadb4b3c4 100644
--- a/scripts/user_retirement/utils/helpers.py
+++ b/scripts/user_retirement/utils/helpers.py
@@ -18,7 +18,7 @@
from six import text_type
from scripts.user_retirement.utils.edx_api import LmsApi # pylint: disable=wrong-import-position
-from scripts.user_retirement.utils.edx_api import CredentialsApi, DemographicsApi, EcommerceApi, LicenseManagerApi
+from scripts.user_retirement.utils.edx_api import CredentialsApi, EcommerceApi, LicenseManagerApi
from scripts.user_retirement.utils.thirdparty_apis.amplitude_api import \
AmplitudeApi # pylint: disable=wrong-import-position
from scripts.user_retirement.utils.thirdparty_apis.braze_api import BrazeApi # pylint: disable=wrong-import-position
@@ -143,17 +143,16 @@ def _setup_lms_api_or_exit(fail_func, fail_code, config):
def _setup_all_apis_or_exit(fail_func, fail_code, config):
"""
- Performs setup of EdxRestClientApi instances for LMS, E-Commerce, Credentials, and
- Demographics, as well as fetching the learner's record from LMS and validating that
- it is in a state to work on. Returns the learner dict and their current stage in the
- retirement flow.
+ Performs setup of EdxRestClientApi instances for LMS, E-Commerce, and Credentials,
+ as well as fetching the learner's record from LMS and validating that it is
+ in a state to work on. Returns the learner dict and their current stage in
+ the retirement flow.
"""
try:
lms_base_url = config['base_urls']['lms']
ecommerce_base_url = config['base_urls'].get('ecommerce', None)
credentials_base_url = config['base_urls'].get('credentials', None)
segment_base_url = config['base_urls'].get('segment', None)
- demographics_base_url = config['base_urls'].get('demographics', None)
license_manager_base_url = config['base_urls'].get('license_manager', None)
client_id = config['client_id']
client_secret = config['client_secret']
@@ -181,7 +180,6 @@ def _setup_all_apis_or_exit(fail_func, fail_code, config):
('CREDENTIALS', credentials_base_url),
('SEGMENT', segment_base_url),
('HUBSPOT', hubspot_api_key),
- ('DEMOGRAPHICS', demographics_base_url)
):
if state[2] == service and service_url is None:
fail_func(fail_code, 'Service URL is not configured, but required for state {}'.format(state))
@@ -223,9 +221,6 @@ def _setup_all_apis_or_exit(fail_func, fail_code, config):
if credentials_base_url:
config['CREDENTIALS'] = CredentialsApi(lms_base_url, credentials_base_url, client_id, client_secret)
- if demographics_base_url:
- config['DEMOGRAPHICS'] = DemographicsApi(lms_base_url, demographics_base_url, client_id, client_secret)
-
if license_manager_base_url:
config['LICENSE_MANAGER'] = LicenseManagerApi(
lms_base_url,
diff --git a/scripts/xblock/requirements.txt b/scripts/xblock/requirements.txt
index af62756937c0..5b7f07242e6d 100644
--- a/scripts/xblock/requirements.txt
+++ b/scripts/xblock/requirements.txt
@@ -4,7 +4,7 @@
#
# make upgrade
#
-certifi==2024.6.2
+certifi==2024.7.4
# via requests
charset-normalizer==2.0.12
# via
@@ -14,7 +14,7 @@ idna==3.7
# via requests
requests==2.32.3
# via -r scripts/xblock/requirements.in
-urllib3==1.26.18
+urllib3==1.26.19
# via
# -c scripts/xblock/../../requirements/constraints.txt
# requests
diff --git a/themes/red-theme/lms/templates/ace_common/edx_ace/common/base_body.html b/themes/red-theme/lms/templates/ace_common/edx_ace/common/base_body.html
index 8d51b16498d7..9319217aa4cf 100644
--- a/themes/red-theme/lms/templates/ace_common/edx_ace/common/base_body.html
+++ b/themes/red-theme/lms/templates/ace_common/edx_ace/common/base_body.html
@@ -63,7 +63,7 @@
|
diff --git a/webpack.builtinblocks.config.js b/webpack.builtinblocks.config.js
index 53ce30a9c0e2..d86f891dc6ce 100644
--- a/webpack.builtinblocks.config.js
+++ b/webpack.builtinblocks.config.js
@@ -1,17 +1,5 @@
module.exports = {
entry: {
- AboutBlockDisplay: [
- './xmodule/js/src/xmodule.js',
- './xmodule/js/src/html/display.js',
- './xmodule/js/src/javascript_loader.js',
- './xmodule/js/src/collapsible.js',
- './xmodule/js/src/html/imageModal.js',
- './xmodule/js/common_static/js/vendor/draggabilly.js'
- ],
- AboutBlockEditor: [
- './xmodule/js/src/xmodule.js',
- './xmodule/js/src/html/edit.js'
- ],
AnnotatableBlockDisplay: [
'./xmodule/js/src/xmodule.js',
'./xmodule/js/src/html/display.js',
@@ -33,18 +21,6 @@ module.exports = {
'./xmodule/js/src/xmodule.js',
'./xmodule/js/src/sequence/edit.js'
],
- CourseInfoBlockDisplay: [
- './xmodule/js/src/xmodule.js',
- './xmodule/js/src/html/display.js',
- './xmodule/js/src/javascript_loader.js',
- './xmodule/js/src/collapsible.js',
- './xmodule/js/src/html/imageModal.js',
- './xmodule/js/common_static/js/vendor/draggabilly.js'
- ],
- CourseInfoBlockEditor: [
- './xmodule/js/src/xmodule.js',
- './xmodule/js/src/html/edit.js'
- ],
CustomTagBlockDisplay: './xmodule/js/src/xmodule.js',
CustomTagBlockEditor: [
'./xmodule/js/src/xmodule.js',
@@ -104,18 +80,6 @@ module.exports = {
'./xmodule/js/src/xmodule.js',
'./xmodule/js/src/sequence/edit.js'
],
- StaticTabBlockDisplay: [
- './xmodule/js/src/xmodule.js',
- './xmodule/js/src/html/display.js',
- './xmodule/js/src/javascript_loader.js',
- './xmodule/js/src/collapsible.js',
- './xmodule/js/src/html/imageModal.js',
- './xmodule/js/common_static/js/vendor/draggabilly.js'
- ],
- StaticTabBlockEditor: [
- './xmodule/js/src/xmodule.js',
- './xmodule/js/src/html/edit.js'
- ],
VideoBlockDisplay: [
'./xmodule/js/src/xmodule.js',
'./xmodule/js/src/video/10_main.js'
diff --git a/webpack.common.config.js b/webpack.common.config.js
index 1ea0f5b5ea1c..322e252c6ae2 100644
--- a/webpack.common.config.js
+++ b/webpack.common.config.js
@@ -98,9 +98,6 @@ module.exports = Merge.smart({
StudentAccountDeletion: './lms/static/js/student_account/components/StudentAccountDeletion.jsx',
StudentAccountDeletionInitializer: './lms/static/js/student_account/StudentAccountDeletionInitializer.js',
ProblemBrowser: './lms/djangoapps/instructor/static/instructor/ProblemBrowser/index.jsx',
- DemographicsCollectionBanner: './lms/static/js/demographics_collection/DemographicsCollectionBanner.jsx',
- DemographicsCollectionModal: './lms/static/js/demographics_collection/DemographicsCollectionModal.jsx',
- AxiosJwtTokenService: './lms/static/js/jwt_auth/AxiosJwtTokenService.js',
EnterpriseLearnerPortalModal: './lms/static/js/learner_dashboard/EnterpriseLearnerPortalModal.jsx',
// Learner Dashboard
diff --git a/xmodule/assets/annotatable/_display.scss b/xmodule/assets/annotatable/_display.scss
index 124011e2bd2b..9aaa8649c6c5 100644
--- a/xmodule/assets/annotatable/_display.scss
+++ b/xmodule/assets/annotatable/_display.scss
@@ -9,7 +9,7 @@
@import 'bootstrap/scss/variables';
@import 'lms/theme/variables-v1';
-$annotatable--border-color: $gray-l3;
+$annotatable--border-color: var(--gray-l3);
$annotatable--body-font-size: em(14);
.annotatable-wrapper {
@@ -116,18 +116,18 @@ $annotatable--body-font-size: em(14);
border: 1px solid #333;
border-radius: 1em;
background-color: rgba(0, 0, 0, 0.85);
- color: $white;
+ color: var(--white);
-webkit-font-smoothing: antialiased;
.ui-tooltip-titlebar {
font-size: em(16);
color: inherit;
background-color: transparent;
- padding: ($baseline/4) ($baseline/2);
+ padding: calc((var(--baseline)/4)) calc((var(--baseline)/2));
border: none;
.ui-tooltip-title {
- padding: ($baseline/4) 0;
+ padding: calc((var(--baseline)/4)) 0;
border-bottom: 2px solid #333;
font-weight: bold;
}
@@ -139,7 +139,7 @@ $annotatable--body-font-size: em(14);
.ui-state-hover {
color: inherit;
- border: 1px solid $gray-l3;
+ border: 1px solid var(--gray-l3);
}
}
@@ -148,7 +148,7 @@ $annotatable--body-font-size: em(14);
font-size: em(14);
text-align: left;
font-weight: 400;
- padding: 0 ($baseline/2) ($baseline/2) ($baseline/2);
+ padding: 0 calc((var(--baseline)/2)) calc((var(--baseline)/2)) calc((var(--baseline)/2));
background-color: transparent;
border-color: transparent;
}
@@ -163,11 +163,11 @@ $annotatable--body-font-size: em(14);
max-width: 375px;
.ui-tooltip-content {
- padding: 0 ($baseline/2);
+ padding: 0 calc((var(--baseline)/2));
.annotatable-comment {
display: block;
- margin: 0 0 ($baseline/2) 0;
+ margin: 0 0 calc((var(--baseline)/2)) 0;
max-height: 225px;
overflow: auto;
line-height: normal;
@@ -176,7 +176,7 @@ $annotatable--body-font-size: em(14);
.annotatable-reply {
display: block;
border-top: 2px solid #333;
- padding: ($baseline/4) 0;
+ padding: calc((var(--baseline)/4)) 0;
margin: 0;
text-align: center;
}
@@ -190,7 +190,7 @@ $annotatable--body-font-size: em(14);
left: 50%;
height: 0;
width: 0;
- margin-left: -($baseline/4);
+ margin-left: calc(-1 * (var(--baseline) / 4));
border: 10px solid transparent;
border-top-color: rgba(0, 0, 0, 0.85);
}
diff --git a/xmodule/assets/video/_display.scss b/xmodule/assets/video/_display.scss
index 1a28e9150df7..fd5cd73b2105 100644
--- a/xmodule/assets/video/_display.scss
+++ b/xmodule/assets/video/_display.scss
@@ -91,6 +91,7 @@ $cool-dark: rgb(79, 89, 93); // UXPL cool dark
.wrapper-video-bottom-section {
display: flex;
+ justify-content: space-between;
.wrapper-download-video,
.wrapper-download-transcripts,
@@ -177,6 +178,13 @@ $cool-dark: rgb(79, 89, 93); // UXPL cool dark
}
}
+ .google-disclaimer {
+ display: none;
+ margin-top: $baseline;
+ @include padding-right($baseline);
+ vertical-align: top;
+ }
+
.video-wrapper {
@include float(left);
@include margin-right(flex-gutter(9));
diff --git a/xmodule/assets/word_cloud/_display.scss b/xmodule/assets/word_cloud/_display.scss
index 0b8940ab9abe..4f7320380a12 100644
--- a/xmodule/assets/word_cloud/_display.scss
+++ b/xmodule/assets/word_cloud/_display.scss
@@ -4,7 +4,7 @@
@import 'lms/theme/variables-v1';
.input-cloud {
- margin: ($baseline/4);
+ margin: calc((var(--baseline)/4));
}
.result_cloud_section {
diff --git a/xmodule/capa_block.py b/xmodule/capa_block.py
index 53bb93a56c18..54ca0cbc312f 100644
--- a/xmodule/capa_block.py
+++ b/xmodule/capa_block.py
@@ -795,12 +795,25 @@ def generate_report_data(self, user_state_iterator, limit_responses=None):
}
yield (user_state.username, report)
+ @property
+ def course_end_date(self):
+ """
+ Return the end date of the problem's course
+ """
+
+ try:
+ course_block_key = self.runtime.course_entry.structure['root']
+ return self.runtime.course_entry.structure['blocks'][course_block_key].fields['end']
+ except (AttributeError, KeyError):
+ return None
+
@property
def close_date(self):
"""
Return the date submissions should be closed from.
"""
- due_date = self.due
+
+ due_date = self.due or self.course_end_date
if self.graceperiod is not None and due_date:
return due_date + self.graceperiod
diff --git a/xmodule/contentstore/mongo.py b/xmodule/contentstore/mongo.py
index 66d9474cde7c..e44f03cede05 100644
--- a/xmodule/contentstore/mongo.py
+++ b/xmodule/contentstore/mongo.py
@@ -3,6 +3,7 @@
"""
+import hashlib
import json
import os
@@ -40,16 +41,29 @@ def __init__(
# GridFS will throw an exception if the Database is wrapped in a MongoProxy. So don't wrap it.
# The appropriate methods below are marked as autoretry_read - those methods will handle
# the AutoReconnect errors.
- proxy = False
- mongo_db = connect_to_mongodb(
- db, host,
- port=port, tz_aware=tz_aware, user=user, password=password, proxy=proxy, **kwargs
- )
+ self.connection_params = {
+ 'db': db,
+ 'host': host,
+ 'port': port,
+ 'tz_aware': tz_aware,
+ 'user': user,
+ 'password': password,
+ 'proxy': False,
+ **kwargs
+ }
+ self.bucket = bucket
+ self.do_connection()
+
+ def do_connection(self):
+ """
+ Connects to mongodb.
+ """
+ mongo_db = connect_to_mongodb(**self.connection_params)
- self.fs = gridfs.GridFS(mongo_db, bucket) # pylint: disable=invalid-name
+ self.fs = gridfs.GridFS(mongo_db, self.bucket) # pylint: disable=invalid-name
- self.fs_files = mongo_db[bucket + ".files"] # the underlying collection GridFS uses
- self.chunks = mongo_db[bucket + ".chunks"]
+ self.fs_files = mongo_db[self.bucket + ".files"] # the underlying collection GridFS uses
+ self.chunks = mongo_db[self.bucket + ".chunks"]
def close_connections(self):
"""
@@ -57,6 +71,25 @@ def close_connections(self):
"""
self.fs_files.database.client.close()
+ def ensure_connection(self):
+ """
+ Ensure that mongodb connection is open.
+ """
+ if self.check_connection():
+ return
+ self.do_connection()
+
+ def check_connection(self):
+ """
+ Check if mongodb connection is open or not.
+ """
+ connection = self.fs_files.database.client
+ try:
+ connection.admin.command('ping')
+ return True
+ except pymongo.errors.InvalidOperation:
+ return False
+
def _drop_database(self, database=True, collections=True, connections=True):
"""
A destructive operation to drop the underlying database and close all connections.
@@ -69,8 +102,8 @@ def _drop_database(self, database=True, collections=True, connections=True):
If connections is True, then close the connection to the database as well.
"""
+ self.ensure_connection()
connection = self.fs_files.database.client
-
if database:
connection.drop_database(self.fs_files.database.name)
elif collections:
@@ -103,16 +136,22 @@ def save(self, content):
# but many more objects have this in python3 and shouldn't be using the chunking logic. For string and
# byte streams we write them directly to gridfs and convert them to byetarrys if necessary.
if hasattr(content.data, '__iter__') and not isinstance(content.data, (bytes, (str,))):
+ custom_md5 = hashlib.md5()
for chunk in content.data:
fp.write(chunk)
+ custom_md5.update(chunk)
+ fp.custom_md5 = custom_md5.hexdigest()
else:
# Ideally we could just ensure that we don't get strings in here and only byte streams
# but being confident of that wolud be a lot more work than we have time for so we just
# handle both cases here.
if isinstance(content.data, str):
- fp.write(content.data.encode('utf-8'))
+ encoded_data = content.data.encode('utf-8')
+ fp.write(encoded_data)
+ fp.custom_md5 = hashlib.md5(encoded_data).hexdigest()
else:
fp.write(content.data)
+ fp.custom_md5 = hashlib.md5(content.data).hexdigest()
return content
@@ -142,12 +181,13 @@ def find(self, location, throw_on_not_found=True, as_stream=False): # lint-amne
'thumbnail',
thumbnail_location[4]
)
+
return StaticContentStream(
location, fp.displayname, fp.content_type, fp, last_modified_at=fp.uploadDate,
thumbnail_location=thumbnail_location,
import_path=getattr(fp, 'import_path', None),
length=fp.length, locked=getattr(fp, 'locked', False),
- content_digest=getattr(fp, 'md5', None),
+ content_digest=getattr(fp, 'custom_md5', None),
)
else:
with self.fs.get(content_id) as fp:
@@ -161,12 +201,13 @@ def find(self, location, throw_on_not_found=True, as_stream=False): # lint-amne
'thumbnail',
thumbnail_location[4]
)
+
return StaticContent(
location, fp.displayname, fp.content_type, fp.read(), last_modified_at=fp.uploadDate,
thumbnail_location=thumbnail_location,
import_path=getattr(fp, 'import_path', None),
length=fp.length, locked=getattr(fp, 'locked', False),
- content_digest=getattr(fp, 'md5', None),
+ content_digest=getattr(fp, 'custom_md5', None),
)
except NoFile:
if throw_on_not_found: # lint-amnesty, pylint: disable=no-else-raise
diff --git a/xmodule/course_block.py b/xmodule/course_block.py
index 52422ffa3427..46ad7476f6d1 100644
--- a/xmodule/course_block.py
+++ b/xmodule/course_block.py
@@ -228,7 +228,7 @@ class ProctoringProvider(String):
and default that pulls from edx platform settings.
"""
- def from_json(self, value):
+ def from_json(self, value, validate_providers=False):
"""
Return ProctoringProvider as full featured Python type. Perform validation on the provider
and include any inherited values from the platform default.
@@ -237,7 +237,8 @@ def from_json(self, value):
if settings.FEATURES.get('ENABLE_PROCTORED_EXAMS'):
# Only validate the provider value if ProctoredExams are enabled on the environment
# Otherwise, the passed in provider does not matter. We should always return default
- self._validate_proctoring_provider(value)
+ if validate_providers:
+ self._validate_proctoring_provider(value)
value = self._get_proctoring_value(value)
return value
else:
diff --git a/xmodule/js/src/tabs/tabs-aggregator.js b/xmodule/js/src/tabs/tabs-aggregator.js
index 83baca4cf6e9..da982b6afc9d 100644
--- a/xmodule/js/src/tabs/tabs-aggregator.js
+++ b/xmodule/js/src/tabs/tabs-aggregator.js
@@ -63,8 +63,8 @@
if ($.isFunction(onSwitchFunction)) {
onSwitchFunction();
}
- this.$tabs.removeClass('current');
- $currentTarget.addClass('current');
+ this.$tabs.attr('aria-current', 'false').removeClass('current');
+ $currentTarget.attr('aria-current', 'true').addClass('current');
/*
Tabs are implemeted like anchors. Therefore we can use hash to find
diff --git a/xmodule/js/src/video/09_video_caption.js b/xmodule/js/src/video/09_video_caption.js
index 5584a9503beb..f5db26514b64 100644
--- a/xmodule/js/src/video/09_video_caption.js
+++ b/xmodule/js/src/video/09_video_caption.js
@@ -37,7 +37,8 @@
'previousLanguageMenuItem', 'nextLanguageMenuItem', 'handleCaptionToggle',
'showClosedCaptions', 'hideClosedCaptions', 'toggleClosedCaptions',
'updateCaptioningCookie', 'handleCaptioningCookie', 'handleTranscriptToggle',
- 'listenForDragDrop', 'setTranscriptVisibility', 'updateTranscriptCookie'
+ 'listenForDragDrop', 'setTranscriptVisibility', 'updateTranscriptCookie',
+ 'toggleGoogleDisclaimer'
);
this.state = state;
@@ -492,6 +493,31 @@
};
},
+ /**
+ * @desc Shows/Hides Google disclaimer based on captions being AI generated and
+ * if ClosedCaptions are being shown.
+ *
+ * @param {array} captions List of captions for the video.
+ *
+ * @returns {boolean}
+ */
+ toggleGoogleDisclaimer: function(captions) {
+ var self = this,
+ state = this.state,
+ aIGeneratedSpan = '',
+ captionsAIGenerated = captions.some(caption => caption.includes(aIGeneratedSpan));
+
+ if (!self.hideCaptionsOnLoad && !state.captionsHidden) {
+ if (captionsAIGenerated) {
+ state.el.find('.google-disclaimer').show();
+ self.shouldShowGoogleDisclaimer = true;
+ } else {
+ state.el.find('.google-disclaimer').hide();
+ self.shouldShowGoogleDisclaimer = false;
+ }
+ }
+ },
+
/**
* @desc Fetch the caption file specified by the user. Upon successful
* receipt of the file, the captions will be rendered.
@@ -547,6 +573,8 @@
start = results.start;
captions = results.captions;
+ self.toggleGoogleDisclaimer(captions);
+
if (self.loaded) {
if (self.rendered) {
self.renderCaption(start, captions);
@@ -1068,12 +1096,13 @@
if (typeof this.currentIndex !== 'undefined') {
this.subtitlesEl
.find('li.current')
+ .attr('aria-current', 'false')
.removeClass('current');
- }
-
+ }
this.subtitlesEl
.find("span[data-index='" + newIndex + "']")
.parent()
+ .attr('aria-current', 'true')
.addClass('current');
this.currentIndex = newIndex;
@@ -1299,6 +1328,7 @@
*/
hideCaptions: function(hideCaptions, triggerEvent) {
var transcriptControlEl = this.transcriptControlEl,
+ self = this,
state = this.state,
text;
@@ -1310,6 +1340,8 @@
this.state.el.trigger('transcript:hide');
}
+ state.el.find('.google-disclaimer').hide();
+
transcriptControlEl
.removeClass('is-active')
.attr('title', gettext(text))
@@ -1323,6 +1355,10 @@
this.state.el.trigger('transcript:show');
}
+ if (self.shouldShowGoogleDisclaimer) {
+ state.el.find('.google-disclaimer').show();
+ }
+
transcriptControlEl
.addClass('is-active')
.attr('title', gettext(text))
diff --git a/xmodule/modulestore/mongo/base.py b/xmodule/modulestore/mongo/base.py
index c5cc935861d2..16a8c134c1d6 100644
--- a/xmodule/modulestore/mongo/base.py
+++ b/xmodule/modulestore/mongo/base.py
@@ -473,30 +473,9 @@ def __init__(self, contentstore, doc_store_config, fs_root, render_template,
super().__init__(contentstore=contentstore, **kwargs)
- def do_connection(
- db, collection, host, port=27017, tz_aware=True, user=None, password=None, asset_collection=None, **kwargs
- ):
- """
- Create & open the connection, authenticate, and provide pointers to the collection
- """
- # Set a write concern of 1, which makes writes complete successfully to the primary
- # only before returning. Also makes pymongo report write errors.
- kwargs['w'] = 1
-
- self.database = connect_to_mongodb(
- db, host,
- port=port, tz_aware=tz_aware, user=user, password=password,
- retry_wait_time=retry_wait_time, **kwargs
- )
-
- self.collection = self.database[collection]
-
- # Collection which stores asset metadata.
- if asset_collection is None:
- asset_collection = self.DEFAULT_ASSET_COLLECTION_NAME
- self.asset_collection = self.database[asset_collection]
-
- do_connection(**doc_store_config)
+ self.doc_store_config = doc_store_config
+ self.retry_wait_time = retry_wait_time
+ self.do_connection(**self.doc_store_config)
if default_class is not None:
module_path, _, class_name = default_class.rpartition('.')
@@ -523,6 +502,48 @@ def do_connection(
self._course_run_cache = {}
self.signal_handler = signal_handler
+ def check_connection(self):
+ """
+ Check if mongodb connection is open or not.
+ """
+ try:
+ # The ismaster command is cheap and does not require auth.
+ self.database.client.admin.command('ismaster')
+ return True
+ except pymongo.errors.InvalidOperation:
+ return False
+
+ def ensure_connection(self):
+ """
+ Ensure that mongodb connection is open.
+ """
+ if self.check_connection():
+ return
+ self.do_connection(**self.doc_store_config)
+
+ def do_connection(
+ self, db, collection, host, port=27017, tz_aware=True, user=None, password=None, asset_collection=None, **kwargs
+ ):
+ """
+ Create & open the connection, authenticate, and provide pointers to the collection
+ """
+ # Set a write concern of 1, which makes writes complete successfully to the primary
+ # only before returning. Also makes pymongo report write errors.
+ kwargs['w'] = 1
+
+ self.database = connect_to_mongodb(
+ db, host,
+ port=port, tz_aware=tz_aware, user=user, password=password,
+ retry_wait_time=self.retry_wait_time, **kwargs
+ )
+
+ self.collection = self.database[collection]
+
+ # Collection which stores asset metadata.
+ if asset_collection is None:
+ asset_collection = self.DEFAULT_ASSET_COLLECTION_NAME
+ self.asset_collection = self.database[asset_collection]
+
def close_connections(self):
"""
Closes any open connections to the underlying database
@@ -541,6 +562,7 @@ def _drop_database(self, database=True, collections=True, connections=True):
If connections is True, then close the connection to the database as well.
"""
+ self.ensure_connection()
# drop the assets
super()._drop_database(database, collections, connections)
@@ -872,6 +894,8 @@ def has_course(self, course_key, ignore_case=False, **kwargs): # lint-amnesty,
course_query[key] = re.compile(r"(?i)^{}$".format(course_query[key]))
else:
course_query = {'_id': location.to_deprecated_son()}
+
+ self.ensure_connection()
course = self.collection.find_one(course_query, projection={'_id': True})
if course:
return CourseKey.from_string('/'.join([
diff --git a/xmodule/modulestore/split_mongo/mongo_connection.py b/xmodule/modulestore/split_mongo/mongo_connection.py
index bfb20fe0f5d5..9c00b0c22fc8 100644
--- a/xmodule/modulestore/split_mongo/mongo_connection.py
+++ b/xmodule/modulestore/split_mongo/mongo_connection.py
@@ -279,20 +279,30 @@ def __init__(
#make sure the course index cache is fresh.
RequestCache(namespace="course_index_cache").clear()
- self.database = connect_to_mongodb(
- db, host,
- port=port, tz_aware=tz_aware, user=user, password=password,
- retry_wait_time=retry_wait_time, **kwargs
- )
-
- self.course_index = self.database[collection + '.active_versions']
- self.structures = self.database[collection + '.structures']
- self.definitions = self.database[collection + '.definitions']
+ self.collection = collection
+ self.connection_params = {
+ 'db': db,
+ 'host': host,
+ 'port': port,
+ 'tz_aware': tz_aware,
+ 'user': user,
+ 'password': password,
+ 'retry_wait_time': retry_wait_time,
+ **kwargs
+ }
+
+ self.do_connection()
# Is the MySQL subclass in use, passing through some reads/writes to us? If so this will be True.
# If this MongoPersistenceBackend is being used directly (only MongoDB is involved), this is False.
self.with_mysql_subclass = with_mysql_subclass
+ def do_connection(self):
+ self.database = connect_to_mongodb(**self.connection_params)
+ self.course_index = self.database[self.collection + '.active_versions']
+ self.structures = self.database[self.collection + '.structures']
+ self.definitions = self.database[self.collection + '.definitions']
+
def heartbeat(self):
"""
Check that the db is reachable.
@@ -304,6 +314,24 @@ def heartbeat(self):
except pymongo.errors.ConnectionFailure:
raise HeartbeatFailure(f"Can't connect to {self.database.name}", 'mongo') # lint-amnesty, pylint: disable=raise-missing-from
+ def check_connection(self):
+ """
+ Check if mongodb connection is open or not.
+ """
+ try:
+ self.database.client.admin.command("ping")
+ return True
+ except pymongo.errors.InvalidOperation:
+ return False
+
+ def ensure_connection(self):
+ """
+ Ensure that mongodb connection is open.
+ """
+ if self.check_connection():
+ return
+ self.do_connection()
+
def get_structure(self, key, course_context=None):
"""
Get the structure from the persistence mechanism whose id is the given key.
@@ -502,6 +530,7 @@ def delete_course_index(self, course_key):
"""
Delete the course_index from the persistence mechanism whose id is the given course_index
"""
+ self.ensure_connection()
with TIMER.timer("delete_course_index", course_key):
query = {
key_attr: getattr(course_key, key_attr)
@@ -561,7 +590,8 @@ def close_connections(self):
Closes any open connections to the underlying databases
"""
RequestCache(namespace="course_index_cache").clear()
- self.database.client.close()
+ if self.check_connection():
+ self.database.client.close()
def _drop_database(self, database=True, collections=True, connections=True):
"""
@@ -576,6 +606,8 @@ def _drop_database(self, database=True, collections=True, connections=True):
If connections is True, then close the connection to the database as well.
"""
RequestCache(namespace="course_index_cache").clear()
+
+ self.ensure_connection()
connection = self.database.client
if database:
diff --git a/xmodule/modulestore/tests/test_mixed_modulestore.py b/xmodule/modulestore/tests/test_mixed_modulestore.py
index 91292cd88d71..8adbfcb911a4 100644
--- a/xmodule/modulestore/tests/test_mixed_modulestore.py
+++ b/xmodule/modulestore/tests/test_mixed_modulestore.py
@@ -156,8 +156,8 @@ def setUp(self):
tz_aware=True,
)
self.connection.drop_database(self.DB)
- self.addCleanup(self.connection.drop_database, self.DB)
- self.addCleanup(self.connection.close)
+ self.addCleanup(self._drop_database)
+ self.addCleanup(self._close_connection)
# define attrs which get set in initdb to quell pylint
self.writable_chapter_location = self.store = self.fake_location = None
@@ -165,6 +165,43 @@ def setUp(self):
self.user_id = ModuleStoreEnum.UserID.test
+ def _check_connection(self):
+ """
+ Check mongodb connection is open or not.
+ """
+ try:
+ self.connection.admin.command('ping')
+ return True
+ except pymongo.errors.InvalidOperation:
+ return False
+
+ def _ensure_connection(self):
+ """
+ Make sure that mongodb connection is open.
+ """
+ if not self._check_connection():
+ self.connection = pymongo.MongoClient(
+ host=self.HOST,
+ port=self.PORT,
+ tz_aware=True,
+ )
+
+ def _drop_database(self):
+ """
+ Drop mongodb database.
+ """
+ self._ensure_connection()
+ self.connection.drop_database(self.DB)
+
+ def _close_connection(self):
+ """
+ Close mongodb connection.
+ """
+ try:
+ self.connection.close()
+ except pymongo.errors.InvalidOperation:
+ pass
+
def _create_course(self, course_key, asides=None):
"""
Create a course w/ one item in the persistence store using the given course & item location.
diff --git a/xmodule/mongo_utils.py b/xmodule/mongo_utils.py
index 5daeff034e99..5aecbfc405df 100644
--- a/xmodule/mongo_utils.py
+++ b/xmodule/mongo_utils.py
@@ -30,8 +30,11 @@ def connect_to_mongodb(
handles AutoReconnect errors by retrying read operations, since these exceptions
typically indicate a temporary step-down condition for MongoDB.
"""
- # If the MongoDB server uses a separate authentication database that should be specified here
- auth_source = kwargs.get('authsource', '') or None
+ # If the MongoDB server uses a separate authentication database that should be specified here.
+ # Convert the lowercased authsource parameter to the camel-cased authSource expected by MongoClient.
+ auth_source = db
+ if auth_source_key := {'authSource', 'authsource'}.intersection(set(kwargs.keys())):
+ auth_source = kwargs.pop(auth_source_key.pop()) or db
# sanitize a kwarg which may be present and is no longer expected
# AED 2020-03-02 TODO: Remove this when 'auth_source' will no longer exist in kwargs
@@ -51,27 +54,30 @@ def connect_to_mongodb(
if read_preference is not None:
kwargs['read_preference'] = read_preference
- mongo_conn = pymongo.database.Database(
- pymongo.MongoClient(
- host=host,
- port=port,
- tz_aware=tz_aware,
- document_class=dict,
- **kwargs
- ),
- db
- )
+ if 'replicaSet' in kwargs and kwargs['replicaSet'] == '':
+ kwargs['replicaSet'] = None
+
+ connection_params = {
+ 'host': host,
+ 'port': port,
+ 'tz_aware': tz_aware,
+ 'document_class': dict,
+ **kwargs,
+ }
+
+ if user is not None and password is not None and not db.startswith('test_'):
+ connection_params.update({'username': user, 'password': password, 'authSource': auth_source})
+
+ mongo_conn = pymongo.MongoClient(**connection_params)
if proxy:
mongo_conn = MongoProxy(
- mongo_conn,
+ mongo_conn[db],
wait_time=retry_wait_time
)
- # If credentials were provided, authenticate the user.
- if user is not None and password is not None:
- mongo_conn.authenticate(user, password, source=auth_source)
+ return mongo_conn
- return mongo_conn
+ return mongo_conn[db]
def create_collection_index(
diff --git a/xmodule/partitions/tests/test_partitions.py b/xmodule/partitions/tests/test_partitions.py
index bc8c74cd3485..fde6afd3141b 100644
--- a/xmodule/partitions/tests/test_partitions.py
+++ b/xmodule/partitions/tests/test_partitions.py
@@ -14,7 +14,6 @@
from openedx.features.content_type_gating.models import ContentTypeGatingConfig
from xmodule.partitions.partitions import (
- ENROLLMENT_TRACK_PARTITION_ID,
USER_PARTITION_SCHEME_NAMESPACE,
Group,
NoSuchUserPartitionGroupError,
@@ -552,32 +551,6 @@ def _enable_enrollment_track_partition(enable):
"""
FEATURES['ENABLE_ENROLLMENT_TRACK_USER_PARTITION'] = enable
- def test_enrollment_track_partition_not_added_if_conflict(self):
- """
- Test that the dynamic enrollment track scheme is NOT added if a UserPartition exists with that ID.
- """
- self.user_partition = UserPartition(
- ENROLLMENT_TRACK_PARTITION_ID,
- self.TEST_NAME,
- self.TEST_DESCRIPTION,
- self.TEST_GROUPS,
- self.non_random_scheme,
- self.TEST_PARAMETERS,
- )
- self.course.user_partitions = [self.user_partition]
- all_partitions = get_all_partitions_for_course(self.course)
- assert 1 == len(all_partitions)
- assert self.TEST_SCHEME_NAME == all_partitions[0].scheme.name
-
- def test_enrollment_track_partition_not_added_if_disabled(self):
- """
- Test that the dynamic enrollment track scheme is NOT added if the settings FEATURE flag is disabled.
- """
- TestGetCourseUserPartitions._enable_enrollment_track_partition(False)
- all_partitions = get_all_partitions_for_course(self.course)
- assert 1 == len(all_partitions)
- assert self.TEST_SCHEME_NAME == all_partitions[0].scheme.name
-
def test_filter_inactive_user_partitions(self):
"""
Tests supplying the `active_only` parameter.
diff --git a/xmodule/studio_editable.py b/xmodule/studio_editable.py
index 29312014f96a..d190c966cab8 100644
--- a/xmodule/studio_editable.py
+++ b/xmodule/studio_editable.py
@@ -2,6 +2,7 @@
Mixin to support editing in Studio.
"""
from xblock.core import XBlock, XBlockMixin
+
from xmodule.x_module import AUTHOR_VIEW, STUDENT_VIEW
@@ -12,6 +13,7 @@ class StudioEditableBlock(XBlockMixin):
This class is only intended to be used with an XBlock!
"""
+
has_author_view = True
def render_children(self, context, fragment, can_reorder=False, can_add=False):
@@ -49,47 +51,6 @@ def get_preview_view_name(block):
"""
return AUTHOR_VIEW if has_author_view(block) else STUDENT_VIEW
- # Some parts of the code use getattr to dynamically check for the following methods on subclasses.
- # We'd like to refactor so that we can actually declare them here as overridable methods.
- # For now, we leave them here as documentation.
- # See https://github.com/openedx/edx-platform/issues/33715.
- #
- # def editor_saved(self, old_metadata, old_content) -> None: # pylint: disable=unused-argument
- # """
- # Called right *before* the block is written to the DB. Can be used, e.g., to modify fields before saving.
- #
- # By default, is a no-op. Can be overriden in subclasses.
- # """
- #
- # def post_editor_saved(self, old_metadata, old_content) -> None: # pylint: disable=unused-argument
- # """
- # Called right *after* the block is written to the DB. Can be used, e.g., to spin up followup tasks.
- #
- # By default, is a no-op. Can be overriden in subclasses.
- # """
- #
- # def studio_post_duplicate(self, store, source_block) -> bool: # pylint: disable=unused-argument
- # """
- # Called when a the block is duplicated. Can be used, e.g., for special handling of child duplication.
- #
- # Returns 'True' if children have been handled and thus shouldn't be handled by the standard
- # duplication logic.
- #
- # By default, is a no-op. Can be overriden in subclasses.
- # """
- # return False
- #
- # def studio_post_paste(self, store, source_node) -> bool: # pylint: disable=unused-argument
- # """
- # Called after a block is copy-pasted. Can be used, e.g., for special handling of child duplication.
- #
- # Returns 'True' if children have been handled and thus shouldn't be handled by the standard
- # duplication logic.
- #
- # By default, is a no-op. Can be overriden in subclasses.
- # """
- # return False
-
def has_author_view(block):
"""
diff --git a/xmodule/tests/test_capa_block.py b/xmodule/tests/test_capa_block.py
index 96ec64de61e5..d1c01e109718 100644
--- a/xmodule/tests/test_capa_block.py
+++ b/xmodule/tests/test_capa_block.py
@@ -655,6 +655,37 @@ def test_closed(self):
due=self.yesterday_str)
assert block.closed()
+ @patch.object(ProblemBlock, 'course_end_date', new_callable=PropertyMock)
+ def test_closed_for_archive(self, mock_course_end_date):
+
+ # Utility to create a datetime object in the past
+ def past_datetime(days):
+ return (datetime.datetime.now(UTC) - datetime.timedelta(days=days))
+
+ # Utility to create a datetime object in the future
+ def future_datetime(days):
+ return (datetime.datetime.now(UTC) + datetime.timedelta(days=days))
+
+ block = CapaFactory.create(max_attempts="1", attempts="0")
+
+ # For active courses without graceperiod
+ mock_course_end_date.return_value = future_datetime(10)
+ assert not block.closed()
+
+ # For archive courses without graceperiod
+ mock_course_end_date.return_value = past_datetime(10)
+ assert block.closed()
+
+ # For active courses with graceperiod
+ mock_course_end_date.return_value = future_datetime(10)
+ block.graceperiod = datetime.timedelta(days=2)
+ assert not block.closed()
+
+ # For archive courses with graceperiod
+ mock_course_end_date.return_value = past_datetime(2)
+ block.graceperiod = datetime.timedelta(days=3)
+ assert not block.closed()
+
def test_parse_get_params(self):
# Valid GET param dict
diff --git a/xmodule/tests/test_course_block.py b/xmodule/tests/test_course_block.py
index 39c6c39e8783..f2956cca0d7e 100644
--- a/xmodule/tests/test_course_block.py
+++ b/xmodule/tests/test_course_block.py
@@ -542,14 +542,27 @@ def test_from_json_with_invalid_provider(self, proctored_exams_setting_enabled):
with override_settings(FEATURES=FEATURES_WITH_PROCTORED_EXAMS):
if proctored_exams_setting_enabled:
with pytest.raises(InvalidProctoringProvider) as context_manager:
- self.proctoring_provider.from_json(provider)
+ self.proctoring_provider.from_json(provider, validate_providers=True)
expected_error = f'The selected proctoring provider, {provider}, is not a valid provider. ' \
f'Please select from one of {allowed_proctoring_providers}.'
assert str(context_manager.value) == expected_error
else:
- provider_value = self.proctoring_provider.from_json(provider)
+ provider_value = self.proctoring_provider.from_json(provider, validate_providers=True)
assert provider_value == self.proctoring_provider.default
+ def test_from_json_validate_providers(self):
+ """
+ Test that an invalid provider is ignored if validate providers is set to false
+ """
+ provider = 'invalid-provider'
+
+ FEATURES_WITH_PROCTORED_EXAMS = settings.FEATURES.copy()
+ FEATURES_WITH_PROCTORED_EXAMS['ENABLE_PROCTORED_EXAMS'] = True
+
+ with override_settings(FEATURES=FEATURES_WITH_PROCTORED_EXAMS):
+ provider_value = self.proctoring_provider.from_json(provider, validate_providers=False)
+ assert provider_value == provider
+
def test_from_json_adds_platform_default_for_missing_provider(self):
"""
Test that a value with no provider will inherit the default provider
diff --git a/xmodule/video_block/transcripts_utils.py b/xmodule/video_block/transcripts_utils.py
index d82a5d3f4789..e41f925295f0 100644
--- a/xmodule/video_block/transcripts_utils.py
+++ b/xmodule/video_block/transcripts_utils.py
@@ -866,8 +866,12 @@ def get_default_transcript_language(self, transcripts, dest_lang=None):
"""
sub, other_lang = transcripts["sub"], transcripts["transcripts"]
+ # language in plugin selector exists as transcript
if dest_lang and dest_lang in other_lang.keys():
transcript_language = dest_lang
+ # language in plugin selector is english and empty transcripts or transcripts and sub exists
+ elif dest_lang and dest_lang == 'en' and (not other_lang or (other_lang and sub)):
+ transcript_language = 'en'
elif self.transcript_language in other_lang:
transcript_language = self.transcript_language
elif sub:
| |