Files
edx-platform/lms/djangoapps/course_blocks/transformers/library_content.py
Kyle McCormick 2bbd8ecd18 feat!: Remove outdated Libraries Relaunch cruft (#35644)
The V2 libraries project had a few past iterations which were never
launched. This commit cleans up pieces from those which we don't need
for the real Libraries Relaunch MVP in Sumac:

* Remove ENABLE_LIBRARY_AUTHORING_MICROFRONTEND,
  LIBRARY_AUTHORING_FRONTEND_URL, and
  REDIRECT_TO_LIBRARY_AUTHORING_MICROFRONTEND, all of which are obsolete
  now that library authoring has been merged into
  https://github.com/openedx/frontend-app-authoring.
  More details on the new Content Libraries configuration settings are
  here: https://github.com/openedx/frontend-app-authoring/issues/1334

* Remove dangling support for syncing V2 (learning core-backed) library
  content using the LibraryContentBlock. This code was all based on an
  older understanding of V2 Content Libraries, where the libraries were
  smaller and versioned as a whole rather then versioned by-item.
  Reference to V2 libraries will be done on a per-block basis using
  the upstream/downstream system, described here:
  https://github.com/openedx/edx-platform/blob/master/docs/decisions/0020-upstream-downstream.rst
  It's important that we remove this support now so that OLX course
  authors don't stuble upon it and use it, which would be buggy and
  complicate future migrations.

* Remove the "mode" parameter from LibraryContentBlock. The only
  supported mode was and is "random". We will not be adding any further
  modes. Going forward for V2, we will have an ItemBank block for
  randomizing items (regardless of source), which can be synthesized
  with upstream referenced as described above. Existing
  LibraryContentBlocks will be migrated.

* Finally, some renamings:

  * LibraryContentBlock -> LegacyLibraryContentBlock
  * LibraryToolsService -> LegacyLibraryToolsService
  * LibrarySummary -> LegacyLibrarySummary

  Module names and the old OLX tag (library_content) are unchanged.

Closes: https://github.com/openedx/frontend-app-authoring/issues/1115
2024-10-15 11:32:01 -04:00

245 lines
9.9 KiB
Python

"""
Content Library Transformer.
"""
import json
import logging
from eventtracking import tracker
from common.djangoapps.track import contexts
from lms.djangoapps.courseware.models import StudentModule
from openedx.core.djangoapps.content.block_structure.transformer import (
BlockStructureTransformer,
FilteringTransformerMixin
)
from xmodule.library_content_block import LegacyLibraryContentBlock # lint-amnesty, pylint: disable=wrong-import-order
from xmodule.modulestore.django import modulestore # lint-amnesty, pylint: disable=wrong-import-order
from ..utils import get_student_module_as_dict
logger = logging.getLogger(__name__)
class ContentLibraryTransformer(FilteringTransformerMixin, BlockStructureTransformer):
"""
A transformer that manipulates the block structure by removing all
blocks within a library_content block to which a user should not
have access.
Staff users are not to be exempted from library content pathways.
"""
WRITE_VERSION = 1
READ_VERSION = 1
@classmethod
def name(cls):
"""
Unique identifier for the transformer's class;
same identifier used in setup.py.
"""
return "library_content"
@classmethod
def collect(cls, block_structure):
"""
Collects any information that's necessary to execute this
transformer's transform method.
"""
block_structure.request_xblock_fields('max_count')
block_structure.request_xblock_fields('category')
store = modulestore()
# needed for analytics purposes
def summarize_block(usage_key):
""" Basic information about the given block """
orig_key, orig_version = store.get_block_original_usage(usage_key)
return {
"usage_key": str(usage_key),
"original_usage_key": str(orig_key) if orig_key else None,
"original_usage_version": str(orig_version) if orig_version else None,
}
# For each block check if block is library_content.
# If library_content add children array to content_library_children field
for block_key in block_structure.topological_traversal(
filter_func=lambda block_key: block_key.block_type == 'library_content',
yield_descendants_of_unyielded=True,
):
xblock = block_structure.get_xblock(block_key)
for child_key in xblock.children:
summary = summarize_block(child_key)
block_structure.set_transformer_block_field(child_key, cls, 'block_analytics_summary', summary)
def transform_block_filters(self, usage_info, block_structure):
all_library_children = set()
all_selected_children = set()
for block_key in block_structure:
if block_key.block_type != 'library_content':
continue
library_children = block_structure.get_children(block_key)
if library_children:
all_library_children.update(library_children)
selected = []
max_count = block_structure.get_xblock_field(block_key, 'max_count')
if max_count < 0:
max_count = len(library_children)
# Retrieve "selected" json from LMS MySQL database.
state_dict = get_student_module_as_dict(usage_info.user, usage_info.course_key, block_key)
for selected_block in state_dict.get('selected', []):
# Add all selected entries for this user for this
# library block to the selected list.
block_type, block_id = selected_block
usage_key = usage_info.course_key.make_usage_key(block_type, block_id)
if usage_key in library_children:
selected.append(selected_block)
# Update selected
previous_count = len(selected)
block_keys = LegacyLibraryContentBlock.make_selection(selected, library_children, max_count)
selected = block_keys['selected']
# Save back any changes
if any(block_keys[changed] for changed in ('invalid', 'overlimit', 'added')):
state_dict['selected'] = selected
StudentModule.save_state(
student=usage_info.user,
course_id=usage_info.course_key,
module_state_key=block_key,
defaults={
'state': json.dumps(state_dict),
},
)
# publish events for analytics
self._publish_events(
block_structure,
block_key,
previous_count,
max_count,
block_keys,
usage_info.user.id,
)
all_selected_children.update(usage_info.course_key.make_usage_key(s[0], s[1]) for s in selected)
def check_child_removal(block_key):
"""
Return True if selected block should be removed.
Block is removed if it is part of library_content, but has
not been selected for current user.
"""
if block_key not in all_library_children:
return False
if block_key in all_selected_children:
return False
return True
return [block_structure.create_removal_filter(check_child_removal)]
def _publish_events(self, block_structure, location, previous_count, max_count, block_keys, user_id):
"""
Helper method to publish events for analytics purposes
"""
def format_block_keys(keys):
"""
Helper function to format block keys
"""
json_result = []
for key in keys:
info = block_structure.get_transformer_block_field(
key, ContentLibraryTransformer, 'block_analytics_summary'
)
json_result.append(info)
return json_result
def publish_event(event_name, result, **kwargs):
"""
Helper function to publish an event for analytics purposes
"""
event_data = {
"location": str(location),
"previous_count": previous_count,
"result": result,
"max_count": max_count,
}
event_data.update(kwargs)
context = contexts.course_context_from_course_id(location.course_key)
if user_id:
context['user_id'] = user_id
full_event_name = f"edx.librarycontentblock.content.{event_name}"
with tracker.get_tracker().context(full_event_name, context):
tracker.emit(full_event_name, event_data)
LegacyLibraryContentBlock.publish_selected_children_events(
block_keys,
format_block_keys,
publish_event,
)
class ContentLibraryOrderTransformer(BlockStructureTransformer):
"""
A transformer that manipulates the block structure by modifying the order of the
selected blocks within a library_content block to match the order of the selections
made by the ContentLibraryTransformer or the corresponding XBlock. So this transformer
requires the selections for the randomized content block to be already
made either by the ContentLibraryTransformer or the XBlock.
Staff users are *not* exempted from library content pathways.
"""
WRITE_VERSION = 1
READ_VERSION = 1
@classmethod
def name(cls):
"""
Unique identifier for the transformer's class;
same identifier used in setup.py
"""
return "library_content_randomize"
@classmethod
def collect(cls, block_structure):
"""
Collects any information that's necessary to execute this
transformer's transform method.
"""
# There is nothing to collect
pass # pylint:disable=unnecessary-pass
def transform(self, usage_info, block_structure):
"""
Transforms the order of the children of the randomized content block
to match the order of the selections made and stored in the XBlock 'selected' field.
"""
for block_key in block_structure:
if block_key.block_type != 'library_content':
continue
library_children = block_structure.get_children(block_key)
if library_children:
state_dict = get_student_module_as_dict(usage_info.user, usage_info.course_key, block_key)
current_children_blocks = {block.block_id for block in library_children}
current_selected_blocks = {item[1] for item in state_dict.get('selected', [])}
# As the selections should have already been made by the ContentLibraryTransformer,
# the current children of the library_content block should be the same as the stored
# selections. If they aren't, some other transformer that ran before this transformer
# has modified those blocks (for example, content gating may have affected this). So do not
# transform the order in that case.
if current_children_blocks != current_selected_blocks:
logger.debug(
'Mismatch between the children of %s in the stored state and the actual children for user %s. '
'Continuing without order transformation.',
str(block_key),
usage_info.user.username
)
else:
ordering_data = {block[1]: position for position, block in enumerate(state_dict['selected'])}
library_children.sort(key=lambda block, data=ordering_data: data[block.block_id])