diff --git a/.github/workflows/migrations-check.yml b/.github/workflows/migrations-check.yml index 8bc8260c69fa..eb50010763f6 100644 --- a/.github/workflows/migrations-check.yml +++ b/.github/workflows/migrations-check.yml @@ -16,16 +16,11 @@ jobs: os: [ ubuntu-20.04 ] python-version: [ '3.9' ] # 'pinned' is used to install the latest patch version of Django - # within the global constraint i.e. Django==3.2.21 in current case - # because we have global constraint of Django<4.2 - django-version: ["pinned", "4.2"] + # within the global constraint i.e. Django==4.2.8 in current case + # because we have global constraint of Django<4.2 + django-version: ["pinned"] mongo-version: ["4"] - mysql-version: ["5.7", "8"] - # excluding mysql5.7 with Django 4.2 since Django 4.2 has - # dropped support for MySQL<8 - exclude: - - django-version: "4.2" - mysql-version: "5.7" + mysql-version: ["8"] services: mongo: image: mongo:${{ matrix.mongo-version }} diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml index d1822414043e..69763f30a4b0 100644 --- a/.github/workflows/unit-tests.yml +++ b/.github/workflows/unit-tests.yml @@ -17,7 +17,6 @@ jobs: - "3.9" django-version: - "pinned" - - "4.2" # When updating the shards, remember to make the same changes in # .github/workflows/unit-tests-gh-hosted.yml shard_name: diff --git a/Makefile b/Makefile index cc6fa5591376..cd6ba204a239 100644 --- a/Makefile +++ b/Makefile @@ -56,8 +56,14 @@ endif push_translations: ## push source strings to Transifex for translation i18n_tool transifex push +pull_plugin_translations: ## Pull translations from Transifex for edx_django_utils.plugins for both lms and cms + rm -rf conf/plugins-locale/plugins # Clean up existing atlas translations + mkdir -p conf/plugins-locale/plugins + python manage.py lms pull_plugin_translations --verbose $(ATLAS_OPTIONS) + python manage.py lms compile_plugin_translations + pull_xblock_translations: ## pull xblock translations via atlas - rm -rf conf/plugins-locale # Clean up existing atlas translations + rm -rf conf/plugins-locale/xblock.v1 # Clean up existing atlas translations rm -rf lms/static/i18n/xblock.v1 cms/static/i18n/xblock.v1 # Clean up existing xblock compiled translations mkdir -p conf/plugins-locale/xblock.v1/ lms/static/js/xblock.v1-i18n cms/static/js python manage.py lms pull_xblock_translations --verbose $(ATLAS_OPTIONS) @@ -76,6 +82,7 @@ ifeq ($(OPENEDX_ATLAS_PULL),) i18n_tool validate --verbose else make pull_xblock_translations + make pull_plugin_translations find conf/locale -mindepth 1 -maxdepth 1 -type d -exec rm -r {} \; atlas pull $(ATLAS_OPTIONS) translations/edx-platform/conf/locale:conf/locale i18n_tool generate @@ -150,6 +157,8 @@ compile-requirements: pre-requirements $(COMMON_CONSTRAINTS_TXT) ## Re-compile * @# time someone tries to use the outputs. sed '/^django-simple-history==/d' requirements/common_constraints.txt > requirements/common_constraints.tmp mv requirements/common_constraints.tmp requirements/common_constraints.txt + sed 's/Django<4.0//g' requirements/common_constraints.txt > requirements/common_constraints.tmp + mv requirements/common_constraints.tmp requirements/common_constraints.txt pip-compile -v --allow-unsafe ${COMPILE_OPTS} -o requirements/pip.txt requirements/pip.in pip install -r requirements/pip.txt diff --git a/cms/djangoapps/contentstore/course_info_model.py b/cms/djangoapps/contentstore/course_info_model.py index 3a9d5d92de42..8f59998e716c 100644 --- a/cms/djangoapps/contentstore/course_info_model.py +++ b/cms/djangoapps/contentstore/course_info_model.py @@ -19,6 +19,7 @@ from django.http import HttpResponseBadRequest from django.utils.translation import gettext as _ +from cms.djangoapps.contentstore.utils import track_course_update_event from openedx.core.lib.xblock_utils import get_course_update_items from xmodule.html_block import CourseInfoBlock # lint-amnesty, pylint: disable=wrong-import-order from xmodule.modulestore.django import modulestore # lint-amnesty, pylint: disable=wrong-import-order @@ -85,6 +86,8 @@ def update_course_updates(location, update, passed_id=None, user=None): # update db record save_course_update_items(location, course_updates, course_update_items, user) + # track course update event + track_course_update_event(location.course_key, user, course_update_dict) # remove status key if "status" in course_update_dict: del course_update_dict["status"] diff --git a/cms/djangoapps/contentstore/management/commands/reindex_course.py b/cms/djangoapps/contentstore/management/commands/reindex_course.py index f5b6b9fadd55..accbc077c4fc 100644 --- a/cms/djangoapps/contentstore/management/commands/reindex_course.py +++ b/cms/djangoapps/contentstore/management/commands/reindex_course.py @@ -4,6 +4,7 @@ import logging from textwrap import dedent from time import time +from datetime import date from django.core.management import BaseCommand, CommandError from elasticsearch import exceptions @@ -38,6 +39,9 @@ def add_arguments(self, parser): parser.add_argument('--all', action='store_true', help='Reindex all courses') + parser.add_argument('--active', + action='store_true', + help='Reindex active courses only') parser.add_argument('--setup', action='store_true', help='Reindex all courses on developers stack setup') @@ -58,19 +62,24 @@ def _parse_course_key(self, raw_value): return result - def handle(self, *args, **options): + def handle(self, *args, **options): # pylint: disable=too-many-statements """ By convention set by Django developers, this method actually executes command's actions. So, there could be no better docstring than emphasize this once again. """ course_ids = options['course_ids'] all_option = options['all'] + active_option = options['active'] setup_option = options['setup'] readable_option = options['warning'] index_all_courses_option = all_option or setup_option - if (not len(course_ids) and not index_all_courses_option) or (len(course_ids) and index_all_courses_option): # lint-amnesty, pylint: disable=len-as-condition - raise CommandError("reindex_course requires one or more s OR the --all or --setup flags.") + if ((not course_ids and not (index_all_courses_option or active_option)) or + (course_ids and (index_all_courses_option or active_option))): + raise CommandError(( + "reindex_course requires one or more s" + " OR the --all, --active or --setup flags." + )) store = modulestore() @@ -79,8 +88,8 @@ def handle(self, *args, **options): logging.warning('Reducing logging to WARNING level for easier progress tracking') if index_all_courses_option: - index_names = (CoursewareSearchIndexer.INDEX_NAME, CourseAboutSearchIndexer.INDEX_NAME) if setup_option: + index_names = (CoursewareSearchIndexer.INDEX_NAME, CourseAboutSearchIndexer.INDEX_NAME) for index_name in index_names: try: searcher = SearchEngine.get_search_engine(index_name) @@ -104,22 +113,49 @@ def handle(self, *args, **options): course_keys = [course.id for course in modulestore().get_courses()] else: return + elif active_option: + # in case of --active, we get the list of course keys from all courses + # that are stored in the modulestore and filter out the non-active + all_courses = modulestore().get_courses() + + today = date.today() + # We keep the courses that has a start date and either don't have an end date + # or the end date is not in the past. + active_courses = filter(lambda course: course.start + and (not course.end or course.end.date() >= today), + all_courses) + course_keys = list(map(lambda course: course.id, active_courses)) + + logging.warning(f'Selected {len(course_keys)} active courses over a total of {len(all_courses)}.') + else: # in case course keys are provided as arguments course_keys = list(map(self._parse_course_key, course_ids)) total = len(course_keys) - logging.warning(f'Reindexing {total} courses') - reindexed = 0 + logging.warning(f'Reindexing {total} courses...') start = time() + count = 0 + success = 0 + errors = [] + for course_key in course_keys: try: + count += 1 CoursewareSearchIndexer.do_course_reindex(store, course_key) - reindexed += 1 - if reindexed % 10 == 0 or reindexed == total: - now = time() - t = now - start - logging.warning(f'{reindexed}/{total} reindexed in {t:.1f} seconds') + success += 1 + if count % 10 == 0 or count == total: + t = time() - start + remaining = total - success - len(errors) + logging.warning(f'{success} courses reindexed in {t:.1f} seconds. {remaining} remaining...') except Exception as exc: # lint-amnesty, pylint: disable=broad-except - logging.exception('Error indexing course %s due to the error: %s', course_key, exc) + errors.append(course_key) + logging.exception('Error indexing course %s due to the error: %s.', course_key, exc) + + t = time() - start + logging.warning(f'{success} of {total} courses reindexed succesfully. Total running time: {t:.1f} seconds.') + if errors: + logging.warning('Reindex failed for %s courses:', len(errors)) + for course_key in errors: + logging.warning(course_key) diff --git a/cms/djangoapps/contentstore/management/commands/tests/test_reindex_courses.py b/cms/djangoapps/contentstore/management/commands/tests/test_reindex_courses.py index 57534f30c4a0..13d33c48f92a 100644 --- a/cms/djangoapps/contentstore/management/commands/tests/test_reindex_courses.py +++ b/cms/djangoapps/contentstore/management/commands/tests/test_reindex_courses.py @@ -10,6 +10,7 @@ from xmodule.modulestore.django import modulestore # lint-amnesty, pylint: disable=wrong-import-order from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase # lint-amnesty, pylint: disable=wrong-import-order from xmodule.modulestore.tests.factories import CourseFactory, LibraryFactory # lint-amnesty, pylint: disable=wrong-import-order +from datetime import datetime, timedelta @ddt.ddt @@ -26,11 +27,18 @@ def setUp(self): org="test", library="lib2", display_name="run2", default_store=ModuleStoreEnum.Type.split ) + yesterday = datetime.min.today() - timedelta(days=1) + self.first_course = CourseFactory.create( - org="test", course="course1", display_name="run1" + org="test", course="course1", display_name="run1", start=yesterday, end=None ) + self.second_course = CourseFactory.create( - org="test", course="course2", display_name="run1" + org="test", course="course2", display_name="run1", start=yesterday, end=yesterday + ) + + self.third_course = CourseFactory.create( + org="test", course="course3", display_name="run1", start=None, end=None ) REINDEX_PATH_LOCATION = ( @@ -103,7 +111,7 @@ def test_given_all_key_prompts_and_reindexes_all_courses(self): call_command('reindex_course', all=True) patched_yes_no.assert_called_once_with(ReindexCommand.CONFIRMATION_PROMPT, default='no') - expected_calls = self._build_calls(self.first_course, self.second_course) + expected_calls = self._build_calls(self.first_course, self.second_course, self.third_course) self.assertCountEqual(patched_index.mock_calls, expected_calls) def test_given_all_key_prompts_and_reindexes_all_courses_cancelled(self): @@ -116,3 +124,15 @@ def test_given_all_key_prompts_and_reindexes_all_courses_cancelled(self): patched_yes_no.assert_called_once_with(ReindexCommand.CONFIRMATION_PROMPT, default='no') patched_index.assert_not_called() + + def test_given_active_key_prompt(self): + """ + Test that reindexes all active courses when --active key is given + Active courses have a start date but no end date, or the end date is in the future. + """ + with mock.patch(self.REINDEX_PATH_LOCATION) as patched_index, \ + mock.patch(self.MODULESTORE_PATCH_LOCATION, mock.Mock(return_value=self.store)): + call_command('reindex_course', active=True) + + expected_calls = self._build_calls(self.first_course) + self.assertCountEqual(patched_index.mock_calls, expected_calls) diff --git a/cms/djangoapps/contentstore/rest_api/v1/serializers/__init__.py b/cms/djangoapps/contentstore/rest_api/v1/serializers/__init__.py index 7e99a729abb3..d5b41dd5b99a 100644 --- a/cms/djangoapps/contentstore/rest_api/v1/serializers/__init__.py +++ b/cms/djangoapps/contentstore/rest_api/v1/serializers/__init__.py @@ -6,7 +6,7 @@ from .course_team import CourseTeamSerializer from .course_index import CourseIndexSerializer from .grading import CourseGradingModelSerializer, CourseGradingSerializer -from .home import CourseHomeSerializer, CourseTabSerializer, LibraryTabSerializer +from .home import CourseHomeSerializer, CourseHomeTabSerializer, LibraryTabSerializer from .proctoring import ( LimitedProctoredExamSettingsSerializer, ProctoredExamConfigurationSerializer, @@ -21,3 +21,4 @@ VideoUsageSerializer, VideoDownloadSerializer ) +from .vertical_block import ContainerHandlerSerializer diff --git a/cms/djangoapps/contentstore/rest_api/v1/serializers/course_index.py b/cms/djangoapps/contentstore/rest_api/v1/serializers/course_index.py index d423f7e9dab4..29577d9a75b5 100644 --- a/cms/djangoapps/contentstore/rest_api/v1/serializers/course_index.py +++ b/cms/djangoapps/contentstore/rest_api/v1/serializers/course_index.py @@ -20,6 +20,7 @@ class CourseIndexSerializer(serializers.Serializer): deprecated_blocks_info = serializers.DictField() discussions_incontext_feedback_url = serializers.CharField() discussions_incontext_learnmore_url = serializers.CharField() + discussions_settings = serializers.DictField() initial_state = InitialIndexStateSerializer() initial_user_clipboard = serializers.DictField() language_code = serializers.CharField() @@ -29,3 +30,5 @@ class CourseIndexSerializer(serializers.Serializer): proctoring_errors = ProctoringErrorListSerializer(many=True) reindex_link = serializers.CharField() rerun_notification_id = serializers.IntegerField() + advance_settings_url = serializers.CharField() + is_custom_relative_dates_active = serializers.BooleanField() diff --git a/cms/djangoapps/contentstore/rest_api/v1/serializers/home.py b/cms/djangoapps/contentstore/rest_api/v1/serializers/home.py index 80296b9a766c..1afc51ed77af 100644 --- a/cms/djangoapps/contentstore/rest_api/v1/serializers/home.py +++ b/cms/djangoapps/contentstore/rest_api/v1/serializers/home.py @@ -31,7 +31,7 @@ class LibraryViewSerializer(serializers.Serializer): can_edit = serializers.BooleanField() -class CourseTabSerializer(serializers.Serializer): +class CourseHomeTabSerializer(serializers.Serializer): archived_courses = CourseCommonSerializer(required=False, many=True) courses = CourseCommonSerializer(required=False, many=True) in_process_course_actions = UnsucceededCourseSerializer(many=True, required=False, allow_null=True) diff --git a/cms/djangoapps/contentstore/rest_api/v1/serializers/vertical_block.py b/cms/djangoapps/contentstore/rest_api/v1/serializers/vertical_block.py new file mode 100644 index 000000000000..c5b54e200e75 --- /dev/null +++ b/cms/djangoapps/contentstore/rest_api/v1/serializers/vertical_block.py @@ -0,0 +1,92 @@ +""" +API Serializers for unit page +""" + +from django.urls import reverse +from rest_framework import serializers + +from cms.djangoapps.contentstore.helpers import ( + xblock_studio_url, + xblock_type_display_name, +) + + +class ChildAncestorSerializer(serializers.Serializer): + """ + Serializer for representing child blocks in the ancestor XBlock. + """ + + url = serializers.SerializerMethodField() + display_name = serializers.CharField(source="display_name_with_default") + + def get_url(self, obj): + """ + Method to generate studio URL for the child block. + """ + return xblock_studio_url(obj) + + +class AncestorXBlockSerializer(serializers.Serializer): + """ + Serializer for representing the ancestor XBlock and its children. + """ + + children = ChildAncestorSerializer(many=True) + title = serializers.CharField() + is_last = serializers.BooleanField() + + +class ContainerXBlock(serializers.Serializer): + """ + Serializer for representing XBlock data. Doesn't include all data about XBlock. + """ + + display_name = serializers.CharField(source="display_name_with_default") + display_type = serializers.SerializerMethodField() + category = serializers.CharField() + + def get_display_type(self, obj): + """ + Method to get the display type name for the container XBlock. + """ + return xblock_type_display_name(obj) + + +class ContainerHandlerSerializer(serializers.Serializer): + """ + Serializer for container handler + """ + + language_code = serializers.CharField() + action = serializers.CharField() + xblock = ContainerXBlock() + is_unit_page = serializers.BooleanField() + is_collapsible = serializers.BooleanField() + position = serializers.IntegerField(min_value=1) + prev_url = serializers.CharField(allow_null=True) + next_url = serializers.CharField(allow_null=True) + new_unit_category = serializers.CharField() + outline_url = serializers.CharField() + ancestor_xblocks = AncestorXBlockSerializer(many=True) + component_templates = serializers.ListField(child=serializers.DictField()) + xblock_info = serializers.DictField() + draft_preview_link = serializers.CharField() + published_preview_link = serializers.CharField() + show_unit_tags = serializers.BooleanField() + user_clipboard = serializers.DictField() + is_fullwidth_content = serializers.BooleanField() + assets_url = serializers.SerializerMethodField() + unit_block_id = serializers.CharField(source="unit.location.block_id") + subsection_location = serializers.CharField(source="subsection.location") + + def get_assets_url(self, obj): + """ + Method to get the assets URL based on the course id. + """ + + context_course = obj.get("context_course", None) + if context_course: + return reverse( + "assets_handler", kwargs={"course_key_string": context_course.id} + ) + return None diff --git a/cms/djangoapps/contentstore/rest_api/v1/urls.py b/cms/djangoapps/contentstore/rest_api/v1/urls.py index 1af7cf46a675..66760ea3c303 100644 --- a/cms/djangoapps/contentstore/rest_api/v1/urls.py +++ b/cms/djangoapps/contentstore/rest_api/v1/urls.py @@ -1,10 +1,12 @@ """ Contenstore API v1 URLs. """ +from django.conf import settings from django.urls import re_path, path from openedx.core.constants import COURSE_ID_PATTERN from .views import ( + ContainerHandlerView, CourseDetailsView, CourseTeamView, CourseIndexView, @@ -100,6 +102,11 @@ CourseRerunView.as_view(), name="course_rerun" ), + re_path( + fr'^container_handler/{settings.USAGE_KEY_PATTERN}$', + ContainerHandlerView.as_view(), + name="container_handler" + ), # Authoring API # Do not use under v1 yet (Nov. 23). The Authoring API is still experimental and the v0 versions should be used diff --git a/cms/djangoapps/contentstore/rest_api/v1/views/__init__.py b/cms/djangoapps/contentstore/rest_api/v1/views/__init__.py index 57d68ebd081f..b7415b78c29a 100644 --- a/cms/djangoapps/contentstore/rest_api/v1/views/__init__.py +++ b/cms/djangoapps/contentstore/rest_api/v1/views/__init__.py @@ -15,3 +15,4 @@ VideoDownloadView ) from .help_urls import HelpUrlsView +from .vertical_block import ContainerHandlerView diff --git a/cms/djangoapps/contentstore/rest_api/v1/views/course_index.py b/cms/djangoapps/contentstore/rest_api/v1/views/course_index.py index 1ffac5ba6900..2a6fb0f4bcc4 100644 --- a/cms/djangoapps/contentstore/rest_api/v1/views/course_index.py +++ b/cms/djangoapps/contentstore/rest_api/v1/views/course_index.py @@ -7,6 +7,7 @@ from rest_framework.response import Response from rest_framework.views import APIView +from cms.djangoapps.contentstore.config.waffle import CUSTOM_RELATIVE_DATES from cms.djangoapps.contentstore.rest_api.v1.serializers import CourseIndexSerializer from cms.djangoapps.contentstore.utils import get_course_index_context from common.djangoapps.student.auth import has_studio_read_access @@ -92,6 +93,7 @@ def get(self, request: Request, course_id: str): course_index_context.update({ "discussions_incontext_learnmore_url": settings.DISCUSSIONS_INCONTEXT_LEARNMORE_URL, "discussions_incontext_feedback_url": settings.DISCUSSIONS_INCONTEXT_FEEDBACK_URL, + "is_custom_relative_dates_active": CUSTOM_RELATIVE_DATES.is_enabled(course_key), }) serializer = CourseIndexSerializer(course_index_context) diff --git a/cms/djangoapps/contentstore/rest_api/v1/views/home.py b/cms/djangoapps/contentstore/rest_api/v1/views/home.py index f8ee907d2e9f..b06cec44b7d3 100644 --- a/cms/djangoapps/contentstore/rest_api/v1/views/home.py +++ b/cms/djangoapps/contentstore/rest_api/v1/views/home.py @@ -8,7 +8,7 @@ from openedx.core.lib.api.view_utils import view_auth_classes from ....utils import get_home_context, get_course_context, get_library_context -from ..serializers import CourseHomeSerializer, CourseTabSerializer, LibraryTabSerializer +from ..serializers import CourseHomeSerializer, CourseHomeTabSerializer, LibraryTabSerializer @view_auth_classes(is_authenticated=True) @@ -102,7 +102,7 @@ class HomePageCoursesView(APIView): description="Query param to filter by course org", )], responses={ - 200: CourseTabSerializer, + 200: CourseHomeTabSerializer, 401: "The requester is not authenticated.", }, ) @@ -160,7 +160,7 @@ def get(self, request: Request): "archived_courses": archived_courses, "in_process_course_actions": in_process_course_actions, } - serializer = CourseTabSerializer(courses_context) + serializer = CourseHomeTabSerializer(courses_context) return Response(serializer.data) diff --git a/cms/djangoapps/contentstore/rest_api/v1/views/tests/test_course_index.py b/cms/djangoapps/contentstore/rest_api/v1/views/tests/test_course_index.py index e65ae8429567..c9ff5f9c36e3 100644 --- a/cms/djangoapps/contentstore/rest_api/v1/views/tests/test_course_index.py +++ b/cms/djangoapps/contentstore/rest_api/v1/views/tests/test_course_index.py @@ -5,6 +5,9 @@ from django.urls import reverse from rest_framework import status +from edx_toggles.toggles.testutils import override_waffle_flag + +from cms.djangoapps.contentstore.config.waffle import CUSTOM_RELATIVE_DATES from cms.djangoapps.contentstore.rest_api.v1.mixins import PermissionAccessMixin from cms.djangoapps.contentstore.tests.utils import CourseTestCase from cms.djangoapps.contentstore.utils import get_lms_link_for_item @@ -46,6 +49,7 @@ def setUp(self): kwargs={"course_id": self.course.id}, ) + @override_waffle_flag(CUSTOM_RELATIVE_DATES, active=True) def test_course_index_response(self): """Check successful response content""" response = self.client.get(self.url) @@ -59,6 +63,7 @@ def test_course_index_response(self): }, "discussions_incontext_feedback_url": "", "discussions_incontext_learnmore_url": "", + "is_custom_relative_dates_active": True, "initial_state": None, "initial_user_clipboard": { "content": None, @@ -72,12 +77,19 @@ def test_course_index_response(self): "notification_dismiss_url": None, "proctoring_errors": [], "reindex_link": f"/course/{self.course.id}/search_reindex", - "rerun_notification_id": None + "rerun_notification_id": None, + "discussions_settings": { + "enable_in_context": True, + "enable_graded_units": False, + "unit_level_visibility": True, + }, + "advance_settings_url": f"/settings/advanced/{self.course.id}", } self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertDictEqual(expected_response, response.data) + @override_waffle_flag(CUSTOM_RELATIVE_DATES, active=False) def test_course_index_response_with_show_locators(self): """Check successful response content with show query param""" response = self.client.get(self.url, {"show": str(self.unit.location)}) @@ -91,6 +103,7 @@ def test_course_index_response_with_show_locators(self): }, "discussions_incontext_feedback_url": "", "discussions_incontext_learnmore_url": "", + "is_custom_relative_dates_active": False, "initial_state": { "expanded_locators": [ str(self.unit.location), @@ -110,7 +123,13 @@ def test_course_index_response_with_show_locators(self): "notification_dismiss_url": None, "proctoring_errors": [], "reindex_link": f"/course/{self.course.id}/search_reindex", - "rerun_notification_id": None + "rerun_notification_id": None, + "discussions_settings": { + "enable_in_context": True, + "enable_graded_units": False, + "unit_level_visibility": True, + }, + "advance_settings_url": f"/settings/advanced/{self.course.id}", } self.assertEqual(response.status_code, status.HTTP_200_OK) diff --git a/cms/djangoapps/contentstore/rest_api/v1/views/tests/test_vertical_block.py b/cms/djangoapps/contentstore/rest_api/v1/views/tests/test_vertical_block.py new file mode 100644 index 000000000000..ff117c5ecfe6 --- /dev/null +++ b/cms/djangoapps/contentstore/rest_api/v1/views/tests/test_vertical_block.py @@ -0,0 +1,67 @@ +""" +Unit tests for the vertical block. +""" +from django.urls import reverse +from rest_framework import status + +from cms.djangoapps.contentstore.tests.utils import CourseTestCase +from xmodule.modulestore.django import modulestore # lint-amnesty, pylint: disable=wrong-import-order +from xmodule.modulestore.tests.factories import BlockFactory # lint-amnesty, pylint: disable=wrong-import-order + + +class ContainerHandlerViewTest(CourseTestCase): + """ + Unit tests for the ContainerHandlerView. + """ + + def setUp(self): + super().setUp() + self.chapter = BlockFactory.create( + parent=self.course, category="chapter", display_name="Week 1" + ) + self.sequential = BlockFactory.create( + parent=self.chapter, category="sequential", display_name="Lesson 1" + ) + self.vertical = self._create_block(self.sequential, "vertical", "Unit") + + self.store = modulestore() + self.store.publish(self.vertical.location, self.user.id) + + def _get_reverse_url(self, location): + """ + Creates url to current handler view api + """ + return reverse( + "cms.djangoapps.contentstore:v1:container_handler", + kwargs={"usage_key_string": location}, + ) + + def _create_block(self, parent, category, display_name, **kwargs): + """ + Creates a block without publishing it. + """ + return BlockFactory.create( + parent=parent, + category=category, + display_name=display_name, + publish_item=False, + user_id=self.user.id, + **kwargs + ) + + def test_success_response(self): + """ + Check that endpoint is valid and success response. + """ + url = self._get_reverse_url(self.vertical.location) + response = self.client.get(url) + self.assertEqual(response.status_code, status.HTTP_200_OK) + + def test_not_valid_usage_key_string(self): + """ + Check that invalid 'usage_key_string' raises Http404. + """ + usage_key_string = "i4x://InvalidOrg/InvalidCourse/vertical/static/InvalidContent" + url = self._get_reverse_url(usage_key_string) + response = self.client.get(url) + self.assertEqual(response.status_code, 404) diff --git a/cms/djangoapps/contentstore/rest_api/v1/views/vertical_block.py b/cms/djangoapps/contentstore/rest_api/v1/views/vertical_block.py new file mode 100644 index 000000000000..3f9a04851173 --- /dev/null +++ b/cms/djangoapps/contentstore/rest_api/v1/views/vertical_block.py @@ -0,0 +1,143 @@ +""" API Views for unit page """ + +import edx_api_doc_tools as apidocs +from django.http import Http404, HttpResponseBadRequest +from opaque_keys import InvalidKeyError +from opaque_keys.edx.keys import UsageKey +from rest_framework.request import Request +from rest_framework.response import Response +from rest_framework.views import APIView + +from cms.djangoapps.contentstore.utils import get_container_handler_context +from cms.djangoapps.contentstore.views.component import _get_item_in_course +from cms.djangoapps.contentstore.rest_api.v1.serializers import ContainerHandlerSerializer +from openedx.core.lib.api.view_utils import view_auth_classes +from xmodule.modulestore.django import modulestore # lint-amnesty, pylint: disable=wrong-import-order +from xmodule.modulestore.exceptions import ItemNotFoundError # lint-amnesty, pylint: disable=wrong-import-order + + +@view_auth_classes(is_authenticated=True) +class ContainerHandlerView(APIView): + """ + View for container xblock requests to get vertical data. + """ + + def get_object(self, usage_key_string): + """ + Get an object by usage-id of the block + """ + try: + usage_key = UsageKey.from_string(usage_key_string) + except InvalidKeyError: + raise Http404 # lint-amnesty, pylint: disable=raise-missing-from + return usage_key + + @apidocs.schema( + parameters=[ + apidocs.string_parameter( + "usage_key_string", + apidocs.ParameterLocation.PATH, + description="Container usage key", + ), + ], + responses={ + 200: ContainerHandlerSerializer, + 401: "The requester is not authenticated.", + 404: "The requested locator does not exist.", + }, + ) + def get(self, request: Request, usage_key_string: str): + """ + Get an object containing vertical data. + + **Example Request** + + GET /api/contentstore/v1/container_handler/{usage_key_string} + + **Response Values** + + If the request is successful, an HTTP 200 "OK" response is returned. + + The HTTP 200 response contains a single dict that contains keys that + are the vertical's container data. + + **Example Response** + + ```json + { + "language_code": "zh-cn", + "action": "view", + "xblock": { + "display_name": "Labs and Demos", + "display_type": "单元", + "category": "vertical" + }, + "is_unit_page": true, + "is_collapsible": false, + "position": 1, + "prev_url": "block-v1-edX%2BDemo_Course%2Btype%40vertical%2Bblock%404e592689563243c484", + "next_url": "block-v1%3AedX%2BDemoX%2BDemo_Course%2Btype%40vertical%2Bblock%40vertical_aae927868e55", + "new_unit_category": "vertical", + "outline_url": "/course/course-v1:edX+DemoX+Demo_Course?format=concise", + "ancestor_xblocks": [ + { + "children": [ + { + "url": "/course/course-v1:edX+DemoX+Demo_Course?show=block-v1%3AedX%2BDemoX%2BDemo_Course%2Btype%", + "display_name": "Introduction" + }, + ... + ], + "title": "Example Week 2: Get Interactive", + "is_last": false + }, + ... + ], + "component_templates": [ + { + "type": "advanced", + "templates": [ + { + "display_name": "批注", + "category": "annotatable", + "boilerplate_name": null, + "hinted": false, + "tab": "common", + "support_level": true + }, + ... + }, + ... + ], + "xblock_info": {}, + "draft_preview_link": "//preview.localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/...", + "published_preview_link": "///courses/course-v1:edX+DemoX+Demo_Course/jump_to/...", + "show_unit_tags": false, + "user_clipboard": { + "content": null, + "source_usage_key": "", + "source_context_title": "", + "source_edit_url": "" + }, + "is_fullwidth_content": false, + "assets_url": "/assets/course-v1:edX+DemoX+Demo_Course/", + "unit_block_id": "d6cee45205a449369d7ef8f159b22bdf", + "subsection_location": "block-v1:edX+DemoX+Demo_Course+type@sequential+block@graded_simulations" + } + ``` + """ + usage_key = self.get_object(usage_key_string) + course_key = usage_key.course_key + with modulestore().bulk_operations(course_key): + try: + course, xblock, lms_link, preview_lms_link = _get_item_in_course(request, usage_key) + except ItemNotFoundError: + return HttpResponseBadRequest() + + context = get_container_handler_context(request, usage_key, course, xblock) + context.update({ + 'draft_preview_link': preview_lms_link, + 'published_preview_link': lms_link, + }) + serializer = ContainerHandlerSerializer(context) + return Response(serializer.data) diff --git a/cms/djangoapps/contentstore/signals/handlers.py b/cms/djangoapps/contentstore/signals/handlers.py index 20c14089e0a6..e0bc9fcc9558 100644 --- a/cms/djangoapps/contentstore/signals/handlers.py +++ b/cms/djangoapps/contentstore/signals/handlers.py @@ -127,6 +127,17 @@ def listen_for_course_publish(sender, course_key, **kwargs): # pylint: disable= dump_course_to_neo4j ) + # DEVELOPER README: probably all tasks here should use transaction.on_commit + # to avoid stale data, but the tasks are owned by many teams and are often + # working well enough. Several instead use a waiting strategy. + # If you are in here trying to figure out why your task is not working correctly, + # consider whether it is getting stale data and if so choose to wait for the transaction + # like exams or put your task to sleep for a while like discussions. + # You will not be able to replicate these errors in an environment where celery runs + # in process because it will be inside the transaction. Use the settings from + # devstack_with_worker.py, and consider adding a time.sleep into send_bulk_published_signal + # if you really want to make sure that the task happens before the data is ready. + # register special exams asynchronously after the data is ready course_key_str = str(course_key) transaction.on_commit(lambda: update_special_exams_and_publish.delay(course_key_str)) @@ -139,10 +150,9 @@ def listen_for_course_publish(sender, course_key, **kwargs): # pylint: disable= # Push the course out to CourseGraph asynchronously. dump_course_to_neo4j.delay(course_key_str) - # Finally, call into the course search subsystem - # to kick off an indexing action + # Kick off a courseware indexing action after the data is ready if CoursewareSearchIndexer.indexing_is_enabled() and CourseAboutSearchIndexer.indexing_is_enabled(): - update_search_index.delay(course_key_str, datetime.now(UTC).isoformat()) + transaction.on_commit(lambda: update_search_index.delay(course_key_str, datetime.now(UTC).isoformat())) update_discussions_settings_from_course_task.apply_async( args=[course_key_str], diff --git a/cms/djangoapps/contentstore/tests/test_contentstore.py b/cms/djangoapps/contentstore/tests/test_contentstore.py index b82605f8933f..9f90840156ca 100644 --- a/cms/djangoapps/contentstore/tests/test_contentstore.py +++ b/cms/djangoapps/contentstore/tests/test_contentstore.py @@ -49,7 +49,6 @@ delete_course, reverse_course_url, reverse_url, - get_taxonomy_tags_widget_url, ) from cms.djangoapps.contentstore.views.component import ADVANCED_COMPONENT_TYPES from common.djangoapps.course_action_state.managers import CourseActionStateItemNotFoundError @@ -1416,15 +1415,12 @@ def test_course_overview_view_with_course(self): course.location.course_key ) - taxonomy_tags_widget_url = get_taxonomy_tags_widget_url(course.id) - self.assertContains( resp, - '
'.format( # lint-amnesty, pylint: disable=line-too-long + '
'.format( # lint-amnesty, pylint: disable=line-too-long locator=str(course.location), course_key=str(course.id), assets_url=assets_url, - taxonomy_tags_widget_url=taxonomy_tags_widget_url, ), status_code=200, html=True diff --git a/cms/djangoapps/contentstore/tests/test_courseware_index.py b/cms/djangoapps/contentstore/tests/test_courseware_index.py index 7d7a0d533b07..98a60dce901f 100644 --- a/cms/djangoapps/contentstore/tests/test_courseware_index.py +++ b/cms/djangoapps/contentstore/tests/test_courseware_index.py @@ -5,7 +5,7 @@ import time from datetime import datetime from unittest import skip -from unittest.mock import patch +from unittest.mock import patch, Mock import ddt import pytest @@ -585,6 +585,8 @@ def test_large_course_deletion(self): self._test_large_course_deletion(self.store) +@patch('cms.djangoapps.contentstore.signals.handlers.transaction.on_commit', + new=Mock(side_effect=lambda func: func()),) # run right away class TestTaskExecution(SharedModuleStoreTestCase): """ Set of tests to ensure that the task code will do the right thing when diff --git a/cms/djangoapps/contentstore/utils.py b/cms/djangoapps/contentstore/utils.py index b2c3f59b9d17..91e85f8df660 100644 --- a/cms/djangoapps/contentstore/utils.py +++ b/cms/djangoapps/contentstore/utils.py @@ -7,6 +7,7 @@ from collections import defaultdict from contextlib import contextmanager from datetime import datetime, timezone +from urllib.parse import quote_plus from uuid import uuid4 from django.conf import settings @@ -14,6 +15,7 @@ from django.urls import reverse from django.utils import translation from django.utils.translation import gettext as _ +from eventtracking import tracker from help_tokens.core import HelpUrlExpert from lti_consumer.models import CourseAllowPIISharingInLTIFlag from opaque_keys.edx.keys import CourseKey, UsageKey @@ -37,6 +39,7 @@ CourseStaffRole, GlobalStaff, ) +from common.djangoapps.track import contexts from common.djangoapps.util.course import get_link_for_about_page from common.djangoapps.util.milestones_helpers import ( is_prerequisite_courses_enabled, @@ -1757,6 +1760,7 @@ def _get_course_index_context(request, course_key, course_block): course_index_context = { 'language_code': request.LANGUAGE_CODE, 'context_course': course_block, + 'discussions_settings': course_block.discussions_settings, 'lms_link': lms_link, 'sections': sections, 'course_structure': course_structure, @@ -1784,6 +1788,129 @@ def _get_course_index_context(request, course_key, course_block): return course_index_context +def get_container_handler_context(request, usage_key, course, xblock): # pylint: disable=too-many-statements + """ + Utils is used to get context for container xblock requests. + It is used for both DRF and django views. + """ + + from cms.djangoapps.contentstore.views.component import ( + get_component_templates, + get_unit_tags, + CONTAINER_TEMPLATES, + LIBRARY_BLOCK_TYPES, + ) + from cms.djangoapps.contentstore.helpers import get_parent_xblock, is_unit + from cms.djangoapps.contentstore.xblock_storage_handlers.view_handlers import ( + add_container_page_publishing_info, + create_xblock_info, + ) + from openedx.core.djangoapps.content_staging import api as content_staging_api + + component_templates = get_component_templates(course) + ancestor_xblocks = [] + parent = get_parent_xblock(xblock) + action = request.GET.get('action', 'view') + + is_unit_page = is_unit(xblock) + unit = xblock if is_unit_page else None + + is_first = True + block = xblock + + # Build the breadcrumbs and find the ``Unit`` ancestor + # if it is not the immediate parent. + while parent: + + if unit is None and is_unit(block): + unit = block + + # add all to nav except current xblock page + if xblock != block: + current_block = { + 'title': block.display_name_with_default, + 'children': parent.get_children(), + 'is_last': is_first + } + is_first = False + ancestor_xblocks.append(current_block) + + block = parent + parent = get_parent_xblock(parent) + + ancestor_xblocks.reverse() + + if unit is None: + raise ValueError("Could not determine unit page") + + subsection = get_parent_xblock(unit) + if subsection is None: + raise ValueError(f"Could not determine parent subsection from unit {unit.location}") + + section = get_parent_xblock(subsection) + if section is None: + raise ValueError(f"Could not determine ancestor section from unit {unit.location}") + + # for the sequence navigator + prev_url, next_url = get_sibling_urls(subsection, unit.location) + # these are quoted here because they'll end up in a query string on the page, + # and quoting with mako will trigger the xss linter... + prev_url = quote_plus(prev_url) if prev_url else None + next_url = quote_plus(next_url) if next_url else None + + show_unit_tags = use_tagging_taxonomy_list_page() + unit_tags = None + if show_unit_tags and is_unit_page: + unit_tags = get_unit_tags(usage_key) + + # Fetch the XBlock info for use by the container page. Note that it includes information + # about the block's ancestors and siblings for use by the Unit Outline. + xblock_info = create_xblock_info(xblock, include_ancestor_info=is_unit_page, tags=unit_tags) + + if is_unit_page: + add_container_page_publishing_info(xblock, xblock_info) + + # need to figure out where this item is in the list of children as the + # preview will need this + index = 1 + for child in subsection.get_children(): + if child.location == unit.location: + break + index += 1 + + # Get the status of the user's clipboard so they can paste components if they have something to paste + user_clipboard = content_staging_api.get_user_clipboard_json(request.user.id, request) + library_block_types = [problem_type['component'] for problem_type in LIBRARY_BLOCK_TYPES] + is_library_xblock = xblock.location.block_type in library_block_types + + context = { + 'language_code': request.LANGUAGE_CODE, + 'context_course': course, # Needed only for display of menus at top of page. + 'action': action, + 'xblock': xblock, + 'xblock_locator': xblock.location, + 'unit': unit, + 'is_unit_page': is_unit_page, + 'is_collapsible': is_library_xblock, + 'subsection': subsection, + 'section': section, + 'position': index, + 'prev_url': prev_url, + 'next_url': next_url, + 'new_unit_category': 'vertical', + 'outline_url': '{url}?format=concise'.format(url=reverse_course_url('course_handler', course.id)), + 'ancestor_xblocks': ancestor_xblocks, + 'component_templates': component_templates, + 'xblock_info': xblock_info, + 'templates': CONTAINER_TEMPLATES, + 'show_unit_tags': show_unit_tags, + # Status of the user's clipboard, exactly as would be returned from the "GET clipboard" REST API. + 'user_clipboard': user_clipboard, + 'is_fullwidth_content': is_library_xblock, + } + return context + + class StudioPermissionsService: """ Service that can provide information about a user's permissions. @@ -1803,3 +1930,21 @@ def can_read(self, course_key): def can_write(self, course_key): """ Does the user have read access to the given course/library? """ return has_studio_write_access(self._user, course_key) + + +def track_course_update_event(course_key, user, event_data=None): + """ + Track course update event + """ + event_name = 'edx.contentstore.course_update' + event_data['course_id'] = str(course_key) + event_data['user_id'] = str(user.id) + event_data['user_forums_roles'] = [ + role.name for role in user.roles.filter(course_id=str(course_key)) + ] + event_data['user_course_roles'] = [ + role.role for role in user.courseaccessrole_set.filter(course_id=str(course_key)) + ] + context = contexts.course_context_from_course_id(course_key) + with tracker.get_tracker().context(event_name, context): + tracker.emit(event_name, event_data) diff --git a/cms/djangoapps/contentstore/views/component.py b/cms/djangoapps/contentstore/views/component.py index bafcdad35c71..ae0f62e3316e 100644 --- a/cms/djangoapps/contentstore/views/component.py +++ b/cms/djangoapps/contentstore/views/component.py @@ -4,7 +4,6 @@ import logging -from urllib.parse import quote_plus from django.conf import settings from django.contrib.auth.decorators import login_required @@ -25,24 +24,14 @@ from common.djangoapps.student.auth import has_course_author_access from common.djangoapps.xblock_django.api import authorable_xblocks, disabled_xblocks from common.djangoapps.xblock_django.models import XBlockStudioConfigurationFlag -from cms.djangoapps.contentstore.toggles import ( - use_new_problem_editor, - use_tagging_taxonomy_list_page, -) +from cms.djangoapps.contentstore.helpers import is_unit +from cms.djangoapps.contentstore.toggles import use_new_problem_editor, use_new_unit_page +from cms.djangoapps.contentstore.xblock_storage_handlers.view_handlers import load_services_for_studio from openedx.core.lib.xblock_utils import get_aside_from_xblock, is_xblock_aside from openedx.core.djangoapps.discussions.models import DiscussionsConfiguration -from openedx.core.djangoapps.content_staging import api as content_staging_api from openedx.core.djangoapps.content_tagging.api import get_content_tags from xmodule.modulestore.django import modulestore # lint-amnesty, pylint: disable=wrong-import-order from xmodule.modulestore.exceptions import ItemNotFoundError # lint-amnesty, pylint: disable=wrong-import-order -from ..toggles import use_new_unit_page -from ..utils import get_lms_link_for_item, get_sibling_urls, reverse_course_url, get_unit_url -from ..helpers import get_parent_xblock, is_unit, xblock_type_display_name -from cms.djangoapps.contentstore.xblock_storage_handlers.view_handlers import ( - add_container_page_publishing_info, - create_xblock_info, - load_services_for_studio, -) __all__ = [ 'container_handler', @@ -121,6 +110,9 @@ def container_handler(request, usage_key_string): # pylint: disable=too-many-st html: returns the HTML page for editing a container json: not currently supported """ + + from ..utils import get_container_handler_context, get_unit_url + if 'text/html' in request.META.get('HTTP_ACCEPT', 'text/html'): try: @@ -132,10 +124,6 @@ def container_handler(request, usage_key_string): # pylint: disable=too-many-st course, xblock, lms_link, preview_lms_link = _get_item_in_course(request, usage_key) except ItemNotFoundError: return HttpResponseBadRequest() - component_templates = get_component_templates(course) - ancestor_xblocks = [] - parent = get_parent_xblock(xblock) - action = request.GET.get('action', 'view') is_unit_page = is_unit(xblock) unit = xblock if is_unit_page else None @@ -143,97 +131,12 @@ def container_handler(request, usage_key_string): # pylint: disable=too-many-st if is_unit_page and use_new_unit_page(course.id): return redirect(get_unit_url(course.id, unit.location)) - is_first = True - block = xblock - - # Build the breadcrumbs and find the ``Unit`` ancestor - # if it is not the immediate parent. - while parent: - - if unit is None and is_unit(block): - unit = block - - # add all to nav except current xblock page - if xblock != block: - current_block = { - 'title': block.display_name_with_default, - 'children': parent.get_children(), - 'is_last': is_first - } - is_first = False - ancestor_xblocks.append(current_block) - - block = parent - parent = get_parent_xblock(parent) - - ancestor_xblocks.reverse() - - assert unit is not None, "Could not determine unit page" - subsection = get_parent_xblock(unit) - assert subsection is not None, "Could not determine parent subsection from unit " + str( - unit.location) - section = get_parent_xblock(subsection) - assert section is not None, "Could not determine ancestor section from unit " + str(unit.location) - - # for the sequence navigator - prev_url, next_url = get_sibling_urls(subsection, unit.location) - # these are quoted here because they'll end up in a query string on the page, - # and quoting with mako will trigger the xss linter... - prev_url = quote_plus(prev_url) if prev_url else None - next_url = quote_plus(next_url) if next_url else None - - show_unit_tags = use_tagging_taxonomy_list_page() - unit_tags = None - if show_unit_tags and is_unit_page: - unit_tags = get_unit_tags(usage_key) - - # Fetch the XBlock info for use by the container page. Note that it includes information - # about the block's ancestors and siblings for use by the Unit Outline. - xblock_info = create_xblock_info(xblock, include_ancestor_info=is_unit_page, tags=unit_tags) - - if is_unit_page: - add_container_page_publishing_info(xblock, xblock_info) - - # need to figure out where this item is in the list of children as the - # preview will need this - index = 1 - for child in subsection.get_children(): - if child.location == unit.location: - break - index += 1 - - # Get the status of the user's clipboard so they can paste components if they have something to paste - user_clipboard = content_staging_api.get_user_clipboard_json(request.user.id, request) - library_block_types = [problem_type['component'] for problem_type in LIBRARY_BLOCK_TYPES] - is_library_xblock = xblock.location.block_type in library_block_types - - return render_to_response('container.html', { - 'language_code': request.LANGUAGE_CODE, - 'context_course': course, # Needed only for display of menus at top of page. - 'action': action, - 'xblock': xblock, - 'xblock_locator': xblock.location, - 'unit': unit, - 'is_unit_page': is_unit_page, - 'is_collapsible': is_library_xblock, - 'subsection': subsection, - 'section': section, - 'position': index, - 'prev_url': prev_url, - 'next_url': next_url, - 'new_unit_category': 'vertical', - 'outline_url': '{url}?format=concise'.format(url=reverse_course_url('course_handler', course.id)), - 'ancestor_xblocks': ancestor_xblocks, - 'component_templates': component_templates, - 'xblock_info': xblock_info, + container_handler_context = get_container_handler_context(request, usage_key, course, xblock) + container_handler_context.update({ 'draft_preview_link': preview_lms_link, 'published_preview_link': lms_link, - 'templates': CONTAINER_TEMPLATES, - 'show_unit_tags': show_unit_tags, - # Status of the user's clipboard, exactly as would be returned from the "GET clipboard" REST API. - 'user_clipboard': user_clipboard, - 'is_fullwidth_content': is_library_xblock, }) + return render_to_response('container.html', container_handler_context) else: return HttpResponseBadRequest("Only supports HTML requests") @@ -242,6 +145,9 @@ def get_component_templates(courselike, library=False): # lint-amnesty, pylint: """ Returns the applicable component templates that can be used by the specified course or library. """ + + from ..helpers import xblock_type_display_name + def create_template_dict(name, category, support_level, boilerplate_name=None, tab="common", hinted=False): """ Creates a component template dict. @@ -545,6 +451,9 @@ def _get_item_in_course(request, usage_key): Verifies that the caller has permission to access this item. """ + + from ..utils import get_lms_link_for_item + # usage_key's course_key may have an empty run property usage_key = usage_key.replace(course_key=modulestore().fill_in_run(usage_key.course_key)) diff --git a/cms/djangoapps/contentstore/views/preview.py b/cms/djangoapps/contentstore/views/preview.py index a897a38ad21a..9c9926a5b25d 100644 --- a/cms/djangoapps/contentstore/views/preview.py +++ b/cms/djangoapps/contentstore/views/preview.py @@ -315,6 +315,7 @@ def _studio_wrap_xblock(xblock, view, frag, context, display_name_only=False): 'is_reorderable': is_reorderable, 'can_edit': can_edit, 'can_edit_visibility': context.get('can_edit_visibility', is_course), + 'course_authoring_url': settings.COURSE_AUTHORING_MICROFRONTEND_URL, 'is_loading': context.get('is_loading', False), 'is_selected': context.get('is_selected', False), 'selectable': context.get('selectable', False), diff --git a/cms/djangoapps/contentstore/views/tests/test_block.py b/cms/djangoapps/contentstore/views/tests/test_block.py index ac86961b2293..c8ac9b89dc2d 100644 --- a/cms/djangoapps/contentstore/views/tests/test_block.py +++ b/cms/djangoapps/contentstore/views/tests/test_block.py @@ -288,15 +288,9 @@ def test_tag_count_in_container_fragment(self, mock_get_object_tag_counts): self.assertEqual(resp.status_code, 200) usage_key = self.response_usage_key(resp) - # Get the preview HTML without tags - mock_get_object_tag_counts.return_value = {} - html, __ = self._get_container_preview(root_usage_key) - self.assertIn("wrapper-xblock", html) - self.assertNotIn('data-testid="tag-count-button"', html) - # Get the preview HTML with tags mock_get_object_tag_counts.return_value = { - str(usage_key): 13 + str(usage_key): 13, } html, __ = self._get_container_preview(root_usage_key) self.assertIn("wrapper-xblock", html) diff --git a/cms/djangoapps/contentstore/views/tests/test_course_index.py b/cms/djangoapps/contentstore/views/tests/test_course_index.py index 73db1a10b944..b30f8c95a631 100644 --- a/cms/djangoapps/contentstore/views/tests/test_course_index.py +++ b/cms/djangoapps/contentstore/views/tests/test_course_index.py @@ -6,7 +6,6 @@ import datetime import json from unittest import mock, skip -from unittest.mock import patch import ddt import lxml @@ -644,7 +643,7 @@ def test_verify_warn_only_on_enabled_blocks(self, enabled_block_types, deprecate ) @override_settings(FEATURES={'ENABLE_EXAM_SETTINGS_HTML_VIEW': True}) - @patch('cms.djangoapps.models.settings.course_metadata.CourseMetadata.validate_proctoring_settings') + @mock.patch('cms.djangoapps.models.settings.course_metadata.CourseMetadata.validate_proctoring_settings') def test_proctoring_link_is_visible(self, mock_validate_proctoring_settings): """ Test to check proctored exam settings mfe url is rendering properly @@ -685,9 +684,11 @@ class TestCourseReIndex(CourseTestCase): ENABLED_SIGNALS = ['course_published'] + @mock.patch('cms.djangoapps.contentstore.signals.handlers.transaction.on_commit', + new=mock.Mock(side_effect=lambda func: func()), ) # run index right away def setUp(self): """ - Set up the for the course outline tests. + Set up the for the course reindex tests. """ super().setUp() diff --git a/cms/djangoapps/contentstore/xblock_storage_handlers/view_handlers.py b/cms/djangoapps/contentstore/xblock_storage_handlers/view_handlers.py index eb79181c4659..8b122d8c8da0 100644 --- a/cms/djangoapps/contentstore/xblock_storage_handlers/view_handlers.py +++ b/cms/djangoapps/contentstore/xblock_storage_handlers/view_handlers.py @@ -1206,6 +1206,7 @@ def create_xblock_info( # lint-amnesty, pylint: disable=too-many-statements xblock_info["tags"] = tags if use_tagging_taxonomy_list_page(): xblock_info["taxonomy_tags_widget_url"] = get_taxonomy_tags_widget_url() + xblock_info["course_authoring_url"] = settings.COURSE_AUTHORING_MICROFRONTEND_URL if course_outline: if xblock_info["has_explicit_staff_lock"]: diff --git a/cms/djangoapps/pipeline_js/js/xmodule.js b/cms/djangoapps/pipeline_js/js/xmodule.js index 44789c8cc6e2..8a19355c1a93 100644 --- a/cms/djangoapps/pipeline_js/js/xmodule.js +++ b/cms/djangoapps/pipeline_js/js/xmodule.js @@ -23,11 +23,6 @@ define( 'mathjax', function() { window.MathJax.Hub.Config({ - styles: { - '.MathJax_SVG>svg': {'max-width': '100%'}, - // This is to resolve for people who use center mathjax with tables - 'table>tbody>tr>td>.MathJax_SVG>svg': {'max-width': 'inherit'}, - }, tex2jax: { inlineMath: [ ['\\(', '\\)'], @@ -65,14 +60,7 @@ define( if (oldWidth !== document.documentElement.scrollWidth) { t = window.setTimeout(function() { oldWidth = document.documentElement.scrollWidth; - MathJax.Hub.Queue( - ['Rerender', MathJax.Hub], - [() => $('.MathJax_SVG>svg').toArray().forEach(el => { - if ($(el).width() === 0) { - $(el).css('max-width', 'inherit'); - } - })] - ); + MathJax.Hub.Queue(['Rerender', MathJax.Hub]); t = -1; }, delay); } diff --git a/cms/envs/common.py b/cms/envs/common.py index 4ded7a9cff34..8f974e73e521 100644 --- a/cms/envs/common.py +++ b/cms/envs/common.py @@ -1105,8 +1105,8 @@ } DEFAULT_AUTO_FIELD = 'django.db.models.AutoField' -# This will be overridden through CMS config -DEFAULT_HASHING_ALGORITHM = 'sha1' +DEFAULT_HASHING_ALGORITHM = 'sha256' + #################### Python sandbox ############################################ CODE_JAIL = { diff --git a/cms/envs/devstack.py b/cms/envs/devstack.py index 74f5933822fe..8ea4a8441000 100644 --- a/cms/envs/devstack.py +++ b/cms/envs/devstack.py @@ -301,6 +301,9 @@ def should_show_debug_toolbar(request): # lint-amnesty, pylint: disable=missing CREDENTIALS_INTERNAL_SERVICE_URL = 'http://localhost:18150' CREDENTIALS_PUBLIC_SERVICE_URL = 'http://localhost:18150' +########################## ORA MFE APP ############################## +ORA_MICROFRONTEND_URL = 'http://localhost:1992' + ############################ AI_TRANSLATIONS ################################## AI_TRANSLATIONS_API_URL = 'http://localhost:18760/api/v1' diff --git a/cms/static/js/factories/tag_count.js b/cms/static/js/factories/tag_count.js new file mode 100644 index 000000000000..cadcfa220f1a --- /dev/null +++ b/cms/static/js/factories/tag_count.js @@ -0,0 +1,13 @@ +import * as TagCountView from 'js/views/tag_count'; +import * as TagCountModel from 'js/models/tag_count'; + +// eslint-disable-next-line no-unused-expressions +'use strict'; +export default function TagCountFactory(TagCountJson, el) { + var model = new TagCountModel(TagCountJson, {parse: true}); + var tagCountView = new TagCountView({el, model}); + tagCountView.setupMessageListener(); + tagCountView.render(); +} + +export {TagCountFactory}; diff --git a/cms/static/js/features/import/views/import.js b/cms/static/js/features/import/views/import.js index e34965a0927d..8d6d0a6ace30 100644 --- a/cms/static/js/features/import/views/import.js +++ b/cms/static/js/features/import/views/import.js @@ -332,7 +332,7 @@ define( * @return {JSON} the data of the previous import */ storedImport: function() { - return JSON.parse($.cookie(COOKIE_NAME)); + return JSON.parse($.cookie(COOKIE_NAME) || null); } }; diff --git a/cms/static/js/models/tag_count.js b/cms/static/js/models/tag_count.js new file mode 100644 index 000000000000..7007dfc9dc30 --- /dev/null +++ b/cms/static/js/models/tag_count.js @@ -0,0 +1,13 @@ +define(['backbone', 'underscore'], function(Backbone, _) { + /** + * Model for Tag count view + */ + var TagCountModel = Backbone.Model.extend({ + defaults: { + content_id: null, + tags_count: 0, + course_authoring_url: null, + }, + }); + return TagCountModel; +}); diff --git a/cms/static/js/views/course_outline.js b/cms/static/js/views/course_outline.js index 4b7107cc4ddc..04dc98513002 100644 --- a/cms/static/js/views/course_outline.js +++ b/cms/static/js/views/course_outline.js @@ -12,11 +12,11 @@ define(['jquery', 'underscore', 'js/views/xblock_outline', 'edx-ui-toolkit/js/ut 'common/js/components/utils/view_utils', 'js/views/utils/xblock_utils', 'js/models/xblock_outline_info', 'js/views/modals/course_outline_modals', 'js/utils/drag_and_drop', 'common/js/components/views/feedback_notification', 'common/js/components/views/feedback_prompt', - 'js/views/utils/tagging_drawer_utils',], + 'js/views/utils/tagging_drawer_utils', 'js/views/tag_count', 'js/models/tag_count'], function( $, _, XBlockOutlineView, StringUtils, ViewUtils, XBlockViewUtils, XBlockOutlineInfo, CourseOutlineModalsFactory, ContentDragger, NotificationView, PromptView, - TaggingDrawerUtils + TaggingDrawerUtils, TagCountView, TagCountModel ) { var CourseOutlineView = XBlockOutlineView.extend({ // takes XBlockOutlineInfo as a model @@ -28,9 +28,28 @@ function( this.makeContentDraggable(this.el); // Show/hide the paste button this.initializePasteButton(this.el); + this.renderTagCount(); return renderResult; }, + renderTagCount: function() { + const contentId = this.model.get('id'); + const tagCountsByUnit = this.model.get('tag_counts_by_unit') + const tagsCount = tagCountsByUnit !== undefined ? tagCountsByUnit[contentId] : 0 + var countModel = new TagCountModel({ + content_id: contentId, + tags_count: tagsCount, + course_authoring_url: this.model.get('course_authoring_url'), + }, {parse: true}); + var tagCountView = new TagCountView({el: this.$('.tag-count'), model: countModel}); + tagCountView.setupMessageListener(); + tagCountView.render(); + this.$('.tag-count').click((event) => { + event.preventDefault(); + this.openManageTagsDrawer(); + }); + }, + shouldExpandChildren: function() { return this.expandedLocators.contains(this.model.get('id')); }, @@ -461,10 +480,8 @@ function( }, openManageTagsDrawer() { - const article = document.querySelector('[data-taxonomy-tags-widget-url]'); - const taxonomyTagsWidgetUrl = $(article).attr('data-taxonomy-tags-widget-url'); + const taxonomyTagsWidgetUrl = this.model.get('taxonomy_tags_widget_url'); const contentId = this.model.get('id'); - TaggingDrawerUtils.openDrawer(taxonomyTagsWidgetUrl, contentId); }, diff --git a/cms/static/js/views/export.js b/cms/static/js/views/export.js index 73007363b927..18f1cc182a61 100644 --- a/cms/static/js/views/export.js +++ b/cms/static/js/views/export.js @@ -191,7 +191,7 @@ define([ * @return {JSON} the data of the previous export */ storedExport: function(contentHomeUrl) { - var storedData = JSON.parse($.cookie(COOKIE_NAME)); + var storedData = JSON.parse($.cookie(COOKIE_NAME) || null); if (storedData) { successUnixDate = storedData.date; } diff --git a/cms/static/js/views/pages/container.js b/cms/static/js/views/pages/container.js index 783133af21be..3268b60e416a 100644 --- a/cms/static/js/views/pages/container.js +++ b/cms/static/js/views/pages/container.js @@ -111,6 +111,7 @@ function($, _, Backbone, gettext, BasePage, el: this.$('.unit-tags'), model: this.model }); + this.tagListView.setupMessageListener(); this.tagListView.render(); this.unitOutlineView = new UnitOutlineView({ diff --git a/cms/static/js/views/pages/container_subviews.js b/cms/static/js/views/pages/container_subviews.js index 8848abc246e2..7ea09eff086e 100644 --- a/cms/static/js/views/pages/container_subviews.js +++ b/cms/static/js/views/pages/container_subviews.js @@ -370,6 +370,83 @@ function($, _, gettext, BaseView, ViewUtils, XBlockViewUtils, MoveXBlockUtils, H } }, + setupMessageListener: function () { + window.addEventListener( + "message", (event) => { + // Listen any message from Manage tags drawer. + var data = event.data; + var courseAuthoringUrl = this.model.get("course_authoring_url") + if (event.origin == courseAuthoringUrl + && data.includes('[Manage tags drawer] Tags updated:')) { + // This message arrives when there is a change in the tag list. + // The message contains the new list of tags. + let jsonData = data.replace(/\[Manage tags drawer\] Tags updated: /g, ""); + jsonData = JSON.parse(jsonData); + if (jsonData.contentId == this.model.id) { + this.model.set('tags', this.buildTaxonomyTree(jsonData)); + this.render(); + } + } + }, + ); + }, + + buildTaxonomyTree: function(data) { + // TODO We can use this function for the initial request of tags + // and avoid to use two functions (see get_unit_tags on contentstore/views/component.py) + + var taxonomyList = []; + var totalCount = 0; + var actualId = 0; + data.taxonomies.forEach((taxonomy) => { + // Build a tag tree for each taxonomy + var rootTagsValues = []; + var tags = {}; + taxonomy.tags.forEach((tag) => { + // Creates the tags for all the lineage of this tag + for (let i = tag.lineage.length - 1; i >= 0; i--){ + var tagValue = tag.lineage[i] + var tagProcessedBefore = tags.hasOwnProperty(tagValue); + if (!tagProcessedBefore) { + tags[tagValue] = { + id: actualId, + value: tagValue, + children: [], + } + actualId++; + if (i == 0) { + rootTagsValues.push(tagValue); + } + } + if (i !== tag.lineage.length - 1) { + // Add a child into the children list + tags[tagValue].children.push(tags[tag.lineage[i + 1]]) + } + if (tagProcessedBefore) { + // Break this loop if the tag has been processed before, + // we don't need to process lineage again to avoid duplicates. + break; + } + } + }) + + var tagCount = Object.keys(tags).length; + // Add the tree to the taxonomy list + taxonomyList.push({ + id: taxonomy.taxonomyId, + value: taxonomy.name, + tags: rootTagsValues.map(rootValue => tags[rootValue]), + count: tagCount, + }); + totalCount += tagCount; + }); + + return { + count: totalCount, + taxonomies: taxonomyList, + }; + }, + handleKeyDownOnHeader: function(event) { if (event.key === 'Enter' || event.key === ' ') { event.preventDefault(); diff --git a/cms/static/js/views/tag_count.js b/cms/static/js/views/tag_count.js new file mode 100644 index 000000000000..c7ba4d79e5ed --- /dev/null +++ b/cms/static/js/views/tag_count.js @@ -0,0 +1,54 @@ +define(['jquery', 'underscore', 'js/views/baseview', 'edx-ui-toolkit/js/utils/html-utils'], +function($, _, BaseView, HtmlUtils) { + 'use strict'; + + /** + * TagCountView displays the tag count of a unit/component + * + * This component is being rendered in this way to allow receiving + * messages from the Manage tags drawer and being able to update the count. + */ + var TagCountView = BaseView.extend({ + // takes TagCountModel as a model + + initialize: function() { + BaseView.prototype.initialize.call(this); + this.template = this.loadTemplate('tag-count'); + }, + + setupMessageListener: function () { + window.addEventListener( + 'message', (event) => { + // Listen any message from Manage tags drawer. + var data = event.data; + var courseAuthoringUrl = this.model.get("course_authoring_url") + if (event.origin == courseAuthoringUrl + && data.includes('[Manage tags drawer] Count updated:')) { + // This message arrives when there is a change in the tag list. + // The message contains the new count of tags. + let jsonData = data.replace(/\[Manage tags drawer\] Count updated: /g, ""); + jsonData = JSON.parse(jsonData); + if (jsonData.contentId == this.model.get("content_id")) { + this.model.set('tags_count', jsonData.count); + this.render(); + } + } + } + ); + }, + + render: function() { + HtmlUtils.setHtml( + this.$el, + HtmlUtils.HTML( + this.template({ + tags_count: this.model.get("tags_count"), + }) + ) + ); + return this; + } + }); + + return TagCountView; +}); diff --git a/cms/static/sass/elements/_drawer.scss b/cms/static/sass/elements/_drawer.scss index c18073be9864..96edfe1983f1 100644 --- a/cms/static/sass/elements/_drawer.scss +++ b/cms/static/sass/elements/_drawer.scss @@ -13,6 +13,10 @@ background: rgba(0, 0, 0, 0.8); } +.drawer-cover.gray-cover { + background: rgba(112, 112, 112, 0.8); +} + .drawer { @extend %ui-depth4; diff --git a/cms/static/sass/elements/_system-feedback.scss b/cms/static/sass/elements/_system-feedback.scss index f3cfed83ef94..9f74e625fb17 100644 --- a/cms/static/sass/elements/_system-feedback.scss +++ b/cms/static/sass/elements/_system-feedback.scss @@ -325,6 +325,12 @@ .action-secondary { @extend %t-action4; + cursor: pointer; + color: $white; + + &:hover { + color: $gray-l3; + } } } } diff --git a/cms/templates/container.html b/cms/templates/container.html index cd9530ed5120..d61ce60e9189 100644 --- a/cms/templates/container.html +++ b/cms/templates/container.html @@ -281,5 +281,5 @@
${_("Location ID")}
-
+
diff --git a/cms/templates/content_libraries/xblock_iframe.html b/cms/templates/content_libraries/xblock_iframe.html index b73a25f257b5..84d83de42ded 100644 --- a/cms/templates/content_libraries/xblock_iframe.html +++ b/cms/templates/content_libraries/xblock_iframe.html @@ -81,11 +81,6 @@ @@ -281,7 +281,7 @@

${_("Page Actions")}

assets_url = reverse('assets_handler', kwargs={'course_key_string': str(course_locator.course_key)}) %>

${_("Course Outline")}

-
+
@@ -323,5 +323,5 @@

${_("Changing the content learners see")}

-
+
diff --git a/cms/templates/js/course-outline.underscore b/cms/templates/js/course-outline.underscore index be5e1477549b..c62979bced1f 100644 --- a/cms/templates/js/course-outline.underscore +++ b/cms/templates/js/course-outline.underscore @@ -7,7 +7,8 @@ var hasPartitionGroups = xblockInfo.get('has_partition_group_components'); var userPartitionInfo = xblockInfo.get('user_partition_info'); var selectedGroupsLabel = userPartitionInfo['selected_groups_label']; var selectedPartitionIndex = userPartitionInfo['selected_partition_index']; -var tagsCount = (xblockInfo.get('tag_counts_by_unit') || {})[xblockInfo.get('id')] || 0; +var xblockId = xblockInfo.get('id') +var tagsCount = (xblockInfo.get('tag_counts_by_unit') || {})[xblockId] || 0; var statusMessages = []; var messageType; @@ -171,14 +172,8 @@ if (is_proctored_exam) { <% } %> - <% if (xblockInfo.isVertical() && typeof useTaggingTaxonomyListPage !== "undefined" && useTaggingTaxonomyListPage && tagsCount > 0) { %> -
  • - - - <%- tagsCount %> - <%- gettext('Manage Tags') %> - -
  • + <% if (xblockInfo.isVertical() && typeof useTaggingTaxonomyListPage !== "undefined" && useTaggingTaxonomyListPage) { %> +
  • <% } %> <% if (typeof enableCopyPasteUnits !== "undefined" && enableCopyPasteUnits) { %> diff --git a/cms/templates/js/tag-count.underscore b/cms/templates/js/tag-count.underscore new file mode 100644 index 000000000000..253323109f3c --- /dev/null +++ b/cms/templates/js/tag-count.underscore @@ -0,0 +1,7 @@ +<% if (tags_count && tags_count > 0) { %> + +<% } %> diff --git a/cms/templates/studio_xblock_wrapper.html b/cms/templates/studio_xblock_wrapper.html index 328a0a37e90d..9ae3a3a5dd21 100644 --- a/cms/templates/studio_xblock_wrapper.html +++ b/cms/templates/studio_xblock_wrapper.html @@ -29,6 +29,9 @@ + +<%static:webpack entry="js/factories/tag_count"> + TagCountFactory({ + tags_count: "${tags_count | n, js_escaped_string}", + content_id: "${xblock.location | n, js_escaped_string}", + course_authoring_url: "${course_authoring_url | n, js_escaped_string}", + }, + $('li.tag-count[data-locator="${xblock.location | n, js_escaped_string}"]') + ); + + % if not is_root: % if is_reorderable:
  • @@ -99,14 +112,8 @@ diff --git a/requirements/common_constraints.txt b/requirements/common_constraints.txt index 53dff4a22d75..7313473a1675 100644 --- a/requirements/common_constraints.txt +++ b/requirements/common_constraints.txt @@ -17,7 +17,7 @@ # using LTS django version -Django<4.0 + # elasticsearch>=7.14.0 includes breaking changes in it which caused issues in discovery upgrade process. # elastic search changelog: https://www.elastic.co/guide/en/enterprise-search/master/release-notes-7.14.0.html diff --git a/requirements/constraints.txt b/requirements/constraints.txt index 3d2270db8886..9c62d2eea053 100644 --- a/requirements/constraints.txt +++ b/requirements/constraints.txt @@ -23,12 +23,17 @@ click>=8.0,<9.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.9.2 +edx-enterprise==4.11.6 + +# Stay on LTS version, remove once this is added to common constraint +Django<5.0 # django-oauth-toolkit version >=2.0.0 has breaking changes. More details # mentioned on this issue https://github.com/openedx/edx-platform/issues/32884 django-oauth-toolkit==1.7.1 +# incremental upgrade +django-simple-history==3.4.0 # constrained in opaque_keys. migration guide here: https://pymongo.readthedocs.io/en/4.0/migrate-to-pymongo4.html # Major upgrade will be done in separate ticket. @@ -103,7 +108,17 @@ libsass==0.10.0 click==8.1.6 # pinning this version to avoid updates while the library is being developed -openedx-learning==0.4.2 +openedx-learning==0.4.4 # Open AI version 1.0.0 dropped support for openai.ChatCompletion which is currently in use in enterprise. openai<=0.28.1 + +# optimizely-sdk 5.0.0 is breaking following test with segmentation fault +# common/djangoapps/third_party_auth/tests/test_views.py::SAMLMetadataTest::test_secure_key_configuration +# needs to be fixed in the follow up issue +# https://github.com/openedx/edx-platform/issues/34103 +optimizely-sdk<5.0 + +# lxml 5.1.0 introduced a breaking change in unit test shards +# This constraint can probably be removed once lxml==5.1.1 is released on PyPI +lxml<5.0 diff --git a/requirements/edx-sandbox/py38.txt b/requirements/edx-sandbox/py38.txt index c8ad2eb22462..14ce80ce5456 100644 --- a/requirements/edx-sandbox/py38.txt +++ b/requirements/edx-sandbox/py38.txt @@ -23,16 +23,18 @@ cryptography==38.0.4 cycler==0.12.1 # via matplotlib fonttools==4.47.2 +importlib-resources==6.1.1 # via matplotlib joblib==1.3.2 # via nltk kiwisolver==1.4.5 # via matplotlib -lxml==5.1.0 +lxml==4.9.4 # via + # -c requirements/edx-sandbox/../constraints.txt # -r requirements/edx-sandbox/py38.in # openedx-calc -markupsafe==2.1.3 +markupsafe==2.1.4 # via # chem # openedx-calc diff --git a/requirements/edx/base.txt b/requirements/edx/base.txt index 315dd010e965..043e4ca47dd6 100644 --- a/requirements/edx/base.txt +++ b/requirements/edx/base.txt @@ -8,7 +8,7 @@ # via -r requirements/edx/github.in acid-xblock==0.2.1 # via -r requirements/edx/kernel.in -aiohttp==3.9.1 +aiohttp==3.9.3 # via # geoip2 # openai @@ -53,7 +53,13 @@ babel==2.14.0 # enmerkar-underscore backoff==1.10.0 # via analytics-python -beautifulsoup4==4.12.2 +backports-zoneinfo[tzdata]==0.2.1 + # via + # celery + # django + # icalendar + # kombu +beautifulsoup4==4.12.3 # via pynliner billiard==4.2.0 # via celery @@ -68,13 +74,13 @@ bleach[css]==6.1.0 # xblock-poll boto==2.49.0 # via -r requirements/edx/kernel.in -boto3==1.34.17 +boto3==1.34.30 # via # -r requirements/edx/kernel.in # django-ses # fs-s3fs # ora2 -botocore==1.34.17 +botocore==1.34.30 # via # -r requirements/edx/kernel.in # boto3 @@ -168,9 +174,9 @@ defusedxml==0.7.1 # social-auth-core deprecated==1.2.14 # via jwcrypto -django==3.2.23 +django==4.2.9 # via - # -c requirements/edx/../common_constraints.txt + # -c requirements/edx/../constraints.txt # -r requirements/edx/kernel.in # django-appconf # django-celery-results @@ -238,6 +244,7 @@ django==3.2.23 # openedx-learning # ora2 # super-csv + # xblock-google-drive # xss-utils django-appconf==1.0.6 # via django-statici18n @@ -332,6 +339,7 @@ django-ses==3.5.2 # via -r requirements/edx/bundled.in django-simple-history==3.4.0 # via + # -c requirements/edx/../constraints.txt # -r requirements/edx/kernel.in # edx-enterprise # edx-name-affirmation @@ -395,7 +403,7 @@ drf-jwt==1.19.2 # via edx-drf-extensions drf-nested-routers==0.93.5 # via openedx-blockstore -drf-spectacular==0.27.0 +drf-spectacular==0.27.1 # via -r requirements/edx/kernel.in drf-yasg==1.21.5 # via @@ -413,7 +421,7 @@ edx-auth-backends==4.2.0 # via # -r requirements/edx/kernel.in # openedx-blockstore -edx-braze-client==0.1.8 +edx-braze-client==0.2.2 # via # -r requirements/edx/bundled.in # edx-enterprise @@ -425,7 +433,7 @@ edx-ccx-keys==1.2.1 # via # -r requirements/edx/kernel.in # lti-consumer-xblock -edx-celeryutils==1.2.3 +edx-celeryutils==1.2.5 # via # -r requirements/edx/kernel.in # edx-name-affirmation @@ -441,7 +449,7 @@ edx-django-release-util==1.3.0 # openedx-blockstore edx-django-sites-extensions==4.0.2 # via -r requirements/edx/kernel.in -edx-django-utils==5.9.0 +edx-django-utils==5.10.1 # via # -r requirements/edx/kernel.in # django-config-models @@ -457,7 +465,7 @@ edx-django-utils==5.9.0 # openedx-blockstore # ora2 # super-csv -edx-drf-extensions==9.1.2 +edx-drf-extensions==10.1.0 # via # -r requirements/edx/kernel.in # edx-completion @@ -469,11 +477,11 @@ edx-drf-extensions==9.1.2 # edx-when # edxval # openedx-learning -edx-enterprise==4.9.2 +edx-enterprise==4.11.6 # via # -c requirements/edx/../constraints.txt # -r requirements/edx/kernel.in -edx-event-bus-kafka==5.5.0 +edx-event-bus-kafka==5.6.0 # via -r requirements/edx/kernel.in edx-event-bus-redis==0.3.2 # via -r requirements/edx/kernel.in @@ -514,7 +522,7 @@ edx-rest-api-client==5.6.1 # edx-proctoring edx-search==3.8.2 # via -r requirements/edx/kernel.in -edx-sga==0.23.0 +edx-sga==0.23.1 # via -r requirements/edx/bundled.in edx-submissions==3.6.0 # via @@ -596,6 +604,13 @@ idna==3.6 # requests # snowflake-connector-python # yarl +importlib-metadata==7.0.1 + # via markdown +importlib-resources==5.13.0 + # via + # jsonschema + # jsonschema-specifications + # pycountry inflection==0.5.1 # via # drf-spectacular @@ -629,7 +644,7 @@ jsonfield==3.1.0 # edx-submissions # lti-consumer-xblock # ora2 -jsonschema==4.20.0 +jsonschema==4.21.1 # via # drf-spectacular # optimizely-sdk @@ -639,7 +654,7 @@ jwcrypto==1.5.1 # via # django-oauth-toolkit # pylti1p3 -kombu==5.3.4 +kombu==5.3.5 # via celery laboratory==1.0.2 # via -r requirements/edx/kernel.in @@ -656,10 +671,11 @@ libsass==0.10.0 # -r requirements/edx/paver.txt loremipsum==1.0.5 # via ora2 -lti-consumer-xblock==9.8.1 +lti-consumer-xblock==9.8.3 # via -r requirements/edx/kernel.in -lxml==5.1.0 +lxml==4.9.4 # via + # -c requirements/edx/../constraints.txt # -r requirements/edx/kernel.in # edx-i18n-tools # edxval @@ -678,7 +694,6 @@ mako==1.3.0 # acid-xblock # lti-consumer-xblock # xblock - # xblock-google-drive # xblock-utils markdown==3.3.7 # via @@ -689,7 +704,7 @@ markdown==3.3.7 # xblock-poll markey==0.8 # via enmerkar-underscore -markupsafe==2.1.3 +markupsafe==2.1.4 # via # -r requirements/edx/paver.txt # chem @@ -717,7 +732,7 @@ mysqlclient==2.2.1 # via # -r requirements/edx/kernel.in # openedx-blockstore -newrelic==9.5.0 +newrelic==9.6.0 # via # -r requirements/edx/bundled.in # edx-django-utils @@ -744,7 +759,7 @@ openai==0.28.1 # via # -c requirements/edx/../constraints.txt # edx-enterprise -openedx-atlas==0.5.0 +openedx-atlas==0.6.0 # via -r requirements/edx/kernel.in openedx-blockstore==1.4.0 # via -r requirements/edx/kernel.in @@ -758,7 +773,7 @@ openedx-django-require==2.1.0 # via -r requirements/edx/kernel.in openedx-django-wiki==2.0.3 # via -r requirements/edx/kernel.in -openedx-events==9.2.0 +openedx-events==9.3.0 # via # -r requirements/edx/kernel.in # edx-event-bus-kafka @@ -767,15 +782,17 @@ openedx-filters==1.6.0 # via # -r requirements/edx/kernel.in # lti-consumer-xblock -openedx-learning==0.4.2 +openedx-learning==0.4.4 # via # -c requirements/edx/../constraints.txt # -r requirements/edx/kernel.in openedx-mongodbproxy==0.2.0 # via -r requirements/edx/kernel.in optimizely-sdk==4.1.1 - # via -r requirements/edx/bundled.in -ora2==6.0.25 + # via + # -c requirements/edx/../constraints.txt + # -r requirements/edx/bundled.in +ora2==6.0.31 # via -r requirements/edx/bundled.in packaging==23.2 # via @@ -818,7 +835,7 @@ polib==1.2.0 # via edx-i18n-tools prompt-toolkit==3.0.43 # via click-repl -psutil==5.9.7 +psutil==5.9.8 # via # -r requirements/edx/paver.txt # edx-django-utils @@ -907,9 +924,9 @@ python-dateutil==2.8.2 # xblock python-ipware==2.0.1 # via django-ipware -python-memcached==1.61 +python-memcached==1.62 # via -r requirements/edx/paver.txt -python-slugify==8.0.1 +python-slugify==8.0.2 # via code-annotations python-swiftclient==4.4.0 # via ora2 @@ -919,10 +936,10 @@ python3-openid==3.2.0 ; python_version >= "3" # social-auth-core python3-saml==1.16.0 # via -r requirements/edx/kernel.in -pytz==2023.3.post1 +pytz==2023.4 # via # -r requirements/edx/kernel.in - # django + # babel # django-ses # djangorestframework # drf-yasg @@ -952,13 +969,13 @@ pyyaml==6.0.1 # xblock random2==1.0.2 # via -r requirements/edx/kernel.in -recommender-xblock==2.0.1 +recommender-xblock==2.1.1 # via -r requirements/edx/bundled.in redis==5.0.1 # via # -r requirements/edx/kernel.in # walrus -referencing==0.32.1 +referencing==0.33.0 # via # jsonschema # jsonschema-specifications @@ -987,11 +1004,12 @@ requests==2.31.0 # slumber # snowflake-connector-python # social-auth-core + # xblock-google-drive requests-oauthlib==1.3.1 # via # -r requirements/edx/kernel.in # social-auth-core -rpds-py==0.16.2 +rpds-py==0.17.1 # via # jsonschema # referencing @@ -1060,7 +1078,7 @@ slumber==0.7.1 # edx-bulk-grades # edx-enterprise # edx-rest-api-client -snowflake-connector-python==3.6.0 +snowflake-connector-python==3.7.0 # via edx-enterprise social-auth-app-django==5.0.0 # via @@ -1124,7 +1142,9 @@ typing-extensions==4.9.0 # pylti1p3 # snowflake-connector-python tzdata==2023.4 - # via celery + # via + # backports-zoneinfo + # celery unicodecsv==0.14.1 # via # -r requirements/edx/kernel.in @@ -1178,7 +1198,7 @@ wrapt==1.16.0 # via # -r requirements/edx/paver.txt # deprecated -xblock[django]==1.9.1 +xblock[django]==1.10.0 # via # -r requirements/edx/kernel.in # acid-xblock @@ -1194,16 +1214,14 @@ xblock[django]==1.9.1 # xblock-google-drive # xblock-poll # xblock-utils -xblock-drag-and-drop-v2==3.3.0 +xblock-drag-and-drop-v2==3.4.0 # via -r requirements/edx/bundled.in -xblock-google-drive==0.5.0 +xblock-google-drive==0.6.1 # via -r requirements/edx/bundled.in xblock-poll==1.13.0 # via -r requirements/edx/bundled.in xblock-utils==4.0.0 - # via - # edx-sga - # xblock-google-drive + # via edx-sga xmlsec==1.3.13 # via python3-saml xss-utils==0.5.0 diff --git a/requirements/edx/coverage.txt b/requirements/edx/coverage.txt index 6b446423b258..ffd300d407b5 100644 --- a/requirements/edx/coverage.txt +++ b/requirements/edx/coverage.txt @@ -6,15 +6,15 @@ # chardet==5.2.0 # via diff-cover -coverage==7.4.0 +coverage==7.4.1 # via -r requirements/edx/coverage.in -diff-cover==8.0.2 +diff-cover==8.0.3 # via -r requirements/edx/coverage.in jinja2==3.1.3 # via diff-cover -markupsafe==2.1.3 +markupsafe==2.1.4 # via jinja2 -pluggy==1.3.0 +pluggy==1.4.0 # via diff-cover pygments==2.17.2 # via diff-cover diff --git a/requirements/edx/development.txt b/requirements/edx/development.txt index 072f74e41f43..5db9ff78e21a 100644 --- a/requirements/edx/development.txt +++ b/requirements/edx/development.txt @@ -16,7 +16,7 @@ acid-xblock==0.2.1 # via # -r requirements/edx/doc.txt # -r requirements/edx/testing.txt -aiohttp==3.9.1 +aiohttp==3.9.3 # via # -r requirements/edx/doc.txt # -r requirements/edx/testing.txt @@ -110,7 +110,16 @@ backoff==1.10.0 # -r requirements/edx/doc.txt # -r requirements/edx/testing.txt # analytics-python -beautifulsoup4==4.12.2 +backports-zoneinfo[tzdata]==0.2.1 + # via + # -r requirements/edx/doc.txt + # -r requirements/edx/testing.txt + # backports-zoneinfo + # celery + # django + # icalendar + # kombu +beautifulsoup4==4.12.3 # via # -r requirements/edx/doc.txt # -r requirements/edx/testing.txt @@ -136,14 +145,14 @@ boto==2.49.0 # via # -r requirements/edx/doc.txt # -r requirements/edx/testing.txt -boto3==1.34.17 +boto3==1.34.30 # via # -r requirements/edx/doc.txt # -r requirements/edx/testing.txt # django-ses # fs-s3fs # ora2 -botocore==1.34.17 +botocore==1.34.30 # via # -r requirements/edx/doc.txt # -r requirements/edx/testing.txt @@ -272,7 +281,7 @@ coreschema==0.0.4 # -r requirements/edx/testing.txt # coreapi # drf-yasg -coverage[toml]==7.4.0 +coverage[toml]==7.4.1 # via # -r requirements/edx/testing.txt # coverage @@ -323,9 +332,9 @@ deprecated==1.2.14 # -r requirements/edx/doc.txt # -r requirements/edx/testing.txt # jwcrypto -diff-cover==8.0.2 +diff-cover==8.0.3 # via -r requirements/edx/testing.txt -dill==0.3.7 +dill==0.3.8 # via # -r requirements/edx/testing.txt # pylint @@ -333,9 +342,9 @@ distlib==0.3.8 # via # -r requirements/edx/testing.txt # virtualenv -django==3.2.23 +django==4.2.9 # via - # -c requirements/edx/../common_constraints.txt + # -c requirements/edx/../constraints.txt # -r requirements/edx/doc.txt # -r requirements/edx/testing.txt # django-appconf @@ -407,6 +416,7 @@ django==3.2.23 # openedx-learning # ora2 # super-csv + # xblock-google-drive # xss-utils django-appconf==1.0.6 # via @@ -550,6 +560,7 @@ django-ses==3.5.2 # -r requirements/edx/testing.txt django-simple-history==3.4.0 # via + # -c requirements/edx/../constraints.txt # -r requirements/edx/doc.txt # -r requirements/edx/testing.txt # edx-enterprise @@ -651,7 +662,7 @@ drf-nested-routers==0.93.5 # -r requirements/edx/doc.txt # -r requirements/edx/testing.txt # openedx-blockstore -drf-spectacular==0.27.0 +drf-spectacular==0.27.1 # via # -r requirements/edx/doc.txt # -r requirements/edx/testing.txt @@ -677,7 +688,7 @@ edx-auth-backends==4.2.0 # -r requirements/edx/doc.txt # -r requirements/edx/testing.txt # openedx-blockstore -edx-braze-client==0.1.8 +edx-braze-client==0.2.2 # via # -r requirements/edx/doc.txt # -r requirements/edx/testing.txt @@ -692,7 +703,7 @@ edx-ccx-keys==1.2.1 # -r requirements/edx/doc.txt # -r requirements/edx/testing.txt # lti-consumer-xblock -edx-celeryutils==1.2.3 +edx-celeryutils==1.2.5 # via # -r requirements/edx/doc.txt # -r requirements/edx/testing.txt @@ -716,7 +727,7 @@ edx-django-sites-extensions==4.0.2 # via # -r requirements/edx/doc.txt # -r requirements/edx/testing.txt -edx-django-utils==5.9.0 +edx-django-utils==5.10.1 # via # -r requirements/edx/doc.txt # -r requirements/edx/testing.txt @@ -733,7 +744,7 @@ edx-django-utils==5.9.0 # openedx-blockstore # ora2 # super-csv -edx-drf-extensions==9.1.2 +edx-drf-extensions==10.1.0 # via # -r requirements/edx/doc.txt # -r requirements/edx/testing.txt @@ -746,12 +757,12 @@ edx-drf-extensions==9.1.2 # edx-when # edxval # openedx-learning -edx-enterprise==4.9.2 +edx-enterprise==4.11.6 # via # -c requirements/edx/../constraints.txt # -r requirements/edx/doc.txt # -r requirements/edx/testing.txt -edx-event-bus-kafka==5.5.0 +edx-event-bus-kafka==5.6.0 # via # -r requirements/edx/doc.txt # -r requirements/edx/testing.txt @@ -815,7 +826,7 @@ edx-search==3.8.2 # via # -r requirements/edx/doc.txt # -r requirements/edx/testing.txt -edx-sga==0.23.0 +edx-sga==0.23.1 # via # -r requirements/edx/doc.txt # -r requirements/edx/testing.txt @@ -887,7 +898,7 @@ execnet==2.0.2 # pytest-xdist factory-boy==3.3.0 # via -r requirements/edx/testing.txt -faker==22.2.0 +faker==22.6.0 # via # -r requirements/edx/testing.txt # factory-boy @@ -988,6 +999,22 @@ imagesize==1.4.1 # sphinx import-linter==2.0 # via -r requirements/edx/testing.txt +importlib-metadata==7.0.1 + # via + # -r requirements/edx/../pip-tools.txt + # -r requirements/edx/doc.txt + # -r requirements/edx/testing.txt + # build + # markdown + # pytest-randomly + # sphinx +importlib-resources==5.13.0 + # via + # -r requirements/edx/doc.txt + # -r requirements/edx/testing.txt + # jsonschema + # jsonschema-specifications + # pycountry inflection==0.5.1 # via # -r requirements/edx/doc.txt @@ -1055,7 +1082,7 @@ jsonfield==3.1.0 # edx-submissions # lti-consumer-xblock # ora2 -jsonschema==4.20.0 +jsonschema==4.21.1 # via # -r requirements/edx/doc.txt # -r requirements/edx/testing.txt @@ -1073,7 +1100,7 @@ jwcrypto==1.5.1 # -r requirements/edx/testing.txt # django-oauth-toolkit # pylti1p3 -kombu==5.3.4 +kombu==5.3.5 # via # -r requirements/edx/doc.txt # -r requirements/edx/testing.txt @@ -1105,12 +1132,13 @@ loremipsum==1.0.5 # -r requirements/edx/doc.txt # -r requirements/edx/testing.txt # ora2 -lti-consumer-xblock==9.8.1 +lti-consumer-xblock==9.8.3 # via # -r requirements/edx/doc.txt # -r requirements/edx/testing.txt -lxml==5.1.0 +lxml==4.9.4 # via + # -c requirements/edx/../constraints.txt # -r requirements/edx/doc.txt # -r requirements/edx/testing.txt # edx-i18n-tools @@ -1134,7 +1162,6 @@ mako==1.3.0 # acid-xblock # lti-consumer-xblock # xblock - # xblock-google-drive # xblock-utils markdown==3.3.7 # via @@ -1149,7 +1176,7 @@ markey==0.8 # -r requirements/edx/doc.txt # -r requirements/edx/testing.txt # enmerkar-underscore -markupsafe==2.1.3 +markupsafe==2.1.4 # via # -r requirements/edx/doc.txt # -r requirements/edx/testing.txt @@ -1208,7 +1235,7 @@ mysqlclient==2.2.1 # -r requirements/edx/doc.txt # -r requirements/edx/testing.txt # openedx-blockstore -newrelic==9.5.0 +newrelic==9.6.0 # via # -r requirements/edx/doc.txt # -r requirements/edx/testing.txt @@ -1249,7 +1276,7 @@ openai==0.28.1 # -r requirements/edx/doc.txt # -r requirements/edx/testing.txt # edx-enterprise -openedx-atlas==0.5.0 +openedx-atlas==0.6.0 # via # -r requirements/edx/doc.txt # -r requirements/edx/testing.txt @@ -1275,7 +1302,7 @@ openedx-django-wiki==2.0.3 # via # -r requirements/edx/doc.txt # -r requirements/edx/testing.txt -openedx-events==9.2.0 +openedx-events==9.3.0 # via # -r requirements/edx/doc.txt # -r requirements/edx/testing.txt @@ -1286,7 +1313,7 @@ openedx-filters==1.6.0 # -r requirements/edx/doc.txt # -r requirements/edx/testing.txt # lti-consumer-xblock -openedx-learning==0.4.2 +openedx-learning==0.4.4 # via # -c requirements/edx/../constraints.txt # -r requirements/edx/doc.txt @@ -1297,9 +1324,10 @@ openedx-mongodbproxy==0.2.0 # -r requirements/edx/testing.txt optimizely-sdk==4.1.1 # via + # -c requirements/edx/../constraints.txt # -r requirements/edx/doc.txt # -r requirements/edx/testing.txt -ora2==6.0.25 +ora2==6.0.31 # via # -r requirements/edx/doc.txt # -r requirements/edx/testing.txt @@ -1377,7 +1405,7 @@ platformdirs==3.11.0 # snowflake-connector-python # tox # virtualenv -pluggy==1.3.0 +pluggy==1.4.0 # via # -r requirements/edx/testing.txt # diff-cover @@ -1393,7 +1421,7 @@ prompt-toolkit==3.0.43 # -r requirements/edx/doc.txt # -r requirements/edx/testing.txt # click-repl -psutil==5.9.7 +psutil==5.9.8 # via # -r requirements/edx/doc.txt # -r requirements/edx/testing.txt @@ -1432,11 +1460,11 @@ pycryptodomex==3.20.0 # edx-proctoring # lti-consumer-xblock # pyjwkest -pydantic==2.5.3 +pydantic==2.6.0 # via # -r requirements/edx/testing.txt # fastapi -pydantic-core==2.14.6 +pydantic-core==2.16.1 # via # -r requirements/edx/testing.txt # pydantic @@ -1561,7 +1589,7 @@ pysrt==1.1.2 # -r requirements/edx/doc.txt # -r requirements/edx/testing.txt # edxval -pytest==7.4.4 +pytest==8.0.0 # via # -r requirements/edx/testing.txt # pylint-pytest @@ -1611,11 +1639,11 @@ python-ipware==2.0.1 # -r requirements/edx/doc.txt # -r requirements/edx/testing.txt # django-ipware -python-memcached==1.61 +python-memcached==1.62 # via # -r requirements/edx/doc.txt # -r requirements/edx/testing.txt -python-slugify==8.0.1 +python-slugify==8.0.2 # via # -r requirements/edx/doc.txt # -r requirements/edx/testing.txt @@ -1634,11 +1662,11 @@ python3-saml==1.16.0 # via # -r requirements/edx/doc.txt # -r requirements/edx/testing.txt -pytz==2023.3.post1 +pytz==2023.4 # via # -r requirements/edx/doc.txt # -r requirements/edx/testing.txt - # django + # babel # django-ses # djangorestframework # drf-yasg @@ -1676,7 +1704,7 @@ random2==1.0.2 # via # -r requirements/edx/doc.txt # -r requirements/edx/testing.txt -recommender-xblock==2.0.1 +recommender-xblock==2.1.1 # via # -r requirements/edx/doc.txt # -r requirements/edx/testing.txt @@ -1685,7 +1713,7 @@ redis==5.0.1 # -r requirements/edx/doc.txt # -r requirements/edx/testing.txt # walrus -referencing==0.32.1 +referencing==0.33.0 # via # -r requirements/edx/doc.txt # -r requirements/edx/testing.txt @@ -1723,12 +1751,13 @@ requests==2.31.0 # snowflake-connector-python # social-auth-core # sphinx + # xblock-google-drive requests-oauthlib==1.3.1 # via # -r requirements/edx/doc.txt # -r requirements/edx/testing.txt # social-auth-core -rpds-py==0.16.2 +rpds-py==0.17.1 # via # -r requirements/edx/doc.txt # -r requirements/edx/testing.txt @@ -1839,7 +1868,7 @@ snowballstemmer==2.2.0 # via # -r requirements/edx/doc.txt # sphinx -snowflake-connector-python==3.6.0 +snowflake-connector-python==3.7.0 # via # -r requirements/edx/doc.txt # -r requirements/edx/testing.txt @@ -1980,8 +2009,6 @@ tinycss2==1.2.1 # -r requirements/edx/doc.txt # -r requirements/edx/testing.txt # bleach -toml==0.10.2 - # via vulture tomli==2.0.1 # via # -r requirements/edx/../pip-tools.txt @@ -1997,6 +2024,7 @@ tomli==2.0.1 # pyproject-hooks # pytest # tox + # vulture tomlkit==0.12.3 # via # -r requirements/edx/doc.txt @@ -2011,7 +2039,7 @@ tqdm==4.66.1 # -r requirements/edx/testing.txt # nltk # openai -types-pytz==2023.3.1.1 +types-pytz==2023.4.0.20240130 # via django-stubs types-pyyaml==6.0.12.12 # via @@ -2025,6 +2053,7 @@ typing-extensions==4.9.0 # via # -r requirements/edx/doc.txt # -r requirements/edx/testing.txt + # annotated-types # anyio # asgiref # astroid @@ -2075,7 +2104,7 @@ user-util==1.0.0 # via # -r requirements/edx/doc.txt # -r requirements/edx/testing.txt -uvicorn==0.25.0 +uvicorn==0.27.0.post1 # via # -r requirements/edx/testing.txt # pact-python @@ -2095,7 +2124,7 @@ voluptuous==0.14.1 # -r requirements/edx/doc.txt # -r requirements/edx/testing.txt # ora2 -vulture==2.10 +vulture==2.11 # via -r requirements/edx/development.in walrus==0.9.3 # via @@ -2143,7 +2172,7 @@ wrapt==1.16.0 # -r requirements/edx/testing.txt # astroid # deprecated -xblock[django]==1.9.1 +xblock[django]==1.10.0 # via # -r requirements/edx/doc.txt # -r requirements/edx/testing.txt @@ -2161,11 +2190,11 @@ xblock[django]==1.9.1 # xblock-google-drive # xblock-poll # xblock-utils -xblock-drag-and-drop-v2==3.3.0 +xblock-drag-and-drop-v2==3.4.0 # via # -r requirements/edx/doc.txt # -r requirements/edx/testing.txt -xblock-google-drive==0.5.0 +xblock-google-drive==0.6.1 # via # -r requirements/edx/doc.txt # -r requirements/edx/testing.txt @@ -2178,7 +2207,6 @@ xblock-utils==4.0.0 # -r requirements/edx/doc.txt # -r requirements/edx/testing.txt # edx-sga - # xblock-google-drive xmlsec==1.3.13 # via # -r requirements/edx/doc.txt diff --git a/requirements/edx/doc.txt b/requirements/edx/doc.txt index 944d53ec0ff6..bf026eef3ef8 100644 --- a/requirements/edx/doc.txt +++ b/requirements/edx/doc.txt @@ -10,7 +10,7 @@ accessible-pygments==0.0.4 # via pydata-sphinx-theme acid-xblock==0.2.1 # via -r requirements/edx/base.txt -aiohttp==3.9.1 +aiohttp==3.9.3 # via # -r requirements/edx/base.txt # geoip2 @@ -74,7 +74,15 @@ backoff==1.10.0 # via # -r requirements/edx/base.txt # analytics-python -beautifulsoup4==4.12.2 +backports-zoneinfo[tzdata]==0.2.1 + # via + # -r requirements/edx/base.txt + # backports-zoneinfo + # celery + # django + # icalendar + # kombu +beautifulsoup4==4.12.3 # via # -r requirements/edx/base.txt # pydata-sphinx-theme @@ -95,13 +103,13 @@ bleach[css]==6.1.0 # xblock-poll boto==2.49.0 # via -r requirements/edx/base.txt -boto3==1.34.17 +boto3==1.34.30 # via # -r requirements/edx/base.txt # django-ses # fs-s3fs # ora2 -botocore==1.34.17 +botocore==1.34.30 # via # -r requirements/edx/base.txt # boto3 @@ -216,9 +224,9 @@ deprecated==1.2.14 # via # -r requirements/edx/base.txt # jwcrypto -django==3.2.23 +django==4.2.9 # via - # -c requirements/edx/../common_constraints.txt + # -c requirements/edx/../constraints.txt # -r requirements/edx/base.txt # django-appconf # django-celery-results @@ -286,6 +294,7 @@ django==3.2.23 # openedx-learning # ora2 # super-csv + # xblock-google-drive # xss-utils django-appconf==1.0.6 # via @@ -396,6 +405,7 @@ django-ses==3.5.2 # via -r requirements/edx/base.txt django-simple-history==3.4.0 # via + # -c requirements/edx/../constraints.txt # -r requirements/edx/base.txt # edx-enterprise # edx-name-affirmation @@ -470,7 +480,7 @@ drf-nested-routers==0.93.5 # via # -r requirements/edx/base.txt # openedx-blockstore -drf-spectacular==0.27.0 +drf-spectacular==0.27.1 # via -r requirements/edx/base.txt drf-yasg==1.21.5 # via @@ -489,7 +499,7 @@ edx-auth-backends==4.2.0 # via # -r requirements/edx/base.txt # openedx-blockstore -edx-braze-client==0.1.8 +edx-braze-client==0.2.2 # via # -r requirements/edx/base.txt # edx-enterprise @@ -501,7 +511,7 @@ edx-ccx-keys==1.2.1 # via # -r requirements/edx/base.txt # lti-consumer-xblock -edx-celeryutils==1.2.3 +edx-celeryutils==1.2.5 # via # -r requirements/edx/base.txt # edx-name-affirmation @@ -517,7 +527,7 @@ edx-django-release-util==1.3.0 # openedx-blockstore edx-django-sites-extensions==4.0.2 # via -r requirements/edx/base.txt -edx-django-utils==5.9.0 +edx-django-utils==5.10.1 # via # -r requirements/edx/base.txt # django-config-models @@ -533,7 +543,7 @@ edx-django-utils==5.9.0 # openedx-blockstore # ora2 # super-csv -edx-drf-extensions==9.1.2 +edx-drf-extensions==10.1.0 # via # -r requirements/edx/base.txt # edx-completion @@ -545,11 +555,11 @@ edx-drf-extensions==9.1.2 # edx-when # edxval # openedx-learning -edx-enterprise==4.9.2 +edx-enterprise==4.11.6 # via # -c requirements/edx/../constraints.txt # -r requirements/edx/base.txt -edx-event-bus-kafka==5.5.0 +edx-event-bus-kafka==5.6.0 # via -r requirements/edx/base.txt edx-event-bus-redis==0.3.2 # via -r requirements/edx/base.txt @@ -594,7 +604,7 @@ edx-rest-api-client==5.6.1 # edx-proctoring edx-search==3.8.2 # via -r requirements/edx/base.txt -edx-sga==0.23.0 +edx-sga==0.23.1 # via -r requirements/edx/base.txt edx-submissions==3.6.0 # via @@ -694,6 +704,17 @@ idna==3.6 # yarl imagesize==1.4.1 # via sphinx +importlib-metadata==7.0.1 + # via + # -r requirements/edx/base.txt + # markdown + # sphinx +importlib-resources==5.13.0 + # via + # -r requirements/edx/base.txt + # jsonschema + # jsonschema-specifications + # pycountry inflection==0.5.1 # via # -r requirements/edx/base.txt @@ -741,7 +762,7 @@ jsonfield==3.1.0 # edx-submissions # lti-consumer-xblock # ora2 -jsonschema==4.20.0 +jsonschema==4.21.1 # via # -r requirements/edx/base.txt # drf-spectacular @@ -756,7 +777,7 @@ jwcrypto==1.5.1 # -r requirements/edx/base.txt # django-oauth-toolkit # pylti1p3 -kombu==5.3.4 +kombu==5.3.5 # via # -r requirements/edx/base.txt # celery @@ -777,10 +798,11 @@ loremipsum==1.0.5 # via # -r requirements/edx/base.txt # ora2 -lti-consumer-xblock==9.8.1 +lti-consumer-xblock==9.8.3 # via -r requirements/edx/base.txt -lxml==5.1.0 +lxml==4.9.4 # via + # -c requirements/edx/../constraints.txt # -r requirements/edx/base.txt # edx-i18n-tools # edxval @@ -799,7 +821,6 @@ mako==1.3.0 # acid-xblock # lti-consumer-xblock # xblock - # xblock-google-drive # xblock-utils markdown==3.3.7 # via @@ -812,7 +833,7 @@ markey==0.8 # via # -r requirements/edx/base.txt # enmerkar-underscore -markupsafe==2.1.3 +markupsafe==2.1.4 # via # -r requirements/edx/base.txt # chem @@ -848,7 +869,7 @@ mysqlclient==2.2.1 # via # -r requirements/edx/base.txt # openedx-blockstore -newrelic==9.5.0 +newrelic==9.6.0 # via # -r requirements/edx/base.txt # edx-django-utils @@ -879,7 +900,7 @@ openai==0.28.1 # -c requirements/edx/../constraints.txt # -r requirements/edx/base.txt # edx-enterprise -openedx-atlas==0.5.0 +openedx-atlas==0.6.0 # via -r requirements/edx/base.txt openedx-blockstore==1.4.0 # via -r requirements/edx/base.txt @@ -894,7 +915,7 @@ openedx-django-require==2.1.0 # via -r requirements/edx/base.txt openedx-django-wiki==2.0.3 # via -r requirements/edx/base.txt -openedx-events==9.2.0 +openedx-events==9.3.0 # via # -r requirements/edx/base.txt # edx-event-bus-kafka @@ -903,15 +924,17 @@ openedx-filters==1.6.0 # via # -r requirements/edx/base.txt # lti-consumer-xblock -openedx-learning==0.4.2 +openedx-learning==0.4.4 # via # -c requirements/edx/../constraints.txt # -r requirements/edx/base.txt openedx-mongodbproxy==0.2.0 # via -r requirements/edx/base.txt optimizely-sdk==4.1.1 - # via -r requirements/edx/base.txt -ora2==6.0.25 + # via + # -c requirements/edx/../constraints.txt + # -r requirements/edx/base.txt +ora2==6.0.31 # via -r requirements/edx/base.txt packaging==23.2 # via @@ -969,7 +992,7 @@ prompt-toolkit==3.0.43 # via # -r requirements/edx/base.txt # click-repl -psutil==5.9.7 +psutil==5.9.8 # via # -r requirements/edx/base.txt # edx-django-utils @@ -1078,9 +1101,9 @@ python-ipware==2.0.1 # via # -r requirements/edx/base.txt # django-ipware -python-memcached==1.61 +python-memcached==1.62 # via -r requirements/edx/base.txt -python-slugify==8.0.1 +python-slugify==8.0.2 # via # -r requirements/edx/base.txt # code-annotations @@ -1094,10 +1117,10 @@ python3-openid==3.2.0 ; python_version >= "3" # social-auth-core python3-saml==1.16.0 # via -r requirements/edx/base.txt -pytz==2023.3.post1 +pytz==2023.4 # via # -r requirements/edx/base.txt - # django + # babel # django-ses # djangorestframework # drf-yasg @@ -1128,13 +1151,13 @@ pyyaml==6.0.1 # xblock random2==1.0.2 # via -r requirements/edx/base.txt -recommender-xblock==2.0.1 +recommender-xblock==2.1.1 # via -r requirements/edx/base.txt redis==5.0.1 # via # -r requirements/edx/base.txt # walrus -referencing==0.32.1 +referencing==0.33.0 # via # -r requirements/edx/base.txt # jsonschema @@ -1167,11 +1190,12 @@ requests==2.31.0 # snowflake-connector-python # social-auth-core # sphinx + # xblock-google-drive requests-oauthlib==1.3.1 # via # -r requirements/edx/base.txt # social-auth-core -rpds-py==0.16.2 +rpds-py==0.17.1 # via # -r requirements/edx/base.txt # jsonschema @@ -1256,7 +1280,7 @@ smmap==5.0.1 # via gitdb snowballstemmer==2.2.0 # via sphinx -snowflake-connector-python==3.6.0 +snowflake-connector-python==3.7.0 # via # -r requirements/edx/base.txt # edx-enterprise @@ -1443,7 +1467,7 @@ wrapt==1.16.0 # via # -r requirements/edx/base.txt # deprecated -xblock[django]==1.9.1 +xblock[django]==1.10.0 # via # -r requirements/edx/base.txt # acid-xblock @@ -1460,9 +1484,9 @@ xblock[django]==1.9.1 # xblock-google-drive # xblock-poll # xblock-utils -xblock-drag-and-drop-v2==3.3.0 +xblock-drag-and-drop-v2==3.4.0 # via -r requirements/edx/base.txt -xblock-google-drive==0.5.0 +xblock-google-drive==0.6.1 # via -r requirements/edx/base.txt xblock-poll==1.13.0 # via -r requirements/edx/base.txt @@ -1470,7 +1494,6 @@ xblock-utils==4.0.0 # via # -r requirements/edx/base.txt # edx-sga - # xblock-google-drive xmlsec==1.3.13 # via # -r requirements/edx/base.txt diff --git a/requirements/edx/kernel.in b/requirements/edx/kernel.in index 744f1bd632ce..3139561ff819 100644 --- a/requirements/edx/kernel.in +++ b/requirements/edx/kernel.in @@ -73,8 +73,8 @@ edx-codejail edx-django-utils>=5.4.0 # Utilities for cache, monitoring, and plugins edx-drf-extensions edx-enterprise -# edx-event-bus-kafka 4.0.0 adds support for configurable consumer API -edx-event-bus-kafka>=4.0.1 # Kafka implementation of event bus +# edx-event-bus-kafka 5.6.0 adds support for putting client ids on event producers/consumers +edx-event-bus-kafka>=5.6.0 # Kafka implementation of event bus edx-event-bus-redis edx-milestones edx-name-affirmation diff --git a/requirements/edx/paver.txt b/requirements/edx/paver.txt index 57bd85c813fa..ec8ee8b4c87c 100644 --- a/requirements/edx/paver.txt +++ b/requirements/edx/paver.txt @@ -20,7 +20,7 @@ libsass==0.10.0 # via # -c requirements/edx/../constraints.txt # -r requirements/edx/paver.in -markupsafe==2.1.3 +markupsafe==2.1.4 # via -r requirements/edx/paver.in mock==5.1.0 # via -r requirements/edx/paver.in @@ -30,7 +30,7 @@ paver==1.3.4 # via -r requirements/edx/paver.in pbr==6.0.0 # via stevedore -psutil==5.9.7 +psutil==5.9.8 # via -r requirements/edx/paver.in pymemcache==4.0.0 # via -r requirements/edx/paver.in @@ -39,7 +39,7 @@ pymongo==3.13.0 # -c requirements/edx/../constraints.txt # -r requirements/edx/paver.in # edx-opaque-keys -python-memcached==1.61 +python-memcached==1.62 # via -r requirements/edx/paver.in requests==2.31.0 # via -r requirements/edx/paver.in diff --git a/requirements/edx/semgrep.txt b/requirements/edx/semgrep.txt index 87782d647ed9..74a25beb4845 100644 --- a/requirements/edx/semgrep.txt +++ b/requirements/edx/semgrep.txt @@ -40,7 +40,11 @@ glom==22.1.0 # via semgrep idna==3.6 # via requests -jsonschema==4.20.0 +importlib-resources==6.1.1 + # via + # jsonschema + # jsonschema-specifications +jsonschema==4.21.1 # via semgrep jsonschema-specifications==2023.12.1 # via jsonschema @@ -54,7 +58,7 @@ peewee==3.17.0 # via semgrep pygments==2.17.2 # via rich -referencing==0.32.1 +referencing==0.33.0 # via # jsonschema # jsonschema-specifications @@ -62,7 +66,7 @@ requests==2.31.0 # via semgrep rich==13.7.0 # via semgrep -rpds-py==0.16.2 +rpds-py==0.17.1 # via # jsonschema # referencing diff --git a/requirements/edx/testing.txt b/requirements/edx/testing.txt index 663be648e31d..c032e127db99 100644 --- a/requirements/edx/testing.txt +++ b/requirements/edx/testing.txt @@ -8,7 +8,7 @@ # via -r requirements/edx/base.txt acid-xblock==0.2.1 # via -r requirements/edx/base.txt -aiohttp==3.9.1 +aiohttp==3.9.3 # via # -r requirements/edx/base.txt # geoip2 @@ -76,7 +76,15 @@ backoff==1.10.0 # via # -r requirements/edx/base.txt # analytics-python -beautifulsoup4==4.12.2 +backports-zoneinfo[tzdata]==0.2.1 + # via + # -r requirements/edx/base.txt + # backports-zoneinfo + # celery + # django + # icalendar + # kombu +beautifulsoup4==4.12.3 # via # -r requirements/edx/base.txt # -r requirements/edx/testing.in @@ -97,13 +105,13 @@ bleach[css]==6.1.0 # xblock-poll boto==2.49.0 # via -r requirements/edx/base.txt -boto3==1.34.17 +boto3==1.34.30 # via # -r requirements/edx/base.txt # django-ses # fs-s3fs # ora2 -botocore==1.34.17 +botocore==1.34.30 # via # -r requirements/edx/base.txt # boto3 @@ -201,7 +209,7 @@ coreschema==0.0.4 # -r requirements/edx/base.txt # coreapi # drf-yasg -coverage[toml]==7.4.0 +coverage[toml]==7.4.1 # via # -r requirements/edx/coverage.txt # pytest-cov @@ -241,15 +249,15 @@ deprecated==1.2.14 # via # -r requirements/edx/base.txt # jwcrypto -diff-cover==8.0.2 +diff-cover==8.0.3 # via -r requirements/edx/coverage.txt -dill==0.3.7 +dill==0.3.8 # via pylint distlib==0.3.8 # via virtualenv -django==3.2.23 +django==4.2.9 # via - # -c requirements/edx/../common_constraints.txt + # -c requirements/edx/../constraints.txt # -r requirements/edx/base.txt # django-appconf # django-celery-results @@ -317,6 +325,7 @@ django==3.2.23 # openedx-learning # ora2 # super-csv + # xblock-google-drive # xss-utils django-appconf==1.0.6 # via @@ -427,6 +436,7 @@ django-ses==3.5.2 # via -r requirements/edx/base.txt django-simple-history==3.4.0 # via + # -c requirements/edx/../constraints.txt # -r requirements/edx/base.txt # edx-enterprise # edx-name-affirmation @@ -496,7 +506,7 @@ drf-nested-routers==0.93.5 # via # -r requirements/edx/base.txt # openedx-blockstore -drf-spectacular==0.27.0 +drf-spectacular==0.27.1 # via -r requirements/edx/base.txt drf-yasg==1.21.5 # via @@ -515,7 +525,7 @@ edx-auth-backends==4.2.0 # via # -r requirements/edx/base.txt # openedx-blockstore -edx-braze-client==0.1.8 +edx-braze-client==0.2.2 # via # -r requirements/edx/base.txt # edx-enterprise @@ -527,7 +537,7 @@ edx-ccx-keys==1.2.1 # via # -r requirements/edx/base.txt # lti-consumer-xblock -edx-celeryutils==1.2.3 +edx-celeryutils==1.2.5 # via # -r requirements/edx/base.txt # edx-name-affirmation @@ -543,7 +553,7 @@ edx-django-release-util==1.3.0 # openedx-blockstore edx-django-sites-extensions==4.0.2 # via -r requirements/edx/base.txt -edx-django-utils==5.9.0 +edx-django-utils==5.10.1 # via # -r requirements/edx/base.txt # django-config-models @@ -559,7 +569,7 @@ edx-django-utils==5.9.0 # openedx-blockstore # ora2 # super-csv -edx-drf-extensions==9.1.2 +edx-drf-extensions==10.1.0 # via # -r requirements/edx/base.txt # edx-completion @@ -571,11 +581,11 @@ edx-drf-extensions==9.1.2 # edx-when # edxval # openedx-learning -edx-enterprise==4.9.2 +edx-enterprise==4.11.6 # via # -c requirements/edx/../constraints.txt # -r requirements/edx/base.txt -edx-event-bus-kafka==5.5.0 +edx-event-bus-kafka==5.6.0 # via -r requirements/edx/base.txt edx-event-bus-redis==0.3.2 # via -r requirements/edx/base.txt @@ -623,7 +633,7 @@ edx-rest-api-client==5.6.1 # edx-proctoring edx-search==3.8.2 # via -r requirements/edx/base.txt -edx-sga==0.23.0 +edx-sga==0.23.1 # via -r requirements/edx/base.txt edx-submissions==3.6.0 # via @@ -677,7 +687,7 @@ execnet==2.0.2 # via pytest-xdist factory-boy==3.3.0 # via -r requirements/edx/testing.in -faker==22.2.0 +faker==22.6.0 # via factory-boy fastapi==0.109.0 # via pact-python @@ -742,6 +752,17 @@ idna==3.6 # yarl import-linter==2.0 # via -r requirements/edx/testing.in +importlib-metadata==7.0.1 + # via + # -r requirements/edx/base.txt + # markdown + # pytest-randomly +importlib-resources==5.13.0 + # via + # -r requirements/edx/base.txt + # jsonschema + # jsonschema-specifications + # pycountry inflection==0.5.1 # via # -r requirements/edx/base.txt @@ -796,7 +817,7 @@ jsonfield==3.1.0 # edx-submissions # lti-consumer-xblock # ora2 -jsonschema==4.20.0 +jsonschema==4.21.1 # via # -r requirements/edx/base.txt # drf-spectacular @@ -810,7 +831,7 @@ jwcrypto==1.5.1 # -r requirements/edx/base.txt # django-oauth-toolkit # pylti1p3 -kombu==5.3.4 +kombu==5.3.5 # via # -r requirements/edx/base.txt # celery @@ -833,10 +854,11 @@ loremipsum==1.0.5 # via # -r requirements/edx/base.txt # ora2 -lti-consumer-xblock==9.8.1 +lti-consumer-xblock==9.8.3 # via -r requirements/edx/base.txt -lxml==5.1.0 +lxml==4.9.4 # via + # -c requirements/edx/../constraints.txt # -r requirements/edx/base.txt # edx-i18n-tools # edxval @@ -856,7 +878,6 @@ mako==1.3.0 # acid-xblock # lti-consumer-xblock # xblock - # xblock-google-drive # xblock-utils markdown==3.3.7 # via @@ -869,7 +890,7 @@ markey==0.8 # via # -r requirements/edx/base.txt # enmerkar-underscore -markupsafe==2.1.3 +markupsafe==2.1.4 # via # -r requirements/edx/base.txt # -r requirements/edx/coverage.txt @@ -906,7 +927,7 @@ mysqlclient==2.2.1 # via # -r requirements/edx/base.txt # openedx-blockstore -newrelic==9.5.0 +newrelic==9.6.0 # via # -r requirements/edx/base.txt # edx-django-utils @@ -937,7 +958,7 @@ openai==0.28.1 # -c requirements/edx/../constraints.txt # -r requirements/edx/base.txt # edx-enterprise -openedx-atlas==0.5.0 +openedx-atlas==0.6.0 # via -r requirements/edx/base.txt openedx-blockstore==1.4.0 # via -r requirements/edx/base.txt @@ -952,7 +973,7 @@ openedx-django-require==2.1.0 # via -r requirements/edx/base.txt openedx-django-wiki==2.0.3 # via -r requirements/edx/base.txt -openedx-events==9.2.0 +openedx-events==9.3.0 # via # -r requirements/edx/base.txt # edx-event-bus-kafka @@ -961,15 +982,17 @@ openedx-filters==1.6.0 # via # -r requirements/edx/base.txt # lti-consumer-xblock -openedx-learning==0.4.2 +openedx-learning==0.4.4 # via # -c requirements/edx/../constraints.txt # -r requirements/edx/base.txt openedx-mongodbproxy==0.2.0 # via -r requirements/edx/base.txt optimizely-sdk==4.1.1 - # via -r requirements/edx/base.txt -ora2==6.0.25 + # via + # -c requirements/edx/../constraints.txt + # -r requirements/edx/base.txt +ora2==6.0.31 # via -r requirements/edx/base.txt packaging==23.2 # via @@ -1023,7 +1046,7 @@ platformdirs==3.11.0 # snowflake-connector-python # tox # virtualenv -pluggy==1.3.0 +pluggy==1.4.0 # via # -r requirements/edx/coverage.txt # diff-cover @@ -1038,7 +1061,7 @@ prompt-toolkit==3.0.43 # via # -r requirements/edx/base.txt # click-repl -psutil==5.9.7 +psutil==5.9.8 # via # -r requirements/edx/base.txt # edx-django-utils @@ -1070,9 +1093,9 @@ pycryptodomex==3.20.0 # edx-proctoring # lti-consumer-xblock # pyjwkest -pydantic==2.5.3 +pydantic==2.6.0 # via fastapi -pydantic-core==2.14.6 +pydantic-core==2.16.1 # via pydantic pygments==2.17.2 # via @@ -1160,7 +1183,7 @@ pysrt==1.1.2 # via # -r requirements/edx/base.txt # edxval -pytest==7.4.4 +pytest==8.0.0 # via # -r requirements/edx/testing.in # pylint-pytest @@ -1206,9 +1229,9 @@ python-ipware==2.0.1 # via # -r requirements/edx/base.txt # django-ipware -python-memcached==1.61 +python-memcached==1.62 # via -r requirements/edx/base.txt -python-slugify==8.0.1 +python-slugify==8.0.2 # via # -r requirements/edx/base.txt # code-annotations @@ -1222,10 +1245,10 @@ python3-openid==3.2.0 ; python_version >= "3" # social-auth-core python3-saml==1.16.0 # via -r requirements/edx/base.txt -pytz==2023.3.post1 +pytz==2023.4 # via # -r requirements/edx/base.txt - # django + # babel # django-ses # djangorestframework # drf-yasg @@ -1255,13 +1278,13 @@ pyyaml==6.0.1 # xblock random2==1.0.2 # via -r requirements/edx/base.txt -recommender-xblock==2.0.1 +recommender-xblock==2.1.1 # via -r requirements/edx/base.txt redis==5.0.1 # via # -r requirements/edx/base.txt # walrus -referencing==0.32.1 +referencing==0.33.0 # via # -r requirements/edx/base.txt # jsonschema @@ -1294,11 +1317,12 @@ requests==2.31.0 # slumber # snowflake-connector-python # social-auth-core + # xblock-google-drive requests-oauthlib==1.3.1 # via # -r requirements/edx/base.txt # social-auth-core -rpds-py==0.16.2 +rpds-py==0.17.1 # via # -r requirements/edx/base.txt # jsonschema @@ -1384,7 +1408,7 @@ slumber==0.7.1 # edx-rest-api-client sniffio==1.3.0 # via anyio -snowflake-connector-python==3.6.0 +snowflake-connector-python==3.7.0 # via # -r requirements/edx/base.txt # edx-enterprise @@ -1472,6 +1496,7 @@ tqdm==4.66.1 typing-extensions==4.9.0 # via # -r requirements/edx/base.txt + # annotated-types # anyio # asgiref # astroid @@ -1511,7 +1536,7 @@ urllib3==1.26.18 # requests user-util==1.0.0 # via -r requirements/edx/base.txt -uvicorn==0.25.0 +uvicorn==0.27.0.post1 # via pact-python vine==5.1.0 # via @@ -1558,7 +1583,7 @@ wrapt==1.16.0 # -r requirements/edx/base.txt # astroid # deprecated -xblock[django]==1.9.1 +xblock[django]==1.10.0 # via # -r requirements/edx/base.txt # acid-xblock @@ -1575,9 +1600,9 @@ xblock[django]==1.9.1 # xblock-google-drive # xblock-poll # xblock-utils -xblock-drag-and-drop-v2==3.3.0 +xblock-drag-and-drop-v2==3.4.0 # via -r requirements/edx/base.txt -xblock-google-drive==0.5.0 +xblock-google-drive==0.6.1 # via -r requirements/edx/base.txt xblock-poll==1.13.0 # via -r requirements/edx/base.txt @@ -1585,7 +1610,6 @@ xblock-utils==4.0.0 # via # -r requirements/edx/base.txt # edx-sga - # xblock-google-drive xmlsec==1.3.13 # via # -r requirements/edx/base.txt diff --git a/requirements/pip-tools.txt b/requirements/pip-tools.txt index fc3544b2c87f..f2bf45571386 100644 --- a/requirements/pip-tools.txt +++ b/requirements/pip-tools.txt @@ -10,6 +10,8 @@ click==8.1.6 # via # -c requirements/constraints.txt # pip-tools +importlib-metadata==7.0.1 + # via build packaging==23.2 # via build pip-tools==7.3.0 diff --git a/webpack.common.config.js b/webpack.common.config.js index 239cfb4f2e39..075bfcf82783 100644 --- a/webpack.common.config.js +++ b/webpack.common.config.js @@ -84,6 +84,7 @@ module.exports = Merge.smart({ 'js/factories/xblock_validation': './cms/static/js/factories/xblock_validation.js', 'js/factories/edit_tabs': './cms/static/js/factories/edit_tabs.js', 'js/sock': './cms/static/js/sock.js', + 'js/factories/tag_count': './cms/static/js/factories/tag_count.js', // LMS SingleSupportForm: './lms/static/support/jsx/single_support_form.jsx', diff --git a/xmodule/modulestore/split_mongo/__init__.py b/xmodule/modulestore/split_mongo/__init__.py index d7664b6faac9..8ebc86efacc7 100644 --- a/xmodule/modulestore/split_mongo/__init__.py +++ b/xmodule/modulestore/split_mongo/__init__.py @@ -1,22 +1,10 @@ """ General utilities """ - - from collections import namedtuple -from opaque_keys.edx.locator import BlockUsageLocator - - -class BlockKey(namedtuple('BlockKey', 'type id')): # lint-amnesty, pylint: disable=missing-class-docstring - __slots__ = () - - def __new__(cls, type, id): # lint-amnesty, pylint: disable=redefined-builtin - return super().__new__(cls, type, id) - - @classmethod - def from_usage_key(cls, usage_key): - return cls(usage_key.block_type, usage_key.block_id) - +# We import BlockKey here for backwards compatibility with modulestore code. +# Feel free to remove this and fix the imports if you have time. +from xmodule.util.keys import BlockKey CourseEnvelope = namedtuple('CourseEnvelope', 'course_key structure') diff --git a/xmodule/modulestore/split_mongo/split.py b/xmodule/modulestore/split_mongo/split.py index c6e4c7889adf..64e19420a152 100644 --- a/xmodule/modulestore/split_mongo/split.py +++ b/xmodule/modulestore/split_mongo/split.py @@ -99,11 +99,12 @@ MultipleLibraryBlocksFound, VersionConflictError ) -from xmodule.modulestore.split_mongo import BlockKey, CourseEnvelope +from xmodule.modulestore.split_mongo import CourseEnvelope from xmodule.modulestore.split_mongo.mongo_connection import DuplicateKeyError, DjangoFlexPersistenceBackend -from xmodule.modulestore.store_utilities import DETACHED_XBLOCK_TYPES, derived_key +from xmodule.modulestore.store_utilities import DETACHED_XBLOCK_TYPES from xmodule.partitions.partitions_service import PartitionService from xmodule.util.misc import get_library_or_course_attribute +from xmodule.util.keys import BlockKey, derive_key from ..exceptions import ItemNotFoundError from .caching_descriptor_system import CachingDescriptorSystem @@ -2452,7 +2453,7 @@ def _copy_from_template( raise ItemNotFoundError(usage_key) source_block_info = source_structure['blocks'][block_key] - new_block_key = derived_key(src_course_key, block_key, new_parent_block_key) + new_block_key = derive_key(usage_key, new_parent_block_key) # Now clone block_key to new_block_key: new_block_info = copy.deepcopy(source_block_info) diff --git a/xmodule/modulestore/store_utilities.py b/xmodule/modulestore/store_utilities.py index e9710fbc92ce..c177104be5ad 100644 --- a/xmodule/modulestore/store_utilities.py +++ b/xmodule/modulestore/store_utilities.py @@ -1,5 +1,4 @@ # lint-amnesty, pylint: disable=missing-module-docstring -import hashlib import logging import re import uuid @@ -7,7 +6,6 @@ from xblock.core import XBlock -from xmodule.modulestore.split_mongo import BlockKey DETACHED_XBLOCK_TYPES = {name for name, __ in XBlock.load_tagged_classes("detached")} @@ -106,31 +104,3 @@ def get_draft_subtree_roots(draft_nodes): for draft_node in draft_nodes: if draft_node.parent_url not in urls: yield draft_node - - -def derived_key(courselike_source_key, block_key, dest_parent: BlockKey): - """ - Return a new reproducible block ID for a given root, source block, and destination parent. - - When recursively copying a block structure, we need to generate new block IDs for the - blocks. We don't want to use the exact same IDs as we might copy blocks multiple times. - However, we do want to be able to reproduce the same IDs when copying the same block - so that if we ever need to re-copy the block from its source (that is, to update it with - upstream changes) we don't affect any data tied to the ID, such as grades. - - This is used by the copy_from_template function of the modulestore, and can be used by - pluggable django apps that need to copy blocks from one course to another in an - idempotent way. In the future, this should be created into a proper API function - in the spirit of OEP-49. - """ - hashable_source_id = courselike_source_key.for_version(None) - - # Compute a new block ID. This new block ID must be consistent when this - # method is called with the same (source_key, dest_structure) pair - unique_data = "{}:{}:{}".format( - str(hashable_source_id).encode("utf-8"), - block_key.id, - dest_parent.id, - ) - new_block_id = hashlib.sha1(unique_data.encode('utf-8')).hexdigest()[:20] - return BlockKey(block_key.type, new_block_id) diff --git a/xmodule/modulestore/tests/test_store_utilities.py b/xmodule/modulestore/tests/test_store_utilities.py index a0b5cfcbdbb6..57908abc7f59 100644 --- a/xmodule/modulestore/tests/test_store_utilities.py +++ b/xmodule/modulestore/tests/test_store_utilities.py @@ -2,17 +2,12 @@ Tests for store_utilities.py """ - import unittest -from unittest import TestCase from unittest.mock import Mock import ddt -from opaque_keys.edx.keys import CourseKey - -from xmodule.modulestore.split_mongo import BlockKey -from xmodule.modulestore.store_utilities import draft_node_constructor, get_draft_subtree_roots, derived_key +from xmodule.modulestore.store_utilities import draft_node_constructor, get_draft_subtree_roots @ddt.ddt @@ -86,43 +81,3 @@ def test_get_draft_subtree_roots(self, node_arguments_list, expected_roots_urls) subtree_roots_urls = [root.url for root in get_draft_subtree_roots(block_nodes)] # check that we return the expected urls assert set(subtree_roots_urls) == set(expected_roots_urls) - - -mock_block = Mock() -mock_block.id = CourseKey.from_string('course-v1:Beeper+B33P+BOOP') - - -derived_key_scenarios = [ - { - 'courselike_source_key': CourseKey.from_string('course-v1:edX+DemoX+Demo_Course'), - 'block_key': BlockKey('chapter', 'interactive_demonstrations'), - 'parent': mock_block, - 'expected': BlockKey( - 'chapter', '5793ec64e25ed870a7dd', - ), - }, - { - 'courselike_source_key': CourseKey.from_string('course-v1:edX+DemoX+Demo_Course'), - 'block_key': BlockKey('chapter', 'interactive_demonstrations'), - 'parent': BlockKey( - 'chapter', 'thingy', - ), - 'expected': BlockKey( - 'chapter', '599792a5622d85aa41e6', - ), - } -] - - -@ddt.ddt -class TestDerivedKey(TestCase): - """ - Test reproducible block ID generation. - """ - @ddt.data(*derived_key_scenarios) - @ddt.unpack - def test_derived_key(self, courselike_source_key, block_key, parent, expected): - """ - Test that derived_key returns the expected value. - """ - self.assertEqual(derived_key(courselike_source_key, block_key, parent), expected) diff --git a/xmodule/tests/test_util_keys.py b/xmodule/tests/test_util_keys.py new file mode 100644 index 000000000000..dcd16d6b7873 --- /dev/null +++ b/xmodule/tests/test_util_keys.py @@ -0,0 +1,45 @@ +""" +Tests for xmodule/util/keys.py +""" +import ddt +from unittest import TestCase +from unittest.mock import Mock + +from opaque_keys.edx.locator import BlockUsageLocator +from opaque_keys.edx.keys import CourseKey +from xmodule.util.keys import BlockKey, derive_key + + +mock_block = Mock() +mock_block.id = CourseKey.from_string('course-v1:Beeper+B33P+BOOP') + +derived_key_scenarios = [ + { + 'source': BlockUsageLocator.from_string( + 'block-v1:edX+DemoX+Demo_Course+type@chapter+block@interactive_demonstrations' + ), + 'parent': mock_block, + 'expected': BlockKey('chapter', '5793ec64e25ed870a7dd'), + }, + { + 'source': BlockUsageLocator.from_string( + 'block-v1:edX+DemoX+Demo_Course+type@chapter+block@interactive_demonstrations' + ), + 'parent': BlockKey('chapter', 'thingy'), + 'expected': BlockKey('chapter', '599792a5622d85aa41e6'), + } +] + + +@ddt.ddt +class TestDeriveKey(TestCase): + """ + Test reproducible block ID generation. + """ + @ddt.data(*derived_key_scenarios) + @ddt.unpack + def test_derive_key(self, source, parent, expected): + """ + Test that derive_key returns the expected value. + """ + assert derive_key(source, parent) == expected diff --git a/xmodule/util/keys.py b/xmodule/util/keys.py new file mode 100644 index 000000000000..ceb35b269eb5 --- /dev/null +++ b/xmodule/util/keys.py @@ -0,0 +1,62 @@ +""" +Utilities for working with opaque-keys. + +Consider moving these into opaque-keys if they generalize well. +""" +import hashlib +from typing import NamedTuple + + +from opaque_keys.edx.keys import UsageKey + + +class BlockKey(NamedTuple): + """ + A pair of strings (type, id) that uniquely identify an XBlock Usage within a LearningContext. + + Put another way: LearningContextKey * BlockKey = UsageKey. + + Example: + "course-v1:myOrg+myCourse+myRun" <- LearningContextKey string + ("html", "myBlock") <- BlockKey + "course-v1:myOrg+myCourse+myRun+type@html+block@myBlock" <- UsageKey string + """ + type: str + id: str + + @classmethod + def from_usage_key(cls, usage_key): + return cls(usage_key.block_type, usage_key.block_id) + + +def derive_key(source: UsageKey, dest_parent: BlockKey) -> BlockKey: + """ + Return a new reproducible BlockKey for a given source usage and destination parent block. + + When recursively copying a block structure, we need to generate new block IDs for the + blocks. We don't want to use the exact same IDs as we might copy blocks multiple times. + However, we do want to be able to reproduce the same IDs when copying the same block + so that if we ever need to re-copy the block from its source (that is, to update it with + upstream changes) we don't affect any data tied to the ID, such as grades. + + This is used by the copy_from_template function of the modulestore, and can be used by + pluggable django apps that need to copy blocks from one course to another in an + idempotent way. In the future, this should be created into a proper API function + in the spirit of OEP-49. + """ + source_context = source.context_key + if hasattr(source_context, 'for_version'): + source_context = source_context.for_version(None) + # Compute a new block ID. This new block ID must be consistent when this + # method is called with the same (source, dest_parent) pair. + # Note: years after this was written, mypy pointed out that the way we are + # encoding & formatting the source context means it looks like b'....', ie + # it literally contains the character 'b' and single quotes within the unique_data + # string. So that's a little silly, but it's fine, and we can't change it now. + unique_data = "{}:{}:{}".format( + str(source_context).encode("utf-8"), # type: ignore[str-bytes-safe] + source.block_id, + dest_parent.id, + ) + new_block_id = hashlib.sha1(unique_data.encode('utf-8')).hexdigest()[:20] + return BlockKey(source.block_type, new_block_id)