diff --git a/cms/djangoapps/contentstore/config/waffle.py b/cms/djangoapps/contentstore/config/waffle.py index 49dbab5715..f84290ba83 100644 --- a/cms/djangoapps/contentstore/config/waffle.py +++ b/cms/djangoapps/contentstore/config/waffle.py @@ -53,16 +53,3 @@ REDIRECT_TO_LIBRARY_AUTHORING_MICROFRONTEND = WaffleFlag( # .. toggle_warning: Flag course_experience.relative_dates should also be active for relative dates functionalities to work. # .. toggle_tickets: https://openedx.atlassian.net/browse/AA-844 CUSTOM_RELATIVE_DATES = CourseWaffleFlag(f'{WAFFLE_NAMESPACE}.custom_relative_dates', __name__) - - -# .. toggle_name: studio.enable_course_update_notifications -# .. toggle_implementation: CourseWaffleFlag -# .. toggle_default: False -# .. toggle_description: Waffle flag to enable course update notifications. -# .. toggle_use_cases: temporary, open_edx -# .. toggle_creation_date: 14-Feb-2024 -# .. toggle_target_removal_date: 14-Mar-2024 -ENABLE_COURSE_UPDATE_NOTIFICATIONS = CourseWaffleFlag( - f'{WAFFLE_NAMESPACE}.enable_course_update_notifications', - __name__ -) diff --git a/cms/djangoapps/contentstore/course_info_model.py b/cms/djangoapps/contentstore/course_info_model.py index 77a6a00c4b..e8a359d805 100644 --- a/cms/djangoapps/contentstore/course_info_model.py +++ b/cms/djangoapps/contentstore/course_info_model.py @@ -19,7 +19,6 @@ import re from django.http import HttpResponseBadRequest from django.utils.translation import gettext as _ -from cms.djangoapps.contentstore.config.waffle import ENABLE_COURSE_UPDATE_NOTIFICATIONS from cms.djangoapps.contentstore.utils import track_course_update_event, send_course_update_notification from openedx.core.lib.xblock_utils import get_course_update_items from xmodule.html_block import CourseInfoBlock # lint-amnesty, pylint: disable=wrong-import-order @@ -93,10 +92,9 @@ def update_course_updates(location, update, passed_id=None, user=None, request_m track_course_update_event(location.course_key, user, course_update_dict) # send course update notification - if ENABLE_COURSE_UPDATE_NOTIFICATIONS.is_enabled(location.course_key): - send_course_update_notification( - location.course_key, course_update_dict["content"], user, - ) + send_course_update_notification( + location.course_key, course_update_dict["content"], user, + ) # remove status key if "status" in course_update_dict: diff --git a/cms/djangoapps/contentstore/utils.py b/cms/djangoapps/contentstore/utils.py index b268bd6fcb..631ceeb270 100644 --- a/cms/djangoapps/contentstore/utils.py +++ b/cms/djangoapps/contentstore/utils.py @@ -2255,7 +2255,7 @@ def send_course_update_notification(course_key, content, user): "course_update_content": text_content if len(text_content.strip()) < 10 else "Click here to view", **extra_context, }, - notification_type="course_update", + notification_type="course_updates", content_url=f"{settings.LMS_ROOT_URL}/courses/{str(course_key)}/course/updates", app_name="updates", audience_filters={}, diff --git a/cms/djangoapps/contentstore/views/course.py b/cms/djangoapps/contentstore/views/course.py index 9f6cfb7c43..4647e4fdcc 100644 --- a/cms/djangoapps/contentstore/views/course.py +++ b/cms/djangoapps/contentstore/views/course.py @@ -58,6 +58,7 @@ from common.djangoapps.util.json_request import JsonResponse, JsonResponseBadReq from common.djangoapps.util.string_utils import _has_non_ascii_characters from openedx.core.djangoapps.content.course_overviews.models import CourseOverview from openedx.core.djangoapps.credit.tasks import update_credit_course_requirements +from openedx.core.djangoapps.discussions.tasks import update_discussions_settings_from_course from openedx.core.djangoapps.models.course_details import CourseDetails from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers from openedx.core.djangolib.js_utils import dump_js_escaped_json @@ -302,6 +303,10 @@ def course_handler(request, course_key_string=None): else: return HttpResponseBadRequest() elif request.method == 'GET': # assume html + # Update course discussion settings, sometimes the course discussion settings are not updated + # when the course is created, so we need to update them here. + course_key = CourseKey.from_string(course_key_string) + update_discussions_settings_from_course(course_key) if course_key_string is None: return redirect(reverse('home')) else: diff --git a/cms/djangoapps/contentstore/views/tests/test_course_index.py b/cms/djangoapps/contentstore/views/tests/test_course_index.py index c3dcfe5305..30e02214a1 100644 --- a/cms/djangoapps/contentstore/views/tests/test_course_index.py +++ b/cms/djangoapps/contentstore/views/tests/test_course_index.py @@ -717,8 +717,8 @@ class TestCourseOutline(CourseTestCase): """ Test to check number of queries made to mysql and mongo """ - with self.assertNumQueries(29, table_ignorelist=WAFFLE_TABLES): - with check_mongo_calls(3): + with self.assertNumQueries(32, table_ignorelist=WAFFLE_TABLES): + with check_mongo_calls(5): self.client.get_html(reverse_course_url('course_handler', self.course.id)) diff --git a/cms/static/sass/studio-main-v1.scss b/cms/static/sass/studio-main-v1.scss index ac649970d6..5d0cdda2ea 100644 --- a/cms/static/sass/studio-main-v1.scss +++ b/cms/static/sass/studio-main-v1.scss @@ -15,6 +15,8 @@ // +Libs and Resets - *do not edit* // ==================== + +@import '_builtin-block-variables'; @import 'bourbon/bourbon'; // lib - bourbon @import 'vendor/bi-app/bi-app-ltr'; // set the layout for left to right languages @import 'build-v1'; // shared app style assets/rendering diff --git a/common/static/data/geoip/GeoLite2-Country.mmdb b/common/static/data/geoip/GeoLite2-Country.mmdb index 5eae81b159..b4dcff0b4a 100644 Binary files a/common/static/data/geoip/GeoLite2-Country.mmdb and b/common/static/data/geoip/GeoLite2-Country.mmdb differ diff --git a/common/static/sass/_builtin-block-variables.scss b/common/static/sass/_builtin-block-variables.scss new file mode 100644 index 0000000000..2c567c6fb1 --- /dev/null +++ b/common/static/sass/_builtin-block-variables.scss @@ -0,0 +1,73 @@ +/* + * In pursuit of decoupling the built-in XBlocks from edx-platform's Sass build + * and ensuring comprehensive theming support in the extracted XBlocks, + * we need to expose Sass variables as CSS variables. + * + * Ticket/Issue: https://github.com/openedx/edx-platform/issues/35173 + */ +@import 'bourbon/bourbon'; +@import 'lms/theme/variables'; +@import 'lms/theme/variables-v1'; +@import 'cms/static/sass/partials/cms/theme/_variables'; +@import 'cms/static/sass/partials/cms/theme/_variables-v1'; +@import 'bootstrap/scss/variables'; +@import 'vendor/bi-app/bi-app-ltr'; +@import 'edx-pattern-library-shims/base/_variables.scss'; + +:root { + --action-primary-active-bg: $action-primary-active-bg; + --all-text-inputs: $all-text-inputs; + --base-font-size: $base-font-size; + --base-line-height: $base-line-height; + --baseline: $baseline; + --black: $black; + --black-t2: $black-t2; + --blue: $blue; + --blue-d1: $blue-d1; + --blue-d2: $blue-d2; + --blue-d4: $blue-d4; + --body-color: $body-color; + --border-color: $border-color; + --bp-screen-lg: $bp-screen-lg; + --btn-brand-focus-background: $btn-brand-focus-background; + --correct: $correct; + --danger: $danger; + --darkGrey: $darkGrey; + --error-color: $error-color; + --font-bold: $font-bold; + --font-family-sans-serif: $font-family-sans-serif; + --general-color-accent: $general-color-accent; + --gray: $gray; + --gray-300: $gray-300; + --gray-d1: $gray-d1; + --gray-l2: $gray-l2; + --gray-l3: $gray-l3; + --gray-l4: $gray-l4; + --gray-l6: $gray-l6; + --incorrect: $incorrect; + --lightGrey: $lightGrey; + --lighter-base-font-color: $lighter-base-font-color; + --link-color: $link-color; + --medium-font-size: $medium-font-size; + --partially-correct: $partially-correct; + --primary: $primary; + --shadow: $shadow; + --shadow-l1: $shadow-l1; + --sidebar-color: $sidebar-color; + --small-font-size: $small-font-size; + --static-path: $static-path; + --submitted: $submitted; + --success: $success; + --tmg-f2: $tmg-f2; + --tmg-s2: $tmg-s2; + --transparent: $transparent; + --uxpl-gray-background: $uxpl-gray-background; + --uxpl-gray-base: $uxpl-gray-base; + --uxpl-gray-dark: $uxpl-gray-dark; + --very-light-text: $very-light-text; + --warning: $warning; + --warning-color: $warning-color; + --warning-color-accent: $warning-color-accent; + --white: $white; + --yellow: $yellow; +} diff --git a/lms/djangoapps/bulk_email/apps.py b/lms/djangoapps/bulk_email/apps.py index 2cfb725ba8..63a44fcfcd 100644 --- a/lms/djangoapps/bulk_email/apps.py +++ b/lms/djangoapps/bulk_email/apps.py @@ -7,3 +7,7 @@ class BulkEmailConfig(AppConfig): Application Configuration for bulk_email. """ name = 'lms.djangoapps.bulk_email' + + def ready(self): + import lms.djangoapps.bulk_email.signals # lint-amnesty, pylint: disable=unused-import + from edx_ace.signals import ACE_MESSAGE_SENT # lint-amnesty, pylint: disable=unused-import diff --git a/lms/djangoapps/bulk_email/signals.py b/lms/djangoapps/bulk_email/signals.py index 818d222b7a..d45d0ae017 100644 --- a/lms/djangoapps/bulk_email/signals.py +++ b/lms/djangoapps/bulk_email/signals.py @@ -1,12 +1,13 @@ """ Signal handlers for the bulk_email app """ - - +from django.contrib.auth import get_user_model from django.dispatch import receiver +from eventtracking import tracker from common.djangoapps.student.models import CourseEnrollment from openedx.core.djangoapps.user_api.accounts.signals import USER_RETIRE_MAILINGS +from edx_ace.signals import ACE_MESSAGE_SENT from .models import Optout @@ -24,3 +25,28 @@ def force_optout_all(sender, **kwargs): # lint-amnesty, pylint: disable=unused- for enrollment in CourseEnrollment.objects.filter(user=user): Optout.objects.get_or_create(user=user, course_id=enrollment.course.id) + + +@receiver(ACE_MESSAGE_SENT) +def ace_email_sent_handler(sender, **kwargs): + """ + When an email is sent using ACE, this method will create an event to detect ace email success status + """ + # Fetch the message object from kwargs, defaulting to None if not present + message = kwargs.get('message', None) + + user_model = get_user_model() + try: + user_id = user_model.objects.get(email=message.recipient.email_address).id + except user_model.DoesNotExist: + user_id = None + course_email = message.context.get('course_email', None) + course_id = course_email.course_id if course_email else None + tracker.emit( + 'edx.bulk_email.sent', + { + 'message_type': message.name, + 'course_id': course_id, + 'user_id': user_id, + } + ) diff --git a/lms/djangoapps/bulk_email/tasks.py b/lms/djangoapps/bulk_email/tasks.py index afad888fe0..60d45c1564 100644 --- a/lms/djangoapps/bulk_email/tasks.py +++ b/lms/djangoapps/bulk_email/tasks.py @@ -26,6 +26,7 @@ from django.utils import timezone from django.utils.translation import gettext as _ from django.utils.translation import override as override_language from edx_django_utils.monitoring import set_code_owner_attribute +from eventtracking import tracker from markupsafe import escape from common.djangoapps.util.date_utils import get_default_time_display @@ -467,7 +468,14 @@ def _send_course_email(entry_id, email_id, to_list, global_email_context, subtas "send." ) raise exc - + tracker.emit( + 'edx.bulk_email.created', + { + 'course_id': str(course_email.course_id), + 'to_list': to_list, + 'total_recipients': total_recipients, + } + ) # Exclude optouts (if not a retry): # Note that we don't have to do the optout logic at all if this is a retry, # because we have presumably already performed the optout logic on the first diff --git a/lms/djangoapps/bulk_email/views.py b/lms/djangoapps/bulk_email/views.py index 528baf97b5..9276990915 100644 --- a/lms/djangoapps/bulk_email/views.py +++ b/lms/djangoapps/bulk_email/views.py @@ -7,6 +7,7 @@ import logging from django.contrib.auth.models import User # lint-amnesty, pylint: disable=imported-auth-user from django.http import Http404 +from eventtracking import tracker from opaque_keys import InvalidKeyError from opaque_keys.edx.keys import CourseKey @@ -60,4 +61,12 @@ def opt_out_email_updates(request, token, course_id): course_id, ) + tracker.emit( + 'edx.bulk_email.opt_out', + { + 'course_id': course_id, + 'user_id': user.id, + } + ) + return render_to_response('bulk_email/unsubscribe_success.html', context) diff --git a/lms/djangoapps/discussion/django_comment_client/base/tests.py b/lms/djangoapps/discussion/django_comment_client/base/tests.py index f8242efa0c..62af24f0ee 100644 --- a/lms/djangoapps/discussion/django_comment_client/base/tests.py +++ b/lms/djangoapps/discussion/django_comment_client/base/tests.py @@ -1458,6 +1458,8 @@ class CreateSubCommentUnicodeTestCase( @disable_signal(views, 'comment_created') @disable_signal(views, 'comment_voted') @disable_signal(views, 'comment_deleted') +@disable_signal(views, 'comment_flagged') +@disable_signal(views, 'thread_flagged') class TeamsPermissionsTestCase(ForumsEnableMixin, UrlResetMixin, SharedModuleStoreTestCase, MockRequestSetupMixin): # Most of the test points use the same ddt data. # args: user, commentable_id, status_code diff --git a/lms/djangoapps/discussion/rest_api/discussions_notifications.py b/lms/djangoapps/discussion/rest_api/discussions_notifications.py index f72271f3a6..21b1e27fcd 100644 --- a/lms/djangoapps/discussion/rest_api/discussions_notifications.py +++ b/lms/djangoapps/discussion/rest_api/discussions_notifications.py @@ -118,9 +118,10 @@ class DiscussionNotificationSender: self.parent_response and self.creator.id != int(self.thread.user_id) ): + author_name = f"{self.parent_response.username}'s" # use your if author of response is same as author of post. # use 'their' if comment author is also response author. - author_name = ( + author_pronoun = ( # Translators: Replier commented on "your" response to your post _("your") if self._response_and_thread_has_same_creator() @@ -129,10 +130,12 @@ class DiscussionNotificationSender: _("their") if self._response_and_comment_has_same_creator() else f"{self.parent_response.username}'s" + ) ) context = { "author_name": str(author_name), + "author_pronoun": str(author_pronoun), } self._send_notification([self.thread.user_id], "new_comment", extra_context=context) @@ -189,10 +192,21 @@ class DiscussionNotificationSender: if not self.parent_id: self._send_notification(users, "response_on_followed_post") else: + author_name = f"{self.parent_response.username}'s" + # use 'their' if comment author is also response author. + author_pronoun = ( + # Translators: Replier commented on "their" response in a post you're following + _("their") + if self._response_and_comment_has_same_creator() + else f"{self.parent_response.username}'s" + ) self._send_notification( users, "comment_on_followed_post", - extra_context={"author_name": self.parent_response.username} + extra_context={ + "author_name": str(author_name), + "author_pronoun": str(author_pronoun), + } ) def _create_cohort_course_audience(self): @@ -300,7 +314,7 @@ class DiscussionNotificationSender: content_type = thread_types[self.thread.type][getattr(self.thread, 'depth', 0)] context = { - 'username': self.creator.username, + 'username': self.thread.username, 'content_type': content_type, 'content': thread_body } diff --git a/lms/djangoapps/discussion/rest_api/tests/test_discussions_notifications.py b/lms/djangoapps/discussion/rest_api/tests/test_discussions_notifications.py index eeed292037..b4d3f3d18a 100644 --- a/lms/djangoapps/discussion/rest_api/tests/test_discussions_notifications.py +++ b/lms/djangoapps/discussion/rest_api/tests/test_discussions_notifications.py @@ -44,7 +44,7 @@ class TestDiscussionNotificationSender(unittest.TestCase): self.assertEqual(notification_type, "content_reported") self.assertEqual(context, { - 'username': 'test_user', + 'username': self.thread.username, 'content_type': expected_content_type, 'content': 'Thread body' }) diff --git a/lms/djangoapps/discussion/rest_api/tests/test_tasks.py b/lms/djangoapps/discussion/rest_api/tests/test_tasks.py index f3f2a68ae6..28cfe3395c 100644 --- a/lms/djangoapps/discussion/rest_api/tests/test_tasks.py +++ b/lms/djangoapps/discussion/rest_api/tests/test_tasks.py @@ -338,6 +338,7 @@ class TestSendResponseNotifications(DiscussionAPIViewTestMixin, ModuleStoreTestC 'replier_name': self.user_3.username, 'post_title': self.thread.title, 'author_name': 'dummy\'s', + 'author_pronoun': 'dummy\'s', 'course_name': self.course.display_name, 'sender_id': self.user_3.id } @@ -399,7 +400,8 @@ class TestSendResponseNotifications(DiscussionAPIViewTestMixin, ModuleStoreTestC expected_context = { 'replier_name': self.user_3.username, 'post_title': self.thread.title, - 'author_name': 'your', + 'author_name': 'dummy\'s', + 'author_pronoun': 'your', 'course_name': self.course.display_name, 'sender_id': self.user_3.id, } @@ -441,7 +443,8 @@ class TestSendResponseNotifications(DiscussionAPIViewTestMixin, ModuleStoreTestC 'sender_id': self.user_2.id, } if parent_id: - expected_context['author_name'] = 'dummy' + expected_context['author_name'] = 'dummy\'s' + expected_context['author_pronoun'] = 'dummy\'s' self.assertDictEqual(args.context, expected_context) self.assertEqual( args.content_url, @@ -531,7 +534,8 @@ class TestSendCommentNotification(DiscussionAPIViewTestMixin, ModuleStoreTestCas send_response_notifications(thread.id, str(self.course.id), self.user_2.id, parent_id=response.id) handler.assert_called_once() context = handler.call_args[1]['notification_data'].context - self.assertEqual(context['author_name'], 'their') + self.assertEqual(context['author_name'], 'dummy\'s') + self.assertEqual(context['author_pronoun'], 'their') @override_waffle_flag(ENABLE_NOTIFICATIONS, active=True) diff --git a/lms/djangoapps/grades/grade_utils.py b/lms/djangoapps/grades/grade_utils.py index 0344cf6c20..05d2058f37 100644 --- a/lms/djangoapps/grades/grade_utils.py +++ b/lms/djangoapps/grades/grade_utils.py @@ -7,6 +7,7 @@ import logging from datetime import timedelta from django.utils import timezone +from django.conf import settings from openedx.core.djangoapps.content.course_overviews.models import CourseOverview @@ -22,7 +23,7 @@ def are_grades_frozen(course_key): if ENFORCE_FREEZE_GRADE_AFTER_COURSE_END.is_enabled(course_key): course = CourseOverview.get_from_id(course_key) if course.end: - freeze_grade_date = course.end + timedelta(30) + freeze_grade_date = course.end + timedelta(settings.GRADEBOOK_FREEZE_DAYS) now = timezone.now() return now > freeze_grade_date return False diff --git a/lms/djangoapps/instructor/permissions.py b/lms/djangoapps/instructor/permissions.py index e1a1cbf466..24e0079fcc 100644 --- a/lms/djangoapps/instructor/permissions.py +++ b/lms/djangoapps/instructor/permissions.py @@ -1,11 +1,13 @@ """ Permissions for the instructor dashboard and associated actions """ - from bridgekeeper import perms from bridgekeeper.rules import is_staff +from opaque_keys.edx.keys import CourseKey +from rest_framework.permissions import BasePermission from lms.djangoapps.courseware.rules import HasAccessRule, HasRolesRule +from openedx.core.lib.courses import get_course_by_id ALLOW_STUDENT_TO_BYPASS_ENTRANCE_EXAM = 'instructor.allow_student_to_bypass_entrance_exam' ASSIGN_TO_COHORTS = 'instructor.assign_to_cohorts' @@ -72,3 +74,11 @@ perms[VIEW_DASHBOARD] = \ ) | HasAccessRule('staff') | HasAccessRule('instructor') perms[VIEW_ENROLLMENTS] = HasAccessRule('staff') perms[VIEW_FORUM_MEMBERS] = HasAccessRule('staff') + + +class InstructorPermission(BasePermission): + """Generic permissions""" + def has_permission(self, request, view): + course = get_course_by_id(CourseKey.from_string(view.kwargs.get('course_id'))) + permission = getattr(view, 'permission_name', None) + return request.user.has_perm(permission, course) diff --git a/lms/djangoapps/instructor/tests/test_api.py b/lms/djangoapps/instructor/tests/test_api.py index f5d8b04089..b0e533ee6f 100644 --- a/lms/djangoapps/instructor/tests/test_api.py +++ b/lms/djangoapps/instructor/tests/test_api.py @@ -642,6 +642,25 @@ class TestInstructorAPIBulkAccountCreationAndEnrollment(SharedModuleStoreTestCas last_name='Student' ) + def test_api_without_login(self): + """ + verify in case of no authentication it returns 401. + """ + self.client.logout() + uploaded_file = SimpleUploadedFile("temp.jpg", io.BytesIO(b"some initial binary data: \x00\x01").read()) + response = self.client.post(self.url, {'students_list': uploaded_file}) + assert response.status_code == 401 + + def test_api_without_permission(self): + """ + verify in case of no authentication it returns 403. + """ + # removed role from course for instructor + CourseInstructorRole(self.course.id).remove_users(self.instructor) + uploaded_file = SimpleUploadedFile("temp.jpg", io.BytesIO(b"some initial binary data: \x00\x01").read()) + response = self.client.post(self.url, {'students_list': uploaded_file}) + assert response.status_code == 403 + @patch('lms.djangoapps.instructor.views.api.log.info') @ddt.data( b"test_student@example.com,test_student_1,tester1,USA", # Typical use case. @@ -2945,7 +2964,37 @@ class TestInstructorAPILevelsDataDump(SharedModuleStoreTestCase, LoginEnrollment response = self.client.post(url, data) assert response.status_code == 200 res_json = json.loads(response.content.decode('utf-8')) - assert 'progress_url' in res_json + expected_data = { + 'course_id': str(self.course.id), + 'progress_url': f'/courses/{self.course.id}/progress/{self.students[0].id}/' + } + + for key, value in expected_data.items(): + self.assertIn(key, res_json) + self.assertEqual(res_json[key], value) + + def test_get_student_progress_url_response_headers(self): + """ + Test that the progress_url endpoint returns the correct headers. + """ + url = reverse('get_student_progress_url', kwargs={'course_id': str(self.course.id)}) + data = {'unique_student_identifier': self.students[0].email} + response = self.client.post(url, data) + assert response.status_code == 200 + + expected_headers = { + 'Allow': 'POST, OPTIONS', # drf view brings this key. + 'Cache-Control': 'no-cache, no-store, must-revalidate', + 'Content-Language': 'en', + 'Content-Length': str(len(response.content.decode('utf-8'))), + 'Content-Type': 'application/json', + 'Vary': 'Cookie, Accept-Language, origin', + 'X-Frame-Options': 'DENY' + } + + for key, value in expected_headers.items(): + self.assertIn(key, response.headers) + self.assertEqual(response.headers[key], value) def test_get_student_progress_url_from_uname(self): """ Test that progress_url is in the successful response. """ @@ -2955,6 +3004,14 @@ class TestInstructorAPILevelsDataDump(SharedModuleStoreTestCase, LoginEnrollment assert response.status_code == 200 res_json = json.loads(response.content.decode('utf-8')) assert 'progress_url' in res_json + expected_data = { + 'course_id': str(self.course.id), + 'progress_url': f'/courses/{self.course.id}/progress/{self.students[0].id}/' + } + + for key, value in expected_data.items(): + self.assertIn(key, res_json) + self.assertEqual(res_json[key], value) def test_get_student_progress_url_noparams(self): """ Test that the endpoint 404's without the required query params. """ @@ -2968,6 +3025,17 @@ class TestInstructorAPILevelsDataDump(SharedModuleStoreTestCase, LoginEnrollment response = self.client.post(url) assert response.status_code == 400 + def test_get_student_progress_url_without_permissions(self): + """ Test that progress_url returns 403 without credentials. """ + + # removed both roles from courses for instructor + CourseDataResearcherRole(self.course.id).remove_users(self.instructor) + CourseInstructorRole(self.course.id).remove_users(self.instructor) + url = reverse('get_student_progress_url', kwargs={'course_id': str(self.course.id)}) + data = {'unique_student_identifier': self.students[0].email} + response = self.client.post(url, data) + assert response.status_code == 403 + class TestInstructorAPIRegradeTask(SharedModuleStoreTestCase, LoginEnrollmentTestCase): """ diff --git a/lms/djangoapps/instructor/views/api.py b/lms/djangoapps/instructor/views/api.py index 02a91e7d84..7ca1e70467 100644 --- a/lms/djangoapps/instructor/views/api.py +++ b/lms/djangoapps/instructor/views/api.py @@ -105,6 +105,7 @@ from lms.djangoapps.instructor_task import api as task_api from lms.djangoapps.instructor_task.api_helper import AlreadyRunningError, QueueConnectionError from lms.djangoapps.instructor_task.data import InstructorTaskTypes from lms.djangoapps.instructor_task.models import ReportStore +from lms.djangoapps.instructor.views.serializer import RoleNameSerializer, UserSerializer, AccessSerializer from openedx.core.djangoapps.content.course_overviews.models import CourseOverview from openedx.core.djangoapps.course_groups.cohorts import add_user_to_cohort, is_course_cohorted from openedx.core.djangoapps.course_groups.models import CourseUserGroup @@ -122,6 +123,7 @@ from openedx.core.djangolib.markup import HTML, Text from openedx.core.lib.api.authentication import BearerAuthenticationAllowInactiveUser from openedx.core.lib.api.view_utils import DeveloperErrorViewMixin, view_auth_classes from openedx.core.lib.courses import get_course_by_id +from openedx.core.lib.api.serializers import CourseKeyField from openedx.features.course_experience.url_helpers import get_learning_mfe_home_url from .tools import ( dump_block_extensions, @@ -281,299 +283,305 @@ def require_finance_admin(func): return wrapped -@require_POST -@ensure_csrf_cookie -@cache_control(no_cache=True, no_store=True, must_revalidate=True) -@require_course_permission(permissions.CAN_ENROLL) -def register_and_enroll_students(request, course_id): # pylint: disable=too-many-statements +@method_decorator(cache_control(no_cache=True, no_store=True, must_revalidate=True), name='dispatch') +class RegisterAndEnrollStudents(APIView): """ Create new account and Enroll students in this course. - Passing a csv file that contains a list of students. - Order in csv should be the following email = 0; username = 1; name = 2; country = 3. - If there are more than 4 columns in the csv: cohort = 4, course mode = 5. - Requires staff access. - - -If the email address and username already exists and the user is enrolled in the course, - do nothing (including no email gets sent out) - - -If the email address already exists, but the username is different, - match on the email address only and continue to enroll the user in the course using the email address - as the matching criteria. Note the change of username as a warning message (but not a failure). - Send a standard enrollment email which is the same as the existing manual enrollment - - -If the username already exists (but not the email), assume it is a different user and fail - to create the new account. - The failure will be messaged in a response in the browser. """ + permission_classes = (IsAuthenticated, permissions.InstructorPermission) + permission_name = permissions.CAN_ENROLL - if not configuration_helpers.get_value( - 'ALLOW_AUTOMATED_SIGNUPS', - settings.FEATURES.get('ALLOW_AUTOMATED_SIGNUPS', False), - ): - return HttpResponseForbidden() + @method_decorator(ensure_csrf_cookie) + def post(self, request, course_id): # pylint: disable=too-many-statements + """ + Create new account and Enroll students in this course. + Passing a csv file that contains a list of students. + Order in csv should be the following email = 0; username = 1; name = 2; country = 3. + If there are more than 4 columns in the csv: cohort = 4, course mode = 5. + Requires staff access. - course_id = CourseKey.from_string(course_id) - warnings = [] - row_errors = [] - general_errors = [] + -If the email address and username already exists and the user is enrolled in the course, + do nothing (including no email gets sent out) - # email-students is a checkbox input type; will be present in POST if checked, absent otherwise - notify_by_email = 'email-students' in request.POST + -If the email address already exists, but the username is different, + match on the email address only and continue to enroll the user in the course using the email address + as the matching criteria. Note the change of username as a warning message (but not a failure). + Send a standard enrollment email which is the same as the existing manual enrollment - # for white labels we use 'shopping cart' which uses CourseMode.HONOR as - # course mode for creating course enrollments. - if CourseMode.is_white_label(course_id): - default_course_mode = CourseMode.HONOR - else: - default_course_mode = None + -If the username already exists (but not the email), assume it is a different user and fail + to create the new account. + The failure will be messaged in a response in the browser. + """ + if not configuration_helpers.get_value( + 'ALLOW_AUTOMATED_SIGNUPS', + settings.FEATURES.get('ALLOW_AUTOMATED_SIGNUPS', False), + ): + return HttpResponseForbidden() - # Allow bulk enrollments in all non-expired course modes including "credit" (which is non-selectable) - valid_course_modes = set(map(lambda x: x.slug, CourseMode.modes_for_course( - course_id=course_id, - only_selectable=False, - include_expired=False, - ))) + course_id = CourseKey.from_string(course_id) + warnings = [] + row_errors = [] + general_errors = [] - if 'students_list' in request.FILES: # lint-amnesty, pylint: disable=too-many-nested-blocks - students = [] + # email-students is a checkbox input type; will be present in POST if checked, absent otherwise + notify_by_email = 'email-students' in request.POST - try: - upload_file = request.FILES.get('students_list') - if upload_file.name.endswith('.csv'): - students = list(csv.reader(upload_file.read().decode('utf-8-sig').splitlines())) - course = get_course_by_id(course_id) - else: - general_errors.append({ - 'username': '', 'email': '', - 'response': _( - 'Make sure that the file you upload is in CSV format with no extraneous characters or rows.') - }) + # for white labels we use 'shopping cart' which uses CourseMode.HONOR as + # course mode for creating course enrollments. + if CourseMode.is_white_label(course_id): + default_course_mode = CourseMode.HONOR + else: + default_course_mode = None - except Exception: # pylint: disable=broad-except - general_errors.append({ - 'username': '', 'email': '', 'response': _('Could not read uploaded file.') - }) - finally: - upload_file.close() + # Allow bulk enrollments in all non-expired course modes including "credit" (which is non-selectable) + valid_course_modes = set(map(lambda x: x.slug, CourseMode.modes_for_course( + course_id=course_id, + only_selectable=False, + include_expired=False, + ))) - generated_passwords = [] - # To skip fetching cohorts from the DB while iterating on students, - # {: CourseUserGroup} - cohorts_cache = {} - already_warned_not_cohorted = False - extra_fields_is_enabled = configuration_helpers.get_value( - 'ENABLE_AUTOMATED_SIGNUPS_EXTRA_FIELDS', - settings.FEATURES.get('ENABLE_AUTOMATED_SIGNUPS_EXTRA_FIELDS', False), - ) + if 'students_list' in request.FILES: # lint-amnesty, pylint: disable=too-many-nested-blocks + students = [] - # Iterate each student in the uploaded csv file. - for row_num, student in enumerate(students, 1): - - # Verify that we have the expected number of columns in every row - # but allow for blank lines. - if not student: - continue - - if extra_fields_is_enabled: - is_valid_csv = 4 <= len(student) <= 6 - error = _('Data in row #{row_num} must have between four and six columns: ' - 'email, username, full name, country, cohort, and course mode. ' - 'The last two columns are optional.').format(row_num=row_num) - else: - is_valid_csv = len(student) == 4 - error = _('Data in row #{row_num} must have exactly four columns: ' - 'email, username, full name, and country.').format(row_num=row_num) - - if not is_valid_csv: - general_errors.append({ - 'username': '', - 'email': '', - 'response': error - }) - continue - - # Extract each column, handle optional columns if they exist. - email, username, name, country, *optional_cols = student - if optional_cols: - optional_cols.append(default_course_mode) - cohort_name, course_mode, *_tail = optional_cols - else: - cohort_name = None - course_mode = None - - # Validate cohort name, and get the cohort object. Skip if course - # is not cohorted. - cohort = None - - if cohort_name and not already_warned_not_cohorted: - if not is_course_cohorted(course_id): - row_errors.append({ - 'username': username, - 'email': email, - 'response': _('Course is not cohorted but cohort provided. ' - 'Ignoring cohort assignment for all users.') - }) - already_warned_not_cohorted = True - elif cohort_name in cohorts_cache: - cohort = cohorts_cache[cohort_name] + try: + upload_file = request.FILES.get('students_list') + if upload_file.name.endswith('.csv'): + students = list(csv.reader(upload_file.read().decode('utf-8-sig').splitlines())) + course = get_course_by_id(course_id) else: - # Don't attempt to create cohort or assign student if cohort - # does not exist. - try: - cohort = get_cohort_by_name(course_id, cohort_name) - except CourseUserGroup.DoesNotExist: + general_errors.append({ + 'username': '', 'email': '', + 'response': _( + 'Make sure that the file you upload is in CSV format with no ' + 'extraneous characters or rows.') + }) + + except Exception: # pylint: disable=broad-except + general_errors.append({ + 'username': '', 'email': '', 'response': _('Could not read uploaded file.') + }) + finally: + upload_file.close() + + generated_passwords = [] + # To skip fetching cohorts from the DB while iterating on students, + # {: CourseUserGroup} + cohorts_cache = {} + already_warned_not_cohorted = False + extra_fields_is_enabled = configuration_helpers.get_value( + 'ENABLE_AUTOMATED_SIGNUPS_EXTRA_FIELDS', + settings.FEATURES.get('ENABLE_AUTOMATED_SIGNUPS_EXTRA_FIELDS', False), + ) + + # Iterate each student in the uploaded csv file. + for row_num, student in enumerate(students, 1): + + # Verify that we have the expected number of columns in every row + # but allow for blank lines. + if not student: + continue + + if extra_fields_is_enabled: + is_valid_csv = 4 <= len(student) <= 6 + error = _('Data in row #{row_num} must have between four and six columns: ' + 'email, username, full name, country, cohort, and course mode. ' + 'The last two columns are optional.').format(row_num=row_num) + else: + is_valid_csv = len(student) == 4 + error = _('Data in row #{row_num} must have exactly four columns: ' + 'email, username, full name, and country.').format(row_num=row_num) + + if not is_valid_csv: + general_errors.append({ + 'username': '', + 'email': '', + 'response': error + }) + continue + + # Extract each column, handle optional columns if they exist. + email, username, name, country, *optional_cols = student + if optional_cols: + optional_cols.append(default_course_mode) + cohort_name, course_mode, *_tail = optional_cols + else: + cohort_name = None + course_mode = None + + # Validate cohort name, and get the cohort object. Skip if course + # is not cohorted. + cohort = None + + if cohort_name and not already_warned_not_cohorted: + if not is_course_cohorted(course_id): row_errors.append({ 'username': username, 'email': email, - 'response': _('Cohort name not found: {cohort}. ' - 'Ignoring cohort assignment for ' - 'all users.').format(cohort=cohort_name) + 'response': _('Course is not cohorted but cohort provided. ' + 'Ignoring cohort assignment for all users.') }) - cohorts_cache[cohort_name] = cohort - - # Validate course mode. - if not course_mode: - course_mode = default_course_mode - - if (course_mode is not None - and course_mode not in valid_course_modes): - # If `default is None` and the user is already enrolled, - # `CourseEnrollment.change_mode()` will not update the mode, - # hence two error messages. - if default_course_mode is None: - err_msg = _( - 'Invalid course mode: {mode}. Falling back to the ' - 'default mode, or keeping the current mode in case the ' - 'user is already enrolled.' - ).format(mode=course_mode) - else: - err_msg = _( - 'Invalid course mode: {mode}. Failling back to ' - '{default_mode}, or resetting to {default_mode} in case ' - 'the user is already enrolled.' - ).format(mode=course_mode, default_mode=default_course_mode) - row_errors.append({ - 'username': username, - 'email': email, - 'response': err_msg, - }) - course_mode = default_course_mode - - email_params = get_email_params(course, True, secure=request.is_secure()) - try: - validate_email(email) # Raises ValidationError if invalid - except ValidationError: - row_errors.append({ - 'username': username, - 'email': email, - 'response': _('Invalid email {email_address}.').format(email_address=email) - }) - else: - if User.objects.filter(email=email).exists(): - # Email address already exists. assume it is the correct user - # and just register the user in the course and send an enrollment email. - user = User.objects.get(email=email) - - # see if it is an exact match with email and username - # if it's not an exact match then just display a warning message, but continue onwards - if not User.objects.filter(email=email, username=username).exists(): - warning_message = _( - 'An account with email {email} exists but the provided username {username} ' - 'is different. Enrolling anyway with {email}.' - ).format(email=email, username=username) - - warnings.append({ - 'username': username, 'email': email, 'response': warning_message - }) - log.warning('email %s already exist', email) + already_warned_not_cohorted = True + elif cohort_name in cohorts_cache: + cohort = cohorts_cache[cohort_name] else: - log.info( - "user already exists with username '%s' and email '%s'", - username, - email - ) - - # enroll a user if it is not already enrolled. - if not is_user_enrolled_in_course(user, course_id): - # Enroll user to the course and add manual enrollment audit trail - create_manual_course_enrollment( - user=user, - course_id=course_id, - mode=course_mode, - enrolled_by=request.user, - reason='Enrolling via csv upload', - state_transition=UNENROLLED_TO_ENROLLED, - ) - enroll_email(course_id=course_id, - student_email=email, - auto_enroll=True, - email_students=notify_by_email, - email_params=email_params) - else: - # update the course mode if already enrolled - existing_enrollment = CourseEnrollment.get_enrollment(user, course_id) - if existing_enrollment.mode != course_mode: - existing_enrollment.change_mode(mode=course_mode) - if cohort: + # Don't attempt to create cohort or assign student if cohort + # does not exist. try: - add_user_to_cohort(cohort, user) - except ValueError: - # user already in this cohort; ignore - pass - elif is_email_retired(email): - # We are either attempting to enroll a retired user or create a new user with an email which is - # already associated with a retired account. Simply block these attempts. - row_errors.append({ - 'username': username, - 'email': email, - 'response': _('Invalid email {email_address}.').format(email_address=email), - }) - log.warning('Email address %s is associated with a retired user, so course enrollment was ' + # lint-amnesty, pylint: disable=logging-not-lazy - 'blocked.', email) - else: - # This email does not yet exist, so we need to create a new account - # If username already exists in the database, then create_and_enroll_user - # will raise an IntegrityError exception. - password = generate_unique_password(generated_passwords) - errors = create_and_enroll_user( - email, - username, - name, - country, - password, - course_id, - course_mode, - request.user, - email_params, - email_user=notify_by_email, - ) - row_errors.extend(errors) - if cohort: - try: - add_user_to_cohort(cohort, email) - except ValueError: - # user already in this cohort; ignore - # NOTE: Checking this here may be unnecessary if we can prove that a new user will never be - # automatically assigned to a cohort from the above. - pass - except ValidationError: + cohort = get_cohort_by_name(course_id, cohort_name) + except CourseUserGroup.DoesNotExist: row_errors.append({ 'username': username, 'email': email, - 'response': _('Invalid email {email_address}.').format(email_address=email), + 'response': _('Cohort name not found: {cohort}. ' + 'Ignoring cohort assignment for ' + 'all users.').format(cohort=cohort_name) }) + cohorts_cache[cohort_name] = cohort - else: - general_errors.append({ - 'username': '', 'email': '', 'response': _('File is not attached.') - }) + # Validate course mode. + if not course_mode: + course_mode = default_course_mode - results = { - 'row_errors': row_errors, - 'general_errors': general_errors, - 'warnings': warnings - } - return JsonResponse(results) + if (course_mode is not None + and course_mode not in valid_course_modes): + # If `default is None` and the user is already enrolled, + # `CourseEnrollment.change_mode()` will not update the mode, + # hence two error messages. + if default_course_mode is None: + err_msg = _( + 'Invalid course mode: {mode}. Falling back to the ' + 'default mode, or keeping the current mode in case the ' + 'user is already enrolled.' + ).format(mode=course_mode) + else: + err_msg = _( + 'Invalid course mode: {mode}. Failling back to ' + '{default_mode}, or resetting to {default_mode} in case ' + 'the user is already enrolled.' + ).format(mode=course_mode, default_mode=default_course_mode) + row_errors.append({ + 'username': username, + 'email': email, + 'response': err_msg, + }) + course_mode = default_course_mode + + email_params = get_email_params(course, True, secure=request.is_secure()) + try: + validate_email(email) # Raises ValidationError if invalid + except ValidationError: + row_errors.append({ + 'username': username, + 'email': email, + 'response': _('Invalid email {email_address}.').format(email_address=email) + }) + else: + if User.objects.filter(email=email).exists(): + # Email address already exists. assume it is the correct user + # and just register the user in the course and send an enrollment email. + user = User.objects.get(email=email) + + # see if it is an exact match with email and username + # if it's not an exact match then just display a warning message, but continue onwards + if not User.objects.filter(email=email, username=username).exists(): + warning_message = _( + 'An account with email {email} exists but the provided username {username} ' + 'is different. Enrolling anyway with {email}.' + ).format(email=email, username=username) + + warnings.append({ + 'username': username, 'email': email, 'response': warning_message + }) + log.warning('email %s already exist', email) + else: + log.info( + "user already exists with username '%s' and email '%s'", + username, + email + ) + + # enroll a user if it is not already enrolled. + if not is_user_enrolled_in_course(user, course_id): + # Enroll user to the course and add manual enrollment audit trail + create_manual_course_enrollment( + user=user, + course_id=course_id, + mode=course_mode, + enrolled_by=request.user, + reason='Enrolling via csv upload', + state_transition=UNENROLLED_TO_ENROLLED, + ) + enroll_email(course_id=course_id, + student_email=email, + auto_enroll=True, + email_students=notify_by_email, + email_params=email_params) + else: + # update the course mode if already enrolled + existing_enrollment = CourseEnrollment.get_enrollment(user, course_id) + if existing_enrollment.mode != course_mode: + existing_enrollment.change_mode(mode=course_mode) + if cohort: + try: + add_user_to_cohort(cohort, user) + except ValueError: + # user already in this cohort; ignore + pass + elif is_email_retired(email): + # We are either attempting to enroll a retired user or create a new user with an email which is + # already associated with a retired account. Simply block these attempts. + row_errors.append({ + 'username': username, + 'email': email, + 'response': _('Invalid email {email_address}.').format(email_address=email), + }) + log.warning('Email address %s is associated with a retired user, so course enrollment was ' + # lint-amnesty, pylint: disable=logging-not-lazy + 'blocked.', email) + else: + # This email does not yet exist, so we need to create a new account + # If username already exists in the database, then create_and_enroll_user + # will raise an IntegrityError exception. + password = generate_unique_password(generated_passwords) + errors = create_and_enroll_user( + email, + username, + name, + country, + password, + course_id, + course_mode, + request.user, + email_params, + email_user=notify_by_email, + ) + row_errors.extend(errors) + if cohort: + try: + add_user_to_cohort(cohort, email) + except ValueError: + # user already in this cohort; ignore + # NOTE: Checking this here may be unnecessary if we can prove that a + # new user will never be + # automatically assigned to a cohort from the above. + pass + except ValidationError: + row_errors.append({ + 'username': username, + 'email': email, + 'response': _('Invalid email {email_address}.').format(email_address=email), + }) + + else: + general_errors.append({ + 'username': '', 'email': '', 'response': _('File is not attached.') + }) + + results = { + 'row_errors': row_errors, + 'general_errors': general_errors, + 'warnings': warnings + } + return JsonResponse(results) def generate_random_string(length): @@ -979,17 +987,8 @@ def bulk_beta_modify_access(request, course_id): return JsonResponse(response_payload) -@require_POST -@ensure_csrf_cookie -@cache_control(no_cache=True, no_store=True, must_revalidate=True) -@require_course_permission(permissions.EDIT_COURSE_ACCESS) -@require_post_params( - unique_student_identifier="email or username of user to change access", - rolename="'instructor', 'staff', 'beta', or 'ccx_coach'", - action="'allow' or 'revoke'" -) -@common_exceptions_400 -def modify_access(request, course_id): +@method_decorator(cache_control(no_cache=True, no_store=True, must_revalidate=True), name='dispatch') +class ModifyAccess(APIView): """ Modify staff/instructor access of other user. Requires instructor access. @@ -1001,77 +1000,83 @@ def modify_access(request, course_id): rolename is one of ['instructor', 'staff', 'beta', 'ccx_coach'] action is one of ['allow', 'revoke'] """ - course_id = CourseKey.from_string(course_id) - course = get_course_with_access( - request.user, 'instructor', course_id, depth=None - ) - unique_student_identifier = request.POST.get('unique_student_identifier') - try: - user = get_student_from_identifier(unique_student_identifier) - except User.DoesNotExist: - response_payload = { - 'unique_student_identifier': unique_student_identifier, - 'userDoesNotExist': True, - } - return JsonResponse(response_payload) + permission_classes = (IsAuthenticated, permissions.InstructorPermission) + permission_name = permissions.EDIT_COURSE_ACCESS + serializer_class = AccessSerializer - # Check that user is active, because add_users - # in common/djangoapps/student/roles.py fails - # silently when we try to add an inactive user. - if not user.is_active: - response_payload = { - 'unique_student_identifier': user.username, - 'inactiveUser': True, - } - return JsonResponse(response_payload) + @method_decorator(ensure_csrf_cookie) + def post(self, request, course_id): + """ + Modify staff/instructor access of other user. + Requires instructor access. + """ + course_id = CourseKey.from_string(course_id) + course = get_course_with_access( + request.user, 'instructor', course_id, depth=None + ) - rolename = request.POST.get('rolename') - action = request.POST.get('action') + serializer_data = AccessSerializer(data=request.data) + if not serializer_data.is_valid(): + return HttpResponseBadRequest(reason=serializer_data.errors) - if rolename not in ROLES: - error = strip_tags(f"unknown rolename '{rolename}'") - log.error(error) - return HttpResponseBadRequest(error) + user = serializer_data.validated_data.get('unique_student_identifier') + if not user: + response_payload = { + 'unique_student_identifier': request.data.get('unique_student_identifier'), + 'userDoesNotExist': True, + } + return JsonResponse(response_payload) + + if not user.is_active: + response_payload = { + 'unique_student_identifier': user.username, + 'inactiveUser': True, + } + return JsonResponse(response_payload) + + rolename = serializer_data.data['rolename'] + action = serializer_data.data['action'] + + if rolename not in ROLES: + error = strip_tags(f"unknown rolename '{rolename}'") + log.error(error) + return HttpResponseBadRequest(error) + + # disallow instructors from removing their own instructor access. + if rolename == 'instructor' and user == request.user and action != 'allow': + response_payload = { + 'unique_student_identifier': user.username, + 'rolename': rolename, + 'action': action, + 'removingSelfAsInstructor': True, + } + return JsonResponse(response_payload) + + if action == 'allow': + allow_access(course, user, rolename) + if not is_user_enrolled_in_course(user, course_id): + CourseEnrollment.enroll(user, course_id) + elif action == 'revoke': + revoke_access(course, user, rolename) + else: + return HttpResponseBadRequest(strip_tags( + f"unrecognized action u'{action}'" + )) - # disallow instructors from removing their own instructor access. - if rolename == 'instructor' and user == request.user and action != 'allow': response_payload = { 'unique_student_identifier': user.username, 'rolename': rolename, 'action': action, - 'removingSelfAsInstructor': True, + 'success': 'yes', } return JsonResponse(response_payload) - if action == 'allow': - allow_access(course, user, rolename) - if not is_user_enrolled_in_course(user, course_id): - CourseEnrollment.enroll(user, course_id) - elif action == 'revoke': - revoke_access(course, user, rolename) - else: - return HttpResponseBadRequest(strip_tags( - f"unrecognized action u'{action}'" - )) - response_payload = { - 'unique_student_identifier': user.username, - 'rolename': rolename, - 'action': action, - 'success': 'yes', - } - return JsonResponse(response_payload) - - -@require_POST -@ensure_csrf_cookie -@cache_control(no_cache=True, no_store=True, must_revalidate=True) -@require_course_permission(permissions.EDIT_COURSE_ACCESS) -@require_post_params(rolename="'instructor', 'staff', or 'beta'") -def list_course_role_members(request, course_id): +@method_decorator(cache_control(no_cache=True, no_store=True, must_revalidate=True), name='dispatch') +class ListCourseRoleMembersView(APIView): """ - List instructors and staff. - Requires instructor access. + View to list instructors and staff for a specific course. + Requires the user to have instructor access. rolename is one of ['instructor', 'staff', 'beta', 'ccx_coach'] @@ -1087,33 +1092,41 @@ def list_course_role_members(request, course_id): ] } """ - course_id = CourseKey.from_string(course_id) - course = get_course_with_access( - request.user, 'instructor', course_id, depth=None - ) + permission_classes = (IsAuthenticated, permissions.InstructorPermission) + permission_name = permissions.EDIT_COURSE_ACCESS - rolename = request.POST.get('rolename') + @method_decorator(ensure_csrf_cookie) + def post(self, request, course_id): + """ + Handles POST request to list instructors and staff. - if rolename not in ROLES: - return HttpResponseBadRequest() + Args: + request (HttpRequest): The request object containing user data. + course_id (str): The ID of the course to list instructors and staff for. - def extract_user_info(user): - """ convert user into dicts for json view """ + Returns: + Response: A Response object containing the list of instructors and staff or an error message. - return { - 'username': user.username, - 'email': user.email, - 'first_name': user.first_name, - 'last_name': user.last_name, + Raises: + Http404: If the course does not exist. + """ + course_id = CourseKey.from_string(course_id) + course = get_course_with_access( + request.user, 'instructor', course_id, depth=None + ) + role_serializer = RoleNameSerializer(data=request.data) + role_serializer.is_valid(raise_exception=True) + rolename = role_serializer.data['rolename'] + + users = list_with_level(course.id, rolename) + serializer = UserSerializer(users, many=True) + + response_payload = { + 'course_id': str(course_id), + rolename: serializer.data, } - response_payload = { - 'course_id': str(course_id), - rolename: list(map(extract_user_info, list_with_level( - course.id, rolename - ))), - } - return JsonResponse(response_payload) + return Response(response_payload, status=status.HTTP_200_OK) class ProblemResponseReportPostParamsSerializer(serializers.Serializer): # pylint: disable=abstract-method @@ -1718,15 +1731,35 @@ def get_student_enrollment_status(request, course_id): return JsonResponse(response_payload) -@require_POST -@ensure_csrf_cookie -@cache_control(no_cache=True, no_store=True, must_revalidate=True) -@require_course_permission(permissions.ENROLLMENT_REPORT) -@require_post_params( - unique_student_identifier="email or username of student for whom to get progress url" -) -@common_exceptions_400 -def get_student_progress_url(request, course_id): +class StudentProgressUrlSerializer(serializers.Serializer): + """Serializer for course renders""" + unique_student_identifier = serializers.CharField(write_only=True) + course_id = CourseKeyField(required=False) + progress_url = serializers.SerializerMethodField() + + def get_progress_url(self, obj): # pylint: disable=unused-argument + """ + Return the progress URL for the student. + Args: + obj (dict): The dictionary containing data for the serializer. + Returns: + str: The URL for the progress of the student in the course. + """ + user = get_student_from_identifier(obj.get('unique_student_identifier')) + course_id = obj.get('course_id') # Adjust based on your data structure + + if course_home_mfe_progress_tab_is_active(course_id): + progress_url = get_learning_mfe_home_url(course_id, url_fragment='progress') + if user is not None: + progress_url += '/{}/'.format(user.id) + else: + progress_url = reverse('student_progress', kwargs={'course_id': str(course_id), 'student_id': user.id}) + + return progress_url + + +@method_decorator(cache_control(no_cache=True, no_store=True, must_revalidate=True), name='dispatch') +class StudentProgressUrl(APIView): """ Get the progress url of a student. Limited to staff access. @@ -1736,21 +1769,25 @@ def get_student_progress_url(request, course_id): 'progress_url': '/../...' } """ - course_id = CourseKey.from_string(course_id) - user = get_student_from_identifier(request.POST.get('unique_student_identifier')) + authentication_classes = ( + JwtAuthentication, + BearerAuthenticationAllowInactiveUser, + SessionAuthenticationAllowInactiveUser, + ) + permission_classes = (IsAuthenticated, permissions.InstructorPermission) + serializer_class = StudentProgressUrlSerializer + permission_name = permissions.ENROLLMENT_REPORT - if course_home_mfe_progress_tab_is_active(course_id): - progress_url = get_learning_mfe_home_url(course_id, url_fragment='progress') - if user is not None: - progress_url += '/{}/'.format(user.id) - else: - progress_url = reverse('student_progress', kwargs={'course_id': str(course_id), 'student_id': user.id}) - - response_payload = { - 'course_id': str(course_id), - 'progress_url': progress_url, - } - return JsonResponse(response_payload) + @method_decorator(ensure_csrf_cookie) + def post(self, request, course_id): + """Post method for validating incoming data and generating progress URL""" + data = { + 'course_id': course_id, + 'unique_student_identifier': request.data.get('unique_student_identifier') + } + serializer = self.serializer_class(data=data) + serializer.is_valid(raise_exception=True) + return Response(serializer.data) @transaction.non_atomic_requests @@ -2135,23 +2172,35 @@ def list_background_email_tasks(request, course_id): return JsonResponse(response_payload) -@require_POST -@ensure_csrf_cookie -@cache_control(no_cache=True, no_store=True, must_revalidate=True) -@require_course_permission(permissions.EMAIL) -def list_email_content(request, course_id): +@method_decorator(cache_control(no_cache=True, no_store=True, must_revalidate=True), name='dispatch') +class ListEmailContent(APIView): """ List the content of bulk emails sent """ - course_id = CourseKey.from_string(course_id) - task_type = InstructorTaskTypes.BULK_COURSE_EMAIL - # First get tasks list of bulk emails sent - emails = task_api.get_instructor_task_history(course_id, task_type=task_type) + permission_classes = (IsAuthenticated, permissions.InstructorPermission) + permission_name = permissions.EMAIL - response_payload = { - 'emails': list(map(extract_email_features, emails)), - } - return JsonResponse(response_payload) + @method_decorator(ensure_csrf_cookie) + def post(self, request, course_id): + """ + List the content of bulk emails sent for a specific course. + + Args: + request (HttpRequest): The HTTP request object. + course_id (str): The ID of the course for which to list the bulk emails. + + Returns: + HttpResponse: A response object containing the list of bulk email contents. + """ + course_id = CourseKey.from_string(course_id) + task_type = InstructorTaskTypes.BULK_COURSE_EMAIL + # First get tasks list of bulk emails sent + emails = task_api.get_instructor_task_history(course_id, task_type=task_type) + + response_payload = { + 'emails': list(map(extract_email_features, emails)), + } + return JsonResponse(response_payload) class InstructorTaskSerializer(serializers.Serializer): # pylint: disable=abstract-method @@ -2341,46 +2390,51 @@ def _list_instructor_tasks(request, course_id): return JsonResponse(response_payload) -@require_POST -@ensure_csrf_cookie -@cache_control(no_cache=True, no_store=True, must_revalidate=True) -@require_course_permission(permissions.SHOW_TASKS) -def list_entrance_exam_instructor_tasks(request, course_id): +@method_decorator(cache_control(no_cache=True, no_store=True, must_revalidate=True), name='dispatch') +class ListEntranceExamInstructorTasks(APIView): """ List entrance exam related instructor tasks. - - Takes either of the following query parameters - - unique_student_identifier is an email or username - - all_students is a boolean """ - course_id = CourseKey.from_string(course_id) - course = get_course_by_id(course_id) - student = request.POST.get('unique_student_identifier', None) - if student is not None: - student = get_student_from_identifier(student) + permission_classes = (IsAuthenticated, permissions.InstructorPermission) + permission_name = permissions.SHOW_TASKS - try: - entrance_exam_key = UsageKey.from_string(course.entrance_exam_id).map_into_course(course_id) - except InvalidKeyError: - return HttpResponseBadRequest(_("Course has no valid entrance exam section.")) - if student: - # Specifying for a single student's entrance exam history - tasks = task_api.get_entrance_exam_instructor_task_history( - course_id, - entrance_exam_key, - student - ) - else: - # Specifying for all student's entrance exam history - tasks = task_api.get_entrance_exam_instructor_task_history( - course_id, - entrance_exam_key - ) + @method_decorator(ensure_csrf_cookie) + def post(self, request, course_id): + """ + List entrance exam related instructor tasks. - response_payload = { - 'tasks': list(map(extract_task_features, tasks)), - } - return JsonResponse(response_payload) + Takes either of the following query parameters + - unique_student_identifier is an email or username + - all_students is a boolean + """ + course_id = CourseKey.from_string(course_id) + course = get_course_by_id(course_id) + student = request.POST.get('unique_student_identifier', None) + if student is not None: + student = get_student_from_identifier(student) + + try: + entrance_exam_key = UsageKey.from_string(course.entrance_exam_id).map_into_course(course_id) + except InvalidKeyError: + return HttpResponseBadRequest(_("Course has no valid entrance exam section.")) + if student: + # Specifying for a single student's entrance exam history + tasks = task_api.get_entrance_exam_instructor_task_history( + course_id, + entrance_exam_key, + student + ) + else: + # Specifying for all student's entrance exam history + tasks = task_api.get_entrance_exam_instructor_task_history( + course_id, + entrance_exam_key + ) + + response_payload = { + 'tasks': list(map(extract_task_features, tasks)), + } + return JsonResponse(response_payload) class ReportDownloadSerializer(serializers.Serializer): # pylint: disable=abstract-method diff --git a/lms/djangoapps/instructor/views/api_urls.py b/lms/djangoapps/instructor/views/api_urls.py index 0b4a88d1b7..c0e12e4675 100644 --- a/lms/djangoapps/instructor/views/api_urls.py +++ b/lms/djangoapps/instructor/views/api_urls.py @@ -1,3 +1,4 @@ + """ Instructor API endpoint urls. """ @@ -21,9 +22,9 @@ v1_api_urls = [ urlpatterns = [ path('students_update_enrollment', api.students_update_enrollment, name='students_update_enrollment'), - path('register_and_enroll_students', api.register_and_enroll_students, name='register_and_enroll_students'), - path('list_course_role_members', api.list_course_role_members, name='list_course_role_members'), - path('modify_access', api.modify_access, name='modify_access'), + path('register_and_enroll_students', api.RegisterAndEnrollStudents.as_view(), name='register_and_enroll_students'), + path('list_course_role_members', api.ListCourseRoleMembersView.as_view(), name='list_course_role_members'), + path('modify_access', api.ModifyAccess.as_view(), name='modify_access'), path('bulk_beta_modify_access', api.bulk_beta_modify_access, name='bulk_beta_modify_access'), path('get_problem_responses', api.get_problem_responses, name='get_problem_responses'), path('get_grading_config', api.get_grading_config, name='get_grading_config'), @@ -32,20 +33,20 @@ urlpatterns = [ path('get_students_who_may_enroll', api.get_students_who_may_enroll, name='get_students_who_may_enroll'), path('get_anon_ids', api.get_anon_ids, name='get_anon_ids'), path('get_student_enrollment_status', api.get_student_enrollment_status, name="get_student_enrollment_status"), - path('get_student_progress_url', api.get_student_progress_url, name='get_student_progress_url'), + path('get_student_progress_url', api.StudentProgressUrl.as_view(), name='get_student_progress_url'), path('reset_student_attempts', api.reset_student_attempts, name='reset_student_attempts'), path('rescore_problem', api.rescore_problem, name='rescore_problem'), path('override_problem_score', api.override_problem_score, name='override_problem_score'), path('reset_student_attempts_for_entrance_exam', api.reset_student_attempts_for_entrance_exam, name='reset_student_attempts_for_entrance_exam'), path('rescore_entrance_exam', api.rescore_entrance_exam, name='rescore_entrance_exam'), - path('list_entrance_exam_instructor_tasks', api.list_entrance_exam_instructor_tasks, + path('list_entrance_exam_instructor_tasks', api.ListEntranceExamInstructorTasks.as_view(), name='list_entrance_exam_instructor_tasks'), path('mark_student_can_skip_entrance_exam', api.mark_student_can_skip_entrance_exam, name='mark_student_can_skip_entrance_exam'), path('list_instructor_tasks', api.list_instructor_tasks, name='list_instructor_tasks'), path('list_background_email_tasks', api.list_background_email_tasks, name='list_background_email_tasks'), - path('list_email_content', api.list_email_content, name='list_email_content'), + path('list_email_content', api.ListEmailContent.as_view(), name='list_email_content'), path('list_forum_members', api.list_forum_members, name='list_forum_members'), path('update_forum_role_membership', api.update_forum_role_membership, name='update_forum_role_membership'), path('send_email', api.send_email, name='send_email'), diff --git a/lms/djangoapps/instructor/views/serializer.py b/lms/djangoapps/instructor/views/serializer.py new file mode 100644 index 0000000000..463f05ad45 --- /dev/null +++ b/lms/djangoapps/instructor/views/serializer.py @@ -0,0 +1,61 @@ +""" Instructor apis serializers. """ + +from django.contrib.auth.models import User # lint-amnesty, pylint: disable=imported-auth-user +from django.core.exceptions import ValidationError +from django.utils.translation import gettext as _ +from rest_framework import serializers +from .tools import get_student_from_identifier + +from lms.djangoapps.instructor.access import ROLES + + +class RoleNameSerializer(serializers.Serializer): # pylint: disable=abstract-method + """ + Serializer that describes the response of the problem response report generation API. + """ + + rolename = serializers.CharField(help_text=_("Role name")) + + def validate_rolename(self, value): + """ + Check that the rolename is valid. + """ + if value not in ROLES: + raise ValidationError(_("Invalid role name.")) + return value + + +class UserSerializer(serializers.ModelSerializer): + class Meta: + model = User + fields = ['username', 'email', 'first_name', 'last_name'] + + +class AccessSerializer(serializers.Serializer): + """ + Serializer for managing user access changes. + This serializer validates and processes the data required to modify + user access within a system. + """ + unique_student_identifier = serializers.CharField( + max_length=255, + help_text="Email or username of user to change access" + ) + rolename = serializers.CharField( + help_text="Role name to assign to the user" + ) + action = serializers.ChoiceField( + choices=['allow', 'revoke'], + help_text="Action to perform on the user's access" + ) + + def validate_unique_student_identifier(self, value): + """ + Validate that the unique_student_identifier corresponds to an existing user. + """ + try: + user = get_student_from_identifier(value) + except User.DoesNotExist: + return None + + return user diff --git a/lms/djangoapps/static_template_view/views.py b/lms/djangoapps/static_template_view/views.py index 01b99d51c8..a788f77a95 100644 --- a/lms/djangoapps/static_template_view/views.py +++ b/lms/djangoapps/static_template_view/views.py @@ -5,7 +5,7 @@ # List of valid templates is explicitly managed for (short-term) # security reasons. - +import logging import mimetypes from django.conf import settings @@ -23,6 +23,8 @@ from common.djangoapps.util.cache import cache_if_anonymous from common.djangoapps.util.views import fix_crum_request from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers +log = logging.getLogger(__name__) + valid_templates = [] if settings.STATIC_GRAB: @@ -122,4 +124,21 @@ def render_429(request, exception=None): # lint-amnesty, pylint: disable=unused @fix_crum_request def render_500(request): - return HttpResponseServerError(render_to_string('static_templates/server-error.html', {}, request=request)) + """ + Render the generic error page when we have an uncaught error. + """ + try: + return HttpResponseServerError(render_to_string('static_templates/server-error.html', {}, request=request)) + except BaseException as e: + # If we can't render the error page, ensure we don't raise another + # exception -- because if we do, we'll probably just end up back + # at the same rendering error. + # + # This is an attempt at working around the recursive error handling issues + # observed in , which + # were triggered by Mako and translation errors. + + log.error("Encountered error while rendering error page.", exc_info=True) + # This message is intentionally hardcoded and does not involve + # any translation, templating, etc. Do not translate. + return HttpResponseServerError("Encountered error while rendering error page.") diff --git a/lms/envs/common.py b/lms/envs/common.py index 9f9004976e..18d07afd71 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -56,7 +56,10 @@ from enterprise.constants import ( ENTERPRISE_FULFILLMENT_OPERATOR_ROLE, ENTERPRISE_REPORTING_CONFIG_ADMIN_ROLE, ENTERPRISE_SSO_ORCHESTRATOR_OPERATOR_ROLE, - ENTERPRISE_OPERATOR_ROLE + ENTERPRISE_OPERATOR_ROLE, + SYSTEM_ENTERPRISE_PROVISIONING_ADMIN_ROLE, + PROVISIONING_ENTERPRISE_CUSTOMER_ADMIN_ROLE, + PROVISIONING_PENDING_ENTERPRISE_CUSTOMER_ADMIN_ROLE, ) from openedx.core.constants import COURSE_KEY_REGEX, COURSE_KEY_PATTERN, COURSE_ID_PATTERN @@ -1101,6 +1104,11 @@ DEFAULT_GROUPS = [] # If this is true, random scores will be generated for the purpose of debugging the profile graphs GENERATE_PROFILE_SCORES = False +# .. setting_name: GRADEBOOK_FREEZE_DAYS +# .. setting_default: 30 +# .. setting_description: Sets the number of days after which the gradebook will freeze following the course's end. +GRADEBOOK_FREEZE_DAYS = 30 + # Used with XQueue XQUEUE_WAITTIME_BETWEEN_REQUESTS = 5 # seconds XQUEUE_INTERFACE = { @@ -4740,6 +4748,10 @@ SYSTEM_TO_FEATURE_ROLE_MAPPING = { ENTERPRISE_FULFILLMENT_OPERATOR_ROLE, ENTERPRISE_SSO_ORCHESTRATOR_OPERATOR_ROLE, ], + SYSTEM_ENTERPRISE_PROVISIONING_ADMIN_ROLE: [ + PROVISIONING_ENTERPRISE_CUSTOMER_ADMIN_ROLE, + PROVISIONING_PENDING_ENTERPRISE_CUSTOMER_ADMIN_ROLE, + ], } DATA_CONSENT_SHARE_CACHE_TIMEOUT = 8 * 60 * 60 # 8 hours diff --git a/lms/static/js/learner_dashboard/spec/program_details_view_spec.js b/lms/static/js/learner_dashboard/spec/program_details_view_spec.js index 2387ade00b..feaf725261 100644 --- a/lms/static/js/learner_dashboard/spec/program_details_view_spec.js +++ b/lms/static/js/learner_dashboard/spec/program_details_view_spec.js @@ -739,8 +739,8 @@ describe('Program Details View', () => { ); }); - it('should render appropriate subscription text when subscription is active with trial', () => { - testSubscriptionState( + it('should not render appropriate subscription text when subscription is active with trial', () => { + testSubscriptionSunsetting( 'active', 'Manage my subscription', 'Trial ends', @@ -748,8 +748,8 @@ describe('Program Details View', () => { ); }); - it('should render appropriate subscription text when subscription is active', () => { - testSubscriptionState( + it('should not render appropriate subscription text when subscription is active', () => { + testSubscriptionSunsetting( 'active', 'Manage my subscription', 'Your next billing date is', diff --git a/lms/static/sass/_variables.scss b/lms/static/sass/_variables.scss index dff9826b94..e1ccc71426 100644 --- a/lms/static/sass/_variables.scss +++ b/lms/static/sass/_variables.scss @@ -1,5 +1,7 @@ // LMS-specific variables +@import '_builtin-block-variables'; + $text-width-readability-max: 1080px; // LMS-only colors diff --git a/lms/templates/learner_dashboard/program_details_tab_view.underscore b/lms/templates/learner_dashboard/program_details_tab_view.underscore index f15265119d..37691cca55 100644 --- a/lms/templates/learner_dashboard/program_details_tab_view.underscore +++ b/lms/templates/learner_dashboard/program_details_tab_view.underscore @@ -46,64 +46,7 @@ <% } %> - <% if (isSubscriptionEligible && ( - completedCount !== totalCount - || subscriptionState === 'active' - ) - ) { %> -
- - target="_blank" - rel="noopener noreferrer" - <% } %> - > - <% if (subscriptionState === 'active') { %> -
- <%- gettext('Manage my subscription') %> -
- <% // xss-lint: disable=underscore-not-escaped %> - <%= launchIcon %> -
-
- <% } else if (subscriptionState === 'inactive') { %> -
-
- <% // xss-lint: disable=underscore-not-escaped %> - <%= restartIcon %> -
- <%- gettext('Restart my subscription') %> -
- <% } else { %> - <%- StringUtils.interpolate( - gettext('Start {trialLength}-day free trial'), - { trialLength } - ) %> - <% } %> -
- - <%- StringUtils.interpolate( - ( - hasActiveTrial - ? gettext('Trial ends {trialEndDate} at {trialEndTime}') - : subscriptionState === 'active' - ? gettext('Your next billing date is {currentPeriodEnd}') - : subscriptionState === 'inactive' - ? gettext('{subscriptionPrice} subscription. Cancel anytime.') - : gettext('{subscriptionPrice} subscription after trial ends. Cancel anytime.') - ), - { - subscriptionPrice, - currentPeriodEnd, - trialEndDate, - trialEndTime, - } - ) %> - -
- <% } else if ( + <% if ( !isSubscriptionEligible && is_learner_eligible_for_one_click_purchase && (typeof is_mobile_only === 'undefined' || is_mobile_only === false) diff --git a/lms/templates/learner_dashboard/program_details_view.underscore b/lms/templates/learner_dashboard/program_details_view.underscore index c2bdca897e..25f1cd5b88 100644 --- a/lms/templates/learner_dashboard/program_details_view.underscore +++ b/lms/templates/learner_dashboard/program_details_view.underscore @@ -21,64 +21,7 @@ <% } %> - <% if (isSubscriptionEligible && ( - completedCount !== totalCount - || subscriptionState === 'active' - ) - ) { %> -
- - target="_blank" - rel="noopener noreferrer" - <% } %> - > - <% if (subscriptionState === 'active') { %> -
- <%- gettext('Manage my subscription') %> -
- <% // xss-lint: disable=underscore-not-escaped %> - <%= launchIcon %> -
-
- <% } else if (subscriptionState === 'inactive') { %> -
-
- <% // xss-lint: disable=underscore-not-escaped %> - <%= restartIcon %> -
- <%- gettext('Restart my subscription') %> -
- <% } else { %> - <%- StringUtils.interpolate( - gettext('Start {trialLength}-day free trial'), - { trialLength } - ) %> - <% } %> -
- - <%- StringUtils.interpolate( - ( - hasActiveTrial - ? gettext('Trial ends {trialEndDate} at {trialEndTime}') - : subscriptionState === 'active' - ? gettext('Your next billing date is {currentPeriodEnd}') - : subscriptionState === 'inactive' - ? gettext('{subscriptionPrice} subscription. Cancel anytime.') - : gettext('{subscriptionPrice} subscription after trial ends. Cancel anytime.') - ), - { - subscriptionPrice, - currentPeriodEnd, - trialEndDate, - trialEndTime, - } - ) %> - -
- <% } else if ( + <% if ( !isSubscriptionEligible && is_learner_eligible_for_one_click_purchase && (typeof is_mobile_only === 'undefined' || is_mobile_only === false) diff --git a/openedx/core/djangoapps/content/search/api.py b/openedx/core/djangoapps/content/search/api.py index 9473dabbe4..4262b8a7b1 100644 --- a/openedx/core/djangoapps/content/search/api.py +++ b/openedx/core/djangoapps/content/search/api.py @@ -467,6 +467,29 @@ def delete_index_doc(usage_key: UsageKey) -> None: _wait_for_meili_tasks(tasks) +def delete_all_draft_docs_for_library(library_key: LibraryLocatorV2) -> None: + """ + Deletes draft documents for the given XBlocks from the search index + """ + current_rebuild_index_name = _get_running_rebuild_index_name() + client = _get_meilisearch_client() + # Delete all documents where last_published is null i.e. never published before. + delete_filter = [ + f'{Fields.context_key}="{library_key}"', + # This field should only be NULL or have a value, but we're also checking IS EMPTY just in case. + # Inner arrays are connected by an OR + [f'{Fields.last_published} IS EMPTY', f'{Fields.last_published} IS NULL'], + ] + + tasks = [] + if current_rebuild_index_name: + # If there is a rebuild in progress, the documents will also be deleted from the new index. + tasks.append(client.index(current_rebuild_index_name).delete_documents(filter=delete_filter)) + tasks.append(client.index(STUDIO_INDEX_NAME).delete_documents(filter=delete_filter)) + + _wait_for_meili_tasks(tasks) + + def upsert_library_block_index_doc(usage_key: UsageKey) -> None: """ Creates or updates the document for the given Library Block in the search index diff --git a/openedx/core/djangoapps/content/search/tasks.py b/openedx/core/djangoapps/content/search/tasks.py index 06ea3e777c..dfd6037769 100644 --- a/openedx/core/djangoapps/content/search/tasks.py +++ b/openedx/core/djangoapps/content/search/tasks.py @@ -9,9 +9,9 @@ import logging from celery import shared_task from celery_utils.logged_task import LoggedTask from edx_django_utils.monitoring import set_code_owner_attribute +from meilisearch.errors import MeilisearchError from opaque_keys.edx.keys import UsageKey from opaque_keys.edx.locator import LibraryLocatorV2, LibraryUsageLocatorV2 -from meilisearch.errors import MeilisearchError from . import api @@ -81,3 +81,6 @@ def update_content_library_index_docs(library_key_str: str) -> None: log.info("Updating content index documents for library with id: %s", library_key) api.upsert_content_library_index_docs(library_key) + # Delete all documents in this library that were not published by above function + # as this task is also triggered on discard event. + api.delete_all_draft_docs_for_library(library_key) diff --git a/openedx/core/djangoapps/content/search/tests/test_api.py b/openedx/core/djangoapps/content/search/tests/test_api.py index 9dcdfb76b4..e8616cee60 100644 --- a/openedx/core/djangoapps/content/search/tests/test_api.py +++ b/openedx/core/djangoapps/content/search/tests/test_api.py @@ -388,3 +388,18 @@ class TestSearchApi(ModuleStoreTestCase): mock_meilisearch.return_value.index.return_value.update_documents.assert_called_once_with( [self.doc_problem1, self.doc_problem2] ) + + @override_settings(MEILISEARCH_ENABLED=True) + def test_delete_all_drafts(self, mock_meilisearch): + """ + Test deleting all draft documents from the index. + """ + api.delete_all_draft_docs_for_library(self.library.key) + + delete_filter = [ + f'context_key="{self.library.key}"', + ['last_published IS EMPTY', 'last_published IS NULL'], + ] + mock_meilisearch.return_value.index.return_value.delete_documents.assert_called_once_with( + filter=delete_filter + ) diff --git a/openedx/core/djangoapps/content_libraries/api.py b/openedx/core/djangoapps/content_libraries/api.py index 888452c890..17bea80b3a 100644 --- a/openedx/core/djangoapps/content_libraries/api.py +++ b/openedx/core/djangoapps/content_libraries/api.py @@ -156,6 +156,9 @@ class ContentLibraryMetadata: version = attr.ib(0) type = attr.ib(default=COMPLEX) last_published = attr.ib(default=None, type=datetime) + last_draft_created = attr.ib(default=None, type=datetime) + last_draft_created_by = attr.ib(default=None, type=datetime) + published_by = attr.ib("") has_unpublished_changes = attr.ib(False) # has_unpublished_deletes will be true when the draft version of the library's bundle # contains deletes of any XBlocks that were in the most recently published version @@ -168,6 +171,8 @@ class ContentLibraryMetadata: # Studio, use it in their courses, and copy content out of this library. allow_public_read = attr.ib(False) license = attr.ib("") + created = attr.ib(default=None, type=datetime) + updated = attr.ib(default=None, type=datetime) class AccessLevel: @@ -206,7 +211,7 @@ class LibraryXBlockMetadata: """ Construct a LibraryXBlockMetadata from a Component object. """ - last_publish_log = authoring_api.get_last_publish(component.pk) + last_publish_log = component.versioning.last_publish_log return cls( usage_key=LibraryUsageLocatorV2( @@ -350,8 +355,11 @@ def get_library(library_key): learning_package = ref.learning_package num_blocks = authoring_api.get_all_drafts(learning_package.id).count() last_publish_log = authoring_api.get_last_publish(learning_package.id) - has_unpublished_changes = authoring_api.get_entities_with_unpublished_changes(learning_package.id) \ - .exists() + last_draft_log = authoring_api.get_entities_with_unpublished_changes(learning_package.id) \ + .order_by('-created').first() + last_draft_created = last_draft_log.created if last_draft_log else None + last_draft_created_by = last_draft_log.created_by.username if last_draft_log and last_draft_log.created_by else None + has_unpublished_changes = last_draft_log is not None # TODO: I'm doing this one to match already-existing behavior, but this is # something that we should remove. It exists to accomodate some complexities @@ -377,6 +385,9 @@ def get_library(library_key): # libraries. The top level version stays for now because LibraryContentBlock # uses it, but that should hopefully change before the Redwood release. version = 0 if last_publish_log is None else last_publish_log.pk + published_by = None + if last_publish_log and last_publish_log.published_by: + published_by = last_publish_log.published_by.username return ContentLibraryMetadata( key=library_key, @@ -386,12 +397,17 @@ def get_library(library_key): num_blocks=num_blocks, version=version, last_published=None if last_publish_log is None else last_publish_log.published_at, + published_by=published_by, + last_draft_created=last_draft_created, + last_draft_created_by=last_draft_created_by, allow_lti=ref.allow_lti, allow_public_learning=ref.allow_public_learning, allow_public_read=ref.allow_public_read, has_unpublished_changes=has_unpublished_changes, has_unpublished_deletes=has_unpublished_deletes, license=ref.license, + created=learning_package.created, + updated=learning_package.updated, ) @@ -735,27 +751,30 @@ def set_library_block_olx(usage_key, new_olx_str): ) -def create_library_block(library_key, block_type, definition_id): +def validate_can_add_block_to_library( + library_key: LibraryLocatorV2, + block_type: str, + block_id: str, +) -> tuple[ContentLibrary, LibraryUsageLocatorV2]: """ - Create a new XBlock in this library of the specified type (e.g. "html"). - """ - # It's in the serializer as ``definition_id``, but for our purposes, it's - # the block_id. See the comments in ``LibraryXBlockCreationSerializer`` for - # more details. TODO: Change the param name once we change the serializer. - block_id = definition_id + Perform checks to validate whether a new block with `block_id` and type `block_type` can be added to + the library with key `library_key`. + Returns the ContentLibrary that has the passed in `library_key` and newly created LibraryUsageLocatorV2 if + validation successful, otherwise raises errors. + """ assert isinstance(library_key, LibraryLocatorV2) - ref = ContentLibrary.objects.get_by_key(library_key) - if ref.type != COMPLEX: - if block_type != ref.type: + content_library = ContentLibrary.objects.get_by_key(library_key) # type: ignore[attr-defined] + if content_library.type != COMPLEX: + if block_type != content_library.type: raise IncompatibleTypesError( _('Block type "{block_type}" is not compatible with library type "{library_type}".').format( - block_type=block_type, library_type=ref.type, + block_type=block_type, library_type=content_library.type, ) ) # If adding a component would take us over our max, return an error. - component_count = authoring_api.get_all_drafts(ref.learning_package.id).count() + component_count = authoring_api.get_all_drafts(content_library.learning_package.id).count() if component_count + 1 > settings.MAX_BLOCKS_PER_CONTENT_LIBRARY: raise BlockLimitReachedError( _("Library cannot have more than {} Components").format( @@ -768,7 +787,7 @@ def create_library_block(library_key, block_type, definition_id): # Ensure the XBlock type is valid and installed: XBlock.load_class(block_type) # Will raise an exception if invalid # Make sure the new ID is not taken already: - usage_key = LibraryUsageLocatorV2( + usage_key = LibraryUsageLocatorV2( # type: ignore[abstract] lib_key=library_key, block_type=block_type, usage_id=block_id, @@ -777,12 +796,26 @@ def create_library_block(library_key, block_type, definition_id): if _component_exists(usage_key): raise LibraryBlockAlreadyExists(f"An XBlock with ID '{usage_key}' already exists") - _create_component_for_block(ref, usage_key) + return content_library, usage_key + + +def create_library_block(library_key, block_type, definition_id, user_id=None): + """ + Create a new XBlock in this library of the specified type (e.g. "html"). + """ + # It's in the serializer as ``definition_id``, but for our purposes, it's + # the block_id. See the comments in ``LibraryXBlockCreationSerializer`` for + # more details. TODO: Change the param name once we change the serializer. + block_id = definition_id + + content_library, usage_key = validate_can_add_block_to_library(library_key, block_type, block_id) + + _create_component_for_block(content_library, usage_key, user_id) # Now return the metadata about the new block: LIBRARY_BLOCK_CREATED.send_event( library_block=LibraryBlockData( - library_key=ref.library_key, + library_key=content_library.library_key, usage_key=usage_key ) ) @@ -804,6 +837,46 @@ def _component_exists(usage_key: UsageKeyV2) -> bool: return True +def import_staged_content_from_user_clipboard(library_key: LibraryLocatorV2, user, block_id) -> XBlock: + """ + Create a new library block and populate it with staged content from clipboard + + Returns the newly created library block + """ + from openedx.core.djangoapps.content_staging import api as content_staging_api + if not content_staging_api: + raise RuntimeError("The required content_staging app is not installed") + + user_clipboard = content_staging_api.get_user_clipboard(user) + if not user_clipboard: + return None + + olx_str = content_staging_api.get_staged_content_olx(user_clipboard.content.id) + + # TODO: Handle importing over static assets + + content_library, usage_key = validate_can_add_block_to_library( + library_key, + user_clipboard.content.block_type, + block_id + ) + + # Create component for block then populate it with clipboard data + _create_component_for_block(content_library, usage_key, user.id) + set_library_block_olx(usage_key, olx_str) + + # Emit library block created event + LIBRARY_BLOCK_CREATED.send_event( + library_block=LibraryBlockData( + library_key=content_library.library_key, + usage_key=usage_key + ) + ) + + # Now return the metadata about the new block + return get_library_block(usage_key) + + def get_or_create_olx_media_type(block_type: str) -> MediaType: """ Get or create a MediaType for the block type. @@ -816,7 +889,7 @@ def get_or_create_olx_media_type(block_type: str) -> MediaType: ) -def _create_component_for_block(content_lib, usage_key): +def _create_component_for_block(content_lib, usage_key, user_id=None): """ Create a Component for an XBlock type, and initialize it. @@ -847,7 +920,7 @@ def _create_component_for_block(content_lib, usage_key): local_key=usage_key.block_id, title=display_name, created=now, - created_by=None, + created_by=user_id, ) content = authoring_api.get_or_create_text_content( learning_package.id, @@ -951,13 +1024,13 @@ def get_allowed_block_types(library_key): # pylint: disable=unused-argument return info -def publish_changes(library_key): +def publish_changes(library_key, user_id=None): """ Publish all pending changes to the specified library. """ learning_package = ContentLibrary.objects.get_by_key(library_key).learning_package - authoring_api.publish_all_drafts(learning_package.id) + authoring_api.publish_all_drafts(learning_package.id, published_by=user_id) CONTENT_LIBRARY_UPDATED.send_event( content_library=ContentLibraryData( diff --git a/openedx/core/djangoapps/content_libraries/serializers.py b/openedx/core/djangoapps/content_libraries/serializers.py index 89c6b611a8..497eda8147 100644 --- a/openedx/core/djangoapps/content_libraries/serializers.py +++ b/openedx/core/djangoapps/content_libraries/serializers.py @@ -36,12 +36,14 @@ class ContentLibraryMetadataSerializer(serializers.Serializer): org = serializers.SlugField(source="key.org") slug = serializers.CharField(source="key.slug", validators=(validate_unicode_slug, )) bundle_uuid = serializers.UUIDField(format='hex_verbose', read_only=True) - #collection_uuid = serializers.UUIDField(format='hex_verbose', write_only=True) title = serializers.CharField() description = serializers.CharField(allow_blank=True) num_blocks = serializers.IntegerField(read_only=True) version = serializers.IntegerField(read_only=True) last_published = serializers.DateTimeField(format=DATETIME_FORMAT, read_only=True) + published_by = serializers.CharField(read_only=True) + last_draft_created = serializers.DateTimeField(format=DATETIME_FORMAT, read_only=True) + last_draft_created_by = serializers.CharField(read_only=True) allow_lti = serializers.BooleanField(default=False, read_only=True) allow_public_learning = serializers.BooleanField(default=False) allow_public_read = serializers.BooleanField(default=False) @@ -49,6 +51,8 @@ class ContentLibraryMetadataSerializer(serializers.Serializer): has_unpublished_deletes = serializers.BooleanField(read_only=True) license = serializers.ChoiceField(choices=LICENSE_OPTIONS, default=ALL_RIGHTS_RESERVED) can_edit_library = serializers.SerializerMethodField() + created = serializers.DateTimeField(format=DATETIME_FORMAT, read_only=True) + updated = serializers.DateTimeField(format=DATETIME_FORMAT, read_only=True) def get_can_edit_library(self, obj): """ @@ -175,6 +179,17 @@ class LibraryXBlockCreationSerializer(serializers.Serializer): # slugs at the moment, but hopefully we can change this soon. definition_id = serializers.CharField(validators=(validate_unicode_slug, )) + # Optional param specified when pasting data from clipboard instead of + # creating new block from scratch + staged_content = serializers.CharField(required=False) + + +class LibraryPasteClipboardSerializer(serializers.Serializer): + """ + Serializer for pasting clipboard data into library + """ + block_id = serializers.CharField(validators=(validate_unicode_slug, )) + class LibraryXBlockOlxSerializer(serializers.Serializer): """ diff --git a/openedx/core/djangoapps/content_libraries/tests/base.py b/openedx/core/djangoapps/content_libraries/tests/base.py index 40fe3ba949..987133255f 100644 --- a/openedx/core/djangoapps/content_libraries/tests/base.py +++ b/openedx/core/djangoapps/content_libraries/tests/base.py @@ -25,6 +25,7 @@ URL_LIB_BLOCKS = URL_LIB_DETAIL + 'blocks/' # Get the list of XBlocks in this l URL_LIB_TEAM = URL_LIB_DETAIL + 'team/' # Get the list of users/groups authorized to use this library URL_LIB_TEAM_USER = URL_LIB_TEAM + 'user/{username}/' # Add/edit/remove a user's permission to use this library URL_LIB_TEAM_GROUP = URL_LIB_TEAM + 'group/{group_name}/' # Add/edit/remove a group's permission to use this library +URL_LIB_PASTE_CLIPBOARD = URL_LIB_DETAIL + 'paste_clipboard/' # Paste user clipboard (POST) containing Xblock data URL_LIB_BLOCK = URL_PREFIX + 'blocks/{block_key}/' # Get data about a block, or delete it URL_LIB_BLOCK_OLX = URL_LIB_BLOCK + 'olx/' # Get or set the OLX of the specified XBlock URL_LIB_BLOCK_ASSETS = URL_LIB_BLOCK + 'assets/' # List the static asset files of the specified XBlock @@ -284,6 +285,12 @@ class ContentLibrariesRestApiTest(APITransactionTestCase): url = URL_LIB_BLOCK_ASSET_FILE.format(block_key=block_key, file_name=file_name) return self._api('delete', url, None, expect_response) + def _paste_clipboard_content_in_library(self, lib_key, block_id, expect_response=200): + """ Paste's the users clipboard content into Library """ + url = URL_LIB_PASTE_CLIPBOARD.format(lib_key=lib_key) + data = {"block_id": block_id} + return self._api('post', url, data, expect_response) + def _render_block_view(self, block_key, view_name, expect_response=200): """ Render an XBlock's view in the active application's runtime. diff --git a/openedx/core/djangoapps/content_libraries/tests/test_content_libraries.py b/openedx/core/djangoapps/content_libraries/tests/test_content_libraries.py index 576d506f9d..95b7309b3c 100644 --- a/openedx/core/djangoapps/content_libraries/tests/test_content_libraries.py +++ b/openedx/core/djangoapps/content_libraries/tests/test_content_libraries.py @@ -5,6 +5,7 @@ from unittest.mock import Mock, patch from unittest import skip import ddt +from uuid import uuid4 from django.contrib.auth.models import Group from django.test.client import Client from organizations.models import Organization @@ -29,6 +30,7 @@ from openedx.core.djangoapps.content_libraries.tests.base import ( URL_BLOCK_XBLOCK_HANDLER, ) from openedx.core.djangoapps.content_libraries.constants import VIDEO, COMPLEX, PROBLEM, CC_4_BY +from openedx.core.djangoapps.xblock import api as xblock_api from openedx.core.djangolib.testing.utils import skip_unless_cms from common.djangoapps.student.tests.factories import UserFactory @@ -974,6 +976,52 @@ class ContentLibrariesTestCase(ContentLibrariesRestApiTest, OpenEdxEventsTestMix event_receiver.call_args.kwargs ) + def test_library_paste_clipboard(self): + """ + Check the a new block is created in the library after pasting from clipboard. + The content of the new block should match the content of the block in the clipboard. + """ + # Importing here since this was failing when tests ran in the LMS + from openedx.core.djangoapps.content_staging.api import save_xblock_to_user_clipboard + + # Create user to perform tests on + author = UserFactory.create(username="Author", email="author@example.com") + with self.as_user(author): + lib = self._create_library( + slug="test_lib_paste_clipboard", + title="Paste Clipboard Test Library", + description="Testing pasting clipboard in library" + ) + lib_id = lib["id"] + + # Add a 'problem' XBlock to the library: + block_data = self._add_block_to_library(lib_id, "problem", "problem1") + + # Get the usage_key of the created block + library_key = LibraryLocatorV2.from_string(lib_id) + usage_key = LibraryUsageLocatorV2( + lib_key=library_key, + block_type="problem", + usage_id="problem1" + ) + + # Get the XBlock created in the previous step + block = xblock_api.load_block(usage_key, user=author) + + # Copy the block to the user's clipboard + save_xblock_to_user_clipboard(block, author.id) + + # Paste the content of the clipboard into the library + pasted_block_id = str(uuid4()) + paste_data = self._paste_clipboard_content_in_library(lib_id, pasted_block_id) + + # Check that the new block was created after the paste and it's content matches + # the the block in the clipboard + self.assertDictContainsEntries(self._get_library_block(paste_data["id"]), { + **block_data, + "id": f"lb:CL-TEST:test_lib_paste_clipboard:problem:{pasted_block_id}", + }) + @ddt.ddt class ContentLibraryXBlockValidationTest(APITestCase): diff --git a/openedx/core/djangoapps/content_libraries/urls.py b/openedx/core/djangoapps/content_libraries/urls.py index 022c288e36..6e450df635 100644 --- a/openedx/core/djangoapps/content_libraries/urls.py +++ b/openedx/core/djangoapps/content_libraries/urls.py @@ -43,6 +43,8 @@ urlpatterns = [ path('team/group//', views.LibraryTeamGroupView.as_view()), # Import blocks into this library. path('import_blocks/', include(import_blocks_router.urls)), + # Paste contents of clipboard into library + path('paste_clipboard/', views.LibraryPasteClipboardView.as_view()), ])), path('blocks//', include([ # Get metadata about a specific XBlock in this library, or delete the block: diff --git a/openedx/core/djangoapps/content_libraries/views.py b/openedx/core/djangoapps/content_libraries/views.py index 810c99f223..bde8142d3f 100644 --- a/openedx/core/djangoapps/content_libraries/views.py +++ b/openedx/core/djangoapps/content_libraries/views.py @@ -112,6 +112,7 @@ from openedx.core.djangoapps.content_libraries.serializers import ( LibraryXBlockStaticFileSerializer, LibraryXBlockStaticFilesSerializer, ContentLibraryAddPermissionByEmailSerializer, + LibraryPasteClipboardSerializer, ) import openedx.core.djangoapps.site_configuration.helpers as configuration_helpers from openedx.core.lib.api.view_utils import view_auth_classes @@ -487,7 +488,7 @@ class LibraryCommitView(APIView): """ key = LibraryLocatorV2.from_string(lib_key_str) api.require_permission_for_library_key(key, request.user, permissions.CAN_EDIT_THIS_CONTENT_LIBRARY) - api.publish_changes(key) + api.publish_changes(key, request.user.id) return Response({}) @convert_exceptions @@ -502,6 +503,34 @@ class LibraryCommitView(APIView): return Response({}) +@method_decorator(non_atomic_requests, name="dispatch") +@view_auth_classes() +class LibraryPasteClipboardView(GenericAPIView): + """ + Paste content of clipboard into Library. + """ + @convert_exceptions + def post(self, request, lib_key_str): + """ + Import the contents of the user's clipboard and paste them into the Library + """ + library_key = LibraryLocatorV2.from_string(lib_key_str) + api.require_permission_for_library_key(library_key, request.user, permissions.CAN_EDIT_THIS_CONTENT_LIBRARY) + serializer = LibraryPasteClipboardSerializer(data=request.data) + serializer.is_valid(raise_exception=True) + + try: + result = api.import_staged_content_from_user_clipboard( + library_key, request.user, **serializer.validated_data + ) + except api.IncompatibleTypesError as err: + raise ValidationError( # lint-amnesty, pylint: disable=raise-missing-from + detail={'block_type': str(err)}, + ) + + return Response(LibraryXBlockMetadataSerializer(result).data) + + @method_decorator(non_atomic_requests, name="dispatch") @view_auth_classes() class LibraryBlocksView(GenericAPIView): @@ -556,7 +585,7 @@ class LibraryBlocksView(GenericAPIView): # Create a new regular top-level block: try: - result = api.create_library_block(library_key, **serializer.validated_data) + result = api.create_library_block(library_key, user_id=request.user.id, **serializer.validated_data) except api.IncompatibleTypesError as err: raise ValidationError( # lint-amnesty, pylint: disable=raise-missing-from detail={'block_type': str(err)}, diff --git a/openedx/core/djangoapps/content_staging/views.py b/openedx/core/djangoapps/content_staging/views.py index e8b97df412..1b9790cfbe 100644 --- a/openedx/core/djangoapps/content_staging/views.py +++ b/openedx/core/djangoapps/content_staging/views.py @@ -10,12 +10,14 @@ from django.utils.decorators import method_decorator import edx_api_doc_tools as apidocs from opaque_keys import InvalidKeyError from opaque_keys.edx.keys import UsageKey -from opaque_keys.edx.locator import CourseLocator +from opaque_keys.edx.locator import CourseLocator, LibraryLocatorV2 from rest_framework.exceptions import NotFound, PermissionDenied, ValidationError from rest_framework.response import Response from rest_framework.views import APIView from common.djangoapps.student.auth import has_studio_read_access +from openedx.core.djangoapps.content_libraries import api as lib_api +from openedx.core.djangoapps.xblock import api as xblock_api from openedx.core.lib.api.view_utils import view_auth_classes from xmodule.modulestore.django import modulestore from xmodule.modulestore.exceptions import ItemNotFoundError @@ -89,18 +91,31 @@ class ClipboardEndpoint(APIView): if usage_key.block_type in ('course', 'chapter', 'sequential'): raise ValidationError('Requested XBlock tree is too large') course_key = usage_key.context_key - if not isinstance(course_key, CourseLocator): - # In the future, we'll support libraries too but for now we don't. - raise ValidationError('Invalid usage key: not a modulestore course') - # Make sure the user has permission on that course - if not has_studio_read_access(request.user, course_key): - raise PermissionDenied("You must be a member of the course team in Studio to export OLX using this API.") # Load the block and copy it to the user's clipboard try: - block = modulestore().get_item(usage_key) + if isinstance(course_key, CourseLocator): + # Make sure the user has permission on that course + if not has_studio_read_access(request.user, course_key): + raise PermissionDenied( + "You must be a member of the course team in Studio to export OLX using this API." + ) + block = modulestore().get_item(usage_key) + + elif isinstance(course_key, LibraryLocatorV2): + lib_api.require_permission_for_library_key( + course_key, + request.user, + lib_api.permissions.CAN_VIEW_THIS_CONTENT_LIBRARY + ) + block = xblock_api.load_block(usage_key, user=None) + + else: + raise ValidationError("Invalid usage_key for the content.") + except ItemNotFoundError as exc: raise NotFound("The requested usage key does not exist.") from exc + clipboard = api.save_xblock_to_user_clipboard(block=block, user_id=request.user.id) # Return the current clipboard exactly as if GET was called: diff --git a/openedx/core/djangoapps/content_tagging/tests/test_objecttag_export_helpers.py b/openedx/core/djangoapps/content_tagging/tests/test_objecttag_export_helpers.py index f84ad4e6df..d3306844ac 100644 --- a/openedx/core/djangoapps/content_tagging/tests/test_objecttag_export_helpers.py +++ b/openedx/core/djangoapps/content_tagging/tests/test_objecttag_export_helpers.py @@ -441,7 +441,7 @@ class TestContentTagChildrenExport(TaggedCourseMixin): # type: ignore[misc] """ Test if we can export a library """ - with self.assertNumQueries(11): + with self.assertNumQueries(8): tagged_library = build_object_tree_with_objecttags(self.library.key, self.all_library_object_tags) assert tagged_library == self.expected_library_tagged_xblock diff --git a/openedx/core/djangoapps/contentserver/test/test_views.py b/openedx/core/djangoapps/contentserver/test/test_views.py new file mode 100644 index 0000000000..9e0de99ed5 --- /dev/null +++ b/openedx/core/djangoapps/contentserver/test/test_views.py @@ -0,0 +1,26 @@ +""" +Tests for the view version of course asset serving. +""" + +import ddt +from django.test import TestCase +from django.urls import resolve + + +@ddt.ddt +class UrlsTest(TestCase): + """ + Tests for ensuring that the urlpatterns registered to the view are + appropriate for the URLs that the middleware historically handled. + """ + + @ddt.data( + '/c4x/edX/Open_DemoX/asset/images_course_image.jpg', + '/asset-v1:edX+DemoX.1+2T2019+type@asset+block/DemoX-poster.jpg', + '/assets/courseware/v1/0123456789abcdef0123456789abcdef/asset-v1:edX+FAKE101+2024+type@asset+block/HW1.png', + ) + def test_sample_urls(self, sample_url): + """ + Regression test -- c4x URL was previously incorrect in urls.py. + """ + assert resolve(sample_url).view_name == 'openedx.core.djangoapps.contentserver.views.course_assets_view' diff --git a/openedx/core/djangoapps/contentserver/urls.py b/openedx/core/djangoapps/contentserver/urls.py index 96bbe6bf38..f3fce5a6f2 100644 --- a/openedx/core/djangoapps/contentserver/urls.py +++ b/openedx/core/djangoapps/contentserver/urls.py @@ -2,7 +2,7 @@ URL patterns for course asset serving. """ -from django.urls import path, re_path +from django.urls import re_path from . import views @@ -10,7 +10,7 @@ from . import views # components of the URLs. That's because the view itself is separately # parsing the paths, for historical reasons. See docstring on views.py. urlpatterns = [ - path("c4x/", views.course_assets_view), + re_path("^c4x/", views.course_assets_view), re_path("^asset-v1:", views.course_assets_view), re_path("^assets/courseware/", views.course_assets_view), ] diff --git a/openedx/core/djangoapps/notifications/base_notification.py b/openedx/core/djangoapps/notifications/base_notification.py index 0287d8f73c..2c696ec60d 100644 --- a/openedx/core/djangoapps/notifications/base_notification.py +++ b/openedx/core/djangoapps/notifications/base_notification.py @@ -7,6 +7,7 @@ from .email_notifications import EmailCadence from common.djangoapps.student.roles import CourseInstructorRole, CourseStaffRole from .utils import find_app_in_normalized_apps, find_pref_in_normalized_prefs from ..django_comment_common.models import FORUM_ROLE_ADMINISTRATOR, FORUM_ROLE_MODERATOR, FORUM_ROLE_COMMUNITY_TA +from .notification_content import get_notification_type_content_function FILTER_AUDIT_EXPIRED_USERS_WITH_NO_ROLE = 'filter_audit_expired_users_with_no_role' @@ -109,8 +110,9 @@ COURSE_NOTIFICATION_TYPES = { 'is_core': True, 'info': '', 'non_editable': [], - 'content_template': _('<{p}><{strong}>{replier_name} commented on {author_name}\'s response in ' - 'a post you’re following <{strong}>{post_title}'), + 'content_template': _('<{p}><{strong}>{replier_name} commented on <{strong}>{author_name}' + ' response in a post you’re following <{strong}>{post_title}' + ''), 'content_context': { 'post_title': 'Post title', 'author_name': 'author name', @@ -169,13 +171,13 @@ COURSE_NOTIFICATION_TYPES = { 'email_template': '', 'filters': [FILTER_AUDIT_EXPIRED_USERS_WITH_NO_ROLE] }, - 'course_update': { + 'course_updates': { 'notification_app': 'updates', - 'name': 'course_update', + 'name': 'course_updates', 'is_core': False, 'info': '', 'web': True, - 'email': True, + 'email': False, 'push': True, 'email_cadence': EmailCadence.DAILY, 'non_editable': [], @@ -197,7 +199,7 @@ COURSE_NOTIFICATION_TYPES = { 'push': False, 'email_cadence': EmailCadence.DAILY, 'non_editable': [], - 'content_template': _('<{p}>You have a new open response submission awaiting for review for : ' + 'content_template': _('<{p}>You have a new open response submission awaiting for review for ' '<{strong}>{ora_name}'), 'content_context': { 'ora_name': 'Name of ORA in course', @@ -465,8 +467,13 @@ def get_notification_content(notification_type, context): 'strong': 'strong', 'p': 'p', } + content_function = get_notification_type_content_function(notification_type) + if notification_type == 'course_update': + notification_type = 'course_updates' notification_type = NotificationTypeManager().notification_types.get(notification_type, None) if notification_type: + if content_function: + return content_function(notification_type, context) notification_type_content_template = notification_type.get('content_template', None) if notification_type_content_template: return notification_type_content_template.format(**context, **html_tags_context) diff --git a/openedx/core/djangoapps/notifications/email/tasks.py b/openedx/core/djangoapps/notifications/email/tasks.py index 75af99aa86..0d450fe9a9 100644 --- a/openedx/core/djangoapps/notifications/email/tasks.py +++ b/openedx/core/djangoapps/notifications/email/tasks.py @@ -70,10 +70,12 @@ def get_user_preferences_for_courses(course_ids, user): return new_preferences -def send_digest_email_to_user(user, cadence_type, course_language='en', courses_data=None): +def send_digest_email_to_user(user, cadence_type, start_date, end_date, course_language='en', courses_data=None): """ Send [cadence_type] email to user. Cadence Type can be EmailCadence.DAILY or EmailCadence.WEEKLY + start_date: Datetime object + end_date: Datetime object """ if cadence_type not in [EmailCadence.DAILY, EmailCadence.WEEKLY]: raise ValueError('Invalid cadence_type') @@ -81,7 +83,6 @@ def send_digest_email_to_user(user, cadence_type, course_language='en', courses_ if not is_email_notification_flag_enabled(user): logger.info(f' Flag disabled for {user.username} ==Temp Log==') return - start_date, end_date = get_start_end_date(cadence_type) notifications = Notification.objects.filter(user=user, email=True, created__gte=start_date, created__lte=end_date) if not notifications: @@ -115,6 +116,7 @@ def send_digest_email_to_all_users(cadence_type): logger.info(f' Sending cadence email of type {cadence_type}') users = get_audience_for_cadence_email(cadence_type) courses_data = {} + start_date, end_date = get_start_end_date(cadence_type) logger.info(f' Email Cadence Audience {len(users)}') for user in users: - send_digest_email_to_user(user, cadence_type, courses_data=courses_data) + send_digest_email_to_user(user, cadence_type, start_date, end_date, courses_data=courses_data) diff --git a/openedx/core/djangoapps/notifications/email/tests/test_tasks.py b/openedx/core/djangoapps/notifications/email/tests/test_tasks.py index 5c88ef3edb..785dcf2a1b 100644 --- a/openedx/core/djangoapps/notifications/email/tests/test_tasks.py +++ b/openedx/core/djangoapps/notifications/email/tests/test_tasks.py @@ -16,6 +16,7 @@ from openedx.core.djangoapps.notifications.email.tasks import ( send_digest_email_to_all_users, send_digest_email_to_user ) +from openedx.core.djangoapps.notifications.email.utils import get_start_end_date from openedx.core.djangoapps.notifications.models import CourseNotificationPreference from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase from xmodule.modulestore.tests.factories import CourseFactory @@ -42,8 +43,9 @@ class TestEmailDigestForUser(ModuleStoreTestCase): """ Tests email is sent iff waffle flag is enabled """ + start_date, end_date = get_start_end_date(EmailCadence.DAILY) with override_waffle_flag(ENABLE_EMAIL_NOTIFICATIONS, True): - send_digest_email_to_user(self.user, EmailCadence.DAILY) + send_digest_email_to_user(self.user, EmailCadence.DAILY, start_date, end_date) assert not mock_func.called @ddt.data(True, False) @@ -54,8 +56,9 @@ class TestEmailDigestForUser(ModuleStoreTestCase): """ created_date = datetime.datetime.now() - datetime.timedelta(days=1) create_notification(self.user, self.course.id, created=created_date) + start_date, end_date = get_start_end_date(EmailCadence.DAILY) with override_waffle_flag(ENABLE_EMAIL_NOTIFICATIONS, flag_value): - send_digest_email_to_user(self.user, EmailCadence.DAILY) + send_digest_email_to_user(self.user, EmailCadence.DAILY, start_date, end_date) assert mock_func.called is flag_value @patch('edx_ace.ace.send') @@ -63,9 +66,10 @@ class TestEmailDigestForUser(ModuleStoreTestCase): """ Tests email is not sent if notification is created on next day """ - create_notification(self.user, self.course.id) + start_date, end_date = get_start_end_date(EmailCadence.DAILY) + create_notification(self.user, self.course.id, created=end_date + datetime.timedelta(minutes=2)) with override_waffle_flag(ENABLE_EMAIL_NOTIFICATIONS, True): - send_digest_email_to_user(self.user, EmailCadence.DAILY) + send_digest_email_to_user(self.user, EmailCadence.DAILY, start_date, end_date) assert not mock_func.called @patch('edx_ace.ace.send') @@ -73,12 +77,35 @@ class TestEmailDigestForUser(ModuleStoreTestCase): """ Tests email is not sent if notification is created day before yesterday """ - created_date = datetime.datetime.now() - datetime.timedelta(days=2) + start_date, end_date = get_start_end_date(EmailCadence.DAILY) + created_date = datetime.datetime.now() - datetime.timedelta(days=1, minutes=18) create_notification(self.user, self.course.id, created=created_date) with override_waffle_flag(ENABLE_EMAIL_NOTIFICATIONS, True): - send_digest_email_to_user(self.user, EmailCadence.DAILY) + send_digest_email_to_user(self.user, EmailCadence.DAILY, start_date, end_date) assert not mock_func.called + @ddt.data( + (EmailCadence.DAILY, datetime.datetime.now() - datetime.timedelta(days=1, minutes=30), False), + (EmailCadence.DAILY, datetime.datetime.now() - datetime.timedelta(minutes=10), True), + (EmailCadence.DAILY, datetime.datetime.now() - datetime.timedelta(days=1), True), + (EmailCadence.DAILY, datetime.datetime.now() + datetime.timedelta(minutes=20), False), + (EmailCadence.WEEKLY, datetime.datetime.now() - datetime.timedelta(days=7, minutes=30), False), + (EmailCadence.WEEKLY, datetime.datetime.now() - datetime.timedelta(days=7), True), + (EmailCadence.WEEKLY, datetime.datetime.now() - datetime.timedelta(minutes=20), True), + (EmailCadence.WEEKLY, datetime.datetime.now() + datetime.timedelta(minutes=20), False), + ) + @ddt.unpack + @patch('edx_ace.ace.send') + def test_notification_content(self, cadence_type, created_time, notification_created, mock_func): + """ + Tests email only contains notification created within date + """ + start_date, end_date = get_start_end_date(cadence_type) + create_notification(self.user, self.course.id, created=created_time) + with override_waffle_flag(ENABLE_EMAIL_NOTIFICATIONS, True): + send_digest_email_to_user(self.user, EmailCadence.DAILY, start_date, end_date) + assert mock_func.called is notification_created + @ddt.ddt class TestEmailDigestAudience(ModuleStoreTestCase): @@ -146,10 +173,11 @@ class TestEmailDigestAudience(ModuleStoreTestCase): """ Tests email is sent only when notifications with email=True exists """ - created_date = datetime.datetime.now() - datetime.timedelta(days=1) + start_date, end_date = get_start_end_date(EmailCadence.DAILY) + created_date = datetime.datetime.now() - datetime.timedelta(hours=23, minutes=59) create_notification(self.user, self.course.id, created=created_date, email=email_value) with override_waffle_flag(ENABLE_EMAIL_NOTIFICATIONS, True): - send_digest_email_to_user(self.user, EmailCadence.DAILY) + send_digest_email_to_user(self.user, EmailCadence.DAILY, start_date, end_date) assert mock_func.called is email_value @@ -166,7 +194,7 @@ class TestPreferences(ModuleStoreTestCase): self.user = UserFactory() self.course = CourseFactory.create(display_name='test course', run="Testing_course") self.preference = CourseNotificationPreference.objects.create(user=self.user, course_id=self.course.id) - created_date = datetime.datetime.now() - datetime.timedelta(days=1) + created_date = datetime.datetime.now() - datetime.timedelta(hours=23) create_notification(self.user, self.course.id, notification_type='new_discussion_post', created=created_date) @patch('edx_ace.ace.send') @@ -174,13 +202,14 @@ class TestPreferences(ModuleStoreTestCase): """ Tests email is send for digest notification preference """ + start_date, end_date = get_start_end_date(EmailCadence.DAILY) config = self.preference.notification_preference_config types = config['discussion']['notification_types'] types['new_discussion_post']['email_cadence'] = EmailCadence.DAILY types['new_discussion_post']['email'] = True self.preference.save() with override_waffle_flag(ENABLE_EMAIL_NOTIFICATIONS, True): - send_digest_email_to_user(self.user, EmailCadence.DAILY) + send_digest_email_to_user(self.user, EmailCadence.DAILY, start_date, end_date) assert mock_func.called @ddt.data(True, False) @@ -189,13 +218,14 @@ class TestPreferences(ModuleStoreTestCase): """ Tests email is sent iff preference value is True """ + start_date, end_date = get_start_end_date(EmailCadence.DAILY) config = self.preference.notification_preference_config types = config['discussion']['notification_types'] types['new_discussion_post']['email_cadence'] = EmailCadence.DAILY types['new_discussion_post']['email'] = pref_value self.preference.save() with override_waffle_flag(ENABLE_EMAIL_NOTIFICATIONS, True): - send_digest_email_to_user(self.user, EmailCadence.DAILY) + send_digest_email_to_user(self.user, EmailCadence.DAILY, start_date, end_date) assert mock_func.called is pref_value @patch('edx_ace.ace.send') @@ -203,10 +233,11 @@ class TestPreferences(ModuleStoreTestCase): """ Tests email is not send if digest notification preference doesnot match """ + start_date, end_date = get_start_end_date(EmailCadence.DAILY) config = self.preference.notification_preference_config types = config['discussion']['notification_types'] types['new_discussion_post']['email_cadence'] = EmailCadence.WEEKLY self.preference.save() with override_waffle_flag(ENABLE_EMAIL_NOTIFICATIONS, True): - send_digest_email_to_user(self.user, EmailCadence.DAILY) + send_digest_email_to_user(self.user, EmailCadence.DAILY, start_date, end_date) assert not mock_func.called diff --git a/openedx/core/djangoapps/notifications/email/tests/test_utils.py b/openedx/core/djangoapps/notifications/email/tests/test_utils.py index 8d72ffd748..6e79c497a4 100644 --- a/openedx/core/djangoapps/notifications/email/tests/test_utils.py +++ b/openedx/core/djangoapps/notifications/email/tests/test_utils.py @@ -70,7 +70,7 @@ class TestUtilFunctions(ModuleStoreTestCase): """ Notification.objects.all().delete() create_notification(self.user, self.course.id, app_name='discussion', notification_type='new_comment') - create_notification(self.user, self.course.id, app_name='updates', notification_type='course_update') + create_notification(self.user, self.course.id, app_name='updates', notification_type='course_updates') app_dict = create_app_notifications_dict(Notification.objects.all()) assert len(app_dict.keys()) == 2 for key in ['discussion', 'updates']: @@ -130,7 +130,7 @@ class TestContextFunctions(ModuleStoreTestCase): discussion_notification = create_notification(self.user, self.course.id, app_name='discussion', notification_type='new_comment') update_notification = create_notification(self.user, self.course.id, app_name='updates', - notification_type='course_update') + notification_type='course_updates') app_dict = create_app_notifications_dict(Notification.objects.all()) end_date = datetime.datetime(2024, 3, 24, 12, 0) params = { diff --git a/openedx/core/djangoapps/notifications/email/utils.py b/openedx/core/djangoapps/notifications/email/utils.py index 3ce2306435..da288750bb 100644 --- a/openedx/core/djangoapps/notifications/email/utils.py +++ b/openedx/core/djangoapps/notifications/email/utils.py @@ -7,7 +7,6 @@ import json from django.conf import settings from django.contrib.auth import get_user_model from django.shortcuts import get_object_or_404 -from django.urls import reverse from pytz import utc from waffle import get_waffle_flag_model # pylint: disable=invalid-django-waffle-import @@ -20,7 +19,10 @@ from openedx.core.djangoapps.notifications.base_notification import ( ) from openedx.core.djangoapps.notifications.config.waffle import ENABLE_EMAIL_NOTIFICATIONS from openedx.core.djangoapps.notifications.email_notifications import EmailCadence -from openedx.core.djangoapps.notifications.models import CourseNotificationPreference +from openedx.core.djangoapps.notifications.models import ( + CourseNotificationPreference, + get_course_notification_preference_config_version +) from xmodule.modulestore.django import modulestore from .notification_icons import NotificationTypeIcons @@ -71,15 +73,7 @@ def get_unsubscribe_link(username, patch): """ encrypted_username = encrypt_string(username) encrypted_patch = encrypt_object(patch) - kwargs = { - 'username': encrypted_username, - 'patch': encrypted_patch - } - relative_url = reverse('preference_update_from_encrypted_username_view', kwargs=kwargs) - protocol = 'https://' - if settings.DEBUG: - protocol = 'http://' - return f"{protocol}{settings.LMS_BASE}{relative_url}" + return f"{settings.LEARNING_MICROFRONTEND_URL}/preferences-unsubscribe/{encrypted_username}/{encrypted_patch}" def create_email_template_context(username): @@ -171,13 +165,10 @@ def get_start_end_date(cadence_type): """ if cadence_type not in [EmailCadence.DAILY, EmailCadence.WEEKLY]: raise ValueError('Invalid cadence_type') - date_today = datetime.datetime.now() - yesterday = date_today - datetime.timedelta(days=1) - end_date = datetime.datetime.combine(yesterday, datetime.time.max) - start_date = end_date + end_date = datetime.datetime.now() + start_date = end_date - datetime.timedelta(days=1, minutes=15) if cadence_type == EmailCadence.WEEKLY: - start_date = end_date - datetime.timedelta(days=6) - start_date = datetime.datetime.combine(start_date, datetime.time.min) + start_date = start_date - datetime.timedelta(days=6) return utc.localize(start_date), utc.localize(end_date) @@ -366,6 +357,14 @@ def update_user_preferences_from_patch(encrypted_username, encrypted_patch): return COURSE_NOTIFICATION_APPS[app_name]['core_email_cadence'] return COURSE_NOTIFICATION_TYPES[notification_type]['email_cadence'] + def get_updated_preference(pref): + """ + Update preference if config version doesn't match + """ + if pref.config_version != get_course_notification_preference_config_version(): + pref = pref.get_user_course_preference(pref.user_id, pref.course_id) + return pref + course_ids = CourseEnrollment.objects.filter(user=user).values_list('course_id', flat=True) CourseNotificationPreference.objects.bulk_create( [ @@ -378,6 +377,7 @@ def update_user_preferences_from_patch(encrypted_username, encrypted_patch): # pylint: disable=too-many-nested-blocks for preference in preferences: + preference = get_updated_preference(preference) preference_json = preference.notification_preference_config for app_name, app_prefs in preference_json.items(): if not is_name_match(app_name, app_value): diff --git a/openedx/core/djangoapps/notifications/models.py b/openedx/core/djangoapps/notifications/models.py index 8bf19edce5..e1bdf94acc 100644 --- a/openedx/core/djangoapps/notifications/models.py +++ b/openedx/core/djangoapps/notifications/models.py @@ -23,7 +23,7 @@ NOTIFICATION_CHANNELS = ['web', 'push', 'email'] ADDITIONAL_NOTIFICATION_CHANNEL_SETTINGS = ['email_cadence'] # Update this version when there is a change to any course specific notification type or app. -COURSE_NOTIFICATION_CONFIG_VERSION = 10 +COURSE_NOTIFICATION_CONFIG_VERSION = 11 def get_course_notification_preference_config(): diff --git a/openedx/core/djangoapps/notifications/notification_content.py b/openedx/core/djangoapps/notifications/notification_content.py new file mode 100644 index 0000000000..1dcdc4fb5a --- /dev/null +++ b/openedx/core/djangoapps/notifications/notification_content.py @@ -0,0 +1,35 @@ +""" +Helper functions for overriding notification content for given notification type. +""" + + +def get_notification_type_content_function(notification_type): + """ + Returns the content function for the given notification if it exists. + """ + try: + return globals()[f"get_{notification_type}_notification_content"] + except KeyError: + return None + + +def get_notification_content_with_author_pronoun(notification_type, context): + """ + Helper function to get notification content with author's pronoun. + """ + html_tags_context = { + 'strong': 'strong', + 'p': 'p', + } + notification_type_content_template = notification_type.get('content_template', None) + if 'author_pronoun' in context: + context['author_name'] = context['author_pronoun'] + if notification_type_content_template: + return notification_type_content_template.format(**context, **html_tags_context) + return '' + + +# Returns notification content for the new_comment notification. +get_new_comment_notification_content = get_notification_content_with_author_pronoun +# Returns notification content for the comment_on_followed_post notification. +get_comment_on_followed_post_notification_content = get_notification_content_with_author_pronoun diff --git a/openedx/core/djangoapps/notifications/templates/notifications/digest_content.html b/openedx/core/djangoapps/notifications/templates/notifications/digest_content.html index 1da86f48f0..13ac89d4ec 100644 --- a/openedx/core/djangoapps/notifications/templates/notifications/digest_content.html +++ b/openedx/core/djangoapps/notifications/templates/notifications/digest_content.html @@ -33,7 +33,7 @@

- {{ notification.content | safe }} + {{ notification.content | truncatechars_html:600 | safe }}

diff --git a/openedx/core/djangoapps/notifications/templates/notifications/digest_footer.html b/openedx/core/djangoapps/notifications/templates/notifications/digest_footer.html index 0419b25665..4fa903d127 100644 --- a/openedx/core/djangoapps/notifications/templates/notifications/digest_footer.html +++ b/openedx/core/djangoapps/notifications/templates/notifications/digest_footer.html @@ -38,7 +38,7 @@ Notification Settings - Unsubscribe + Unsubscribe from email digest for learning activity

diff --git a/openedx/core/djangoapps/notifications/templates/notifications/digest_header.html b/openedx/core/djangoapps/notifications/templates/notifications/digest_header.html index 7957524e8a..1f22ced200 100644 --- a/openedx/core/djangoapps/notifications/templates/notifications/digest_header.html +++ b/openedx/core/djangoapps/notifications/templates/notifications/digest_header.html @@ -5,6 +5,13 @@ style="background: #00262b; color: white; width: 100%; padding: 1.5rem" > + + + + Unsubscribe + + + logo_url 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 0000000000..79e88012f3 --- /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_tasks.py b/openedx/core/djangoapps/notifications/tests/test_tasks.py index 5058c0f492..706ae29898 100644 --- a/openedx/core/djangoapps/notifications/tests/test_tasks.py +++ b/openedx/core/djangoapps/notifications/tests/test_tasks.py @@ -390,7 +390,7 @@ class TestDeleteNotificationTask(ModuleStoreTestCase): """ 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 389c9e7e06..e40e520789 100644 --- a/openedx/core/djangoapps/notifications/tests/test_views.py +++ b/openedx/core/djangoapps/notifications/tests/test_views.py @@ -28,9 +28,13 @@ from openedx.core.djangoapps.django_comment_common.models import ( ) 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 +from openedx.core.djangoapps.notifications.models import ( + CourseNotificationPreference, + Notification, + get_course_notification_preference_config_version +) from openedx.core.djangoapps.notifications.serializers import NotificationCourseEnrollmentSerializer -from openedx.core.djangoapps.notifications.email.utils import get_unsubscribe_link +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 @@ -275,9 +279,9 @@ class UserNotificationPreferenceAPITest(ModuleStoreTestCase): 'enabled': True, 'core_notification_types': [], 'notification_types': { - 'course_update': { + 'course_updates': { 'web': True, - 'email': True, + 'email': False, 'push': True, 'email_cadence': 'Daily', 'info': '' @@ -910,7 +914,13 @@ class UpdatePreferenceFromEncryptedDataView(ModuleStoreTestCase): """ Tests if preference is updated when url is hit """ - url = get_unsubscribe_link(self.user.username, {'channel': 'email', 'value': False}) + 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 @@ -921,6 +931,24 @@ class UpdatePreferenceFromEncryptedDataView(ModuleStoreTestCase): 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): """ diff --git a/openedx/core/djangoapps/notifications/views.py b/openedx/core/djangoapps/notifications/views.py index 858f5da9f5..fdc91c12a9 100644 --- a/openedx/core/djangoapps/notifications/views.py +++ b/openedx/core/djangoapps/notifications/views.py @@ -5,7 +5,6 @@ from datetime import datetime, timedelta from django.conf import settings from django.db.models import Count -from django.http import HttpResponse from django.shortcuts import get_object_or_404 from django.utils.translation import gettext as _ from opaque_keys.edx.keys import CourseKey @@ -442,4 +441,4 @@ def preference_update_from_encrypted_username_view(request, username, patch): username and patch must be string """ update_user_preferences_from_patch(username, patch) - return HttpResponse("Success", status=status.HTTP_200_OK) + return Response({"result": "success"}, status=status.HTTP_200_OK) diff --git a/openedx/core/djangoapps/xblock/rest_api/views.py b/openedx/core/djangoapps/xblock/rest_api/views.py index 501386efba..8c2d16839a 100644 --- a/openedx/core/djangoapps/xblock/rest_api/views.py +++ b/openedx/core/djangoapps/xblock/rest_api/views.py @@ -257,7 +257,7 @@ class BlockFieldsView(APIView): # Signal that we've modified this block context_impl = get_learning_context_impl(usage_key) - context_impl.send_updated_event(usage_key) + context_impl.send_block_updated_event(usage_key) return Response({ "id": str(block.location), diff --git a/package-lock.json b/package-lock.json index 04728ccaab..c2a83bf7a6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,11 +9,11 @@ "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", @@ -23,7 +23,7 @@ "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", @@ -59,7 +59,7 @@ "react-slick": "0.29.0", "redux": "3.7.2", "redux-thunk": "2.2.0", - "requirejs": "2.3.6", + "requirejs": "2.3.7", "rtlcss": "2.6.2", "sass": "^1.54.8", "sass-loader": "^14.1.1", @@ -79,29 +79,29 @@ "@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", @@ -132,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": { @@ -144,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==", + "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.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", + "@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" @@ -180,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" @@ -195,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" }, @@ -232,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==", + "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.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", + "@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": { @@ -254,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" }, @@ -284,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" @@ -356,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" @@ -391,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" @@ -407,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==", + "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" - }, - "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==", - "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" @@ -504,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" }, @@ -514,12 +507,44 @@ "node": ">=6.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==", + "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.0" + "@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.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.8" }, "engines": { "node": ">=6.9.0" @@ -529,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" @@ -545,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" @@ -660,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" @@ -674,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" @@ -710,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" @@ -833,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" @@ -847,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" @@ -864,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" @@ -880,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" @@ -894,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" @@ -908,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" @@ -923,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": { @@ -939,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==", + "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.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", + "@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": { @@ -960,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" @@ -975,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" @@ -989,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" @@ -1004,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" @@ -1017,12 +1057,29 @@ "@babel/core": "^7.0.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==", + "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-plugin-utils": "^7.24.0", + "@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.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.7", "@babel/plugin-syntax-dynamic-import": "^7.8.3" }, "engines": { @@ -1033,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" @@ -1048,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": { @@ -1063,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" @@ -1078,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" @@ -1094,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": { @@ -1109,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" @@ -1123,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": { @@ -1138,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" @@ -1152,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" @@ -1167,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" @@ -1183,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" @@ -1200,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" @@ -1215,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" @@ -1230,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" @@ -1244,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": { @@ -1259,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": { @@ -1274,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" @@ -1288,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" @@ -1305,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" @@ -1320,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": { @@ -1335,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": { @@ -1351,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" @@ -1365,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" @@ -1380,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": { @@ -1397,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" @@ -1411,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" @@ -1425,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" @@ -1443,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" @@ -1457,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" @@ -1472,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": { @@ -1487,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" @@ -1501,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" @@ -1515,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" @@ -1530,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" @@ -1544,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" @@ -1558,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" @@ -1572,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" @@ -1586,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" @@ -1601,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" @@ -1616,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" @@ -1641,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==", + "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.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", + "@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", @@ -1671,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": { @@ -1747,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" @@ -1787,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==", + "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.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", + "@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" }, @@ -1820,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": { @@ -4552,15 +4654,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", @@ -5439,16 +5532,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" @@ -5465,6 +5559,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" }, @@ -5480,6 +5575,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" @@ -5496,6 +5592,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" }, @@ -5507,13 +5604,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" } @@ -5523,6 +5622,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" }, @@ -5956,9 +6056,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" } @@ -6199,9 +6300,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", @@ -6216,11 +6317,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" @@ -6499,9 +6601,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", @@ -6515,7 +6617,8 @@ "type": "github", "url": "https://github.com/sponsors/ai" } - ] + ], + "license": "CC-BY-4.0" }, "node_modules/capture-exit": { "version": "2.0.0", @@ -7288,7 +7391,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", @@ -7324,11 +7428,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", @@ -8130,15 +8235,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", @@ -8204,9 +8300,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", @@ -8861,6 +8958,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" } @@ -11419,6 +11517,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", @@ -12746,14 +12851,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" @@ -12925,28 +13031,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", @@ -13709,14 +13793,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", @@ -15251,6 +15336,59 @@ "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": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/karma": { "version": "0.13.22", "resolved": "https://registry.npmjs.org/karma/-/karma-0.13.22.tgz", @@ -15555,10 +15693,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" @@ -15591,12 +15730,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": { @@ -15790,6 +15930,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", @@ -17143,9 +17293,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", @@ -17713,6 +17864,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", @@ -17946,9 +18104,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", @@ -19971,18 +20130,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": { @@ -20862,9 +21034,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" @@ -21968,12 +22141,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" @@ -21986,7 +22160,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" }, @@ -24613,9 +24789,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", @@ -24630,9 +24806,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" diff --git a/package.json b/package.json index d182d83d31..3bd526c3b4 100644 --- a/package.json +++ b/package.json @@ -15,11 +15,11 @@ "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", @@ -29,7 +29,7 @@ "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", @@ -65,7 +65,7 @@ "react-slick": "0.29.0", "redux": "3.7.2", "redux-thunk": "2.2.0", - "requirejs": "2.3.6", + "requirejs": "2.3.7", "rtlcss": "2.6.2", "sass": "^1.54.8", "sass-loader": "^14.1.1", @@ -85,29 +85,29 @@ "@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", diff --git a/requirements/common_constraints.txt b/requirements/common_constraints.txt index 9405a605c5..f2ef0216ca 100644 --- a/requirements/common_constraints.txt +++ b/requirements/common_constraints.txt @@ -26,15 +26,6 @@ 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 diff --git a/requirements/constraints.txt b/requirements/constraints.txt index b32989514d..aa45d9944d 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.21.9 +edx-enterprise==4.23.9 # Stay on LTS version, remove once this is added to common constraint Django<5.0 @@ -87,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.10.1 # Open AI version 1.0.0 dropped support for openai.ChatCompletion which is currently in use in enterprise. openai<=0.28.1 @@ -130,3 +136,13 @@ numpy<2.0.0 # 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 + +# Xblock==5.0.0 changed how entrypoints were loaded, breaking a workaround for overriding blocks. +# See ticket: https://github.com/openedx/XBlock/issues/777 +xblock[django]==4.0.1 diff --git a/requirements/edx-sandbox/base.txt b/requirements/edx-sandbox/base.txt index 241deba480..4a7b0c0a7d 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,7 +16,7 @@ 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 @@ -35,13 +35,13 @@ markupsafe==2.1.5 # via # chem # openedx-calc -matplotlib==3.9.1 +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 @@ -71,7 +71,7 @@ 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.14.0 # via @@ -82,9 +82,9 @@ six==1.16.0 # via # codejail-includes # python-dateutil -sympy==1.13.0 +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 a7d2d457cc..873446da13 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,14 +47,14 @@ 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.1.3 +bcrypt==4.2.0 # via paramiko beautifulsoup4==4.12.3 # via pynliner @@ -66,19 +70,23 @@ bleach[css]==6.1.0 # xblock-poll boto==2.49.0 # via -r requirements/edx/kernel.in -boto3==1.34.144 +boto3==1.35.1 # via # -r requirements/edx/kernel.in # django-ses # fs-s3fs # ora2 -botocore==1.34.144 +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 @@ -98,7 +106,7 @@ certifi==2024.7.4 # py2neo # requests # snowflake-connector-python -cffi==1.16.0 +cffi==1.17.0 # via # cryptography # pynacl @@ -160,7 +168,7 @@ defusedxml==0.7.1 # ora2 # python3-openid # social-auth-core -django==4.2.14 +django==4.2.15 # via # -c requirements/edx/../common_constraints.txt # -c requirements/edx/../constraints.txt @@ -179,6 +187,7 @@ django==4.2.14 # django-multi-email-field # django-mysql # django-oauth-toolkit + # django-push-notifications # django-sekizai # django-ses # django-statici18n @@ -262,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 @@ -311,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 @@ -390,7 +401,7 @@ drf-yasg==1.21.7 # via # django-user-tasks # edx-api-doc-tools -edx-ace==1.9.1 +edx-ace==1.11.1 # via -r requirements/edx/kernel.in edx-api-doc-tools==1.8.0 # via @@ -418,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.6 +edx-completion==4.6.7 # via -r requirements/edx/kernel.in edx-django-release-util==1.4.0 # via @@ -427,7 +438,7 @@ 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 @@ -456,11 +467,11 @@ edx-drf-extensions==10.3.0 # edx-when # edxval # openedx-learning -edx-enterprise==4.21.9 +edx-enterprise==4.23.9 # 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 @@ -502,11 +513,11 @@ edx-rest-api-client==5.7.1 # -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.5 +edx-submissions==3.7.7 # via # -r requirements/edx/kernel.in # ora2 @@ -551,6 +562,8 @@ fastavro==1.9.5 # via openedx-events filelock==3.15.4 # via snowflake-connector-python +firebase-admin==6.5.0 + # via edx-ace frozenlist==1.4.1 # via # aiohttp @@ -571,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 @@ -579,6 +635,10 @@ html5lib==1.1 # via # -r requirements/edx/kernel.in # ora2 +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 @@ -588,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 @@ -610,7 +668,7 @@ jmespath==1.0.1 # botocore joblib==1.4.2 # via nltk -jsondiff==2.1.2 +jsondiff==2.2.0 # via edx-enterprise jsonfield==3.1.0 # via @@ -631,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 @@ -699,23 +757,25 @@ 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.12.0 +newrelic==9.13.0 # via # -r requirements/edx/bundled.in # edx-django-utils 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 @@ -763,7 +823,7 @@ openedx-filters==1.9.0 # -r requirements/edx/kernel.in # lti-consumer-xblock # ora2 -openedx-learning==0.10.0 +openedx-learning==0.10.1 # via # -c requirements/edx/../constraints.txt # -r requirements/edx/kernel.in @@ -773,7 +833,7 @@ 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 @@ -783,7 +843,7 @@ packaging==24.1 # snowflake-connector-python pansi==2020.7.3 # via py2neo -paramiko==3.4.0 +paramiko==3.4.1 # via edx-enterprise path==16.11.0 # via @@ -819,6 +879,17 @@ polib==1.2.0 # via edx-i18n-tools prompt-toolkit==3.0.47 # via click-repl +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 @@ -828,7 +899,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 @@ -852,7 +928,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 @@ -860,6 +936,7 @@ pyjwt[crypto]==2.8.0 # edx-drf-extensions # edx-proctoring # edx-rest-api-client + # firebase-admin # pylti1p3 # snowflake-connector-python # social-auth-core @@ -884,13 +961,14 @@ pynacl==1.5.0 # 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 @@ -946,7 +1024,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 @@ -960,7 +1038,7 @@ random2==1.0.2 # via -r requirements/edx/kernel.in recommender-xblock==2.2.0 # via -r requirements/edx/bundled.in -redis==5.0.7 +redis==5.0.8 # via # -r requirements/edx/kernel.in # walrus @@ -968,19 +1046,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 @@ -998,10 +1079,12 @@ requests-oauthlib==2.0.0 # via # -r requirements/edx/kernel.in # social-auth-core -rpds-py==0.19.0 +rpds-py==0.20.0 # via # jsonschema # referencing +rsa==4.9 + # via google-auth rules==3.4 # via # -r requirements/edx/kernel.in @@ -1018,9 +1101,9 @@ scipy==1.14.0 # openedx-calc semantic-version==2.10.0 # via edx-drf-extensions -shapely==2.0.5 +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 @@ -1061,10 +1144,11 @@ slumber==0.7.1 # edx-bulk-grades # edx-enterprise # edx-rest-api-client -snowflake-connector-python==3.11.0 +snowflake-connector-python==3.12.0 # via edx-enterprise 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.5.4 @@ -1080,7 +1164,7 @@ sortedcontainers==2.4.0 # via # -r requirements/edx/kernel.in # snowflake-connector-python -soupsieve==2.5 +soupsieve==2.6 # via beautifulsoup4 sqlparse==0.5.1 # via django @@ -1097,7 +1181,7 @@ stevedore==5.2.0 # edx-opaque-keys super-csv==3.2.0 # via edx-bulk-grades -sympy==1.13.0 +sympy==1.13.2 # via openedx-calc testfixtures==8.3.0 # via edx-enterprise @@ -1105,9 +1189,9 @@ text-unidecode==1.3 # via python-slugify tinycss2==1.2.1 # via bleach -tomlkit==0.13.0 +tomlkit==0.13.2 # via snowflake-connector-python -tqdm==4.66.4 +tqdm==4.66.5 # via # nltk # openai @@ -1131,6 +1215,7 @@ uritemplate==4.1.1 # via # drf-spectacular # drf-yasg + # google-api-python-client urllib3==1.26.19 # via # -c requirements/edx/../constraints.txt @@ -1150,7 +1235,7 @@ voluptuous==0.15.2 # via ora2 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 @@ -1167,7 +1252,7 @@ webencodings==0.5.1 # bleach # html5lib # tinycss2 -webob==1.8.7 +webob==1.8.8 # via # -r requirements/edx/kernel.in # xblock @@ -1175,6 +1260,7 @@ wrapt==1.16.0 # via -r requirements/edx/paver.txt xblock[django]==4.0.1 # via + # -c requirements/edx/../constraints.txt # -r requirements/edx/kernel.in # acid-xblock # crowdsourcehinter-xblock @@ -1206,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 8c3b834163..a004eeeb9f 100644 --- a/requirements/edx/coverage.txt +++ b/requirements/edx/coverage.txt @@ -6,9 +6,9 @@ # chardet==5.2.0 # via diff-cover -coverage==7.6.0 +coverage==7.6.1 # via -r requirements/edx/coverage.in -diff-cover==9.1.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 397923ac1e..4951b16608 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 - # httpcore # 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,7 +110,7 @@ backoff==1.10.0 # -r requirements/edx/doc.txt # -r requirements/edx/testing.txt # analytics-python -bcrypt==4.1.3 +bcrypt==4.2.0 # via # -r requirements/edx/doc.txt # -r requirements/edx/testing.txt @@ -136,14 +140,14 @@ boto==2.49.0 # via # -r requirements/edx/doc.txt # -r requirements/edx/testing.txt -boto3==1.34.144 +boto3==1.35.1 # via # -r requirements/edx/doc.txt # -r requirements/edx/testing.txt # django-ses # fs-s3fs # ora2 -botocore==1.34.144 +botocore==1.35.1 # via # -r requirements/edx/doc.txt # -r requirements/edx/testing.txt @@ -157,9 +161,16 @@ build==1.2.1 # via # -r requirements/edx/../pip-tools.txt # pip-tools -cachetools==5.4.0 +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 @@ -182,16 +193,15 @@ certifi==2024.7.4 # -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 # cryptography + # pact-python # pynacl # snowflake-connector-python chardet==5.2.0 @@ -232,7 +242,6 @@ click==8.1.6 # nltk # pact-python # pip-tools - # typer # user-util # uvicorn click-didyoumean==0.3.1 @@ -269,7 +278,7 @@ colorama==0.4.6 # via # -r requirements/edx/testing.txt # tox -coverage[toml]==7.6.0 +coverage[toml]==7.6.1 # via # -r requirements/edx/testing.txt # pytest-cov @@ -314,7 +323,7 @@ defusedxml==0.7.1 # ora2 # python3-openid # social-auth-core -diff-cover==9.1.0 +diff-cover==9.1.1 # via -r requirements/edx/testing.txt dill==0.3.8 # via @@ -324,7 +333,7 @@ distlib==0.3.8 # via # -r requirements/edx/testing.txt # virtualenv -django==4.2.14 +django==4.2.15 # via # -c requirements/edx/../common_constraints.txt # -c requirements/edx/../constraints.txt @@ -345,6 +354,7 @@ django==4.2.14 # django-multi-email-field # django-mysql # django-oauth-toolkit + # django-push-notifications # django-sekizai # django-ses # django-statici18n @@ -451,7 +461,7 @@ django-fernet-fields-v2==0.9 # -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 @@ -520,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 @@ -561,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 @@ -616,7 +631,6 @@ dnspython==2.6.1 # via # -r requirements/edx/doc.txt # -r requirements/edx/testing.txt - # email-validator # pymongo docutils==0.21.2 # via @@ -643,7 +657,7 @@ drf-yasg==1.21.7 # -r requirements/edx/testing.txt # django-user-tasks # edx-api-doc-tools -edx-ace==1.9.1 +edx-ace==1.11.1 # via # -r requirements/edx/doc.txt # -r requirements/edx/testing.txt @@ -682,7 +696,7 @@ edx-codejail==3.4.1 # via # -r requirements/edx/doc.txt # -r requirements/edx/testing.txt -edx-completion==4.6.6 +edx-completion==4.6.7 # via # -r requirements/edx/doc.txt # -r requirements/edx/testing.txt @@ -696,7 +710,7 @@ 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 @@ -727,12 +741,12 @@ edx-drf-extensions==10.3.0 # edx-when # edxval # openedx-learning -edx-enterprise==4.21.9 +edx-enterprise==4.23.9 # 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 @@ -792,7 +806,7 @@ edx-rest-api-client==5.7.1 # -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 @@ -800,7 +814,7 @@ edx-sga==0.25.0 # via # -r requirements/edx/doc.txt # -r requirements/edx/testing.txt -edx-submissions==3.7.5 +edx-submissions==3.7.7 # via # -r requirements/edx/doc.txt # -r requirements/edx/testing.txt @@ -842,10 +856,6 @@ elasticsearch==7.13.4 # -r requirements/edx/doc.txt # -r requirements/edx/testing.txt # edx-search -email-validator==2.2.0 - # via - # -r requirements/edx/testing.txt - # fastapi enmerkar==0.7.1 # via # -r requirements/edx/doc.txt @@ -867,20 +877,16 @@ 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==26.0.0 +faker==27.0.0 # via # -r requirements/edx/testing.txt # factory-boy -fastapi==0.111.1 +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.5 # via # -r requirements/edx/doc.txt @@ -893,6 +899,11 @@ filelock==3.15.4 # 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 @@ -932,18 +943,90 @@ glob2==0.7 # via # -r requirements/edx/doc.txt # -r requirements/edx/testing.txt +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 @@ -954,21 +1037,14 @@ html5lib==1.1 # -r requirements/edx/doc.txt # -r requirements/edx/testing.txt # ora2 -httpcore==0.16.3 +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.23.3 - # via - # -r requirements/edx/testing.txt - # fastapi - # pact-python icalendar==5.0.13 # via # -r requirements/edx/doc.txt @@ -978,10 +1054,8 @@ idna==3.7 # -r requirements/edx/doc.txt # -r requirements/edx/testing.txt # anyio - # email-validator # optimizely-sdk # requests - # rfc3986 # snowflake-connector-python # yarl imagesize==1.4.1 @@ -990,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 @@ -1029,7 +1102,6 @@ jinja2==3.1.4 # -r requirements/edx/testing.txt # code-annotations # diff-cover - # fastapi # sphinx jmespath==1.0.1 # via @@ -1042,7 +1114,7 @@ joblib==1.4.2 # -r requirements/edx/doc.txt # -r requirements/edx/testing.txt # nltk -jsondiff==2.1.2 +jsondiff==2.2.0 # via # -r requirements/edx/doc.txt # -r requirements/edx/testing.txt @@ -1075,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 @@ -1146,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 @@ -1168,10 +1236,6 @@ 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.4 # via # -r requirements/edx/doc.txt @@ -1194,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 @@ -1204,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.1 +mypy==1.11.1 # via # -r requirements/edx/development.in # django-stubs @@ -1221,7 +1290,7 @@ mysqlclient==2.2.4 # via # -r requirements/edx/doc.txt # -r requirements/edx/testing.txt -newrelic==9.12.0 +newrelic==9.13.0 # via # -r requirements/edx/doc.txt # -r requirements/edx/testing.txt @@ -1230,7 +1299,7 @@ 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 @@ -1303,7 +1372,7 @@ openedx-filters==1.9.0 # -r requirements/edx/testing.txt # lti-consumer-xblock # ora2 -openedx-learning==0.10.0 +openedx-learning==0.10.1 # via # -c requirements/edx/../constraints.txt # -r requirements/edx/doc.txt @@ -1317,7 +1386,7 @@ 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 @@ -1336,14 +1405,14 @@ packaging==24.1 # snowflake-connector-python # sphinx # tox -pact-python==2.0.1 +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.0 +paramiko==3.4.1 # via # -r requirements/edx/doc.txt # -r requirements/edx/testing.txt @@ -1417,6 +1486,21 @@ prompt-toolkit==3.0.47 # -r requirements/edx/doc.txt # -r requirements/edx/testing.txt # click-repl +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 @@ -1436,6 +1520,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 @@ -1479,7 +1570,6 @@ pygments==2.18.0 # diff-cover # py2neo # pydata-sphinx-theme - # rich # sphinx # sphinx-mdinclude pyjwkest==1.4.2 @@ -1488,7 +1578,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 @@ -1497,6 +1587,7 @@ pyjwt[crypto]==2.8.0 # edx-drf-extensions # edx-proctoring # edx-rest-api-client + # firebase-admin # pylti1p3 # snowflake-connector-python # social-auth-core @@ -1556,7 +1647,7 @@ 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 @@ -1567,6 +1658,7 @@ pyparsing==3.1.2 # -r requirements/edx/doc.txt # -r requirements/edx/testing.txt # chem + # httplib2 # openedx-calc pyproject-api==1.7.1 # via @@ -1589,7 +1681,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 @@ -1632,10 +1724,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 @@ -1645,10 +1733,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 @@ -1694,7 +1778,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 @@ -1705,7 +1789,6 @@ pyyaml==6.0.1 # edx-i18n-tools # jsondiff # sphinxcontrib-openapi - # uvicorn # xblock random2==1.0.2 # via @@ -1715,7 +1798,7 @@ recommender-xblock==2.2.0 # via # -r requirements/edx/doc.txt # -r requirements/edx/testing.txt -redis==5.0.7 +redis==5.0.8 # via # -r requirements/edx/doc.txt # -r requirements/edx/testing.txt @@ -1726,7 +1809,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 @@ -1737,6 +1820,7 @@ requests==2.32.3 # -r requirements/edx/testing.txt # algoliasearch # analytics-python + # cachecontrol # django-oauth-toolkit # djangorestframework-stubs # edx-bulk-grades @@ -1744,6 +1828,8 @@ requests==2.32.3 # edx-enterprise # edx-rest-api-client # geoip2 + # google-api-core + # google-cloud-storage # mailsnake # meilisearch # openai @@ -1764,20 +1850,17 @@ requests-oauthlib==2.0.0 # -r requirements/edx/doc.txt # -r requirements/edx/testing.txt # social-auth-core -rfc3986[idna2008]==1.5.0 - # via - # -r requirements/edx/testing.txt - # httpx -rich==13.7.1 - # via - # -r requirements/edx/testing.txt - # typer -rpds-py==0.19.0 +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 @@ -1806,15 +1889,11 @@ semantic-version==2.10.0 # -r requirements/edx/doc.txt # -r requirements/edx/testing.txt # edx-drf-extensions -shapely==2.0.5 +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 @@ -1871,19 +1950,18 @@ sniffio==1.3.1 # via # -r requirements/edx/testing.txt # anyio - # httpcore - # httpx snowballstemmer==2.2.0 # via # -r requirements/edx/doc.txt # sphinx -snowflake-connector-python==3.11.0 +snowflake-connector-python==3.12.0 # via # -r requirements/edx/doc.txt # -r requirements/edx/testing.txt # edx-enterprise 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 @@ -1903,12 +1981,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.4.4 +sphinx==8.0.2 # via # -r requirements/edx/doc.txt # pydata-sphinx-theme @@ -1921,23 +1999,23 @@ sphinx==7.4.4 # sphinxext-rediraffe 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.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 @@ -1951,11 +2029,11 @@ 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 @@ -1971,7 +2049,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 @@ -1989,7 +2067,7 @@ super-csv==3.2.0 # -r requirements/edx/doc.txt # -r requirements/edx/testing.txt # edx-bulk-grades -sympy==1.13.0 +sympy==1.13.2 # via # -r requirements/edx/doc.txt # -r requirements/edx/testing.txt @@ -2011,27 +2089,23 @@ tinycss2==1.2.1 # bleach tomli==2.0.1 # via django-stubs -tomlkit==0.13.0 +tomlkit==0.13.2 # via # -r requirements/edx/doc.txt # -r requirements/edx/testing.txt # pylint # snowflake-connector-python -tox==4.16.0 +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 @@ -2058,7 +2132,6 @@ typing-extensions==4.12.2 # pydata-sphinx-theme # pylti1p3 # snowflake-connector-python - # typer tzdata==2024.1 # via # -r requirements/edx/doc.txt @@ -2077,6 +2150,7 @@ uritemplate==4.1.1 # -r requirements/edx/testing.txt # drf-spectacular # drf-yasg + # google-api-python-client urllib3==1.26.19 # via # -c requirements/edx/../constraints.txt @@ -2084,22 +2158,16 @@ urllib3==1.26.19 # -r requirements/edx/testing.txt # botocore # elasticsearch - # pact-python # py2neo # requests 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 @@ -2123,15 +2191,11 @@ walrus==0.9.4 # -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 @@ -2153,16 +2217,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 @@ -2173,6 +2233,7 @@ wrapt==1.16.0 # astroid xblock[django]==4.0.1 # via + # -c requirements/edx/../constraints.txt # -r requirements/edx/doc.txt # -r requirements/edx/testing.txt # acid-xblock @@ -2220,7 +2281,8 @@ yarl==1.9.4 # -r requirements/edx/doc.txt # -r requirements/edx/testing.txt # aiohttp -zipp==3.19.2 + # pact-python +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 182ff8eb86..8c9c7bbbb3 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,7 +78,7 @@ backoff==1.10.0 # via # -r requirements/edx/base.txt # analytics-python -bcrypt==4.1.3 +bcrypt==4.2.0 # via # -r requirements/edx/base.txt # paramiko @@ -96,19 +102,27 @@ bleach[css]==6.1.0 # xblock-poll boto==2.49.0 # via -r requirements/edx/base.txt -boto3==1.34.144 +boto3==1.35.1 # via # -r requirements/edx/base.txt # django-ses # fs-s3fs # ora2 -botocore==1.34.144 +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 @@ -130,7 +144,7 @@ certifi==2024.7.4 # py2neo # requests # snowflake-connector-python -cffi==1.16.0 +cffi==1.17.0 # via # -r requirements/edx/base.txt # cryptography @@ -208,7 +222,7 @@ defusedxml==0.7.1 # ora2 # python3-openid # social-auth-core -django==4.2.14 +django==4.2.15 # via # -c requirements/edx/../common_constraints.txt # -c requirements/edx/../constraints.txt @@ -227,6 +241,7 @@ django==4.2.14 # django-multi-email-field # django-mysql # django-oauth-toolkit + # django-push-notifications # django-sekizai # django-ses # django-statici18n @@ -318,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 @@ -373,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 @@ -462,7 +481,7 @@ drf-yasg==1.21.7 # -r requirements/edx/base.txt # django-user-tasks # edx-api-doc-tools -edx-ace==1.9.1 +edx-ace==1.11.1 # via -r requirements/edx/base.txt edx-api-doc-tools==1.8.0 # via @@ -490,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.6 +edx-completion==4.6.7 # via -r requirements/edx/base.txt edx-django-release-util==1.4.0 # via @@ -499,7 +518,7 @@ 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 @@ -528,11 +547,11 @@ edx-drf-extensions==10.3.0 # edx-when # edxval # openedx-learning -edx-enterprise==4.21.9 +edx-enterprise==4.23.9 # 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 @@ -575,11 +594,11 @@ edx-rest-api-client==5.7.1 # -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.5 +edx-submissions==3.7.7 # via # -r requirements/edx/base.txt # ora2 @@ -633,6 +652,10 @@ 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 @@ -660,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 @@ -668,6 +752,11 @@ html5lib==1.1 # via # -r requirements/edx/base.txt # ora2 +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 @@ -679,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 @@ -712,7 +799,7 @@ joblib==1.4.2 # via # -r requirements/edx/base.txt # nltk -jsondiff==2.1.2 +jsondiff==2.2.0 # via # -r requirements/edx/base.txt # edx-enterprise @@ -740,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 @@ -817,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 @@ -825,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 @@ -832,13 +923,13 @@ multidict==6.0.5 # yarl mysqlclient==2.2.4 # via -r requirements/edx/base.txt -newrelic==9.12.0 +newrelic==9.13.0 # via # -r requirements/edx/base.txt # edx-django-utils 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 @@ -891,7 +982,7 @@ openedx-filters==1.9.0 # -r requirements/edx/base.txt # lti-consumer-xblock # ora2 -openedx-learning==0.10.0 +openedx-learning==0.10.1 # via # -c requirements/edx/../constraints.txt # -r requirements/edx/base.txt @@ -901,7 +992,7 @@ 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 @@ -916,7 +1007,7 @@ pansi==2020.7.3 # via # -r requirements/edx/base.txt # py2neo -paramiko==3.4.0 +paramiko==3.4.1 # via # -r requirements/edx/base.txt # edx-enterprise @@ -964,6 +1055,19 @@ prompt-toolkit==3.0.47 # via # -r requirements/edx/base.txt # click-repl +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 @@ -976,6 +1080,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 @@ -1011,7 +1121,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 @@ -1019,6 +1129,7 @@ pyjwt[crypto]==2.8.0 # edx-drf-extensions # edx-proctoring # edx-rest-api-client + # firebase-admin # pylti1p3 # snowflake-connector-python # social-auth-core @@ -1045,7 +1156,7 @@ pynacl==1.5.0 # 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 @@ -1054,6 +1165,7 @@ pyparsing==3.1.2 # via # -r requirements/edx/base.txt # chem + # httplib2 # openedx-calc pyrsistent==0.20.0 # via @@ -1117,7 +1229,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 @@ -1132,7 +1244,7 @@ random2==1.0.2 # via -r requirements/edx/base.txt recommender-xblock==2.2.0 # via -r requirements/edx/base.txt -redis==5.0.7 +redis==5.0.8 # via # -r requirements/edx/base.txt # walrus @@ -1141,7 +1253,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 @@ -1150,12 +1262,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 @@ -1174,11 +1289,15 @@ requests-oauthlib==2.0.0 # via # -r requirements/edx/base.txt # social-auth-core -rpds-py==0.19.0 +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 @@ -1202,9 +1321,9 @@ semantic-version==2.10.0 # via # -r requirements/edx/base.txt # edx-drf-extensions -shapely==2.0.5 +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 @@ -1249,12 +1368,13 @@ smmap==5.0.1 # via gitdb snowballstemmer==2.2.0 # via sphinx -snowflake-connector-python==3.11.0 +snowflake-connector-python==3.12.0 # via # -r requirements/edx/base.txt # edx-enterprise 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.5.4 @@ -1270,11 +1390,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.4.4 +sphinx==8.0.2 # via # -r requirements/edx/doc.in # pydata-sphinx-theme @@ -1287,17 +1407,17 @@ sphinx==7.4.4 # sphinxext-rediraffe 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.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 @@ -1305,9 +1425,9 @@ 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 @@ -1329,7 +1449,7 @@ super-csv==3.2.0 # via # -r requirements/edx/base.txt # edx-bulk-grades -sympy==1.13.0 +sympy==1.13.2 # via # -r requirements/edx/base.txt # openedx-calc @@ -1345,11 +1465,11 @@ tinycss2==1.2.1 # via # -r requirements/edx/base.txt # bleach -tomlkit==0.13.0 +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 @@ -1378,6 +1498,7 @@ uritemplate==4.1.1 # -r requirements/edx/base.txt # drf-spectacular # drf-yasg + # google-api-python-client urllib3==1.26.19 # via # -c requirements/edx/../constraints.txt @@ -1402,7 +1523,7 @@ 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 @@ -1422,7 +1543,7 @@ webencodings==0.5.1 # bleach # html5lib # tinycss2 -webob==1.8.7 +webob==1.8.8 # via # -r requirements/edx/base.txt # xblock @@ -1430,6 +1551,7 @@ wrapt==1.16.0 # via -r requirements/edx/base.txt xblock[django]==4.0.1 # via + # -c requirements/edx/../constraints.txt # -r requirements/edx/base.txt # acid-xblock # crowdsourcehinter-xblock @@ -1465,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 0b82d71c91..faa0085f16 100644 --- a/requirements/edx/paver.txt +++ b/requirements/edx/paver.txt @@ -61,7 +61,7 @@ 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 5c3ec421aa..292f131904 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,7 +15,7 @@ boltons==21.0.0 # face # glom # semgrep -bracex==2.4 +bracex==2.5 # via wcmatch certifi==2024.7.4 # via requests @@ -62,7 +62,7 @@ requests==2.32.3 # via semgrep rich==13.7.1 # via semgrep -rpds-py==0.19.0 +rpds-py==0.20.0 # via # jsonschema # referencing diff --git a/requirements/edx/testing.txt b/requirements/edx/testing.txt index bf5c2817d5..87c98537d3 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 - # httpcore - # 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,7 +78,7 @@ backoff==1.10.0 # via # -r requirements/edx/base.txt # analytics-python -bcrypt==4.1.3 +bcrypt==4.2.0 # via # -r requirements/edx/base.txt # paramiko @@ -99,21 +102,28 @@ bleach[css]==6.1.0 # xblock-poll boto==2.49.0 # via -r requirements/edx/base.txt -boto3==1.34.144 +boto3==1.35.1 # via # -r requirements/edx/base.txt # django-ses # fs-s3fs # ora2 -botocore==1.34.144 +botocore==1.35.1 # via # -r requirements/edx/base.txt # boto3 # s3transfer bridgekeeper==0.9 # via -r requirements/edx/base.txt -cachetools==5.4.0 - # 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 @@ -132,15 +142,14 @@ 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 + # pact-python # pynacl # snowflake-connector-python chardet==5.2.0 @@ -173,7 +182,6 @@ click==8.1.6 # import-linter # nltk # pact-python - # typer # user-util # uvicorn click-didyoumean==0.3.1 @@ -201,7 +209,7 @@ codejail-includes==1.0.0 # via -r requirements/edx/base.txt colorama==0.4.6 # via tox -coverage[toml]==7.6.0 +coverage[toml]==7.6.1 # via # -r requirements/edx/coverage.txt # pytest-cov @@ -237,13 +245,13 @@ defusedxml==0.7.1 # ora2 # python3-openid # social-auth-core -diff-cover==9.1.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.14 +django==4.2.15 # via # -c requirements/edx/../common_constraints.txt # -c requirements/edx/../constraints.txt @@ -262,6 +270,7 @@ django==4.2.14 # django-multi-email-field # django-mysql # django-oauth-toolkit + # django-push-notifications # django-sekizai # django-ses # django-statici18n @@ -353,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 @@ -408,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 @@ -478,7 +491,6 @@ djangorestframework-xml==2.0.0 dnspython==2.6.1 # via # -r requirements/edx/base.txt - # email-validator # pymongo done-xblock==2.3.0 # via -r requirements/edx/base.txt @@ -493,7 +505,7 @@ drf-yasg==1.21.7 # -r requirements/edx/base.txt # django-user-tasks # edx-api-doc-tools -edx-ace==1.9.1 +edx-ace==1.11.1 # via -r requirements/edx/base.txt edx-api-doc-tools==1.8.0 # via @@ -521,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.6 +edx-completion==4.6.7 # via -r requirements/edx/base.txt edx-django-release-util==1.4.0 # via @@ -530,7 +542,7 @@ 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 @@ -559,11 +571,11 @@ edx-drf-extensions==10.3.0 # edx-when # edxval # openedx-learning -edx-enterprise==4.21.9 +edx-enterprise==4.23.9 # 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 @@ -608,11 +620,11 @@ edx-rest-api-client==5.7.1 # -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.5 +edx-submissions==3.7.7 # via # -r requirements/edx/base.txt # ora2 @@ -645,8 +657,6 @@ elasticsearch==7.13.4 # -c requirements/edx/../common_constraints.txt # -r requirements/edx/base.txt # edx-search -email-validator==2.2.0 - # via fastapi enmerkar==0.7.1 # via # -r requirements/edx/base.txt @@ -662,14 +672,12 @@ event-tracking==3.0.0 # 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==26.0.0 +faker==27.0.0 # via factory-boy -fastapi==0.111.1 +fastapi==0.112.1 # via pact-python -fastapi-cli==0.0.4 - # via fastapi fastavro==1.9.5 # via # -r requirements/edx/base.txt @@ -680,6 +688,10 @@ filelock==3.15.4 # 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 @@ -705,48 +717,100 @@ geoip2==4.8.0 # via -r requirements/edx/base.txt glob2==0.7 # via -r requirements/edx/base.txt +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==0.16.3 - # 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.23.3 - # via - # fastapi - # pact-python icalendar==5.0.13 # via -r requirements/edx/base.txt idna==3.7 # via # -r requirements/edx/base.txt # anyio - # email-validator # optimizely-sdk # requests - # rfc3986 # 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 @@ -774,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 @@ -784,7 +847,7 @@ joblib==1.4.2 # via # -r requirements/edx/base.txt # nltk -jsondiff==2.1.2 +jsondiff==2.2.0 # via # -r requirements/edx/base.txt # edx-enterprise @@ -811,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 @@ -866,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 @@ -883,8 +944,6 @@ maxminddb==2.6.2 # geoip2 mccabe==0.7.0 # via pylint -mdurl==0.1.2 - # via markdown-it-py meilisearch==0.31.4 # via -r requirements/edx/base.txt mock==5.1.0 @@ -896,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 @@ -904,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 @@ -911,13 +974,13 @@ multidict==6.0.5 # yarl mysqlclient==2.2.4 # via -r requirements/edx/base.txt -newrelic==9.12.0 +newrelic==9.13.0 # via # -r requirements/edx/base.txt # edx-django-utils 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 @@ -970,7 +1033,7 @@ openedx-filters==1.9.0 # -r requirements/edx/base.txt # lti-consumer-xblock # ora2 -openedx-learning==0.10.0 +openedx-learning==0.10.1 # via # -c requirements/edx/../constraints.txt # -r requirements/edx/base.txt @@ -980,7 +1043,7 @@ 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 @@ -992,13 +1055,13 @@ packaging==24.1 # pytest # snowflake-connector-python # tox -pact-python==2.0.1 +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.0 +paramiko==3.4.1 # via # -r requirements/edx/base.txt # edx-enterprise @@ -1054,6 +1117,19 @@ prompt-toolkit==3.0.47 # via # -r requirements/edx/base.txt # click-repl +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 @@ -1070,6 +1146,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 @@ -1101,13 +1183,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 @@ -1115,6 +1196,7 @@ pyjwt[crypto]==2.8.0 # edx-drf-extensions # edx-proctoring # edx-rest-api-client + # firebase-admin # pylti1p3 # snowflake-connector-python # social-auth-core @@ -1159,7 +1241,7 @@ pynacl==1.5.0 # 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 @@ -1168,6 +1250,7 @@ pyparsing==3.1.2 # via # -r requirements/edx/base.txt # chem + # httplib2 # openedx-calc pyproject-api==1.7.1 # via tox @@ -1181,7 +1264,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 @@ -1223,16 +1306,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 @@ -1268,7 +1347,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 @@ -1277,13 +1356,12 @@ pyyaml==6.0.1 # edx-django-release-util # edx-i18n-tools # jsondiff - # uvicorn # 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.7 +redis==5.0.8 # via # -r requirements/edx/base.txt # walrus @@ -1292,7 +1370,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 @@ -1301,12 +1379,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 @@ -1325,15 +1406,15 @@ requests-oauthlib==2.0.0 # via # -r requirements/edx/base.txt # social-auth-core -rfc3986[idna2008]==1.5.0 - # via httpx -rich==13.7.1 - # via typer -rpds-py==0.19.0 +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 @@ -1357,11 +1438,9 @@ semantic-version==2.10.0 # via # -r requirements/edx/base.txt # edx-drf-extensions -shapely==2.0.5 +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 @@ -1406,16 +1485,14 @@ slumber==0.7.1 # edx-enterprise # edx-rest-api-client sniffio==1.3.1 - # via - # anyio - # httpcore - # httpx -snowflake-connector-python==3.11.0 + # via anyio +snowflake-connector-python==3.12.0 # via # -r requirements/edx/base.txt # edx-enterprise 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.5.4 @@ -1431,7 +1508,7 @@ 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 @@ -1441,7 +1518,7 @@ sqlparse==0.5.1 # 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 @@ -1455,7 +1532,7 @@ super-csv==3.2.0 # via # -r requirements/edx/base.txt # edx-bulk-grades -sympy==1.13.0 +sympy==1.13.2 # via # -r requirements/edx/base.txt # openedx-calc @@ -1472,20 +1549,18 @@ tinycss2==1.2.1 # via # -r requirements/edx/base.txt # bleach -tomlkit==0.13.0 +tomlkit==0.13.2 # via # -r requirements/edx/base.txt # pylint # snowflake-connector-python -tox==4.16.0 +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 @@ -1499,7 +1574,6 @@ typing-extensions==4.12.2 # pydantic-core # pylti1p3 # snowflake-connector-python - # typer tzdata==2024.1 # via # -r requirements/edx/base.txt @@ -1515,23 +1589,19 @@ uritemplate==4.1.1 # -r requirements/edx/base.txt # drf-spectacular # drf-yasg + # google-api-python-client urllib3==1.26.19 # via # -c requirements/edx/../constraints.txt # -r requirements/edx/base.txt # botocore # elasticsearch - # pact-python # py2neo # 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 @@ -1548,10 +1618,8 @@ 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 @@ -1570,18 +1638,17 @@ 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 # via + # -c requirements/edx/../constraints.txt # -r requirements/edx/base.txt # acid-xblock # crowdsourcehinter-xblock @@ -1617,7 +1684,8 @@ yarl==1.9.4 # via # -r requirements/edx/base.txt # aiohttp -zipp==3.19.2 + # pact-python +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 3630835e94..f7b35489c3 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 df29e61c60..f0cf3d1099 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.1.2 +pip==24.2 # via -r requirements/pip.in -setuptools==70.3.0 +setuptools==73.0.0 # via -r requirements/pip.in diff --git a/scripts/structures_pruning/requirements/testing.txt b/scripts/structures_pruning/requirements/testing.txt index d74b204fad..47648d50fd 100644 --- a/scripts/structures_pruning/requirements/testing.txt +++ b/scripts/structures_pruning/requirements/testing.txt @@ -32,7 +32,7 @@ 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 30d5df674f..3e5ed47380 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.144 +boto3==1.35.1 # via -r scripts/user_retirement/requirements/base.in -botocore==1.34.144 +botocore==1.35.1 # via # boto3 # s3transfer -cachetools==5.4.0 +cachetools==5.5.0 # via google-auth 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.14 +django==4.2.15 # via # -c scripts/user_retirement/requirements/../../../requirements/common_constraints.txt # -c scripts/user_retirement/requirements/../../../requirements/constraints.txt @@ -46,15 +46,15 @@ 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.1 # via -r scripts/user_retirement/requirements/base.in google-api-core==2.19.1 # via google-api-python-client -google-api-python-client==2.137.0 +google-api-python-client==2.141.0 # via -r scripts/user_retirement/requirements/base.in -google-auth==2.32.0 +google-auth==2.34.0 # via # google-api-core # google-api-python-client @@ -81,9 +81,9 @@ 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.12.0 +newrelic==9.13.0 # via edx-django-utils pbr==6.0.0 # via stevedore @@ -91,7 +91,7 @@ platformdirs==4.2.2 # via zeep proto-plus==1.24.0 # via google-api-core -protobuf==5.27.2 +protobuf==5.27.3 # via # google-api-core # googleapis-common-protos @@ -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,7 +120,7 @@ pytz==2024.1 # via # jenkinsapi # zeep -pyyaml==6.0.1 +pyyaml==6.0.2 # via -r scripts/user_retirement/requirements/base.in requests==2.32.3 # via @@ -143,7 +143,7 @@ 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 diff --git a/scripts/user_retirement/requirements/testing.txt b/scripts/user_retirement/requirements/testing.txt index c10b36f0de..f7464dfa06 100644 --- a/scripts/user_retirement/requirements/testing.txt +++ b/scripts/user_retirement/requirements/testing.txt @@ -8,23 +8,23 @@ 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.144 +boto3==1.35.1 # via # -r scripts/user_retirement/requirements/base.txt # moto -botocore==1.34.144 +botocore==1.35.1 # via # -r scripts/user_retirement/requirements/base.txt # boto3 # moto # s3transfer -cachetools==5.4.0 +cachetools==5.5.0 # via # -r scripts/user_retirement/requirements/base.txt # google-auth @@ -32,7 +32,7 @@ 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.14 +django==4.2.15 # via # -r scripts/user_retirement/requirements/base.txt # django-crum @@ -66,7 +66,7 @@ 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 @@ -76,9 +76,9 @@ google-api-core==2.19.1 # via # -r scripts/user_retirement/requirements/base.txt # google-api-python-client -google-api-python-client==2.137.0 +google-api-python-client==2.141.0 # via -r scripts/user_retirement/requirements/base.txt -google-auth==2.32.0 +google-auth==2.34.0 # 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.12.0 +newrelic==9.13.0 # via # -r scripts/user_retirement/requirements/base.txt # edx-django-utils @@ -152,7 +152,7 @@ proto-plus==1.24.0 # via # -r scripts/user_retirement/requirements/base.txt # google-api-core -protobuf==5.27.2 +protobuf==5.27.3 # via # -r scripts/user_retirement/requirements/base.txt # google-api-core @@ -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,7 +200,7 @@ 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 @@ -242,7 +242,7 @@ s3transfer==0.10.2 # 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 diff --git a/scripts/user_retirement/tests/utils/test_edx_api.py b/scripts/user_retirement/tests/utils/test_edx_api.py index 706b1ce6d4..eb826cd1a4 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 @@ from scripts.user_retirement.tests.retirement_helpers import ( 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 @@ class TestDiscoveryApi(OAuth2Mixin, unittest.TestCase): ) -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 1090364045..e891f04019 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 @@ class BaseApiClient: 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 @@ class BaseApiClient: # 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 @@ class BaseApiClient: 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 _retry_lms_api(): # 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 _retry_lms_api(): # 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 @@ class LmsApi(BaseApiClient): """ 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 @@ class LmsApi(BaseApiClient): :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 @@ class LmsApi(BaseApiClient): 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 @@ class LmsApi(BaseApiClient): 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 @@ class LmsApi(BaseApiClient): """ 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 @@ class LmsApi(BaseApiClient): """ 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 @@ class LmsApi(BaseApiClient): 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 @@ class LmsApi(BaseApiClient): 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 @@ class LmsApi(BaseApiClient): 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 @@ class LmsApi(BaseApiClient): 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 @@ class LmsApi(BaseApiClient): [{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 @@ class LmsApi(BaseApiClient): [{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 @@ class EcommerceApi(BaseApiClient): """ 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 @@ class EcommerceApi(BaseApiClient): 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 @@ class EcommerceApi(BaseApiClient): [{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 @@ class CredentialsApi(BaseApiClient): """ 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 @@ class CredentialsApi(BaseApiClient): [{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 @@ class DiscoveryApi(BaseApiClient): [{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 @@ class LicenseManagerApi(BaseApiClient): 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/webpack.builtinblocks.config.js b/webpack.builtinblocks.config.js index 53ce30a9c0..d86f891dc6 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/xmodule/mongo_utils.py b/xmodule/mongo_utils.py index b86abd28b4..5aecbfc405 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 @@ -63,7 +66,7 @@ def connect_to_mongodb( } if user is not None and password is not None and not db.startswith('test_'): - connection_params.update({'username': user, 'password': password, 'authSource': db}) + connection_params.update({'username': user, 'password': password, 'authSource': auth_source}) mongo_conn = pymongo.MongoClient(**connection_params)