Skip to content

Commit

Permalink
feat: add option to drop decoy items from scores
Browse files Browse the repository at this point in the history
Co-authored-by: Agrendalath <[email protected]>
  • Loading branch information
pkulkark and Agrendalath committed Nov 18, 2022
1 parent 760b974 commit d386716
Show file tree
Hide file tree
Showing 7 changed files with 116 additions and 4 deletions.
10 changes: 10 additions & 0 deletions Changelog.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,16 @@
Drag and Drop XBlock changelog
==============================

Version 2.7.0 (2022-11-15)
---------------------------

* Add option to drop decoy items from scores

Version 2.6.0 (2022-10-24)
---------------------------

* Add package publishing workflow.

Version 2.5.0 (2022-10-13)
---------------------------

Expand Down
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,15 @@ score would be `100%`:

score = (3 + 1) / 4

Optionally, there is an alternative grading that can be enabled, by setting the
waffle flag `drag_and_drop_v2.grading_ignore_decoys`, which will drop
the decoy items entirely from the score calculation. The formula will change to:

score = C / R

Where *C* is the number of correctly placed regular items, *R* is the number of
required regular items.

Demo Course
-----------

Expand Down
32 changes: 32 additions & 0 deletions drag_and_drop_v2/compat.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
"""
Compatibility layer to isolate core-platform waffle flags from implementation.
"""

# Waffle flags configuration

# Namespace
WAFFLE_NAMESPACE = "drag_and_drop_v2"

# Course Waffle Flags
# .. toggle_name: drag_and_drop_v2.grading_ignore_decoys
# .. toggle_implementation: CourseWaffleFlag
# .. toggle_default: False
# .. toggle_description: Enables alternative grading for the xblock
# that does not include decoy items in the score.
# .. toggle_use_cases: open_edx
# .. toggle_creation_date: 2022-11-10
GRADING_IGNORE_DECOYS = 'grading_ignore_decoys'


def get_grading_ignore_decoys_waffle_flag():
"""
Import and return Waffle flag for enabling alternative grading for drag_and_drop_v2 Xblock.
"""
# pylint: disable=import-error,import-outside-toplevel
from openedx.core.djangoapps.waffle_utils import CourseWaffleFlag
try:
# HACK: The base class of the `CourseWaffleFlag` was changed in Olive.
# Ref: https://github.com/openedx/public-engineering/issues/28
return CourseWaffleFlag(WAFFLE_NAMESPACE, GRADING_IGNORE_DECOYS, __name__)
except ValueError:
return CourseWaffleFlag(f'{WAFFLE_NAMESPACE}.{GRADING_IGNORE_DECOYS}', __name__)
12 changes: 10 additions & 2 deletions drag_and_drop_v2/drag_and_drop_v2.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
from xblockutils.resources import ResourceLoader
from xblockutils.settings import ThemableXBlockMixin, XBlockWithSettingsMixin

from .compat import get_grading_ignore_decoys_waffle_flag
from .default_data import DEFAULT_DATA
from .utils import (
Constants, SHOWANSWER, DummyTranslationService, FeedbackMessage,
Expand Down Expand Up @@ -1212,8 +1213,15 @@ def _get_item_stats(self):
"""
items = self._get_item_raw_stats()

correct_count = len(items.correctly_placed) + len(items.decoy_in_bank)
total_count = len(items.required) + len(items.decoy)
correct_count = len(items.correctly_placed)
total_count = len(items.required)

if hasattr(self.runtime, 'course_id') and \
get_grading_ignore_decoys_waffle_flag().is_enabled(self.runtime.course_id):
return correct_count, total_count

correct_count += len(items.decoy_in_bank)
total_count += len(items.decoy)

return correct_count, total_count

Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ def package_data(pkg, root_list):

setup(
name='xblock-drag-and-drop-v2',
version='2.5.0',
version='2.7.0',
description='XBlock - Drag-and-Drop v2',
packages=['drag_and_drop_v2'],
install_requires=[
Expand Down
48 changes: 48 additions & 0 deletions tests/unit/test_standard_mode.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import ddt

from drag_and_drop_v2.utils import FeedbackMessages
from mock import Mock, patch
from tests.unit.test_fixtures import BaseDragAndDropAjaxFixture


Expand Down Expand Up @@ -135,6 +136,53 @@ def mock_publish(_, event, params):
self.assertEqual(1, self.block.raw_earned)
self.assertEqual({'value': 1, 'max_value': 1, 'only_if_higher': None}, published_grades[-1])

@patch(
'drag_and_drop_v2.drag_and_drop_v2.get_grading_ignore_decoys_waffle_flag',
lambda: Mock(is_enabled=lambda _: True),
)
@ddt.data(*[random.randint(1, 50) for _ in range(5)]) # pylint: disable=star-args
def test_grading_ignore_decoy(self, weight):
self.block.weight = weight

published_grades = []

def mock_publish(_, event, params):
if event == 'grade':
published_grades.append(params)
self.block.runtime.publish = mock_publish

# Before the user starts working on the problem, grade should equal zero.
self.assertEqual(0, self.block.raw_earned)

# Drag the decoy item into one of the zones
self.call_handler(self.DROP_ITEM_HANDLER, {"val": 2, "zone": self.ZONE_1})

self.assertEqual(1, len(published_grades))
# Decoy items are not considered in the grading
self.assertEqual(0, self.block.raw_earned)
self.assertEqual(0, self.block.weighted_grade())
self.assertEqual({'value': 0, 'max_value': 1, 'only_if_higher': None}, published_grades[-1])

# Drag the first item into the correct zone.
self.call_handler(self.DROP_ITEM_HANDLER, {"val": 0, "zone": self.ZONE_1})

self.assertEqual(2, len(published_grades))
# The DnD test block has four items defined in the data fixtures:
# 1 item that belongs to ZONE_1, 1 item that belongs to ZONE_2, and two decoy items.
# After we drop the first item into ZONE_1, 1 out of 2 items are in the expected correct positions.
# The grade at this point is therefore 1/2 * weight.
self.assertEqual(0.5, self.block.raw_earned)
self.assertEqual(0.5 * self.block.weight, self.block.weighted_grade())
self.assertEqual({'value': 0.5, 'max_value': 1, 'only_if_higher': None}, published_grades[-1])

# Drag the second item into correct zone.
self.call_handler(self.DROP_ITEM_HANDLER, {"val": 1, "zone": self.ZONE_2})

self.assertEqual(3, len(published_grades))
# All items are now placed in the right place, the user therefore gets the full grade.
self.assertEqual(1, self.block.raw_earned)
self.assertEqual({'value': 1, 'max_value': 1, 'only_if_higher': None}, published_grades[-1])

@ddt.data(True, False)
def test_grading_deprecation(self, grade_below_one):
self.assertFalse(self.block.has_submitted_answer())
Expand Down
7 changes: 6 additions & 1 deletion tests/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import random
import re

from mock import patch
from mock import Mock, patch
from six.moves import range
from webob import Request
from workbench.runtime import WorkbenchRuntime
Expand All @@ -30,6 +30,7 @@ def make_block():
key_store = DictKeyValueStore()
field_data = KvsFieldData(key_store)
runtime = WorkbenchRuntime()
runtime.course_id = "dummy_course_id"
def_id = runtime.id_generator.create_definition(block_type)
usage_id = runtime.id_generator.create_usage(def_id)
scope_ids = ScopeIds('user', block_type, def_id, usage_id)
Expand Down Expand Up @@ -64,6 +65,10 @@ def patch_workbench(self):
lambda _, html: re.sub(r'"/static/([^"]*)"', r'"/course/test-course/assets/\1"', html),
create=True,
)
self.apply_patch(
'drag_and_drop_v2.drag_and_drop_v2.get_grading_ignore_decoys_waffle_flag',
lambda: Mock(is_enabled=lambda _: False),
)

def apply_patch(self, *args, **kwargs):
new_patch = patch(*args, **kwargs)
Expand Down

0 comments on commit d386716

Please sign in to comment.