Skip to content

Commit d386716

Browse files
feat: add option to drop decoy items from scores
Co-authored-by: Agrendalath <piotr@surowiec.it>
1 parent 760b974 commit d386716

File tree

7 files changed

+116
-4
lines changed

7 files changed

+116
-4
lines changed

Changelog.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,16 @@
11
Drag and Drop XBlock changelog
22
==============================
33

4+
Version 2.7.0 (2022-11-15)
5+
---------------------------
6+
7+
* Add option to drop decoy items from scores
8+
9+
Version 2.6.0 (2022-10-24)
10+
---------------------------
11+
12+
* Add package publishing workflow.
13+
414
Version 2.5.0 (2022-10-13)
515
---------------------------
616

README.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,15 @@ score would be `100%`:
182182

183183
score = (3 + 1) / 4
184184

185+
Optionally, there is an alternative grading that can be enabled, by setting the
186+
waffle flag `drag_and_drop_v2.grading_ignore_decoys`, which will drop
187+
the decoy items entirely from the score calculation. The formula will change to:
188+
189+
score = C / R
190+
191+
Where *C* is the number of correctly placed regular items, *R* is the number of
192+
required regular items.
193+
185194
Demo Course
186195
-----------
187196

drag_and_drop_v2/compat.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
"""
2+
Compatibility layer to isolate core-platform waffle flags from implementation.
3+
"""
4+
5+
# Waffle flags configuration
6+
7+
# Namespace
8+
WAFFLE_NAMESPACE = "drag_and_drop_v2"
9+
10+
# Course Waffle Flags
11+
# .. toggle_name: drag_and_drop_v2.grading_ignore_decoys
12+
# .. toggle_implementation: CourseWaffleFlag
13+
# .. toggle_default: False
14+
# .. toggle_description: Enables alternative grading for the xblock
15+
# that does not include decoy items in the score.
16+
# .. toggle_use_cases: open_edx
17+
# .. toggle_creation_date: 2022-11-10
18+
GRADING_IGNORE_DECOYS = 'grading_ignore_decoys'
19+
20+
21+
def get_grading_ignore_decoys_waffle_flag():
22+
"""
23+
Import and return Waffle flag for enabling alternative grading for drag_and_drop_v2 Xblock.
24+
"""
25+
# pylint: disable=import-error,import-outside-toplevel
26+
from openedx.core.djangoapps.waffle_utils import CourseWaffleFlag
27+
try:
28+
# HACK: The base class of the `CourseWaffleFlag` was changed in Olive.
29+
# Ref: https://github.com/openedx/public-engineering/issues/28
30+
return CourseWaffleFlag(WAFFLE_NAMESPACE, GRADING_IGNORE_DECOYS, __name__)
31+
except ValueError:
32+
return CourseWaffleFlag(f'{WAFFLE_NAMESPACE}.{GRADING_IGNORE_DECOYS}', __name__)

drag_and_drop_v2/drag_and_drop_v2.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
from xblockutils.resources import ResourceLoader
2828
from xblockutils.settings import ThemableXBlockMixin, XBlockWithSettingsMixin
2929

30+
from .compat import get_grading_ignore_decoys_waffle_flag
3031
from .default_data import DEFAULT_DATA
3132
from .utils import (
3233
Constants, SHOWANSWER, DummyTranslationService, FeedbackMessage,
@@ -1212,8 +1213,15 @@ def _get_item_stats(self):
12121213
"""
12131214
items = self._get_item_raw_stats()
12141215

1215-
correct_count = len(items.correctly_placed) + len(items.decoy_in_bank)
1216-
total_count = len(items.required) + len(items.decoy)
1216+
correct_count = len(items.correctly_placed)
1217+
total_count = len(items.required)
1218+
1219+
if hasattr(self.runtime, 'course_id') and \
1220+
get_grading_ignore_decoys_waffle_flag().is_enabled(self.runtime.course_id):
1221+
return correct_count, total_count
1222+
1223+
correct_count += len(items.decoy_in_bank)
1224+
total_count += len(items.decoy)
12171225

12181226
return correct_count, total_count
12191227

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ def package_data(pkg, root_list):
2323

2424
setup(
2525
name='xblock-drag-and-drop-v2',
26-
version='2.5.0',
26+
version='2.7.0',
2727
description='XBlock - Drag-and-Drop v2',
2828
packages=['drag_and_drop_v2'],
2929
install_requires=[

tests/unit/test_standard_mode.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import ddt
77

88
from drag_and_drop_v2.utils import FeedbackMessages
9+
from mock import Mock, patch
910
from tests.unit.test_fixtures import BaseDragAndDropAjaxFixture
1011

1112

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

139+
@patch(
140+
'drag_and_drop_v2.drag_and_drop_v2.get_grading_ignore_decoys_waffle_flag',
141+
lambda: Mock(is_enabled=lambda _: True),
142+
)
143+
@ddt.data(*[random.randint(1, 50) for _ in range(5)]) # pylint: disable=star-args
144+
def test_grading_ignore_decoy(self, weight):
145+
self.block.weight = weight
146+
147+
published_grades = []
148+
149+
def mock_publish(_, event, params):
150+
if event == 'grade':
151+
published_grades.append(params)
152+
self.block.runtime.publish = mock_publish
153+
154+
# Before the user starts working on the problem, grade should equal zero.
155+
self.assertEqual(0, self.block.raw_earned)
156+
157+
# Drag the decoy item into one of the zones
158+
self.call_handler(self.DROP_ITEM_HANDLER, {"val": 2, "zone": self.ZONE_1})
159+
160+
self.assertEqual(1, len(published_grades))
161+
# Decoy items are not considered in the grading
162+
self.assertEqual(0, self.block.raw_earned)
163+
self.assertEqual(0, self.block.weighted_grade())
164+
self.assertEqual({'value': 0, 'max_value': 1, 'only_if_higher': None}, published_grades[-1])
165+
166+
# Drag the first item into the correct zone.
167+
self.call_handler(self.DROP_ITEM_HANDLER, {"val": 0, "zone": self.ZONE_1})
168+
169+
self.assertEqual(2, len(published_grades))
170+
# The DnD test block has four items defined in the data fixtures:
171+
# 1 item that belongs to ZONE_1, 1 item that belongs to ZONE_2, and two decoy items.
172+
# After we drop the first item into ZONE_1, 1 out of 2 items are in the expected correct positions.
173+
# The grade at this point is therefore 1/2 * weight.
174+
self.assertEqual(0.5, self.block.raw_earned)
175+
self.assertEqual(0.5 * self.block.weight, self.block.weighted_grade())
176+
self.assertEqual({'value': 0.5, 'max_value': 1, 'only_if_higher': None}, published_grades[-1])
177+
178+
# Drag the second item into correct zone.
179+
self.call_handler(self.DROP_ITEM_HANDLER, {"val": 1, "zone": self.ZONE_2})
180+
181+
self.assertEqual(3, len(published_grades))
182+
# All items are now placed in the right place, the user therefore gets the full grade.
183+
self.assertEqual(1, self.block.raw_earned)
184+
self.assertEqual({'value': 1, 'max_value': 1, 'only_if_higher': None}, published_grades[-1])
185+
138186
@ddt.data(True, False)
139187
def test_grading_deprecation(self, grade_below_one):
140188
self.assertFalse(self.block.has_submitted_answer())

tests/utils.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import random
55
import re
66

7-
from mock import patch
7+
from mock import Mock, patch
88
from six.moves import range
99
from webob import Request
1010
from workbench.runtime import WorkbenchRuntime
@@ -30,6 +30,7 @@ def make_block():
3030
key_store = DictKeyValueStore()
3131
field_data = KvsFieldData(key_store)
3232
runtime = WorkbenchRuntime()
33+
runtime.course_id = "dummy_course_id"
3334
def_id = runtime.id_generator.create_definition(block_type)
3435
usage_id = runtime.id_generator.create_usage(def_id)
3536
scope_ids = ScopeIds('user', block_type, def_id, usage_id)
@@ -64,6 +65,10 @@ def patch_workbench(self):
6465
lambda _, html: re.sub(r'"/static/([^"]*)"', r'"/course/test-course/assets/\1"', html),
6566
create=True,
6667
)
68+
self.apply_patch(
69+
'drag_and_drop_v2.drag_and_drop_v2.get_grading_ignore_decoys_waffle_flag',
70+
lambda: Mock(is_enabled=lambda _: False),
71+
)
6772

6873
def apply_patch(self, *args, **kwargs):
6974
new_patch = patch(*args, **kwargs)

0 commit comments

Comments
 (0)