Skip to content

Commit 42e4e0d

Browse files
authored
fix: apply library_content transformer to all ItemBankMixin xblocks (#37830)
Likely fixes: #38027
1 parent 870ac3f commit 42e4e0d

2 files changed

Lines changed: 83 additions & 35 deletions

File tree

lms/djangoapps/course_blocks/transformers/library_content.py

Lines changed: 44 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,24 @@
11
"""
2-
Content Library Transformer.
2+
Item Bank Transformer.
3+
4+
Transformers for handling item bank blocks (library_content, itembank, etc.)
5+
that use ItemBankMixin for randomized content selection.
36
"""
47

58

69
import json
710
import logging
811

912
from eventtracking import tracker
13+
from xblock.core import XBlock
1014

1115
from common.djangoapps.track import contexts
1216
from lms.djangoapps.courseware.models import StudentModule
1317
from openedx.core.djangoapps.content.block_structure.transformer import (
1418
BlockStructureTransformer,
1519
FilteringTransformerMixin,
1620
)
17-
from xmodule.library_content_block import LegacyLibraryContentBlock # lint-amnesty, pylint: disable=wrong-import-order
21+
from xmodule.item_bank_block import ItemBankMixin # lint-amnesty, pylint: disable=wrong-import-order
1822
from xmodule.modulestore.django import modulestore # lint-amnesty, pylint: disable=wrong-import-order
1923

2024
from ..utils import get_student_module_as_dict
@@ -25,10 +29,13 @@
2529
class ContentLibraryTransformer(FilteringTransformerMixin, BlockStructureTransformer):
2630
"""
2731
A transformer that manipulates the block structure by removing all
28-
blocks within a library_content block to which a user should not
29-
have access.
32+
blocks within item bank blocks (library_content, itembank, etc.)
33+
to which a user should not have access.
34+
35+
This transformer works with any XBlock that inherits from ItemBankMixin,
36+
filtering children based on the selection logic defined by each block type.
3037
31-
Staff users are not to be exempted from library content pathways.
38+
Staff users are not to be exempted from item bank pathways.
3239
"""
3340
WRITE_VERSION = 1
3441
READ_VERSION = 1
@@ -61,10 +68,10 @@ def summarize_block(usage_key):
6168
"original_usage_version": str(orig_version) if orig_version else None,
6269
}
6370

64-
# For each block check if block is library_content.
65-
# If library_content add children array to content_library_children field
71+
# For each block check if block uses ItemBankMixin (e.g., library_content, itembank).
72+
# If so add block analytics summary for each of its children.
6673
for block_key in block_structure.topological_traversal(
67-
filter_func=lambda block_key: block_key.block_type == 'library_content',
74+
filter_func=lambda block_key: issubclass(XBlock.load_class(block_key.block_type), ItemBankMixin),
6875
yield_descendants_of_unyielded=True,
6976
):
7077
xblock = block_structure.get_xblock(block_key)
@@ -76,7 +83,9 @@ def transform_block_filters(self, usage_info, block_structure):
7683
all_library_children = set()
7784
all_selected_children = set()
7885
for block_key in block_structure:
79-
if block_key.block_type != 'library_content':
86+
block_class = XBlock.load_class(block_key.block_type)
87+
88+
if block_class is None or not issubclass(block_class, ItemBankMixin):
8089
continue
8190
library_children = block_structure.get_children(block_key)
8291
if library_children:
@@ -98,7 +107,12 @@ def transform_block_filters(self, usage_info, block_structure):
98107

99108
# Update selected
100109
previous_count = len(selected)
101-
block_keys = LegacyLibraryContentBlock.make_selection(selected, library_children, max_count)
110+
# Get the cached block class to call make_selection
111+
block_class = XBlock.load_class(block_key.block_type)
112+
if block_class is None:
113+
logger.error('Failed to load block class for %s', block_key)
114+
continue
115+
block_keys = block_class.make_selection(selected, library_children, max_count)
102116
selected = block_keys['selected']
103117

104118
# Save back any changes
@@ -128,7 +142,7 @@ def check_child_removal(block_key):
128142
"""
129143
Return True if selected block should be removed.
130144
131-
Block is removed if it is part of library_content, but has
145+
Block is removed if it is a child of an item bank block, but has
132146
not been selected for current user.
133147
"""
134148
if block_key not in all_library_children:
@@ -156,6 +170,12 @@ def format_block_keys(keys):
156170
json_result.append(info)
157171
return json_result
158172

173+
# Get the cached block class to call publish_selected_children_events
174+
block_class = XBlock.load_class(location.block_type)
175+
if block_class is None:
176+
logger.error('Failed to load block class for publishing events: %s', location)
177+
return
178+
159179
def publish_event(event_name, result, **kwargs):
160180
"""
161181
Helper function to publish an event for analytics purposes
@@ -170,11 +190,12 @@ def publish_event(event_name, result, **kwargs):
170190
context = contexts.course_context_from_course_id(location.course_key)
171191
if user_id:
172192
context['user_id'] = user_id
173-
full_event_name = f"edx.librarycontentblock.content.{event_name}"
193+
event_prefix = block_class.get_selected_event_prefix()
194+
full_event_name = f"{event_prefix}.{event_name}"
174195
with tracker.get_tracker().context(full_event_name, context):
175196
tracker.emit(full_event_name, event_data)
176197

177-
LegacyLibraryContentBlock.publish_selected_children_events(
198+
block_class.publish_selected_children_events(
178199
block_keys,
179200
format_block_keys,
180201
publish_event,
@@ -184,12 +205,14 @@ def publish_event(event_name, result, **kwargs):
184205
class ContentLibraryOrderTransformer(BlockStructureTransformer):
185206
"""
186207
A transformer that manipulates the block structure by modifying the order of the
187-
selected blocks within a library_content block to match the order of the selections
188-
made by the ContentLibraryTransformer or the corresponding XBlock. So this transformer
189-
requires the selections for the randomized content block to be already
190-
made either by the ContentLibraryTransformer or the XBlock.
208+
selected blocks within item bank blocks (library_content, itembank, etc.)
209+
to match the order of the selections made by the ContentLibraryTransformer or the
210+
corresponding XBlock. This transformer requires the selections for the item bank block
211+
to be already made either by the ContentLibraryTransformer or the XBlock.
212+
213+
This transformer works with any XBlock that inherits from ItemBankMixin.
191214
192-
Staff users are *not* exempted from library content pathways.
215+
Staff users are *not* exempted from item bank pathways.
193216
"""
194217
WRITE_VERSION = 1
195218
READ_VERSION = 1
@@ -217,7 +240,8 @@ def transform(self, usage_info, block_structure):
217240
to match the order of the selections made and stored in the XBlock 'selected' field.
218241
"""
219242
for block_key in block_structure:
220-
if block_key.block_type != 'library_content':
243+
block_class = XBlock.load_class(block_key.block_type)
244+
if block_class is None or not issubclass(block_class, ItemBankMixin):
221245
continue
222246

223247
library_children = block_structure.get_children(block_key)
@@ -228,7 +252,7 @@ def transform(self, usage_info, block_structure):
228252
current_selected_blocks = {item[1] for item in state_dict.get('selected', [])}
229253

230254
# As the selections should have already been made by the ContentLibraryTransformer,
231-
# the current children of the library_content block should be the same as the stored
255+
# the current children of the item bank block should be the same as the stored
232256
# selections. If they aren't, some other transformer that ran before this transformer
233257
# has modified those blocks (for example, content gating may have affected this). So do not
234258
# transform the order in that case.

lms/djangoapps/course_blocks/transformers/tests/test_library_content.py

Lines changed: 39 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44

55
from unittest import mock
66

7+
from ddt import data, ddt
8+
79
import openedx.core.djangoapps.content.block_structure.api as bs_api
810
from common.djangoapps.student.tests.factories import CourseEnrollmentFactory
911
from openedx.core.djangoapps.content.block_structure.api import clear_course_from_cache
@@ -26,6 +28,7 @@ def __init__(self, state):
2628
self.state = state
2729

2830

31+
@ddt
2932
class ContentLibraryTransformerTestCase(CourseStructureTestCase):
3033
"""
3134
ContentLibraryTransformer Test
@@ -37,9 +40,14 @@ def setUp(self):
3740
Setup course structure and create user for content library transformer test.
3841
"""
3942
super().setUp()
43+
self._initialize_course_hierarchy()
4044

45+
def _initialize_course_hierarchy(self, block_type='library_content'):
46+
"""
47+
Initialize course hierarchy with the given block type.
48+
"""
4149
# Build course.
42-
self.course_hierarchy = self.get_course_hierarchy()
50+
self.course_hierarchy = self.get_course_hierarchy(block_type)
4351
self.blocks = self.build_course(self.course_hierarchy)
4452
self.course = self.blocks['course']
4553
# Do this manually because publish signals are not fired by default in tests.
@@ -49,14 +57,14 @@ def setUp(self):
4957
# Enroll user in course.
5058
CourseEnrollmentFactory.create(user=self.user, course_id=self.course.id, is_active=True)
5159

52-
def get_course_hierarchy(self):
60+
def get_course_hierarchy(self, block_type='library_content'):
5361
"""
5462
Get a course hierarchy to test with.
5563
"""
5664
return [{
5765
'org': 'ContentLibraryTransformer',
5866
'course': 'CL101F',
59-
'run': 'test_run',
67+
'run': f'test_run_{block_type}',
6068
'#type': 'course',
6169
'#ref': 'course',
6270
'#children': [
@@ -73,8 +81,8 @@ def get_course_hierarchy(self):
7381
'#ref': 'vertical1',
7482
'#children': [
7583
{
76-
'#type': 'library_content',
77-
'#ref': 'library_content1',
84+
'#type': block_type,
85+
'#ref': f'{block_type}1',
7886
'#children': [
7987
{
8088
'metadata': {'display_name': "CL Vertical 2"},
@@ -111,13 +119,18 @@ def get_course_hierarchy(self):
111119
]
112120
}]
113121

114-
def test_content_library(self):
122+
@data('library_content', 'itembank')
123+
def test_content_library(self, block_type):
115124
"""
116125
Test when course has content library section.
117126
First test user can't see any content library section,
118127
and after that mock response from MySQL db.
119128
Check user can see mocked sections in content library.
120129
"""
130+
# Re-initialize if testing with a different block type
131+
if block_type != 'library_content':
132+
self._initialize_course_hierarchy(block_type)
133+
121134
raw_block_structure = get_course_blocks(
122135
self.user,
123136
self.course.location,
@@ -136,7 +149,7 @@ def test_content_library(self):
136149
# Should dynamically assign a block to student
137150
trans_keys = set(trans_block_structure.get_block_keys())
138151
block_key_set = self.get_block_key_set(
139-
self.blocks, 'course', 'chapter1', 'lesson1', 'vertical1', 'library_content1'
152+
self.blocks, 'course', 'chapter1', 'lesson1', 'vertical1', f'{block_type}1'
140153
)
141154
for key in block_key_set:
142155
assert key in trans_keys
@@ -160,11 +173,12 @@ def test_content_library(self):
160173
assert set(trans_block_structure.get_block_keys()) == self.get_block_key_set(self.blocks, 'course',
161174
'chapter1', 'lesson1',
162175
'vertical1',
163-
'library_content1',
176+
f'{block_type}1',
164177
selected_vertical,
165178
selected_child), f"Expected 'selected' equality failed in iteration {i}." # pylint: disable=line-too-long
166179

167180

181+
@ddt
168182
class ContentLibraryOrderTransformerTestCase(CourseStructureTestCase):
169183
"""
170184
ContentLibraryOrderTransformer Test
@@ -176,7 +190,13 @@ def setUp(self):
176190
Setup course structure and create user for content library order transformer test.
177191
"""
178192
super().setUp()
179-
self.course_hierarchy = self.get_course_hierarchy()
193+
self._initialize_course_hierarchy()
194+
195+
def _initialize_course_hierarchy(self, block_type='library_content'):
196+
"""
197+
Initialize course hierarchy with the given block type.
198+
"""
199+
self.course_hierarchy = self.get_course_hierarchy(block_type)
180200
self.blocks = self.build_course(self.course_hierarchy)
181201
self.course = self.blocks['course']
182202
bs_api.update_course_in_cache(self.course.id)
@@ -185,14 +205,14 @@ def setUp(self):
185205
# Enroll user in course.
186206
CourseEnrollmentFactory.create(user=self.user, course_id=self.course.id, is_active=True)
187207

188-
def get_course_hierarchy(self):
208+
def get_course_hierarchy(self, block_type='library_content'):
189209
"""
190210
Get a course hierarchy to test with.
191211
"""
192212
return [{
193213
'org': 'ContentLibraryTransformer',
194214
'course': 'CL101F',
195-
'run': 'test_run',
215+
'run': f'test_run_{block_type}',
196216
'#type': 'course',
197217
'#ref': 'course',
198218
'#children': [
@@ -209,8 +229,8 @@ def get_course_hierarchy(self):
209229
'#ref': 'vertical1',
210230
'#children': [
211231
{
212-
'#type': 'library_content',
213-
'#ref': 'library_content1',
232+
'#type': block_type,
233+
'#ref': f'{block_type}1',
214234
'#children': [
215235
{
216236
'metadata': {'display_name': "CL Vertical 2"},
@@ -260,11 +280,15 @@ def get_course_hierarchy(self):
260280
}]
261281

262282
@mock.patch('lms.djangoapps.course_blocks.transformers.library_content.get_student_module_as_dict')
263-
def test_content_library_randomize(self, mocked):
283+
@data('library_content', 'itembank')
284+
def test_content_library_randomize(self, block_type, mocked):
264285
"""
265286
Test whether the order of the children blocks matches the order of the selected blocks when
266287
course has content library section
267288
"""
289+
# Re-initialize if testing with a different block type
290+
if block_type != 'library_content':
291+
self._initialize_course_hierarchy(block_type)
268292
mocked.return_value = {
269293
'selected': [
270294
['vertical', 'vertical_vertical3'],
@@ -280,7 +304,7 @@ def test_content_library_randomize(self, mocked):
280304
)
281305
children = []
282306
for block_key in trans_block_structure.topological_traversal():
283-
if block_key.block_type == 'library_content':
307+
if block_key.block_type == block_type:
284308
children = trans_block_structure.get_children(block_key)
285309
break
286310

0 commit comments

Comments
 (0)