|
5 | 5 |
|
6 | 6 | import typing as t |
7 | 7 | from uuid import UUID |
| 8 | +from django.conf import settings |
8 | 9 |
|
9 | 10 | from opaque_keys.edx.keys import UsageKey |
10 | 11 | from opaque_keys.edx.locator import ( |
11 | 12 | LibraryLocatorV2, LibraryUsageLocatorV2, LibraryContainerLocator |
12 | 13 | ) |
13 | | -from openedx_learning.api.authoring import get_draft_version |
| 14 | +from openedx_learning.api.authoring import get_draft_version, get_all_drafts |
14 | 15 | from openedx_learning.api.authoring_models import ( |
15 | 16 | PublishableEntityVersion, PublishableEntity, DraftChangeLogRecord |
16 | 17 | ) |
| 18 | +from xblock.plugin import PluginMissingError |
17 | 19 |
|
18 | 20 | from openedx.core.djangoapps.content_libraries.api import ( |
19 | | - library_component_usage_key, library_container_locator |
| 21 | + library_component_usage_key, library_container_locator, |
| 22 | + validate_can_add_block_to_library, BlockLimitReachedError, |
| 23 | + IncompatibleTypesError, LibraryBlockAlreadyExists, |
| 24 | + ContentLibrary |
| 25 | +) |
| 26 | +from openedx.core.djangoapps.content.search.api import ( |
| 27 | + fetch_block_types, |
| 28 | + get_all_blocks_from_context, |
20 | 29 | ) |
21 | 30 |
|
22 | 31 | from ..data import ( |
|
32 | 41 | 'get_forwarding_for_blocks', |
33 | 42 | 'get_migrations', |
34 | 43 | 'get_migration_blocks', |
| 44 | + 'preview_migration', |
35 | 45 | ) |
36 | 46 |
|
37 | 47 |
|
@@ -242,3 +252,120 @@ def _block_migration_success( |
242 | 252 | target_title=target_title, |
243 | 253 | target_version_num=target_version_num, |
244 | 254 | ) |
| 255 | + |
| 256 | + |
| 257 | +def preview_migration(source_key: SourceContextKey, target_key: LibraryLocatorV2): |
| 258 | + """ |
| 259 | + Returns a summary preview of the migration given a source key and a target key |
| 260 | + on this form: |
| 261 | +
|
| 262 | + ``` |
| 263 | + { |
| 264 | + "state": "partial", |
| 265 | + "unsupported_blocks": 4, |
| 266 | + "unsupported_percentage": 25, |
| 267 | + "blocks_limit": 1000, |
| 268 | + "total_blocks": 20, |
| 269 | + "total_components": 10, |
| 270 | + "sections": 2, |
| 271 | + "subsections": 3, |
| 272 | + "units": 5, |
| 273 | + } |
| 274 | + ``` |
| 275 | +
|
| 276 | + List of states: |
| 277 | + - 'success': The migration can be carried out in its entirety |
| 278 | + - 'partial': The migration will be partial, because there are unsupported blocks. |
| 279 | + - 'block_limit_reached': The migration cannot be performed because the block limit per library has been reached. |
| 280 | +
|
| 281 | + This runs Meilisiearch queries to speed up the response, as it's a summary/analysis. |
| 282 | + The decision has been made not to run a "migration" for each analysis to obtain this summary. |
| 283 | +
|
| 284 | + TODO: For now, the repeat_handling_strategy is not taken into account. This can be taken into |
| 285 | + account for a more advanced summary. |
| 286 | + """ |
| 287 | + # Get all containers and components from the source key |
| 288 | + blocks = get_all_blocks_from_context(str(source_key), ["block_type", "block_id"]) |
| 289 | + |
| 290 | + unsupported_blocks = [] |
| 291 | + total_blocks = 0 |
| 292 | + total_components = 0 |
| 293 | + sections = 0 |
| 294 | + subsections = 0 |
| 295 | + units = 0 |
| 296 | + blocks_limit = settings.MAX_BLOCKS_PER_CONTENT_LIBRARY |
| 297 | + |
| 298 | + # Builds the summary: counts every container and verify if each component can be added to the library |
| 299 | + for block in blocks: |
| 300 | + block_type = block["block_type"] |
| 301 | + block_id = block["block_id"] |
| 302 | + total_blocks += 1 |
| 303 | + if block_type not in ['chapter', 'sequential', 'vertical']: |
| 304 | + total_components += 1 |
| 305 | + try: |
| 306 | + validate_can_add_block_to_library( |
| 307 | + target_key, |
| 308 | + block_type, |
| 309 | + block_id, |
| 310 | + ) |
| 311 | + except BlockLimitReachedError: |
| 312 | + return { |
| 313 | + "state": "block_limit_reached", |
| 314 | + "unsupported_blocks": 0, |
| 315 | + "unsupported_percentage": 0, |
| 316 | + "blocks_limit": blocks_limit, |
| 317 | + "total_blocks": 0, |
| 318 | + "total_components": 0, |
| 319 | + "sections": 0, |
| 320 | + "subsections": 0, |
| 321 | + "units": 0, |
| 322 | + } |
| 323 | + except (IncompatibleTypesError, PluginMissingError): |
| 324 | + unsupported_blocks.append(block["usage_key"]) |
| 325 | + except LibraryBlockAlreadyExists: |
| 326 | + # Skip this validation, The block may be repeated in the library, but that's not a bad thing. |
| 327 | + pass |
| 328 | + elif block_type == "chapter": |
| 329 | + sections += 1 |
| 330 | + elif block_type == "sequential": |
| 331 | + subsections += 1 |
| 332 | + elif block_type == "vertical": |
| 333 | + units += 1 |
| 334 | + |
| 335 | + # Gets the count of children of unsupported blocks |
| 336 | + quoted_keys = ','.join(f'"{key}"' for key in unsupported_blocks) |
| 337 | + unsupportedBlocksChildren = fetch_block_types( |
| 338 | + [ |
| 339 | + f'context_key = "{source_key}"', |
| 340 | + f'breadcrumbs.usage_key IN [{quoted_keys}]' |
| 341 | + ], |
| 342 | + ) |
| 343 | + # Final unsupported blocks count |
| 344 | + # The unsupported children are subtracted from the totals since they have already been counted in the first query. |
| 345 | + unsupported_blocks_count = len(unsupported_blocks) |
| 346 | + total_blocks -= unsupportedBlocksChildren["estimatedTotalHits"] |
| 347 | + total_components -= unsupportedBlocksChildren["estimatedTotalHits"] |
| 348 | + unsupported_percentage = (unsupported_blocks_count / total_blocks) * 100 |
| 349 | + |
| 350 | + state = "success" |
| 351 | + if unsupported_blocks_count: |
| 352 | + state = "partial" |
| 353 | + |
| 354 | + # Checks if this migration reaches the block limit |
| 355 | + content_library = ContentLibrary.objects.get_by_key(target_key) |
| 356 | + assert content_library.learning_package_id is not None |
| 357 | + target_item_counts = get_all_drafts(content_library.learning_package_id).count() |
| 358 | + if (target_item_counts + total_blocks - unsupported_blocks_count) > blocks_limit: |
| 359 | + state = "block_limit_reached" |
| 360 | + |
| 361 | + return { |
| 362 | + "state": state, |
| 363 | + "unsupported_blocks": unsupported_blocks_count, |
| 364 | + "unsupported_percentage": unsupported_percentage, |
| 365 | + "blocks_limit": blocks_limit, |
| 366 | + "total_blocks": total_blocks, |
| 367 | + "total_components": total_components, |
| 368 | + "sections": sections, |
| 369 | + "subsections": subsections, |
| 370 | + "units": units, |
| 371 | + } |
0 commit comments