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:
Kyle McCormick
2020-06-02 09:44:12 -04:00
committed by GitHub
parent f574ed3713
commit 7e3470db42
2 changed files with 147 additions and 22 deletions

View File

@@ -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

View File

@@ -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):