Commit e2600bc5 by Matjaz Gregoric Committed by GitHub

Merge pull request #96 from arbrandes/SOL-1988

[SOL-1988] Account for decoy items in score
parents 6b6574ee 88f08735
...@@ -151,6 +151,32 @@ You can leave all of the checkboxes unchecked in order to create a ...@@ -151,6 +151,32 @@ You can leave all of the checkboxes unchecked in order to create a
You can define an arbitrary number of drag items, each of which may You can define an arbitrary number of drag items, each of which may
be attached to any number of zones. be attached to any number of zones.
Scoring
-------
Student assessment scores for the Drag and Drop XBlock are calculated according
to the following formula:
score = (C + D) / T
Where *C* is the number of correctly placed regular items, *D* is the number of
decoy items that were correctly left unplaced, and *T* is the total number of
items available.
Example: consider a Drag and Drop instance configured with a total of four
items, of which three are regular items and one is a decoy. If a learner
places two of the normal items correctly and one incorrectly (`C = 2`), and
wrongly places the decoy item onto a drop zone (`D = 0`), that learner's score
will be `50%`, as given by:
score = (2 + 0) / 4
If the learner were to then move the decoy item back to the bank (`D = 1`) and
move the wrongly placed regular item to the correct dropzone (`C = 3`), their
score would be `100%`:
score = (3 + 1) / 4
Demo Course Demo Course
----------- -----------
...@@ -514,4 +540,4 @@ Then start python interpreter, import `Dummy` translator and follow instructions ...@@ -514,4 +540,4 @@ Then start python interpreter, import `Dummy` translator and follow instructions
>>> conv = Dummy() >>> conv = Dummy()
>>> print conv.convert("String to translate") >>> print conv.convert("String to translate")
Then copy output and paste it into `translations/eo/LC_MESSAGES/text.po`. Then copy output and paste it into `translations/eo/LC_MESSAGES/text.po`.
\ No newline at end of file
...@@ -16,7 +16,7 @@ from xblock.fragment import Fragment ...@@ -16,7 +16,7 @@ from xblock.fragment import Fragment
from xblockutils.resources import ResourceLoader from xblockutils.resources import ResourceLoader
from xblockutils.settings import XBlockWithSettingsMixin, ThemableXBlockMixin from xblockutils.settings import XBlockWithSettingsMixin, ThemableXBlockMixin
from .utils import _, DummyTranslationService, FeedbackMessage, FeedbackMessages from .utils import _, DummyTranslationService, FeedbackMessage, FeedbackMessages, ItemStats
from .default_data import DEFAULT_DATA from .default_data import DEFAULT_DATA
...@@ -456,9 +456,9 @@ class DragAndDropBlock(XBlock, XBlockWithSettingsMixin, ThemableXBlockMixin): ...@@ -456,9 +456,9 @@ class DragAndDropBlock(XBlock, XBlockWithSettingsMixin, ThemableXBlockMixin):
feedback_key = 'finish' if is_correct else 'start' feedback_key = 'finish' if is_correct else 'start'
return [FeedbackMessage(self.data['feedback'][feedback_key], None)], set() return [FeedbackMessage(self.data['feedback'][feedback_key], None)], set()
required_ids, placed_ids, correct_ids = self._get_item_raw_stats() items = self._get_item_raw_stats()
missing_ids = required_ids - placed_ids missing_ids = items.required - items.placed
misplaced_ids = placed_ids - correct_ids misplaced_ids = items.placed - items.correctly_placed
feedback_msgs = [] feedback_msgs = []
...@@ -469,7 +469,7 @@ class DragAndDropBlock(XBlock, XBlockWithSettingsMixin, ThemableXBlockMixin): ...@@ -469,7 +469,7 @@ class DragAndDropBlock(XBlock, XBlockWithSettingsMixin, ThemableXBlockMixin):
feedback_msgs.append(FeedbackMessage(message, message_class)) feedback_msgs.append(FeedbackMessage(message, message_class))
_add_msg_if_exists( _add_msg_if_exists(
correct_ids, FeedbackMessages.correctly_placed, FeedbackMessages.MessageClasses.CORRECTLY_PLACED items.correctly_placed, FeedbackMessages.correctly_placed, FeedbackMessages.MessageClasses.CORRECTLY_PLACED
) )
_add_msg_if_exists(misplaced_ids, FeedbackMessages.misplaced, FeedbackMessages.MessageClasses.MISPLACED) _add_msg_if_exists(misplaced_ids, FeedbackMessages.misplaced, FeedbackMessages.MessageClasses.MISPLACED)
_add_msg_if_exists(missing_ids, FeedbackMessages.not_placed, FeedbackMessages.MessageClasses.NOT_PLACED) _add_msg_if_exists(missing_ids, FeedbackMessages.not_placed, FeedbackMessages.MessageClasses.NOT_PLACED)
...@@ -740,31 +740,39 @@ class DragAndDropBlock(XBlock, XBlockWithSettingsMixin, ThemableXBlockMixin): ...@@ -740,31 +740,39 @@ class DragAndDropBlock(XBlock, XBlockWithSettingsMixin, ThemableXBlockMixin):
def _get_item_stats(self): def _get_item_stats(self):
""" """
Returns a tuple representing the number of correctly-placed items, Returns a tuple representing the number of correctly placed items,
and the total number of items that must be placed on the board (non-decoy items). and the total number of items required (including decoy items).
""" """
required_items, __, correct_items = self._get_item_raw_stats() items = self._get_item_raw_stats()
return len(correct_items), len(required_items) correct_count = len(items.correctly_placed) + len(items.decoy_in_bank)
total_count = len(items.required) + len(items.decoy)
return correct_count, total_count
def _get_item_raw_stats(self): def _get_item_raw_stats(self):
""" """
Returns a 3-tuple containing required, placed and correct items. Returns a named tuple containing required, decoy, placed, correctly
placed, and correctly unplaced decoy items.
Returns: Returns:
tuple: (required_items, placed_items, correct_items) namedtuple: (required, placed, correctly_placed, decoy, decoy_in_bank)
* required_items - IDs of items that must be placed on the board * required - IDs of items that must be placed on the board
* placed_items - IDs of items actually placed on the board * placed - IDs of items actually placed on the board
* correct_items - IDs of items that were placed correctly * correctly_placed - IDs of items that were placed correctly
* decoy - IDs of decoy items
* decoy_in_bank - IDs of decoy items that were unplaced
""" """
all_items = [str(item['id']) for item in self.data['items']]
item_state = self._get_item_state() item_state = self._get_item_state()
required_items = set(item_id for item_id in all_items if self._get_item_zones(int(item_id)) != []) all_items = set(str(item['id']) for item in self.data['items'])
placed_items = set(item_id for item_id in all_items if item_id in item_state) required = set(item_id for item_id in all_items if self._get_item_zones(int(item_id)) != [])
correct_items = set(item_id for item_id in placed_items if item_state[item_id]['correct']) placed = set(item_id for item_id in all_items if item_id in item_state)
correctly_placed = set(item_id for item_id in placed if item_state[item_id]['correct'])
decoy = all_items - required
decoy_in_bank = set(item_id for item_id in decoy if item_id not in item_state)
return required_items, placed_items, correct_items return ItemStats(required, placed, correctly_placed, decoy, decoy_in_bank)
def _get_grade(self): def _get_grade(self):
""" """
......
...@@ -78,3 +78,7 @@ class FeedbackMessages(object): ...@@ -78,3 +78,7 @@ class FeedbackMessages(object):
FeedbackMessage = namedtuple("FeedbackMessage", ["message", "message_class"]) # pylint: disable=invalid-name FeedbackMessage = namedtuple("FeedbackMessage", ["message", "message_class"]) # pylint: disable=invalid-name
ItemStats = namedtuple( # pylint: disable=invalid-name
'ItemStats',
["required", "placed", "correctly_placed", "decoy", "decoy_in_bank"]
)
...@@ -20,7 +20,6 @@ from drag_and_drop_v2.default_data import ( ...@@ -20,7 +20,6 @@ from drag_and_drop_v2.default_data import (
ITEM_CORRECT_FEEDBACK, ITEM_INCORRECT_FEEDBACK, ITEM_NO_ZONE_FEEDBACK, ITEM_CORRECT_FEEDBACK, ITEM_INCORRECT_FEEDBACK, ITEM_NO_ZONE_FEEDBACK,
ITEM_ANY_ZONE_FEEDBACK, START_FEEDBACK, FINISH_FEEDBACK ITEM_ANY_ZONE_FEEDBACK, START_FEEDBACK, FINISH_FEEDBACK
) )
from drag_and_drop_v2.utils import FeedbackMessages
from .test_base import BaseIntegrationTest from .test_base import BaseIntegrationTest
...@@ -478,36 +477,6 @@ class DefaultDataTestMixin(object): ...@@ -478,36 +477,6 @@ class DefaultDataTestMixin(object):
return "<vertical_demo><drag-and-drop-v2/></vertical_demo>" return "<vertical_demo><drag-and-drop-v2/></vertical_demo>"
class DefaultAssessmentDataTestMixin(DefaultDataTestMixin):
"""
Provides a test scenario with default options in assessment mode.
"""
MAX_ATTEMPTS = 5
def _get_scenario_xml(self): # pylint: disable=no-self-use
return """
<vertical_demo><drag-and-drop-v2 mode='assessment' max_attempts='{max_attempts}'/></vertical_demo>
""".format(max_attempts=self.MAX_ATTEMPTS)
class AssessmentTestMixin(object):
"""
Provides helper methods for assessment tests
"""
@staticmethod
def _wait_until_enabled(element):
wait = WebDriverWait(element, 2)
wait.until(lambda e: e.is_displayed() and e.get_attribute('disabled') is None)
def click_submit(self):
submit_button = self._get_submit_button()
self._wait_until_enabled(submit_button)
submit_button.click()
self.wait_for_ajax()
@ddt @ddt
class StandardInteractionTest(DefaultDataTestMixin, InteractionTestBase, BaseIntegrationTest): class StandardInteractionTest(DefaultDataTestMixin, InteractionTestBase, BaseIntegrationTest):
""" """
...@@ -577,151 +546,6 @@ class StandardInteractionTest(DefaultDataTestMixin, InteractionTestBase, BaseInt ...@@ -577,151 +546,6 @@ class StandardInteractionTest(DefaultDataTestMixin, InteractionTestBase, BaseInt
self.interact_with_keyboard_help(use_keyboard=use_keyboard) self.interact_with_keyboard_help(use_keyboard=use_keyboard)
@ddt
class AssessmentInteractionTest(
DefaultAssessmentDataTestMixin, AssessmentTestMixin, InteractionTestBase, BaseIntegrationTest
):
"""
Testing interactions with Drag and Drop XBlock against default data in assessment mode.
All interactions are tested using mouse (action_key=None) and four different keyboard action keys.
If default data changes this will break.
"""
@data(None, Keys.RETURN, Keys.SPACE, Keys.CONTROL+'m', Keys.COMMAND+'m')
def test_item_no_feedback_on_good_move(self, action_key):
self.parameterized_item_positive_feedback_on_good_move(
self.items_map, action_key=action_key, assessment_mode=True
)
@data(None, Keys.RETURN, Keys.SPACE, Keys.CONTROL+'m', Keys.COMMAND+'m')
def test_item_no_feedback_on_bad_move(self, action_key):
self.parameterized_item_negative_feedback_on_bad_move(
self.items_map, self.all_zones, action_key=action_key, assessment_mode=True
)
@data(None, Keys.RETURN, Keys.SPACE, Keys.CONTROL+'m', Keys.COMMAND+'m')
def test_move_items_between_zones(self, action_key):
self.parameterized_move_items_between_zones(
self.items_map, self.all_zones, action_key=action_key
)
@data(None, Keys.RETURN, Keys.SPACE, Keys.CONTROL+'m', Keys.COMMAND+'m')
def test_final_feedback_and_reset(self, action_key):
self.parameterized_final_feedback_and_reset(
self.items_map, self.feedback, action_key=action_key, assessment_mode=True
)
@data(False, True)
def test_keyboard_help(self, use_keyboard):
self.interact_with_keyboard_help(use_keyboard=use_keyboard)
def test_submit_button_shown(self):
first_item_definition = self._get_items_with_zone(self.items_map).values()[0]
submit_button = self._get_submit_button()
self.assertTrue(submit_button.is_displayed())
self.assertEqual(submit_button.get_attribute('disabled'), 'true') # no items are placed
attempts_info = self._get_attempts_info()
expected_text = "You have used {num} of {max} attempts.".format(num=0, max=self.MAX_ATTEMPTS)
self.assertEqual(attempts_info.text, expected_text)
self.assertEqual(attempts_info.is_displayed(), self.MAX_ATTEMPTS > 0)
self.place_item(first_item_definition.item_id, first_item_definition.zone_ids[0], None)
self.assertEqual(submit_button.get_attribute('disabled'), None)
def test_misplaced_items_returned_to_bank(self):
"""
Test items placed to incorrect zones are returned to item bank after submitting solution
"""
correct_items = {0: TOP_ZONE_ID}
misplaced_items = {1: BOTTOM_ZONE_ID, 2: MIDDLE_ZONE_ID}
for item_id, zone_id in correct_items.iteritems():
self.place_item(item_id, zone_id)
for item_id, zone_id in misplaced_items.iteritems():
self.place_item(item_id, zone_id)
self.click_submit()
for item_id in correct_items:
self.assert_placed_item(item_id, TOP_ZONE_TITLE, assessment_mode=True)
for item_id in misplaced_items:
self.assert_reverted_item(item_id)
def test_max_attempts_reached_submit_and_reset_disabled(self):
"""
Test "Submit" and "Reset" buttons are disabled when no more attempts remaining
"""
self.place_item(0, TOP_ZONE_ID)
submit_button, reset_button = self._get_submit_button(), self._get_reset_button()
attempts_info = self._get_attempts_info()
for index in xrange(self.MAX_ATTEMPTS):
expected_text = "You have used {num} of {max} attempts.".format(num=index, max=self.MAX_ATTEMPTS)
self.assertEqual(attempts_info.text, expected_text) # precondition check
self.assertEqual(submit_button.get_attribute('disabled'), None)
self.assertEqual(reset_button.get_attribute('disabled'), None)
self.click_submit()
self.assertEqual(submit_button.get_attribute('disabled'), 'true')
self.assertEqual(reset_button.get_attribute('disabled'), 'true')
def test_do_attempt_feedback_is_updated(self):
"""
Test updating overall feedback after submitting solution in assessment mode
"""
# used keyboard mode to avoid bug/feature with selenium "selecting" everything instead of dragging an element
self.place_item(0, TOP_ZONE_ID, Keys.RETURN)
self.click_submit()
feedback_lines = [
"FEEDBACK",
FeedbackMessages.correctly_placed(1),
FeedbackMessages.not_placed(3),
START_FEEDBACK
]
expected_feedback = "\n".join(feedback_lines)
self.assertEqual(self._get_feedback().text, expected_feedback)
self.place_item(1, BOTTOM_ZONE_ID, Keys.RETURN)
self.click_submit()
feedback_lines = [
"FEEDBACK",
FeedbackMessages.correctly_placed(1),
FeedbackMessages.misplaced(1),
FeedbackMessages.not_placed(2),
FeedbackMessages.MISPLACED_ITEMS_RETURNED,
START_FEEDBACK
]
expected_feedback = "\n".join(feedback_lines)
self.assertEqual(self._get_feedback().text, expected_feedback)
# reach final attempt
for _ in xrange(self.MAX_ATTEMPTS-3):
self.click_submit()
self.place_item(1, MIDDLE_ZONE_ID, Keys.RETURN)
self.place_item(2, BOTTOM_ZONE_ID, Keys.RETURN)
self.place_item(3, TOP_ZONE_ID, Keys.RETURN)
self.click_submit()
feedback_lines = [
"FEEDBACK",
FeedbackMessages.correctly_placed(4),
FINISH_FEEDBACK,
FeedbackMessages.FINAL_ATTEMPT_TPL.format(score=1.0)
]
expected_feedback = "\n".join(feedback_lines)
self.assertEqual(self._get_feedback().text, expected_feedback)
class MultipleValidOptionsInteractionTest(DefaultDataTestMixin, InteractionTestBase, BaseIntegrationTest): class MultipleValidOptionsInteractionTest(DefaultDataTestMixin, InteractionTestBase, BaseIntegrationTest):
items_map = { items_map = {
...@@ -764,7 +588,7 @@ class EventsFiredTest(DefaultDataTestMixin, InteractionTestBase, BaseIntegration ...@@ -764,7 +588,7 @@ class EventsFiredTest(DefaultDataTestMixin, InteractionTestBase, BaseIntegration
}, },
{ {
'name': 'grade', 'name': 'grade',
'data': {'max_value': 1, 'value': (1.0 / 4)}, 'data': {'max_value': 1, 'value': (2.0 / 5)},
}, },
{ {
'name': 'edx.drag_and_drop_v2.item.dropped', 'name': 'edx.drag_and_drop_v2.item.dropped',
......
# Imports ###########################################################
from ddt import ddt, data
from mock import Mock, patch
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.common.keys import Keys
from workbench.runtime import WorkbenchRuntime
from xblockutils.resources import ResourceLoader
from drag_and_drop_v2.default_data import (
TOP_ZONE_ID, MIDDLE_ZONE_ID, BOTTOM_ZONE_ID,
TOP_ZONE_TITLE, START_FEEDBACK, FINISH_FEEDBACK
)
from drag_and_drop_v2.utils import FeedbackMessages
from .test_base import BaseIntegrationTest
from .test_interaction import InteractionTestBase, DefaultDataTestMixin
# Globals ###########################################################
loader = ResourceLoader(__name__)
# Classes ###########################################################
class DefaultAssessmentDataTestMixin(DefaultDataTestMixin):
"""
Provides a test scenario with default options in assessment mode.
"""
MAX_ATTEMPTS = 5
def _get_scenario_xml(self): # pylint: disable=no-self-use
return """
<vertical_demo><drag-and-drop-v2 mode='assessment' max_attempts='{max_attempts}'/></vertical_demo>
""".format(max_attempts=self.MAX_ATTEMPTS)
class AssessmentTestMixin(object):
"""
Provides helper methods for assessment tests
"""
@staticmethod
def _wait_until_enabled(element):
wait = WebDriverWait(element, 2)
wait.until(lambda e: e.is_displayed() and e.get_attribute('disabled') is None)
def click_submit(self):
submit_button = self._get_submit_button()
self._wait_until_enabled(submit_button)
submit_button.click()
self.wait_for_ajax()
@ddt
class AssessmentInteractionTest(
DefaultAssessmentDataTestMixin, AssessmentTestMixin, InteractionTestBase, BaseIntegrationTest
):
"""
Testing interactions with Drag and Drop XBlock against default data in assessment mode.
All interactions are tested using mouse (action_key=None) and four different keyboard action keys.
If default data changes this will break.
"""
@data(None, Keys.RETURN, Keys.SPACE, Keys.CONTROL+'m', Keys.COMMAND+'m')
def test_item_no_feedback_on_good_move(self, action_key):
self.parameterized_item_positive_feedback_on_good_move(
self.items_map, action_key=action_key, assessment_mode=True
)
@data(None, Keys.RETURN, Keys.SPACE, Keys.CONTROL+'m', Keys.COMMAND+'m')
def test_item_no_feedback_on_bad_move(self, action_key):
self.parameterized_item_negative_feedback_on_bad_move(
self.items_map, self.all_zones, action_key=action_key, assessment_mode=True
)
@data(None, Keys.RETURN, Keys.SPACE, Keys.CONTROL+'m', Keys.COMMAND+'m')
def test_move_items_between_zones(self, action_key):
self.parameterized_move_items_between_zones(
self.items_map, self.all_zones, action_key=action_key
)
@data(None, Keys.RETURN, Keys.SPACE, Keys.CONTROL+'m', Keys.COMMAND+'m')
def test_final_feedback_and_reset(self, action_key):
self.parameterized_final_feedback_and_reset(
self.items_map, self.feedback, action_key=action_key, assessment_mode=True
)
@data(False, True)
def test_keyboard_help(self, use_keyboard):
self.interact_with_keyboard_help(use_keyboard=use_keyboard)
def test_submit_button_shown(self):
first_item_definition = self._get_items_with_zone(self.items_map).values()[0]
submit_button = self._get_submit_button()
self.assertTrue(submit_button.is_displayed())
self.assertEqual(submit_button.get_attribute('disabled'), 'true') # no items are placed
attempts_info = self._get_attempts_info()
expected_text = "You have used {num} of {max} attempts.".format(num=0, max=self.MAX_ATTEMPTS)
self.assertEqual(attempts_info.text, expected_text)
self.assertEqual(attempts_info.is_displayed(), self.MAX_ATTEMPTS > 0)
self.place_item(first_item_definition.item_id, first_item_definition.zone_ids[0], None)
self.assertEqual(submit_button.get_attribute('disabled'), None)
def test_misplaced_items_returned_to_bank(self):
"""
Test items placed to incorrect zones are returned to item bank after submitting solution
"""
correct_items = {0: TOP_ZONE_ID}
misplaced_items = {1: BOTTOM_ZONE_ID, 2: MIDDLE_ZONE_ID}
for item_id, zone_id in correct_items.iteritems():
self.place_item(item_id, zone_id)
for item_id, zone_id in misplaced_items.iteritems():
self.place_item(item_id, zone_id)
self.click_submit()
for item_id in correct_items:
self.assert_placed_item(item_id, TOP_ZONE_TITLE, assessment_mode=True)
for item_id in misplaced_items:
self.assert_reverted_item(item_id)
def test_max_attempts_reached_submit_and_reset_disabled(self):
"""
Test "Submit" and "Reset" buttons are disabled when no more attempts remaining
"""
self.place_item(0, TOP_ZONE_ID)
submit_button, reset_button = self._get_submit_button(), self._get_reset_button()
attempts_info = self._get_attempts_info()
for index in xrange(self.MAX_ATTEMPTS):
expected_text = "You have used {num} of {max} attempts.".format(num=index, max=self.MAX_ATTEMPTS)
self.assertEqual(attempts_info.text, expected_text) # precondition check
self.assertEqual(submit_button.get_attribute('disabled'), None)
self.assertEqual(reset_button.get_attribute('disabled'), None)
self.click_submit()
self.assertEqual(submit_button.get_attribute('disabled'), 'true')
self.assertEqual(reset_button.get_attribute('disabled'), 'true')
def test_do_attempt_feedback_is_updated(self):
"""
Test updating overall feedback after submitting solution in assessment mode
"""
# used keyboard mode to avoid bug/feature with selenium "selecting" everything instead of dragging an element
self.place_item(0, TOP_ZONE_ID, Keys.RETURN)
self.click_submit()
feedback_lines = [
"FEEDBACK",
FeedbackMessages.correctly_placed(1),
FeedbackMessages.not_placed(3),
START_FEEDBACK
]
expected_feedback = "\n".join(feedback_lines)
self.assertEqual(self._get_feedback().text, expected_feedback)
self.place_item(1, BOTTOM_ZONE_ID, Keys.RETURN)
self.click_submit()
feedback_lines = [
"FEEDBACK",
FeedbackMessages.correctly_placed(1),
FeedbackMessages.misplaced(1),
FeedbackMessages.not_placed(2),
FeedbackMessages.MISPLACED_ITEMS_RETURNED,
START_FEEDBACK
]
expected_feedback = "\n".join(feedback_lines)
self.assertEqual(self._get_feedback().text, expected_feedback)
# reach final attempt
for _ in xrange(self.MAX_ATTEMPTS-3):
self.click_submit()
self.place_item(1, MIDDLE_ZONE_ID, Keys.RETURN)
self.place_item(2, BOTTOM_ZONE_ID, Keys.RETURN)
self.place_item(3, TOP_ZONE_ID, Keys.RETURN)
self.click_submit()
feedback_lines = [
"FEEDBACK",
FeedbackMessages.correctly_placed(4),
FINISH_FEEDBACK,
FeedbackMessages.FINAL_ATTEMPT_TPL.format(score=1.0)
]
expected_feedback = "\n".join(feedback_lines)
self.assertEqual(self._get_feedback().text, expected_feedback)
def test_grade(self):
"""
Test grading after submitting solution in assessment mode
"""
mock = Mock()
context = patch.object(WorkbenchRuntime, 'publish', mock)
context.start()
self.addCleanup(context.stop)
self.publish = mock
self.place_item(0, TOP_ZONE_ID, Keys.RETURN) # Correctly placed item
self.place_item(1, BOTTOM_ZONE_ID, Keys.RETURN) # Incorrectly placed item
self.place_item(4, MIDDLE_ZONE_ID, Keys.RETURN) # Incorrectly placed decoy
self.click_submit()
events = self.publish.call_args_list
published_grade = next((event[0][2] for event in events if event[0][1] == 'grade'))
expected_grade = {'max_value': 1, 'value': (1.0 / 5.0)}
self.assertEqual(published_grade, expected_grade)
...@@ -119,7 +119,7 @@ class StandardModeFixture(BaseDragAndDropAjaxFixture): ...@@ -119,7 +119,7 @@ class StandardModeFixture(BaseDragAndDropAjaxFixture):
}) })
self.assertEqual(1, len(published_grades)) self.assertEqual(1, len(published_grades))
self.assertEqual({'value': 0.5, 'max_value': 1}, published_grades[-1]) self.assertEqual({'value': 0.75, 'max_value': 1}, published_grades[-1])
self.call_handler(self.DROP_ITEM_HANDLER, { self.call_handler(self.DROP_ITEM_HANDLER, {
"val": 1, "zone": self.ZONE_2, "y_percent": "90%", "x_percent": "42%" "val": 1, "zone": self.ZONE_2, "y_percent": "90%", "x_percent": "42%"
...@@ -446,7 +446,7 @@ class TestDragAndDropAssessmentData(AssessmentModeFixture, unittest.TestCase): ...@@ -446,7 +446,7 @@ class TestDragAndDropAssessmentData(AssessmentModeFixture, unittest.TestCase):
def _submit_partial_solution(self): def _submit_partial_solution(self):
self._submit_solution({0: self.ZONE_1}) self._submit_solution({0: self.ZONE_1})
return 1.0 / 3.0 return 3.0 / 5.0
def _submit_incorrect_solution(self): def _submit_incorrect_solution(self):
self._submit_solution({0: self.ZONE_2, 1: self.ZONE_1}) self._submit_solution({0: self.ZONE_2, 1: self.ZONE_1})
...@@ -518,9 +518,9 @@ class TestDragAndDropAssessmentData(AssessmentModeFixture, unittest.TestCase): ...@@ -518,9 +518,9 @@ class TestDragAndDropAssessmentData(AssessmentModeFixture, unittest.TestCase):
def test_do_attempt_keeps_highest_score(self): def test_do_attempt_keeps_highest_score(self):
self.assertFalse(self.block.completed) # precondition check self.assertFalse(self.block.completed) # precondition check
expected_score = 2.0 / 3.0 expected_score = 4.0 / 5.0
self._submit_solution({0: self.ZONE_1, 1: self.ZONE_2}) # partial solution, 0.66 score self._submit_solution({0: self.ZONE_1, 1: self.ZONE_2}) # partial solution, 0.8 score
self.call_handler(self.DO_ATTEMPT_HANDLER, data={}) self.call_handler(self.DO_ATTEMPT_HANDLER, data={})
self.assertEqual(self.block.grade, expected_score) self.assertEqual(self.block.grade, expected_score)
...@@ -528,7 +528,7 @@ class TestDragAndDropAssessmentData(AssessmentModeFixture, unittest.TestCase): ...@@ -528,7 +528,7 @@ class TestDragAndDropAssessmentData(AssessmentModeFixture, unittest.TestCase):
# make it a last attempt so we can check feedback # make it a last attempt so we can check feedback
self._set_final_attempt() self._set_final_attempt()
self._submit_solution({0: self.ZONE_1}) # partial solution, 0.33 score self._submit_solution({0: self.ZONE_1}) # partial solution, 0.6 score
res = self.call_handler(self.DO_ATTEMPT_HANDLER, data={}) res = self.call_handler(self.DO_ATTEMPT_HANDLER, data={})
self.assertEqual(self.block.grade, expected_score) self.assertEqual(self.block.grade, expected_score)
...@@ -537,3 +537,25 @@ class TestDragAndDropAssessmentData(AssessmentModeFixture, unittest.TestCase): ...@@ -537,3 +537,25 @@ class TestDragAndDropAssessmentData(AssessmentModeFixture, unittest.TestCase):
FeedbackMessages.MessageClasses.PARTIAL_SOLUTION FeedbackMessages.MessageClasses.PARTIAL_SOLUTION
) )
self.assertIn(expected_feedback, res[self.OVERALL_FEEDBACK_KEY]) self.assertIn(expected_feedback, res[self.OVERALL_FEEDBACK_KEY])
def test_do_attempt_check_score_with_decoy(self):
self.assertFalse(self.block.completed) # precondition check
expected_score = 4.0 / 5.0
self._submit_solution({
0: self.ZONE_1,
1: self.ZONE_2,
2: self.ZONE_2,
3: self.ZONE_1,
}) # incorrect solution, 0.8 score
self.call_handler(self.DO_ATTEMPT_HANDLER, data={})
self.assertEqual(self.block.grade, expected_score)
def test_do_attempt_zero_score_with_all_decoys(self):
self.assertFalse(self.block.completed) # precondition check
expected_score = 0
self._submit_solution({
3: self.ZONE_1,
4: self.ZONE_2,
}) # incorrect solution, 0 score
self.call_handler(self.DO_ATTEMPT_HANDLER, data={})
self.assertEqual(self.block.grade, expected_score)
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment