Merge pull request #14929 from edx/neem/capa-refactor

Refactor CAPA to use scorable XBlock mixin
This commit is contained in:
sanfordstudent
2017-05-02 09:43:06 -04:00
committed by GitHub
11 changed files with 272 additions and 308 deletions

View File

@@ -100,6 +100,17 @@ def _get_xmodule_instance_args(request, task_id):
return xmodule_instance_args
def _supports_rescore(descriptor):
"""
Helper method to determine whether a given item supports rescoring.
In order to accommodate both XModules and XBlocks, we have to check
the descriptor itself then fall back on its module class.
"""
return hasattr(descriptor, 'rescore') or (
hasattr(descriptor, 'module_class') and hasattr(descriptor.module_class, 'rescore')
)
def _update_instructor_task(instructor_task, task_result):
"""
Updates and possibly saves a InstructorTask entry based on a task Result.
@@ -246,10 +257,7 @@ def check_arguments_for_rescoring(usage_key):
corresponding module doesn't support rescoring calls.
"""
descriptor = modulestore().get_item(usage_key)
# TODO: Clean this up as part of TNL-6594 when CAPA uses the ScorableXBlockMixin
if (
not hasattr(descriptor, 'module_class') or not hasattr(descriptor.module_class, 'rescore_problem')
) and not hasattr(descriptor, 'rescore'):
if not _supports_rescore(descriptor):
msg = "Specified module does not support rescoring."
raise NotImplementedError(msg)
@@ -264,8 +272,7 @@ def check_entrance_exam_problems_for_rescoring(exam_key): # pylint: disable=inv
any of the problem in entrance exam doesn't support re-scoring calls.
"""
problems = get_problems_in_section(exam_key).values()
if any(not hasattr(problem, 'module_class') or not hasattr(problem.module_class, 'rescore_problem')
for problem in problems):
if any(not _supports_rescore(problem) for problem in problems):
msg = _("Not all problems in entrance exam support re-scoring.")
raise NotImplementedError(msg)

View File

@@ -10,6 +10,7 @@ from time import time
from eventtracking import tracker
from opaque_keys.edx.keys import UsageKey
from xmodule.modulestore.django import modulestore
from capa.responsetypes import StudentInputError, ResponseError, LoncapaProblemError
from courseware.courses import get_course_by_id, get_problems_in_section
from courseware.models import StudentModule
from courseware.model_data import DjangoKeyValueStore, FieldDataCache
@@ -174,38 +175,28 @@ def rescore_problem_module_state(xmodule_instance_args, module_descriptor, stude
TASK_LOG.warning(msg)
return UPDATE_STATUS_FAILED
# TODO: (TNL-6594) Remove this switch once rescore_problem support
# once CAPA uses ScorableXBlockMixin.
for method in ['rescore', 'rescore_problem']:
rescore_method = getattr(instance, method, None)
if rescore_method is not None:
break
else: # for-else: Neither method exists on the block.
if not hasattr(instance, 'rescore'):
# This should not happen, since it should be already checked in the
# caller, but check here to be sure.
msg = "Specified problem does not support rescoring."
raise UpdateProblemModuleStateError(msg)
# TODO: Remove the first part of this if-else with TNL-6594
# We check here to see if the problem has any submissions. If it does not, we don't want to rescore it
if hasattr(instance, "done"):
if not instance.done:
return UPDATE_STATUS_SKIPPED
elif not instance.has_submitted_answer():
return UPDATE_STATUS_SKIPPED
if not instance.has_submitted_answer():
return UPDATE_STATUS_SKIPPED
# Set the tracking info before this call, because it makes downstream
# calls that create events. We retrieve and store the id here because
# the request cache will be erased during downstream calls.
event_transaction_id = create_new_event_transaction_id()
create_new_event_transaction_id()
set_event_transaction_type(GRADES_RESCORE_EVENT_TYPE)
result = rescore_method(only_if_higher=task_input['only_if_higher'])
instance.save()
if result is None or result.get(u'success') in {u'correct', u'incorrect'}:
TASK_LOG.debug(
u"successfully processed rescore call for course %(course)s, problem %(loc)s "
# specific events from CAPA are not propagated up the stack. Do we want this?
try:
instance.rescore(only_if_higher=task_input['only_if_higher'])
except (LoncapaProblemError, StudentInputError, ResponseError):
TASK_LOG.warning(
u"error processing rescore call for course %(course)s, problem %(loc)s "
u"and student %(student)s",
dict(
course=course_id,
@@ -213,45 +204,21 @@ def rescore_problem_module_state(xmodule_instance_args, module_descriptor, stude
student=student
)
)
if result is not None: # Only for CAPA. This will get moved to the grade handler.
new_weighted_earned, new_weighted_possible = weighted_score(
result['new_raw_earned'] if result else None,
result['new_raw_possible'] if result else None,
module_descriptor.weight,
)
# TODO: remove this context manager after completion of AN-6134
context = course_context_from_course_id(course_id)
with tracker.get_tracker().context(GRADES_RESCORE_EVENT_TYPE, context):
tracker.emit(
unicode(GRADES_RESCORE_EVENT_TYPE),
{
'course_id': unicode(course_id),
'user_id': unicode(student.id),
'problem_id': unicode(usage_key),
'new_weighted_earned': new_weighted_earned,
'new_weighted_possible': new_weighted_possible,
'only_if_higher': task_input['only_if_higher'],
'instructor_id': unicode(xmodule_instance_args['request_info']['user_id']),
'event_transaction_id': unicode(event_transaction_id),
'event_transaction_type': unicode(GRADES_RESCORE_EVENT_TYPE),
}
)
return UPDATE_STATUS_SUCCEEDED
else:
TASK_LOG.warning(
u"error processing rescore call for course %(course)s, problem %(loc)s "
u"and student %(student)s: %(msg)s",
dict(
msg=result.get('success', result),
course=course_id,
loc=usage_key,
student=student
)
)
return UPDATE_STATUS_FAILED
instance.save()
TASK_LOG.debug(
u"successfully processed rescore call for course %(course)s, problem %(loc)s "
u"and student %(student)s",
dict(
course=course_id,
loc=usage_key,
student=student
)
)
return UPDATE_STATUS_SUCCEEDED
@outer_atomic
def reset_attempts_module_state(xmodule_instance_args, _module_descriptor, student_module, _task_input):

View File

@@ -18,6 +18,7 @@ from django.contrib.auth.models import User
from django.core.urlresolvers import reverse
from openedx.core.djangoapps.util.testing import TestConditionalContent
from openedx.core.djangolib.testing.utils import get_mock_request
from capa.tests.response_xml_factory import (CodeResponseXMLFactory,
CustomResponseXMLFactory)
from xmodule.modulestore.tests.factories import ItemFactory
@@ -275,7 +276,7 @@ class TestRescoringTask(TestIntegrationTask):
self.submit_student_answer('u1', problem_url_name, [OPTION_1, OPTION_1])
expected_message = "bad things happened"
with patch('capa.capa_problem.LoncapaProblem.rescore_existing_answers') as mock_rescore:
with patch('capa.capa_problem.LoncapaProblem.get_grade_from_current_answers') as mock_rescore:
mock_rescore.side_effect = ZeroDivisionError(expected_message)
instructor_task = self.submit_rescore_all_student_answers('instructor', problem_url_name)
self._assert_task_failure(instructor_task.id, 'rescore_problem', problem_url_name, expected_message)
@@ -293,7 +294,7 @@ class TestRescoringTask(TestIntegrationTask):
# return an input error as if it were a numerical response, with an embedded unicode character:
expected_message = u"Could not interpret '2/3\u03a9' as a number"
with patch('capa.capa_problem.LoncapaProblem.rescore_existing_answers') as mock_rescore:
with patch('capa.capa_problem.LoncapaProblem.get_grade_from_current_answers') as mock_rescore:
mock_rescore.side_effect = StudentInputError(expected_message)
instructor_task = self.submit_rescore_all_student_answers('instructor', problem_url_name)

View File

@@ -307,30 +307,21 @@ class TestRescoreInstructorTask(TestInstructorTasks):
action_name='rescored'
)
@ddt.data(
('rescore', None),
('rescore_problem', {'success': 'correct', 'new_raw_earned': 1, 'new_raw_possible': 1})
)
@ddt.unpack
def test_rescoring_success(self, rescore_method, rescore_result):
def test_rescoring_success(self):
"""
Tests rescores a problem in a course, for all students succeeds.
"""
mock_instance = MagicMock()
other_method = ({'rescore', 'rescore_problem'} - {rescore_method}).pop()
getattr(mock_instance, rescore_method).return_value = rescore_result
delattr(mock_instance, other_method)
if rescore_method == 'rescore':
del mock_instance.done
mock_instance.has_submitted_answer.return_value = True
else:
mock_instance.done = True
getattr(mock_instance, 'rescore').return_value = None
mock_instance.has_submitted_answer.return_value = True
del mock_instance.done # old CAPA code used to use this value so we delete it here to be sure
num_students = 10
self._create_students_with_state(num_students)
task_entry = self._create_input_entry()
with patch('lms.djangoapps.instructor_task.tasks_helper.module_state.get_module_for_descriptor_internal') as mock_get_module:
with patch(
'lms.djangoapps.instructor_task.tasks_helper.module_state.get_module_for_descriptor_internal'
) as mock_get_module:
mock_get_module.return_value = mock_instance
self._run_task_with_mock_celery(rescore_problem, task_entry.id, task_entry.task_id)
@@ -344,56 +335,6 @@ class TestRescoreInstructorTask(TestInstructorTasks):
action_name='rescored'
)
def test_rescoring_bad_result(self):
"""
Tests and confirm that rescoring does not succeed if "success" key is not an expected value.
"""
input_state = json.dumps({'done': True})
num_students = 10
self._create_students_with_state(num_students, input_state)
task_entry = self._create_input_entry()
mock_instance = Mock()
mock_instance.rescore_problem = Mock(return_value={'success': 'bogus'})
del mock_instance.rescore
with patch('lms.djangoapps.instructor_task.tasks_helper.module_state.get_module_for_descriptor_internal') as mock_get_module:
mock_get_module.return_value = mock_instance
self._run_task_with_mock_celery(rescore_problem, task_entry.id, task_entry.task_id)
self.assert_task_output(
output=self.get_task_output(task_entry.id),
total=num_students,
attempted=num_students,
succeeded=0,
skipped=0,
failed=num_students,
action_name='rescored'
)
def test_rescoring_missing_result(self):
"""
Tests and confirm that rescoring does not succeed if "success" key is not returned.
"""
input_state = json.dumps({'done': True})
num_students = 10
self._create_students_with_state(num_students, input_state)
task_entry = self._create_input_entry()
mock_instance = Mock()
mock_instance.rescore_problem = Mock(return_value={'bogus': 'value'})
del mock_instance.rescore
with patch('lms.djangoapps.instructor_task.tasks_helper.module_state.get_module_for_descriptor_internal') as mock_get_module:
mock_get_module.return_value = mock_instance
self._run_task_with_mock_celery(rescore_problem, task_entry.id, task_entry.task_id)
self.assert_task_output(
output=self.get_task_output(task_entry.id),
total=num_students,
attempted=num_students,
succeeded=0,
skipped=0,
failed=num_students,
action_name='rescored'
)
@attr(shard=3)
class TestResetAttemptsInstructorTask(TestInstructorTasks):