The original issue was that when a sequence was locked due to prerequisites, the API returned an empty items array ([]). This prevented the frontend from knowing what units were inside the locked sequence, meaning it couldn't construct the URLs correctly for navigation so the next/previous buttons stop working.
575 lines
25 KiB
Python
575 lines
25 KiB
Python
"""
|
|
Tests for sequence block.
|
|
"""
|
|
# pylint: disable=no-member
|
|
|
|
|
|
import ast
|
|
import json
|
|
from datetime import datetime, timedelta
|
|
from unittest.mock import Mock, patch
|
|
|
|
import ddt
|
|
from django.test import RequestFactory
|
|
from django.test.utils import override_settings
|
|
from django.utils.timezone import now
|
|
from freezegun import freeze_time
|
|
from web_fragments.fragment import Fragment
|
|
|
|
from edx_toggles.toggles.testutils import override_waffle_flag
|
|
from openedx.features.content_type_gating.models import ContentTypeGatingConfig
|
|
from xmodule.seq_block import TIMED_EXAM_GATING_WAFFLE_FLAG, SequenceBlock
|
|
from xmodule.tests import get_test_system, prepare_block_runtime
|
|
from xmodule.tests.helpers import StubUserService
|
|
from xmodule.tests.xml import XModuleXmlImportTest
|
|
from xmodule.tests.xml import factories as xml
|
|
from xmodule.x_module import PUBLIC_VIEW, STUDENT_VIEW
|
|
|
|
TODAY = now()
|
|
DUE_DATE = TODAY + timedelta(days=7)
|
|
PAST_DUE_BEFORE_END_DATE = TODAY + timedelta(days=14)
|
|
COURSE_END_DATE = TODAY + timedelta(days=21)
|
|
|
|
|
|
@ddt.ddt
|
|
class SequenceBlockTestCase(XModuleXmlImportTest):
|
|
"""
|
|
Base class for tests of Sequence Block.
|
|
"""
|
|
|
|
def setUp(self):
|
|
super().setUp()
|
|
|
|
course_xml = self._set_up_course_xml()
|
|
self.course = self.process_xml(course_xml)
|
|
self._set_up_module_system(self.course)
|
|
|
|
for chapter_index in range(len(self.course.get_children())):
|
|
chapter = self._set_up_block(self.course, chapter_index)
|
|
setattr(self, f'chapter_{chapter_index + 1}', chapter)
|
|
|
|
for sequence_index in range(len(chapter.get_children())):
|
|
sequence = self._set_up_block(chapter, sequence_index)
|
|
setattr(self, f'sequence_{chapter_index + 1}_{sequence_index + 1}', sequence)
|
|
|
|
@staticmethod
|
|
def _set_up_course_xml():
|
|
"""
|
|
Sets up and returns XML course structure.
|
|
"""
|
|
course = xml.CourseFactory.build(end=str(COURSE_END_DATE))
|
|
|
|
chapter_1 = xml.ChapterFactory.build(parent=course) # has 2 child sequences
|
|
xml.ChapterFactory.build(parent=course) # has 0 child sequences
|
|
chapter_3 = xml.ChapterFactory.build(parent=course) # has 1 child sequence
|
|
chapter_4 = xml.ChapterFactory.build(parent=course) # has 1 child sequence, with hide_after_due
|
|
chapter_5 = xml.ChapterFactory.build(parent=course) # has 1 child sequence, with a time limit
|
|
|
|
xml.SequenceFactory.build(parent=chapter_1)
|
|
xml.SequenceFactory.build(parent=chapter_1)
|
|
sequence_3_1 = xml.SequenceFactory.build(parent=chapter_3) # has 3 verticals
|
|
xml.SequenceFactory.build( # sequence_4_1
|
|
parent=chapter_4,
|
|
hide_after_due=str(True),
|
|
due=str(DUE_DATE),
|
|
)
|
|
|
|
for _ in range(3):
|
|
xml.VerticalFactory.build(parent=sequence_3_1)
|
|
|
|
sequence_5_1 = xml.SequenceFactory.build(
|
|
parent=chapter_5,
|
|
is_time_limited=str(True)
|
|
)
|
|
vertical_5_1 = xml.VerticalFactory.build(parent=sequence_5_1)
|
|
xml.ProblemFactory.build(parent=vertical_5_1)
|
|
|
|
return course
|
|
|
|
def _set_up_block(self, parent, index_in_parent):
|
|
"""
|
|
Sets up the stub sequence block for testing.
|
|
"""
|
|
block = parent.get_children()[index_in_parent]
|
|
|
|
self._set_up_module_system(block)
|
|
|
|
block.runtime._services['bookmarks'] = Mock() # pylint: disable=protected-access
|
|
block.runtime._services['user'] = StubUserService(user=Mock()) # pylint: disable=protected-access
|
|
block.parent = parent.location
|
|
return block
|
|
|
|
def _set_up_module_system(self, block):
|
|
"""
|
|
Sets up the test module system for the given block.
|
|
"""
|
|
prepare_block_runtime(block.runtime)
|
|
|
|
# The render operation will ask modulestore for the current course to get some data. As these tests were
|
|
# originally not written to be compatible with a real modulestore, we've mocked out the relevant return values.
|
|
block.runtime.modulestore = Mock()
|
|
block.runtime.modulestore.get_course.return_value = self.course
|
|
|
|
def _get_rendered_view(self,
|
|
sequence,
|
|
requested_child=None,
|
|
extra_context=None,
|
|
self_paced=False,
|
|
view=STUDENT_VIEW):
|
|
"""
|
|
Returns the rendered student view for the given sequence and the
|
|
requested_child parameter.
|
|
"""
|
|
context = {'requested_child': requested_child}
|
|
if extra_context:
|
|
context.update(extra_context)
|
|
|
|
self.course.self_paced = self_paced
|
|
return sequence.runtime.render(sequence, view, context).content
|
|
|
|
def _assert_view_at_position(self, rendered_html, expected_position):
|
|
"""
|
|
Verifies that the rendered view contains the expected position.
|
|
"""
|
|
assert f"'position': {expected_position}" in rendered_html
|
|
|
|
def test_student_view_init(self):
|
|
runtime = get_test_system()
|
|
runtime.position = 2
|
|
seq_block = SequenceBlock(runtime=runtime, scope_ids=Mock())
|
|
seq_block.bind_for_student(34)
|
|
assert seq_block.position == 2
|
|
# matches position set in the runtime
|
|
|
|
@ddt.unpack
|
|
@ddt.data(
|
|
{'view': STUDENT_VIEW},
|
|
{'view': PUBLIC_VIEW},
|
|
)
|
|
def test_render_student_view(self, view):
|
|
html = self._get_rendered_view(
|
|
self.sequence_3_1,
|
|
extra_context=dict(next_url='NextSequential', prev_url='PrevSequential'),
|
|
view=view
|
|
)
|
|
self._assert_view_at_position(html, expected_position=1)
|
|
assert str(self.sequence_3_1.location) in html
|
|
assert "'gated': False" in html
|
|
assert "'next_url': 'NextSequential'" in html
|
|
assert "'prev_url': 'PrevSequential'" in html
|
|
assert 'fa fa-check-circle check-circle is-hidden' not in html
|
|
|
|
# pylint: disable=line-too-long
|
|
@patch('xmodule.seq_block.SequenceBlock.gate_entire_sequence_if_it_is_a_timed_exam_and_contains_content_type_gated_problems')
|
|
def test_timed_exam_gating_waffle_flag(self, mocked_function): # pylint: disable=unused-argument
|
|
"""
|
|
Verify the code inside the waffle flag is not executed with the flag off
|
|
Verify the code inside the waffle flag is executed with the flag on
|
|
"""
|
|
# the order of the overrides is important since the `assert_not_called` does
|
|
# not appear to be limited to just the override_waffle_flag = False scope
|
|
with override_waffle_flag(TIMED_EXAM_GATING_WAFFLE_FLAG, active=False):
|
|
self._get_rendered_view(
|
|
self.sequence_5_1,
|
|
extra_context=dict(next_url='NextSequential', prev_url='PrevSequential'),
|
|
view=STUDENT_VIEW
|
|
)
|
|
mocked_function.assert_not_called()
|
|
|
|
with override_waffle_flag(TIMED_EXAM_GATING_WAFFLE_FLAG, active=True):
|
|
self._get_rendered_view(
|
|
self.sequence_5_1,
|
|
extra_context=dict(next_url='NextSequential', prev_url='PrevSequential'),
|
|
view=STUDENT_VIEW
|
|
)
|
|
mocked_function.assert_called_once()
|
|
|
|
@override_waffle_flag(TIMED_EXAM_GATING_WAFFLE_FLAG, active=True)
|
|
def test_that_timed_sequence_gating_respects_access_configurations(self):
|
|
"""
|
|
Verify that if a time limited sequence contains content type gated problems, we gate the sequence
|
|
"""
|
|
# the one problem in this sequence needs to have graded set to true in order to test content type gating
|
|
self.sequence_5_1.get_children()[0].get_children()[0].graded = True
|
|
gated_fragment = Fragment('i_am_gated')
|
|
|
|
# When a time limited sequence contains content type gated problems, the sequence itself is gated
|
|
self.sequence_5_1.runtime._services['content_type_gating'] = Mock(return_value=Mock( # pylint: disable=protected-access
|
|
check_children_for_content_type_gating_paywall=Mock(return_value=gated_fragment.content),
|
|
))
|
|
view = self._get_rendered_view(
|
|
self.sequence_5_1,
|
|
extra_context=dict(next_url='NextSequential', prev_url='PrevSequential'),
|
|
view=STUDENT_VIEW
|
|
)
|
|
assert 'i_am_gated' in view
|
|
# check a few elements to ensure the correct page was loaded
|
|
assert 'seq_block.html' in view
|
|
assert 'NextSequential' in view
|
|
assert 'PrevSequential' in view
|
|
|
|
@ddt.unpack
|
|
@ddt.data(
|
|
{'view': STUDENT_VIEW},
|
|
{'view': PUBLIC_VIEW},
|
|
)
|
|
def test_student_view_first_child(self, view):
|
|
html = self._get_rendered_view(
|
|
self.sequence_3_1, requested_child='first', view=view
|
|
)
|
|
self._assert_view_at_position(html, expected_position=1)
|
|
|
|
@ddt.unpack
|
|
@ddt.data(
|
|
{'view': STUDENT_VIEW},
|
|
{'view': PUBLIC_VIEW},
|
|
)
|
|
def test_student_view_last_child(self, view):
|
|
html = self._get_rendered_view(self.sequence_3_1, requested_child='last', view=view)
|
|
self._assert_view_at_position(html, expected_position=3)
|
|
|
|
def test_tooltip(self):
|
|
html = self._get_rendered_view(self.sequence_3_1, requested_child=None)
|
|
for child in self.sequence_3_1.children:
|
|
assert f"'page_title': '{child.block_id}'" in html
|
|
|
|
def test_hidden_content_before_due(self):
|
|
html = self._get_rendered_view(self.sequence_4_1)
|
|
assert 'seq_block.html' in html
|
|
assert "'banner_text': None" in html
|
|
|
|
def test_hidden_content_past_due(self):
|
|
with freeze_time(COURSE_END_DATE):
|
|
progress_url = 'http://test_progress_link'
|
|
html = self._get_rendered_view(
|
|
self.sequence_4_1,
|
|
extra_context=dict(progress_url=progress_url),
|
|
)
|
|
assert 'hidden_content.html' in html
|
|
assert progress_url in html
|
|
|
|
def test_masquerade_hidden_content_past_due(self):
|
|
with freeze_time(COURSE_END_DATE):
|
|
html = self._get_rendered_view(
|
|
self.sequence_4_1,
|
|
extra_context=dict(specific_masquerade=True),
|
|
)
|
|
assert 'seq_block.html' in html
|
|
html = self.get_context_dict_from_string(html)
|
|
assert 'Because the due date has passed, this assignment is hidden from the learner.' == html['banner_text']
|
|
|
|
def test_hidden_content_self_paced_past_due_before_end(self):
|
|
with freeze_time(PAST_DUE_BEFORE_END_DATE):
|
|
html = self._get_rendered_view(self.sequence_4_1, self_paced=True)
|
|
assert 'seq_block.html' in html
|
|
assert "'banner_text': None" in html
|
|
|
|
def test_hidden_content_self_paced_past_end(self):
|
|
with freeze_time(COURSE_END_DATE + timedelta(days=7)):
|
|
progress_url = 'http://test_progress_link'
|
|
html = self._get_rendered_view(
|
|
self.sequence_4_1,
|
|
extra_context=dict(progress_url=progress_url),
|
|
self_paced=True,
|
|
)
|
|
assert 'hidden_content.html' in html
|
|
assert progress_url in html
|
|
|
|
def _assert_gated(self, html, sequence):
|
|
"""
|
|
Assert sequence content is gated
|
|
"""
|
|
assert 'seq_block.html' in html
|
|
html = self.get_context_dict_from_string(html)
|
|
assert html['banner_text'] is None
|
|
assert [] == html['items']
|
|
assert html['gated_content']['gated']
|
|
assert 'PrereqUrl' == html['gated_content']['prereq_url']
|
|
assert 'PrereqSectionName' == html['gated_content']['prereq_section_name']
|
|
assert str(sequence.display_name) in html['gated_content']['gated_section_name']
|
|
assert 'NextSequential' == html['next_url']
|
|
assert 'PrevSequential' == html['prev_url']
|
|
|
|
def _assert_prereq(self, html, sequence):
|
|
"""
|
|
Assert sequence is a prerequisite with unfulfilled gates
|
|
"""
|
|
assert 'seq_block.html' in html
|
|
html = self.get_context_dict_from_string(html)
|
|
assert 'This section is a prerequisite. You must complete this section in order to unlock additional content.' == html['banner_text']
|
|
assert not html['gated_content']['gated']
|
|
assert str(sequence.location) == html['item_id']
|
|
assert html['gated_content']['prereq_url'] is None
|
|
assert html['gated_content']['prereq_section_name'] is None
|
|
assert 'NextSequential' == html['next_url']
|
|
assert 'PrevSequential' == html['prev_url']
|
|
|
|
def _assert_ungated(self, html, sequence):
|
|
"""
|
|
Assert sequence is not gated
|
|
"""
|
|
assert 'seq_block.html' in html
|
|
assert "'banner_text': None" in html
|
|
assert "'gated': False" in html
|
|
assert str(sequence.location) in html
|
|
assert "'prereq_url': None" in html
|
|
assert "'prereq_section_name': None" in html
|
|
assert "'next_url': 'NextSequential'" in html
|
|
assert "'prev_url': 'PrevSequential'" in html
|
|
|
|
def test_gated_content(self):
|
|
"""
|
|
Test when sequence is both a prerequisite for a sequence
|
|
and gated on another prerequisite sequence
|
|
"""
|
|
# setup seq_1_2 as a gate and gated
|
|
gating_mock_1_2 = Mock()
|
|
gating_mock_1_2.return_value.is_gate_fulfilled.return_value = False
|
|
gating_mock_1_2.return_value.required_prereq.return_value = True
|
|
gating_mock_1_2.return_value.compute_is_prereq_met.return_value = [
|
|
False,
|
|
{'url': 'PrereqUrl', 'display_name': 'PrereqSectionName', 'id': 'mockId'}
|
|
]
|
|
self.sequence_1_2.runtime._services['gating'] = gating_mock_1_2 # pylint: disable=protected-access
|
|
self.sequence_1_2.display_name = 'sequence_1_2'
|
|
|
|
html = self._get_rendered_view(
|
|
self.sequence_1_2,
|
|
extra_context=dict(next_url='NextSequential', prev_url='PrevSequential'),
|
|
)
|
|
|
|
# expect content to be gated, with no banner
|
|
self._assert_gated(html, self.sequence_1_2)
|
|
|
|
# change seq_1_2 to be ungated, but still a gate (prequiste)
|
|
gating_mock_1_2.return_value.is_gate_fulfilled.return_value = False
|
|
gating_mock_1_2.return_value.required_prereq.return_value = True
|
|
gating_mock_1_2.return_value.compute_is_prereq_met.return_value = [True, {}]
|
|
|
|
html = self._get_rendered_view(
|
|
self.sequence_1_2,
|
|
extra_context=dict(next_url='NextSequential', prev_url='PrevSequential'),
|
|
)
|
|
# assert that content and preq banner is shown
|
|
self._assert_prereq(html, self.sequence_1_2)
|
|
|
|
# change seq_1_2 to have no unfulfilled gates
|
|
gating_mock_1_2.return_value.is_gate_fulfilled.return_value = True
|
|
gating_mock_1_2.return_value.required_prereq.return_value = True
|
|
gating_mock_1_2.return_value.compute_is_prereq_met.return_value = [True, {}]
|
|
|
|
html = self._get_rendered_view(
|
|
self.sequence_1_2,
|
|
extra_context=dict(next_url='NextSequential', prev_url='PrevSequential'),
|
|
)
|
|
|
|
# assert content shown as normal
|
|
self._assert_ungated(html, self.sequence_1_2)
|
|
|
|
def test_xblock_handler_get_completion_success(self):
|
|
"""Test that the completion data is returned successfully on targeted vertical through ajax call"""
|
|
self.sequence_3_1.runtime._services['completion'] = Mock( # pylint: disable=protected-access
|
|
return_value=Mock(vertical_is_complete=Mock(return_value=True))
|
|
)
|
|
for child in self.sequence_3_1.get_children():
|
|
usage_key = str(child.location)
|
|
request = RequestFactory().post(
|
|
'/',
|
|
data=json.dumps({'usage_key': usage_key}),
|
|
content_type='application/json',
|
|
)
|
|
completion_return = self.sequence_3_1.handle('get_completion', request)
|
|
assert completion_return.json == {'complete': True}
|
|
self.sequence_3_1.runtime._services['completion'] = None # pylint: disable=protected-access
|
|
|
|
def test_xblock_handler_get_completion_bad_key(self):
|
|
"""Test that the completion data is returned as False when usage key is None through ajax call"""
|
|
request = RequestFactory().post(
|
|
'/',
|
|
data=json.dumps({'usage_key': None}),
|
|
content_type='application/json',
|
|
)
|
|
completion_return = self.sequence_3_1.handle('get_completion', request)
|
|
assert completion_return.json == {'complete': False}
|
|
|
|
def test_handle_ajax_get_completion_success(self):
|
|
"""Test that the old-style ajax handler for completion still works"""
|
|
self.sequence_3_1.runtime._services['completion'] = Mock( # pylint: disable=protected-access
|
|
return_value=Mock(vertical_is_complete=Mock(return_value=True))
|
|
)
|
|
for child in self.sequence_3_1.get_children():
|
|
usage_key = str(child.location)
|
|
completion_return = self.sequence_3_1.handle_ajax('get_completion', {'usage_key': usage_key})
|
|
assert json.loads(completion_return) == {'complete': True}
|
|
self.sequence_3_1.runtime._services['completion'] = None # pylint: disable=protected-access
|
|
|
|
def test_xblock_handler_goto_position_success(self):
|
|
"""Test that we can set position through ajax call"""
|
|
assert self.sequence_3_1.position != 5
|
|
request = RequestFactory().post(
|
|
'/',
|
|
data=json.dumps({'position': 5}),
|
|
content_type='application/json',
|
|
)
|
|
goto_return = self.sequence_3_1.handle('goto_position', request)
|
|
assert goto_return.json == {'success': True}
|
|
assert self.sequence_3_1.position == 5
|
|
|
|
def test_xblock_handler_goto_position_bad_position(self):
|
|
"""Test that we gracefully handle bad positions as position 1"""
|
|
assert self.sequence_3_1.position != 1
|
|
request = RequestFactory().post(
|
|
'/',
|
|
data=json.dumps({'position': -10}),
|
|
content_type='application/json',
|
|
)
|
|
goto_return = self.sequence_3_1.handle('goto_position', request)
|
|
assert goto_return.json == {'success': True}
|
|
assert self.sequence_3_1.position == 1
|
|
|
|
def test_handle_ajax_goto_position_success(self):
|
|
"""Test that the old-style ajax handler for setting position still works"""
|
|
goto_return = self.sequence_3_1.handle_ajax('goto_position', {'position': 5})
|
|
assert json.loads(goto_return) == {'success': True}
|
|
assert self.sequence_3_1.position == 5
|
|
|
|
def test_get_metadata(self):
|
|
"""Test that the sequence metadata is returned correctly"""
|
|
# rather than dealing with json serialization of the Mock object,
|
|
# let's just disable the bookmarks service
|
|
self.sequence_3_1.runtime._services['bookmarks'] = None # lint-amnesty, pylint: disable=protected-access
|
|
metadata = self.sequence_3_1.get_metadata()
|
|
assert len(metadata['items']) == 3
|
|
assert metadata['tag'] == 'sequential'
|
|
assert metadata['display_name'] == self.sequence_3_1.display_name_with_default
|
|
|
|
def test_get_metadata_navigation_disabled(self):
|
|
"""Test that the sequence metadata is returned correctly when navigation is disabled"""
|
|
self.sequence_3_1.hide_from_toc = True
|
|
metadata = self.sequence_3_1.get_metadata()
|
|
assert len(metadata['items']) == 3
|
|
assert metadata['tag'] == 'sequential'
|
|
assert metadata['display_name'] == self.sequence_3_1.display_name_with_default
|
|
assert metadata['navigation_disabled'] is True
|
|
|
|
@override_settings(FIELD_OVERRIDE_PROVIDERS=(
|
|
'openedx.features.content_type_gating.field_override.ContentTypeGatingFieldOverride',
|
|
))
|
|
def test_get_metadata_content_type_gated_content(self):
|
|
"""The contains_content_type_gated_content field tells whether the item contains content type gated content"""
|
|
self.sequence_5_1.runtime._services['bookmarks'] = None # pylint: disable=protected-access
|
|
ContentTypeGatingConfig.objects.create(enabled=True, enabled_as_of=datetime(2018, 1, 1))
|
|
metadata = self.sequence_5_1.get_metadata()
|
|
assert metadata['items'][0]['contains_content_type_gated_content'] is False
|
|
|
|
# When a block contains content type gated problems, set the contains_content_type_gated_content field
|
|
self.sequence_5_1.get_children()[0].get_children()[0].graded = True
|
|
self.sequence_5_1.runtime._services['content_type_gating'] = Mock(return_value=Mock( # pylint: disable=protected-access
|
|
enabled_for_enrollment=Mock(return_value=True),
|
|
content_type_gate_for_block=Mock(return_value=Fragment('i_am_gated'))
|
|
))
|
|
metadata = self.sequence_5_1.get_metadata()
|
|
assert metadata['items'][0]['contains_content_type_gated_content'] is True
|
|
|
|
def get_context_dict_from_string(self, data):
|
|
"""
|
|
Retrieve dictionary from string.
|
|
"""
|
|
# Replace tuple and un-necessary info from inside string and get the dictionary.
|
|
cleaned_data = data.replace("(('seq_block.html',\n", '').replace("),\n {})", '').strip()
|
|
return ast.literal_eval(cleaned_data)
|
|
|
|
def test_not_gated_blocks_rendered_normally(self):
|
|
"""
|
|
Test that non-gated blocks are rendered with full content when prerequisites are met.
|
|
"""
|
|
# Mock child block
|
|
child = Mock()
|
|
child.scope_ids.usage_id = "block1"
|
|
child.scope_ids.block_type = "vertical"
|
|
child.display_name_with_default = "Test Block"
|
|
children = [child]
|
|
|
|
# Mock context
|
|
context = {"next_url": "next_url", "prev_url": "prev_url"}
|
|
fragment = Mock()
|
|
|
|
# Mock `_render_student_view_for_blocks`
|
|
self.sequence_3_1._render_student_view_for_blocks = Mock(return_value="rendered_blocks") # pylint: disable=protected-access
|
|
|
|
# Call `_get_render_metadata` with prerequisites met
|
|
metadata = self.sequence_3_1._get_render_metadata( # pylint: disable=protected-access
|
|
context, children, prereq_met=True, prereq_meta_info={}, fragment=fragment
|
|
)
|
|
|
|
# Assert that blocks are rendered normally
|
|
assert metadata["items"] == "rendered_blocks"
|
|
assert metadata["next_url"] == "next_url"
|
|
assert metadata["prev_url"] == "prev_url"
|
|
|
|
def test_gated_blocks_rendered_with_basic_info(self):
|
|
"""
|
|
Test that gated blocks are rendered with minimal metadata when prerequisites are not met.
|
|
"""
|
|
# Mock child block
|
|
child = Mock()
|
|
child.scope_ids.usage_id = "block1"
|
|
child.scope_ids.block_type = "vertical"
|
|
child.display_name_with_default = "Test Block"
|
|
children = [child]
|
|
|
|
# Mock context
|
|
context = {"next_url": "next_url", "prev_url": "prev_url"}
|
|
|
|
# Mock prereq_meta_info with required keys
|
|
prereq_meta_info = {
|
|
"url": "http://example.com/prereq",
|
|
"display_name": "Prerequisite Section",
|
|
"id": "prereq_block_id",
|
|
}
|
|
|
|
# Call `_get_render_metadata` with prerequisites not met
|
|
metadata = self.sequence_3_1._get_render_metadata( # pylint: disable=protected-access
|
|
context, children, prereq_met=False, prereq_meta_info=prereq_meta_info
|
|
)
|
|
|
|
# Assert that gated blocks are rendered with basic info
|
|
assert len(metadata["items"]) == 1
|
|
assert metadata["items"][0]["id"] == "block1"
|
|
assert metadata["items"][0]["type"] == "vertical"
|
|
assert metadata["items"][0]["display_name"] == "Test Block"
|
|
assert metadata["items"][0]["is_gated"] is True
|
|
assert metadata["items"][0]["content"] == ""
|
|
|
|
# Assert that next and previous URLs are present
|
|
assert metadata["next_url"] == "next_url"
|
|
assert metadata["prev_url"] == "prev_url"
|
|
|
|
def test_prereqs_met_content_rendered_normally(self):
|
|
"""
|
|
Test that content is rendered normally when prerequisites are met.
|
|
"""
|
|
# Mock child block
|
|
child = Mock()
|
|
child.scope_ids.usage_id = "block1"
|
|
child.scope_ids.block_type = "vertical"
|
|
child.display_name_with_default = "Test Block"
|
|
children = [child]
|
|
|
|
# Mock context
|
|
context = {"next_url": "next_url", "prev_url": "prev_url"}
|
|
fragment = Mock()
|
|
|
|
# Mock `_render_student_view_for_blocks`
|
|
self.sequence_3_1._render_student_view_for_blocks = Mock(return_value="rendered_blocks") # pylint: disable=protected-access
|
|
|
|
# Call `_get_render_metadata` with prerequisites met
|
|
metadata = self.sequence_3_1._get_render_metadata( # pylint: disable=protected-access
|
|
context, children, prereq_met=True, prereq_meta_info={}, fragment=fragment
|
|
)
|
|
|
|
# Assert that content is rendered normally
|
|
assert metadata["items"] == "rendered_blocks"
|
|
assert metadata["next_url"] == "next_url"
|
|
assert metadata["prev_url"] == "prev_url"
|