|
| 1 | +""" |
| 2 | +@@TODO |
| 3 | +""" |
| 4 | +from __future__ import annotations |
| 5 | + |
| 6 | +import logging |
| 7 | +from django.contrib.auth.models import User # pylint: disable=imported-auth-user |
| 8 | +from django.db import transaction |
| 9 | +from opaque_keys.edx.locator import LibraryLocator, LibraryLocatorV2 |
| 10 | +from openedx_learning.api.authoring import add_to_collection, get_collection |
| 11 | +from openedx_learning.api.authoring_models import PublishableEntity, Component |
| 12 | +from openedx_tagging.core.tagging.api import tag_object |
| 13 | +from openedx_tagging.core.tagging.models import Taxonomy |
| 14 | +from organizations.models import Organization |
| 15 | +from xblock.fields import Scope |
| 16 | + |
| 17 | +from openedx.core.djangoapps.xblock.api import load_block |
| 18 | +from openedx.core.djangoapps.content_libraries.api import create_library_block |
| 19 | +from xmodule.util.keys import BlockKey |
| 20 | +from xmodule.modulestore.django import modulestore |
| 21 | + |
| 22 | +from .models import ContentLibrary, ContentLibraryMigration, ContentLibraryBlockMigration |
| 23 | + |
| 24 | + |
| 25 | +log = logging.getLogger(__name__) |
| 26 | + |
| 27 | + |
| 28 | +def migrate_legacy_library( |
| 29 | + source_key: LibraryLocator, |
| 30 | + target_key: LibraryLocatorV2, |
| 31 | + *, |
| 32 | + collection_slug: str | None, |
| 33 | + user: User, |
| 34 | + tags_to_add: dict[Taxonomy, list[str]] | None = None, |
| 35 | +) -> None: |
| 36 | + """ |
| 37 | + Migrate a v1 (legacy) library into a v2 (learning core) library, optionally within a collection. |
| 38 | +
|
| 39 | + Use a single transaction so that if any step fails, nothing happens. |
| 40 | +
|
| 41 | + @@TODO handle or document various exceptions |
| 42 | + @@TODO tags |
| 43 | + """ |
| 44 | + source = modulestore().get_library(source_key) |
| 45 | + target = ContentLibrary.objects.get(org=Organization.objects.get(short_name=target_key.org), slug=target_key.slug) |
| 46 | + assert target.learning_package_id |
| 47 | + collection = get_collection(target.learning_package_id, collection_slug) if collection_slug else None |
| 48 | + |
| 49 | + # We need to be careful not to conflict with any existing block keys in the target library. |
| 50 | + # This is unlikely to happen, since legacy library block ids are genreally randomly-generated GUIDs. |
| 51 | + # Howevever, there are a couple scenarios where it could arise: |
| 52 | + # * An instance has two legacy libraries which were imported from the same source legacy library (and thus share |
| 53 | + # block GUIDs) which the author now wants to merge together into one big new library. |
| 54 | + # * A library was imported from handcrafted OLX, and thus has human-readable block IDs which are liable to overlap. |
| 55 | + # When there is conflict, we'll append "-1" to the end of the id (or "-2", "-3", etc., until we find a free ID). |
| 56 | + all_target_block_keys: set[BlockKey] = { |
| 57 | + BlockKey(*block_type_and_id) |
| 58 | + for block_type_and_id |
| 59 | + in Component.objects.filter( |
| 60 | + learning_package=target.learning_package, |
| 61 | + component_type__namespace="xblock.v1", |
| 62 | + ).values_list("component_type__name", "local_key") |
| 63 | + } |
| 64 | + |
| 65 | + # We also need to be careful not to conflict with other block IDs which we are moving in from the *source* library |
| 66 | + # This is very unlikely, but it could happen if, for example: |
| 67 | + # * the source library has a problem "foo", and |
| 68 | + # * the target library also has a problem "foo", and |
| 69 | + # * the source library ALSO has a problem "foo-1", thus |
| 70 | + # * the source library's "foo" must be moved to the target as "foo-2". |
| 71 | + all_source_block_keys: set[BlockKey] = { |
| 72 | + BlockKey.from_usage_key(child_key) |
| 73 | + for child_key in source.children |
| 74 | + } |
| 75 | + |
| 76 | + target_block_entity_keys: set[str] = set() |
| 77 | + |
| 78 | + with transaction.atomic(): |
| 79 | + migration = ContentLibraryMigration.objects.create( |
| 80 | + source_key=source_key, |
| 81 | + target=target, |
| 82 | + target_collection=collection, |
| 83 | + ) |
| 84 | + |
| 85 | + for source_block in source.get_children(): |
| 86 | + block_type: str = source_block.usage_key.block_type |
| 87 | + |
| 88 | + # Determine an available block_id... |
| 89 | + target_block_key = BlockKey(block_type, source_block.usage_key.block_id) |
| 90 | + if target_block_key in all_target_block_keys: |
| 91 | + suffix = 0 |
| 92 | + while target_block_key in all_target_block_keys | all_source_block_keys: |
| 93 | + suffix += 1 |
| 94 | + target_block_key = BlockKey(block_type, f"{source_block.usage_key.block_id}-{suffix}") |
| 95 | + |
| 96 | + # Create the block in the v2 library |
| 97 | + target_block_meta = create_library_block( |
| 98 | + library_key=target_key, |
| 99 | + block_type=block_type, |
| 100 | + definition_id=target_block_key.id, |
| 101 | + user_id=user.id, |
| 102 | + ) |
| 103 | + target_block_entity_keys.add(f"xblock.v1:{block_type}:{target_block_key.id}") |
| 104 | + |
| 105 | + # Copy its content over from the v1 library |
| 106 | + target_block = load_block(target_block_meta.usage_key, user) |
| 107 | + for field_name, field in source_block.__class__.fields.items(): |
| 108 | + if field.scope not in [Scope.settings, Scope.content]: |
| 109 | + continue |
| 110 | + if not hasattr(target_block, field_name): |
| 111 | + continue |
| 112 | + source_value = getattr(source_block, field_name) |
| 113 | + if getattr(target_block, field_name) != source_value: |
| 114 | + setattr(target_block, field_name, source_value) |
| 115 | + target_block.save() |
| 116 | + |
| 117 | + # If requested, add tags |
| 118 | + for taxonomy, taxonomy_tags in (tags_to_add or {}).items(): |
| 119 | + tag_object(str(target_block_meta.usage_key), taxonomy, taxonomy_tags) |
| 120 | + |
| 121 | + # Make a record of the migration |
| 122 | + ContentLibraryBlockMigration.objects.create( |
| 123 | + library_migration=migration, |
| 124 | + block_type=block_type, |
| 125 | + source_block_id=source_block.usage_key.block_id, |
| 126 | + target_block_id=target_block_key.id, |
| 127 | + ) |
| 128 | + |
| 129 | + # If requested, add to a collection, and add tags |
| 130 | + if collection_slug: |
| 131 | + add_to_collection( |
| 132 | + target.learning_package_id, |
| 133 | + collection_slug, |
| 134 | + PublishableEntity.objects.filter( |
| 135 | + key__in=target_block_entity_keys, |
| 136 | + ), |
| 137 | + ) |
0 commit comments