Skip to content

Commit c9920ce

Browse files
committed
feat: Subsection(sequential) prerequisites persist during course export/import
* feat: adds prerequisite information attributes to exported OLX. e.g. <sequential required_content="91d0290972c4488db10d7ca3694e13ca" min_score="100" min_completion="100" ... > <vertical url_name="some_vertical"/> ... </sequential> * feat: parse prerequisite information attributes during import and create gating relationship. Issue Link: #36995
1 parent f32f8e8 commit c9920ce

File tree

4 files changed

+138
-0
lines changed

4 files changed

+138
-0
lines changed

xmodule/modulestore/tests/test_xml_importer.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
from xmodule.modulestore.xml_importer import StaticContentImporter, _update_block_location
2222
from xmodule.tests import DATA_DIR
2323
from xmodule.x_module import XModuleMixin
24+
from xmodule.modulestore.xml_importer import _process_sequential_prerequisites
2425

2526
OPEN_BUILTIN = 'builtins.open'
2627

@@ -257,3 +258,48 @@ def test_import_static_file(self):
257258
)
258259
mock_file.assert_called_with(full_file_path, 'rb')
259260
self.mocked_content_store.generate_thumbnail.assert_called_once()
261+
262+
263+
class TestSequentialPrerequisitesImport(unittest.TestCase):
264+
"""
265+
Verifies sequential blocks with prerequisite attributes in OLX/XML are processed correctly
266+
by _process_sequential_prerequisites function during import.
267+
"""
268+
269+
def setUp(self):
270+
"""
271+
Set up test course and sequential.
272+
"""
273+
self.course_key = CourseLocator('test_org', 'test_course', 'test_run')
274+
self.sequential_location = self.course_key.make_usage_key('sequential', 'gated_sequential')
275+
276+
# Mock sequential block
277+
self.mock_sequential = mock.Mock()
278+
self.mock_sequential.location = self.sequential_location
279+
280+
def test_gating_api_calls(self):
281+
"""
282+
Verify that _process_sequential_prerequisites correctly processes valid prerequisite data
283+
and calls gating_api functions add_prerequisite and set_required_content with valid parameters
284+
"""
285+
self.mock_sequential.xml_attributes = {
286+
'required_content': 'gated_sequential',
287+
'min_score': '80',
288+
'min_completion': '90'
289+
}
290+
291+
with mock.patch('xmodule.modulestore.xml_importer.gating_api') as mock_gating_api:
292+
_process_sequential_prerequisites(self.mock_sequential)
293+
294+
mock_gating_api.add_prerequisite.assert_called_once_with(
295+
self.course_key,
296+
self.course_key.make_usage_key('sequential', 'gated_sequential')
297+
)
298+
299+
mock_gating_api.set_required_content.assert_called_once_with(
300+
self.course_key,
301+
self.sequential_location,
302+
self.course_key.make_usage_key('sequential', 'gated_sequential'),
303+
80,
304+
90
305+
)

xmodule/modulestore/xml_importer.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@
5959

6060
from .inheritance import own_metadata
6161
from .store_utilities import rewrite_nonportable_content_links
62+
from openedx.core.lib.gating import api as gating_api
6263

6364
log = logging.getLogger(__name__)
6465

@@ -834,6 +835,43 @@ def import_library_from_xml(*args, **kwargs):
834835
return list(manager.run_imports())
835836

836837

838+
def _process_sequential_prerequisites(block):
839+
"""
840+
Extracts sequential prerequisite information, flags required sequential,
841+
and creates a gating relationship.
842+
843+
Args:
844+
block: The sequential block
845+
"""
846+
if not hasattr(block, "xml_attributes") or not block.xml_attributes:
847+
log.debug('Block has no xml_attributes property: %s', block)
848+
return
849+
try:
850+
required_content = block.xml_attributes.get('required_content', None)
851+
min_score = int(block.xml_attributes.get('min_score', None))
852+
min_completion = int(block.xml_attributes.get('min_completion', None))
853+
if not required_content or not min_score or not min_completion:
854+
log.debug('Failed to extract required_content, min_score, and/or min_completion : %s', block.xml_attributes)
855+
return
856+
except (ValueError, TypeError) as e:
857+
log.debug('Failed to extract required_content, min_score, and/or min_completion : %s', {e})
858+
return
859+
860+
course_key = block.location.course_key
861+
862+
prerequisite_usage_key = course_key.make_usage_key('sequential', required_content)
863+
864+
gating_api.add_prerequisite(course_key, prerequisite_usage_key)
865+
866+
gating_api.set_required_content(
867+
course_key,
868+
block.location,
869+
prerequisite_usage_key,
870+
min_score,
871+
min_completion
872+
)
873+
874+
837875
def _update_and_import_block( # pylint: disable=too-many-statements
838876
block, store, user_id,
839877
source_course_id, dest_course_id,
@@ -918,6 +956,9 @@ def _convert_ref_fields_to_new_namespace(reference):
918956
block.location.block_id, fields, runtime, asides=asides
919957
)
920958

959+
if block.location.block_type == "sequential":
960+
_process_sequential_prerequisites(block)
961+
921962
# TODO: Move this code once the following condition is met.
922963
# Get to the point where XML import is happening inside the
923964
# modulestore that is eventually going to store the data.

xmodule/seq_block.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -978,8 +978,31 @@ def definition_to_xml(self, resource_fs):
978978
xml_object = etree.Element('sequential')
979979
for child in self.get_children():
980980
self.runtime.add_block_as_child_node(child, xml_object)
981+
self.add_prerequisite_to_xml(xml_object)
981982
return xml_object
982983

984+
def add_prerequisite_to_xml(self, xml_object):
985+
"""
986+
Add prerequisite information to sequential XML for export.
987+
"""
988+
from openedx.core.lib.gating import api as gating_api
989+
990+
prereq_info = gating_api.get_required_content(
991+
self.location.course_key,
992+
self.location
993+
)
994+
if not prereq_info or not prereq_info[0]:
995+
log.debug('Unable to get required content: %s', prereq_info)
996+
return
997+
prereq_id, min_score, min_completion = prereq_info
998+
if not isinstance(prereq_id, str):
999+
log.debug('Unable to extractusage key, min_score and min_completion: %s', prereq_info)
1000+
return
1001+
prereq_usage_key = UsageKey.from_string(prereq_id)
1002+
xml_object.set('required_content', prereq_usage_key.block_id)
1003+
xml_object.set('min_score', str(min_score))
1004+
xml_object.set('min_completion', str(min_completion))
1005+
9831006
@property
9841007
def non_editable_metadata_fields(self):
9851008
"""

xmodule/tests/test_sequence.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
from xmodule.tests.xml import XModuleXmlImportTest
2525
from xmodule.tests.xml import factories as xml
2626
from xmodule.x_module import PUBLIC_VIEW, STUDENT_VIEW
27+
from lxml import etree
2728

2829
TODAY = now()
2930
DUE_DATE = TODAY + timedelta(days=7)
@@ -572,3 +573,30 @@ def test_prereqs_met_content_rendered_normally(self):
572573
assert metadata["items"] == "rendered_blocks"
573574
assert metadata["next_url"] == "next_url"
574575
assert metadata["prev_url"] == "prev_url"
576+
577+
@patch("openedx.core.lib.gating.api.get_required_content")
578+
def test_export_sequence_with_prerequisites(self, mock_get_required_content):
579+
"""
580+
Test that add_prerequisite_to_xml adds prerequisite information correctly.
581+
"""
582+
# Create a mock XML element
583+
xml_object = etree.Element('sequential')
584+
585+
# When add_prerequisite_to_xml method calls get_required_content mocked
586+
# content prepared here will be returned. Therefore, verification of
587+
# contents in the exported olx/xml to those values will be verified.
588+
mock_get_required_content.return_value = (
589+
"block-v1:TestX+TestCourse+1+type@sequential+block@gated_sequential",
590+
80,
591+
90,
592+
)
593+
594+
self.sequence_3_1.add_prerequisite_to_xml(xml_object)
595+
mock_get_required_content.assert_called_once_with(
596+
self.sequence_3_1.location.course_key,
597+
self.sequence_3_1.location
598+
)
599+
600+
assert xml_object.get('required_content') == 'gated_sequential'
601+
assert xml_object.get('min_score') == '80'
602+
assert xml_object.get('min_completion') == '90'

0 commit comments

Comments
 (0)