Expose was_ever_proctored_exam dict entry on xblock_info (#24040)
In a follow-up PR, this entry will be used to validate on the client-side that a block that *is* or *ever was* a proctored/practice/onboarding exam cannot be configured as a different proctored/practice/onboarding exam, as that has led to problematic exam configuration states between edX and proctoring providers. MST-258
This commit is contained in:
@@ -15,7 +15,12 @@ from django.core.exceptions import PermissionDenied
|
||||
from django.http import Http404, HttpResponse, HttpResponseBadRequest
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.views.decorators.http import require_http_methods
|
||||
from edx_proctoring.api import does_backend_support_onboarding, get_exam_configuration_dashboard_url
|
||||
from edx_proctoring.api import (
|
||||
does_backend_support_onboarding,
|
||||
get_exam_by_content_id,
|
||||
get_exam_configuration_dashboard_url
|
||||
)
|
||||
from edx_proctoring.exceptions import ProctoredExamNotFoundException
|
||||
from help_tokens.core import HelpUrlExpert
|
||||
from opaque_keys.edx.keys import CourseKey
|
||||
from opaque_keys.edx.locator import LibraryUsageLocator
|
||||
@@ -1244,6 +1249,9 @@ def create_xblock_info(xblock, data=None, metadata=None, include_ancestor_info=F
|
||||
|
||||
xblock_info.update({
|
||||
'is_proctored_exam': xblock.is_proctored_exam,
|
||||
'was_ever_proctored_exam': _was_xblock_ever_proctored_exam(
|
||||
course, xblock
|
||||
),
|
||||
'online_proctoring_rules': rules_url,
|
||||
'is_practice_exam': xblock.is_practice_exam,
|
||||
'is_onboarding_exam': xblock.is_onboarding_exam,
|
||||
@@ -1295,6 +1303,32 @@ def create_xblock_info(xblock, data=None, metadata=None, include_ancestor_info=F
|
||||
return xblock_info
|
||||
|
||||
|
||||
def _was_xblock_ever_proctored_exam(course, xblock):
|
||||
"""
|
||||
Determine whether this XBlock is or was ever configured as a proctored exam.
|
||||
|
||||
If this block is *not* currently a proctored exam, the best way for us to tell
|
||||
whether it was was *ever* configured as a proctored exam is by checking whether
|
||||
the proctoring backend has an exam record associated with the block's ID.
|
||||
If an exception is not raised, then we know that such a record exists,
|
||||
indicating that this *was* once a proctored exam.
|
||||
|
||||
Arguments:
|
||||
course (CourseDescriptor)
|
||||
xblock (XBlock)
|
||||
|
||||
Returns: bool
|
||||
"""
|
||||
if xblock.is_proctored_exam:
|
||||
return True
|
||||
try:
|
||||
get_exam_by_content_id(course.id, xblock.location)
|
||||
except ProctoredExamNotFoundException:
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
|
||||
def add_container_page_publishing_info(xblock, xblock_info):
|
||||
"""
|
||||
Adds information about the xblock's publish state to the supplied
|
||||
|
||||
@@ -12,6 +12,7 @@ from django.http import Http404
|
||||
from django.test import TestCase
|
||||
from django.test.client import RequestFactory
|
||||
from django.urls import reverse
|
||||
from edx_proctoring.exceptions import ProctoredExamNotFoundException
|
||||
from mock import Mock, PropertyMock, patch
|
||||
from opaque_keys import InvalidKeyError
|
||||
from opaque_keys.edx.asides import AsideUsageKeyV2
|
||||
@@ -32,6 +33,7 @@ from xblock.validation import ValidationMessage
|
||||
|
||||
from contentstore.tests.utils import CourseTestCase
|
||||
from contentstore.utils import reverse_course_url, reverse_usage_url
|
||||
from contentstore.views import item as item_module
|
||||
from contentstore.views.component import component_handler, get_component_templates
|
||||
from contentstore.views.item import (
|
||||
ALWAYS,
|
||||
@@ -2473,7 +2475,6 @@ class TestXBlockInfo(ItemTest):
|
||||
"""
|
||||
Unit tests for XBlock's outline handling.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
super(TestXBlockInfo, self).setUp()
|
||||
user_id = self.user.id
|
||||
@@ -2783,15 +2784,37 @@ class TestXBlockInfo(ItemTest):
|
||||
else:
|
||||
self.assertIsNone(xblock_info.get('child_info', None))
|
||||
|
||||
@patch.dict('django.conf.settings.FEATURES', {'ENABLE_SPECIAL_EXAMS': True})
|
||||
@patch('contentstore.views.item.does_backend_support_onboarding')
|
||||
@patch('contentstore.views.item.get_exam_configuration_dashboard_url')
|
||||
def test_proctored_exam_xblock_info(self, get_exam_configuration_dashboard_url_patch,
|
||||
does_backend_support_onboarding_patch):
|
||||
|
||||
@patch.dict('django.conf.settings.FEATURES', {'ENABLE_SPECIAL_EXAMS': True})
|
||||
class TestProctoredXBlockInfo(ItemTest):
|
||||
"""
|
||||
Unit tests for XBlock outline handling, specific to proctored exam XBlocks.
|
||||
"""
|
||||
patch_get_exam_configuration_dashboard_url = patch.object(
|
||||
item_module, 'get_exam_configuration_dashboard_url', return_value='test_url'
|
||||
)
|
||||
patch_does_backend_support_onboarding = patch.object(
|
||||
item_module, 'does_backend_support_onboarding', return_value=True
|
||||
)
|
||||
patch_get_exam_by_content_id_success = patch.object(
|
||||
item_module, 'get_exam_by_content_id'
|
||||
)
|
||||
patch_get_exam_by_content_id_not_found = patch.object(
|
||||
item_module, 'get_exam_by_content_id', side_effect=ProctoredExamNotFoundException
|
||||
)
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
user_id = self.user.id
|
||||
self.chapter = ItemFactory.create(
|
||||
parent_location=self.course.location, category='chapter', display_name="Week 1", user_id=user_id,
|
||||
highlights=['highlight'],
|
||||
)
|
||||
self.course.enable_proctored_exams = True
|
||||
self.course.save()
|
||||
self.store.update_item(self.course, self.user.id)
|
||||
|
||||
def test_proctoring_is_enabled_for_course(self):
|
||||
course = modulestore().get_item(self.course.location)
|
||||
xblock_info = create_xblock_info(
|
||||
course,
|
||||
@@ -2799,31 +2822,99 @@ class TestXBlockInfo(ItemTest):
|
||||
include_children_predicate=ALWAYS,
|
||||
)
|
||||
# exam proctoring should be enabled and time limited.
|
||||
self.assertEqual(xblock_info['enable_proctored_exams'], True)
|
||||
assert xblock_info['enable_proctored_exams']
|
||||
|
||||
@patch_get_exam_configuration_dashboard_url
|
||||
@patch_does_backend_support_onboarding
|
||||
@patch_get_exam_by_content_id_success
|
||||
def test_proctored_exam_xblock_info(
|
||||
self,
|
||||
mock_get_exam_by_content_id,
|
||||
_mock_does_backend_support_onboarding,
|
||||
mock_get_exam_configuration_dashboard_url,
|
||||
):
|
||||
sequential = ItemFactory.create(
|
||||
parent_location=self.chapter.location, category='sequential',
|
||||
display_name="Test Lesson 1", user_id=self.user.id,
|
||||
is_proctored_exam=True, is_time_limited=True,
|
||||
default_time_limit_minutes=100, is_onboarding_exam=False
|
||||
parent_location=self.chapter.location,
|
||||
category='sequential',
|
||||
display_name="Test Lesson 1",
|
||||
user_id=self.user.id,
|
||||
is_proctored_exam=True,
|
||||
is_time_limited=True,
|
||||
default_time_limit_minutes=100,
|
||||
is_onboarding_exam=False,
|
||||
)
|
||||
sequential = modulestore().get_item(sequential.location)
|
||||
|
||||
get_exam_configuration_dashboard_url_patch.return_value = 'test_url'
|
||||
does_backend_support_onboarding_patch.return_value = True
|
||||
xblock_info = create_xblock_info(
|
||||
sequential,
|
||||
include_child_info=True,
|
||||
include_children_predicate=ALWAYS,
|
||||
)
|
||||
# exam proctoring should be enabled and time limited.
|
||||
self.assertEqual(xblock_info['is_proctored_exam'], True)
|
||||
self.assertEqual(xblock_info['is_time_limited'], True)
|
||||
self.assertEqual(xblock_info['default_time_limit_minutes'], 100)
|
||||
self.assertEqual(xblock_info['proctoring_exam_configuration_link'], 'test_url')
|
||||
self.assertEqual(xblock_info['supports_onboarding'], True)
|
||||
self.assertEqual(xblock_info['is_onboarding_exam'], False)
|
||||
get_exam_configuration_dashboard_url_patch.assert_called_with(self.course.id, xblock_info['id'])
|
||||
assert xblock_info['is_proctored_exam']
|
||||
assert xblock_info['was_ever_proctored_exam']
|
||||
assert xblock_info['is_time_limited']
|
||||
assert xblock_info['default_time_limit_minutes'] == 100
|
||||
assert xblock_info['proctoring_exam_configuration_link'] == 'test_url'
|
||||
assert xblock_info['supports_onboarding']
|
||||
assert not xblock_info['is_onboarding_exam']
|
||||
mock_get_exam_configuration_dashboard_url.assert_called_with(self.course.id, xblock_info['id'])
|
||||
assert mock_get_exam_by_content_id.call_count == 0
|
||||
|
||||
@patch_get_exam_configuration_dashboard_url
|
||||
@patch_does_backend_support_onboarding
|
||||
@patch_get_exam_by_content_id_success
|
||||
def test_xblock_was_ever_proctored_exam(
|
||||
self,
|
||||
mock_get_exam_by_content_id,
|
||||
_mock_does_backend_support_onboarding_patch,
|
||||
_mock_get_exam_configuration_dashboard_url,
|
||||
):
|
||||
sequential = ItemFactory.create(
|
||||
parent_location=self.chapter.location,
|
||||
category='sequential',
|
||||
display_name="Test Lesson 1",
|
||||
user_id=self.user.id,
|
||||
is_proctored_exam=False,
|
||||
is_time_limited=True,
|
||||
default_time_limit_minutes=100,
|
||||
is_onboarding_exam=False,
|
||||
)
|
||||
sequential = modulestore().get_item(sequential.location)
|
||||
xblock_info = create_xblock_info(
|
||||
sequential,
|
||||
include_child_info=True,
|
||||
include_children_predicate=ALWAYS,
|
||||
)
|
||||
assert xblock_info['was_ever_proctored_exam']
|
||||
assert mock_get_exam_by_content_id.call_count == 1
|
||||
|
||||
@patch_get_exam_configuration_dashboard_url
|
||||
@patch_does_backend_support_onboarding
|
||||
@patch_get_exam_by_content_id_not_found
|
||||
def test_xblock_was_never_proctored_exam(
|
||||
self,
|
||||
mock_get_exam_by_content_id,
|
||||
_mock_does_backend_support_onboarding_patch,
|
||||
_mock_get_exam_configuration_dashboard_url,
|
||||
):
|
||||
sequential = ItemFactory.create(
|
||||
parent_location=self.chapter.location,
|
||||
category='sequential',
|
||||
display_name="Test Lesson 1",
|
||||
user_id=self.user.id,
|
||||
is_proctored_exam=False,
|
||||
is_time_limited=True,
|
||||
default_time_limit_minutes=100,
|
||||
is_onboarding_exam=False,
|
||||
)
|
||||
sequential = modulestore().get_item(sequential.location)
|
||||
xblock_info = create_xblock_info(
|
||||
sequential,
|
||||
include_child_info=True,
|
||||
include_children_predicate=ALWAYS,
|
||||
)
|
||||
assert not xblock_info['was_ever_proctored_exam']
|
||||
assert mock_get_exam_by_content_id.call_count == 1
|
||||
|
||||
|
||||
class TestLibraryXBlockInfo(ModuleStoreTestCase):
|
||||
|
||||
Reference in New Issue
Block a user