diff --git a/cms/envs/common.py b/cms/envs/common.py index 591247388a9d..e1536141109b 100644 --- a/cms/envs/common.py +++ b/cms/envs/common.py @@ -1887,6 +1887,7 @@ "openedx_learning.apps.authoring.components", "openedx_learning.apps.authoring.contents", "openedx_learning.apps.authoring.publishing", + "openedx_learning.apps.authoring.linking", ] diff --git a/lms/envs/common.py b/lms/envs/common.py index 23e0d12b3dde..f35376e7ece4 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -3380,6 +3380,7 @@ def _make_locale_paths(settings): # pylint: disable=missing-function-docstring "openedx_learning.apps.authoring.components", "openedx_learning.apps.authoring.contents", "openedx_learning.apps.authoring.publishing", + "openedx_learning.apps.authoring.linking", ] diff --git a/openedx/core/djangoapps/content/course_overviews/signals.py b/openedx/core/djangoapps/content/course_overviews/signals.py index 4ae2f4bf0f8a..5412e3ebc013 100644 --- a/openedx/core/djangoapps/content/course_overviews/signals.py +++ b/openedx/core/djangoapps/content/course_overviews/signals.py @@ -26,6 +26,8 @@ IMPORT_COURSE_DETAILS = Signal() # providing_args=["courserun_key"] DELETE_COURSE_DETAILS = Signal() +# providing_args=["courserun_key", "old_name", "new_name"] +COURSE_NAME_CHANGED = Signal() @receiver(SignalHandler.course_published) @@ -89,6 +91,7 @@ def _check_for_course_changes(previous_course_overview, updated_course_overview) _check_for_course_start_date_changes(previous_course_overview, updated_course_overview) _check_for_pacing_changes(previous_course_overview, updated_course_overview) _check_for_cert_date_changes(previous_course_overview, updated_course_overview) + _check_for_display_name_change(previous_course_overview, updated_course_overview) def _check_for_course_start_date_changes(previous_course_overview, updated_course_overview): @@ -216,3 +219,16 @@ def _send_course_cert_date_change_signal(): if send_signal: transaction.on_commit(_send_course_cert_date_change_signal) + + +def _check_for_display_name_change(previous_course_overview, updated_course_overview): + """ + Checks for change in display name of course and sends COURSE_NAME_CHANGED signal. + """ + if previous_course_overview.display_name_with_default != updated_course_overview.display_name_with_default: + COURSE_NAME_CHANGED.send( + sender=None, + courserun_key=str(previous_course_overview.id), + old_name=previous_course_overview.display_name_with_default, + new_name=updated_course_overview.display_name_with_default, + ) diff --git a/openedx/core/djangoapps/content_libraries/api.py b/openedx/core/djangoapps/content_libraries/api.py index c51c707fc470..23299a22d53c 100644 --- a/openedx/core/djangoapps/content_libraries/api.py +++ b/openedx/core/djangoapps/content_libraries/api.py @@ -1952,3 +1952,28 @@ def import_blocks_create_task(library_key, course_key, use_course_key_as_block_i log.info(f"Import block task created: import_task={import_task} " f"celery_task={result.id}") return import_task + + +def create_or_update_xblock_upstream_link(xblock, course_key: str, course_name: str, created: datetime | None = None): + """ + Create or update upstream->downstream link in database for given xblock. + """ + if not xblock.upstream: + return None + upstream_usage_key = UsageKeyV2.from_string(xblock.upstream) + try: + lib_component = get_component_from_usage_key(upstream_usage_key) + except ObjectDoesNotExist: + log.error(f"Library component not found for {upstream_usage_key}") + lib_component = None + authoring_api.update_or_create_entity_link( + lib_component, + upstream_usage_key=xblock.upstream, + upstream_context_key=str(upstream_usage_key.context_key), + downstream_context_key=course_key, + downstream_context_title=course_name, + downstream_usage_key=str(xblock.usage_key), + version_synced=xblock.upstream_version, + version_declined=xblock.upstream_version_declined, + created=created, + ) diff --git a/openedx/core/djangoapps/content_libraries/management/commands/recreate_upstream_links.py b/openedx/core/djangoapps/content_libraries/management/commands/recreate_upstream_links.py new file mode 100644 index 000000000000..2d88c26c3017 --- /dev/null +++ b/openedx/core/djangoapps/content_libraries/management/commands/recreate_upstream_links.py @@ -0,0 +1,76 @@ +""" +Management command to recreate upstream-dowstream links in PublishableEntityLink for course(s). + +This command can be run for all the courses or for given list of courses. +""" + +from __future__ import annotations + +import logging +from datetime import datetime, timezone + +from django.core.management.base import BaseCommand, CommandError +from django.utils.translation import gettext as _ + +from openedx.core.djangoapps.content.course_overviews.models import CourseOverview + +from ...tasks import create_or_update_upstream_links + +log = logging.getLogger(__name__) + + +class Command(BaseCommand): + """ + Recreate links for course(s) in PublishableEntityLink table. + + Examples: + # Recreate upstream links for two courses. + $ ./manage.py cms recreate_upstream_links --course course-v1:edX+DemoX.1+2014 \ + --course course-v1:edX+DemoX.2+2015 + # Recreate upstream links for all courses. + $ ./manage.py cms recreate_upstream_links --all + # Force recreate links for all courses including completely processed ones. + $ ./manage.py cms recreate_upstream_links --all + """ + + def add_arguments(self, parser): + parser.add_argument( + '--course', + metavar=_('COURSE_KEY'), + action='append', + help=_('Recreate links for xblocks under given course keys. For eg. course-v1:edX+DemoX.1+2014'), + default=[], + ) + parser.add_argument( + '--all', + action='store_true', + help=_( + 'Recreate links for xblocks under all courses. NOTE: this can take long time depending' + ' on number of course and xblocks' + ), + ) + parser.add_argument( + '--force', + action='store_true', + help=_('Recreate links even for completely processed courses.'), + ) + + def handle(self, *args, **options): + """ + Handle command + """ + courses = options['course'] + should_process_all = options['all'] + force = options['force'] + time_now = datetime.now(tz=timezone.utc) + if not courses and not should_process_all: + raise CommandError('Either --course or --all argument should be provided.') + + if should_process_all and courses: + raise CommandError('Only one of --course or --all argument should be provided.') + + if should_process_all: + courses = CourseOverview.get_all_course_keys() + for course in courses: + log.info(f"Start processing upstream->dowstream links in course: {course}") + create_or_update_upstream_links.delay(str(course), force, created=time_now) diff --git a/openedx/core/djangoapps/content_libraries/signal_handlers.py b/openedx/core/djangoapps/content_libraries/signal_handlers.py index 58f45d218e95..a16ca60d42a1 100644 --- a/openedx/core/djangoapps/content_libraries/signal_handlers.py +++ b/openedx/core/djangoapps/content_libraries/signal_handlers.py @@ -5,7 +5,7 @@ import logging from django.conf import settings -from django.db.models.signals import post_save, post_delete, m2m_changed +from django.db.models.signals import m2m_changed, post_delete, post_save from django.dispatch import receiver from opaque_keys import InvalidKeyError @@ -13,21 +13,26 @@ from openedx_events.content_authoring.data import ( ContentObjectChangedData, LibraryCollectionData, + XBlockData, ) from openedx_events.content_authoring.signals import ( CONTENT_OBJECT_ASSOCIATIONS_CHANGED, LIBRARY_COLLECTION_CREATED, LIBRARY_COLLECTION_DELETED, LIBRARY_COLLECTION_UPDATED, + XBLOCK_CREATED, + XBLOCK_DELETED, + XBLOCK_UPDATED, ) -from openedx_learning.api.authoring import get_component, get_components +from openedx_learning.api.authoring import delete_entity_link, get_component, get_components from openedx_learning.api.authoring_models import Collection, CollectionPublishableEntity, Component, PublishableEntity from lms.djangoapps.grades.api import signals as grades_signals +from openedx.core.djangoapps.content.course_overviews.signals import COURSE_NAME_CHANGED from .api import library_component_usage_key from .models import ContentLibrary, LtiGradedResource - +from .tasks import create_or_update_xblock_upstream_link, update_course_name_in_upstream_links log = logging.getLogger(__name__) @@ -203,3 +208,42 @@ def library_collection_entities_changed(sender, instance, action, pk_set, **kwar for component in components.all(): _library_collection_component_changed(component, library.library_key) + + +@receiver(XBLOCK_CREATED) +@receiver(XBLOCK_UPDATED) +def create_or_update_upstream_downstream_link_handler(**kwargs): + """ + Automatically create or update upstream->downstream link in database. + """ + xblock_info = kwargs.get("xblock_info", None) + if not xblock_info or not isinstance(xblock_info, XBlockData): + log.error("Received null or incorrect data for event") + return + + create_or_update_xblock_upstream_link.delay(str(xblock_info.usage_key)) + + +@receiver(XBLOCK_DELETED) +def delete_upstream_downstream_link_handler(**kwargs): + """ + Delete upstream->downstream link from database on xblock delete. + """ + xblock_info = kwargs.get("xblock_info", None) + if not xblock_info or not isinstance(xblock_info, XBlockData): + log.error("Received null or incorrect data for event") + return + + delete_entity_link(str(xblock_info.usage_key)) + + +@receiver(COURSE_NAME_CHANGED, dispatch_uid="update_course_name_in_upstream_links_handler") +def update_course_name_in_upstream_links_handler(courserun_key, old_name, new_name, **kwargs): + """ + Handler to update course names in upstream->downstream links on change. + """ + log.info(f"Updating course name in upstream->downstream links from '{old_name}' to '{new_name}'") + update_course_name_in_upstream_links.delay( + courserun_key, + new_name, + ) diff --git a/openedx/core/djangoapps/content_libraries/tasks.py b/openedx/core/djangoapps/content_libraries/tasks.py index f56b4adfe313..55a2329681e0 100644 --- a/openedx/core/djangoapps/content_libraries/tasks.py +++ b/openedx/core/djangoapps/content_libraries/tasks.py @@ -17,18 +17,23 @@ from __future__ import annotations import logging +from datetime import datetime, timezone from celery import shared_task from celery_utils.logged_task import LoggedTask from celery.utils.log import get_task_logger from edx_django_utils.monitoring import set_code_owner_attribute, set_code_owner_attribute_from_module +from opaque_keys.edx.keys import UsageKey from user_tasks.tasks import UserTask, UserTaskStatus from xblock.fields import Scope from opaque_keys.edx.keys import CourseKey from opaque_keys.edx.locator import BlockUsageLocator +from openedx.core.djangoapps.content.course_overviews.models import CourseOverview from openedx.core.lib import ensure_cms +from openedx_learning.api.authoring import get_entity_links, get_or_create_learning_context_link_status +from openedx_learning.api.authoring_models import LearningContextLinksStatusChoices from xmodule.capa_block import ProblemBlock from xmodule.library_content_block import ANY_CAPA_TYPE_VALUE, LegacyLibraryContentBlock from xmodule.modulestore import ModuleStoreEnum @@ -169,6 +174,69 @@ def duplicate_children( self.status.fail({'raw_error_msg': str(exception)}) +@shared_task(base=LoggedTask) +@set_code_owner_attribute +def create_or_update_xblock_upstream_link(usage_key): + """ + Create or update upstream link for a single xblock. + """ + ensure_cms("create_or_update_xblock_upstream_link may only be executed in a CMS context") + xblock = modulestore().get_item(UsageKey.from_string(usage_key)) + if not xblock.upstream or not xblock.upstream_version: + return + try: + course_name = CourseOverview.get_from_id(xblock.course_id).display_name_with_default + except CourseOverview.DoesNotExist: + TASK_LOGGER.exception(f'Could not find course: {xblock.course_id}') + return + api.create_or_update_xblock_upstream_link(xblock, str(xblock.course_id), course_name) + + +@shared_task(base=LoggedTask) +@set_code_owner_attribute +def create_or_update_upstream_links(course_key_str: str, force: bool = False, created: datetime | None = None): + """ + A Celery task to create or update upstream downstream links in database from course xblock content. + """ + ensure_cms("create_or_update_upstream_links may only be executed in a CMS context") + + if not created: + created = datetime.now(timezone.utc) + course_status = get_or_create_learning_context_link_status(course_key_str, created) + if course_status.status in [ + LearningContextLinksStatusChoices.COMPLETED, + LearningContextLinksStatusChoices.PROCESSING + ] and not force: + return + store = modulestore() + course_key = CourseKey.from_string(course_key_str) + course_status.status = LearningContextLinksStatusChoices.PROCESSING + course_status.save() + try: + course_name = CourseOverview.get_from_id(course_key).display_name_with_default + except CourseOverview.DoesNotExist: + TASK_LOGGER.exception(f'Could not find course: {course_key_str}') + return + xblocks = store.get_items(course_key, settings={"upstream": lambda x: x is not None}) + for xblock in xblocks: + api.create_or_update_xblock_upstream_link(xblock, course_key_str, course_name, created) + course_status.status = LearningContextLinksStatusChoices.COMPLETED + course_status.save() + + +@shared_task(base=LoggedTask) +@set_code_owner_attribute +def update_course_name_in_upstream_links(course_key_str: str, new_course_name: str): + """ + Celery task to update course name in upstream->downstream entity links. + """ + updated_time = datetime.now(timezone.utc) + get_entity_links({"downstream_context_key": course_key_str}).update( + downstream_context_title=new_course_name, + updated=updated_time + ) + + def _sync_children( task: LibrarySyncChildrenTask, store: MixedModuleStore, diff --git a/openedx/core/djangoapps/content_libraries/tests/test_upstream_downstream_links.py b/openedx/core/djangoapps/content_libraries/tests/test_upstream_downstream_links.py new file mode 100644 index 000000000000..7215c8576c8d --- /dev/null +++ b/openedx/core/djangoapps/content_libraries/tests/test_upstream_downstream_links.py @@ -0,0 +1,170 @@ +""" +Tests for upstream downstream tracking links. +""" + +from io import StringIO +from unittest.mock import patch + +from django.core.management import call_command +from django.core.management.base import CommandError +from django.utils import timezone +from freezegun import freeze_time + +from openedx.core.djangolib.testing.utils import skip_unless_cms +from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase +from xmodule.modulestore.tests.factories import BlockFactory, CourseFactory + +from ..tasks import create_or_update_upstream_links, create_or_update_xblock_upstream_link + + +@skip_unless_cms +class TestRecreateUpstreamLinks(ModuleStoreTestCase): + """ + Test recreate_upstream_links management command. + """ + + ENABLED_SIGNALS = ['course_deleted', 'course_published'] + + def setUp(self): + super().setUp() + self.now = timezone.now() + freezer = freeze_time(self.now) + freezer.start() + self.addCleanup(freezer.stop) + + def call_command(self, *args, **kwargs): + """ + call command with pass args. + """ + out = StringIO() + kwargs['stdout'] = out + call_command('recreate_upstream_links', *args, **kwargs) + return out + + def test_call_with_invalid_args(self): + """ + Test command with invalid args. + """ + with self.assertRaisesRegex(CommandError, 'Either --course or --all argument'): + self.call_command() + with self.assertRaisesRegex(CommandError, 'Only one of --course or --all argument'): + self.call_command('--all', '--course', 'some-course') + + @patch( + 'openedx.core.djangoapps.content_libraries.management.commands.recreate_upstream_links.create_or_update_upstream_links' # pylint: disable=line-too-long + ) + def test_call_for_single_course(self, mock_task): + """ + Test command with single course argument + """ + self.call_command('--course', 'some-course') + mock_task.delay.assert_called_with('some-course', False, created=self.now) + # call with --force + self.call_command('--course', 'some-course', '--force') + mock_task.delay.assert_called_with('some-course', True, created=self.now) + + @patch( + 'openedx.core.djangoapps.content_libraries.management.commands.recreate_upstream_links.create_or_update_upstream_links' # pylint: disable=line-too-long + ) + def test_call_for_multiple_course(self, mock_task): + """ + Test command with multiple course arguments + """ + self.call_command('--course', 'some-course', '--course', 'one-more-course') + mock_task.delay.assert_any_call('some-course', False, created=self.now) + mock_task.delay.assert_any_call('one-more-course', False, created=self.now) + + @patch( + 'openedx.core.djangoapps.content_libraries.management.commands.recreate_upstream_links.create_or_update_upstream_links' # pylint: disable=line-too-long + ) + def test_call_for_all_courses(self, mock_task): + """ + Test command with multiple course arguments + """ + course_key_1 = CourseFactory.create(emit_signals=True).id + course_key_2 = CourseFactory.create(emit_signals=True).id + self.call_command('--all') + mock_task.delay.assert_any_call(str(course_key_1), False, created=self.now) + mock_task.delay.assert_any_call(str(course_key_2), False, created=self.now) + + +@skip_unless_cms +class TestUpstreamLinksTasks(ModuleStoreTestCase): + """ + Test tasks related to managing upstream->downstream links. + """ + + ENABLED_SIGNALS = ['course_deleted', 'course_published'] + + def setUp(self): + super().setUp() + self.now = timezone.now() + freezer = freeze_time(self.now) + freezer.start() + self.addCleanup(freezer.stop) + self.course = course = CourseFactory.create(emit_signals=True) + self.course_key = course_key = self.course.id + self.upstream_1 = "upstream-block-id-1" + self.upstream_2 = "upstream-block-id-2" + with self.store.bulk_operations(course_key): + self.section = BlockFactory.create(parent=course, category="chapter", display_name="Section") + self.sequence = BlockFactory.create(parent=self.section, category="sequential", display_name="Sequence") + self.unit = BlockFactory.create(parent=self.sequence, category="vertical", display_name="Unit") + self.component_1 = BlockFactory.create( + parent=self.unit, + category="html", + display_name="An HTML Block", + upstream=self.upstream_1, + upstream_version=1, + ) + self.component_2 = BlockFactory.create( + parent=self.unit, + category="html", + display_name="Another HTML Block", + upstream=self.upstream_2, + upstream_version=1, + ) + self.component_3 = BlockFactory.create( + parent=self.unit, + category="html", + display_name="Another another HTML Block", + ) + + @patch( + 'openedx.core.djangoapps.content_libraries.api.create_or_update_xblock_upstream_link' + ) + def test_create_or_update_upstream_links_task(self, mock_api): + """ + Test task create_or_update_upstream_links for a course + """ + create_or_update_upstream_links(str(self.course_key), force=False) + expected_calls = [ + (self.component_1.usage_key, str(self.course_key), self.course.display_name_with_default, self.now), + (self.component_2.usage_key, str(self.course_key), self.course.display_name_with_default, self.now), + ] + assert [(x[0][0].usage_key, x[0][1], x[0][2], x[0][3]) for x in mock_api.call_args_list] == expected_calls + mock_api.reset_mock() + # call again with same course, it should not be processed again + # as its LearningContextLinksStatusChoices = COMPLETED + create_or_update_upstream_links(str(self.course_key), force=False) + mock_api.assert_not_called() + # again with same course but with force=True, it should be processed now + create_or_update_upstream_links(str(self.course_key), force=True) + assert [(x[0][0].usage_key, x[0][1], x[0][2], x[0][3]) for x in mock_api.call_args_list] == expected_calls + + @patch( + 'openedx.core.djangoapps.content_libraries.api.create_or_update_xblock_upstream_link' + ) + def test_create_or_update_xblock_upstream_link(self, mock_api): + """ + Test task create_or_update_xblock_upstream_link for a course + """ + create_or_update_xblock_upstream_link(str(self.component_1.usage_key)) + expected_calls = [ + (self.component_1.usage_key, str(self.course_key), self.course.display_name_with_default), + ] + assert [(x[0][0].usage_key, x[0][1], x[0][2]) for x in mock_api.call_args_list] == expected_calls + mock_api.reset_mock() + # call for xblock with no upstream + create_or_update_xblock_upstream_link(str(self.component_3.usage_key)) + mock_api.assert_not_called() diff --git a/requirements/constraints.txt b/requirements/constraints.txt index f99c41c6947b..f6c004476373 100644 --- a/requirements/constraints.txt +++ b/requirements/constraints.txt @@ -131,7 +131,7 @@ optimizely-sdk<5.0 # Date: 2023-09-18 # pinning this version to avoid updates while the library is being developed # Issue for unpinning: https://github.com/openedx/edx-platform/issues/35269 -openedx-learning==0.18.1 +openedx-learning @ git+https://github.com/open-craft/openedx-learning@navin/link-table # Date: 2023-11-29 # Open AI version 1.0.0 dropped support for openai.ChatCompletion which is currently in use in enterprise. diff --git a/requirements/edx/base.txt b/requirements/edx/base.txt index d202a8be1f60..a8a5ceb695a2 100644 --- a/requirements/edx/base.txt +++ b/requirements/edx/base.txt @@ -830,7 +830,7 @@ openedx-filters==1.12.0 # ora2 openedx-forum==0.1.5 # via -r requirements/edx/kernel.in -openedx-learning==0.18.1 +openedx-learning @ git+https://github.com/open-craft/openedx-learning@navin/link-table # via # -c requirements/edx/../constraints.txt # -r requirements/edx/kernel.in diff --git a/requirements/edx/development.txt b/requirements/edx/development.txt index bd1d6b02bd8d..b7df8fe0f87e 100644 --- a/requirements/edx/development.txt +++ b/requirements/edx/development.txt @@ -1392,7 +1392,7 @@ openedx-forum==0.1.5 # via # -r requirements/edx/doc.txt # -r requirements/edx/testing.txt -openedx-learning==0.18.1 +openedx-learning @ git+https://github.com/open-craft/openedx-learning@navin/link-table # via # -c requirements/edx/../constraints.txt # -r requirements/edx/doc.txt diff --git a/requirements/edx/doc.txt b/requirements/edx/doc.txt index 52a4c067c0de..ce3402909684 100644 --- a/requirements/edx/doc.txt +++ b/requirements/edx/doc.txt @@ -1007,7 +1007,7 @@ openedx-filters==1.12.0 # ora2 openedx-forum==0.1.5 # via -r requirements/edx/base.txt -openedx-learning==0.18.1 +openedx-learning @ git+https://github.com/open-craft/openedx-learning@navin/link-table # via # -c requirements/edx/../constraints.txt # -r requirements/edx/base.txt diff --git a/requirements/edx/testing.txt b/requirements/edx/testing.txt index 672c51c48ae7..be73cff37519 100644 --- a/requirements/edx/testing.txt +++ b/requirements/edx/testing.txt @@ -1054,7 +1054,7 @@ openedx-filters==1.12.0 # ora2 openedx-forum==0.1.5 # via -r requirements/edx/base.txt -openedx-learning==0.18.1 +openedx-learning @ git+https://github.com/open-craft/openedx-learning@navin/link-table # via # -c requirements/edx/../constraints.txt # -r requirements/edx/base.txt diff --git a/xmodule/modulestore/tests/test_mixed_modulestore.py b/xmodule/modulestore/tests/test_mixed_modulestore.py index 0928ab253b9c..4d335ceaa554 100644 --- a/xmodule/modulestore/tests/test_mixed_modulestore.py +++ b/xmodule/modulestore/tests/test_mixed_modulestore.py @@ -164,6 +164,13 @@ def setUp(self): self.course_locations = {} self.user_id = ModuleStoreEnum.UserID.test + # mock and ignore create_or_update_xblock_upstream_link task to avoid unnecessary + # errors as it is tested separately + create_or_update_xblock_upstream_link_patch = patch( + 'openedx.core.djangoapps.content_libraries.signal_handlers.create_or_update_xblock_upstream_link' + ) + create_or_update_xblock_upstream_link_patch.start() + self.addCleanup(create_or_update_xblock_upstream_link_patch.stop) def _check_connection(self): """ @@ -1099,7 +1106,7 @@ def test_has_changes_missing_child(self, default_ms, default_branch): # check CONTENT_TAGGING_AUTO CourseWaffleFlag # Find: active_versions, 2 structures (published & draft), definition (unnecessary) # Sends: updated draft and published structures and active_versions - @ddt.data((ModuleStoreEnum.Type.split, 5, 2, 3)) + @ddt.data((ModuleStoreEnum.Type.split, 6, 2, 3)) @ddt.unpack def test_delete_item(self, default_ms, num_mysql, max_find, max_send): """ @@ -1122,7 +1129,7 @@ def test_delete_item(self, default_ms, num_mysql, max_find, max_send): # check CONTENT_TAGGING_AUTO CourseWaffleFlag # find: draft and published structures, definition (unnecessary) # sends: update published (why?), draft, and active_versions - @ddt.data((ModuleStoreEnum.Type.split, 5, 3, 3)) + @ddt.data((ModuleStoreEnum.Type.split, 6, 3, 3)) @ddt.unpack def test_delete_private_vertical(self, default_ms, num_mysql, max_find, max_send): """ @@ -1172,7 +1179,7 @@ def test_delete_private_vertical(self, default_ms, num_mysql, max_find, max_send # check CONTENT_TAGGING_AUTO CourseWaffleFlag # find: structure (cached) # send: update structure and active_versions - @ddt.data((ModuleStoreEnum.Type.split, 5, 1, 2)) + @ddt.data((ModuleStoreEnum.Type.split, 6, 1, 2)) @ddt.unpack def test_delete_draft_vertical(self, default_ms, num_mysql, max_find, max_send): """ diff --git a/xmodule/modulestore/xml_importer.py b/xmodule/modulestore/xml_importer.py index 5b880b4ade2f..a1546f54a71a 100644 --- a/xmodule/modulestore/xml_importer.py +++ b/xmodule/modulestore/xml_importer.py @@ -40,6 +40,8 @@ from xblock.runtime import DictKeyValueStore, KvsFieldData from common.djangoapps.util.monitoring import monitor_import_failure +from openedx.core.djangoapps.content_libraries.tasks import create_or_update_upstream_links +from openedx.core.djangoapps.content_tagging.api import import_course_tags_from_csv from xmodule.assetstore import AssetMetadata from xmodule.contentstore.content import StaticContent from xmodule.errortracker import make_error_tracker @@ -52,7 +54,6 @@ from xmodule.tabs import CourseTabList from xmodule.util.misc import escape_invalid_characters from xmodule.x_module import XModuleMixin -from openedx.core.djangoapps.content_tagging.api import import_course_tags_from_csv from .inheritance import own_metadata from .store_utilities import rewrite_nonportable_content_links @@ -548,6 +549,11 @@ def depth_first(subtree): # pylint: disable=raise-missing-from raise BlockFailedToImport(leftover.display_name, leftover.location) + def post_course_import(self, dest_id): + """ + Tasks that need to triggered after a course is imported. + """ + def run_imports(self): """ Iterate over the given directories and yield courses. @@ -589,6 +595,7 @@ def run_imports(self): logging.info(f'Course import {dest_id}: No tags.csv file present.') except ValueError as e: logging.info(f'Course import {dest_id}: {str(e)}') + self.post_course_import(dest_id) yield courselike @@ -717,6 +724,12 @@ def import_tags(self, data_path, dest_id): csv_path = path(data_path) / 'tags.csv' import_course_tags_from_csv(csv_path, dest_id) + def post_course_import(self, dest_id): + """ + Trigger celery task to create upstream links for newly imported blocks. + """ + create_or_update_upstream_links.delay(str(dest_id)) + class LibraryImportManager(ImportManager): """