Commit b19d2206 by Sanford Student

implements scorablexblockmixin and changes implementation to use raw grades.

for TNL-6593
parent 5c72ac05
......@@ -12,7 +12,7 @@ install:
- "pip install selenium==2.53.0"
- "pip uninstall -y xblock-drag-and-drop-v2"
- "python setup.py sdist"
- "pip install dist/xblock-drag-and-drop-v2-2.0.15.tar.gz"
- "pip install dist/xblock-drag-and-drop-v2-2.0.16.tar.gz"
script:
- pep8 drag_and_drop_v2 tests --max-line-length=120
- pylint drag_and_drop_v2
......
......@@ -14,6 +14,7 @@ from xblock.core import XBlock
from xblock.exceptions import JsonHandlerError
from xblock.fields import Scope, String, Dict, Float, Boolean, Integer
from xblock.fragment import Fragment
from xblock.scorable import ScorableXBlockMixin, Score
from xblockutils.resources import ResourceLoader
from xblockutils.settings import XBlockWithSettingsMixin, ThemableXBlockMixin
......@@ -31,7 +32,12 @@ logger = logging.getLogger(__name__)
@XBlock.wants('settings')
@XBlock.needs('i18n')
class DragAndDropBlock(XBlock, XBlockWithSettingsMixin, ThemableXBlockMixin):
class DragAndDropBlock(
XBlock,
XBlockWithSettingsMixin,
ThemableXBlockMixin,
ScorableXBlockMixin
):
"""
XBlock that implements a friendly Drag-and-Drop problem
"""
......@@ -164,13 +170,18 @@ class DragAndDropBlock(XBlock, XBlockWithSettingsMixin, ThemableXBlockMixin):
)
grade = Float(
help=_("Keeps maximum score achieved by student"),
help=_("DEPRECATED. Keeps maximum score achieved by student as a weighted value."),
scope=Scope.user_state,
default=0
)
raw_earned = Float(
help=_("Keeps maximum score achieved by student as a raw value between 0 and 1."),
scope=Scope.user_state,
default=0
)
block_settings_key = 'drag-and-drop-v2'
has_score = True
def max_score(self): # pylint: disable=no-self-use
"""
......@@ -179,6 +190,44 @@ class DragAndDropBlock(XBlock, XBlockWithSettingsMixin, ThemableXBlockMixin):
"""
return 1
def get_score(self):
"""
Return the problem's current score as raw values.
"""
if not self._get_raw_earned_if_set():
self.raw_earned = self._learner_raw_score()
return Score(self.raw_earned, self.max_score())
def set_score(self, score):
"""
Sets the score on this block.
Takes a Score namedtuple containing a raw
score and possible max (for this block, we expect that this will
always be 1).
"""
assert score.raw_possible == self.max_score()
self.raw_earned = score.raw_earned
def calculate_score(self):
"""
Returns a newly-calculated raw score on the problem for the learner
based on the learner's current state.
"""
return Score(self._learner_raw_score(), self.max_score())
def has_submitted_answer(self):
"""
Returns True if the user has made a submission.
"""
return self.fields['raw_earned'].is_set_on(self) or self.fields['grade'].is_set_on(self)
def weighted_grade(self):
"""
Returns the block's current saved grade multiplied by the block's
weight- the number of points earned by the learner.
"""
return self.raw_earned * self.weight
def _learner_raw_score(self):
"""
Calculate raw score for learner submission.
......@@ -432,7 +481,7 @@ class DragAndDropBlock(XBlock, XBlockWithSettingsMixin, ThemableXBlockMixin):
return {
'correct': correct,
'attempts': self.attempts,
'grade': self._get_grade_if_set(),
'grade': self._get_raw_earned_if_set(),
'misplaced_items': list(misplaced_ids),
'feedback': self._present_feedback(feedback_msgs),
'overall_feedback': self._present_feedback(overall_feedback_msgs)
......@@ -598,7 +647,7 @@ class DragAndDropBlock(XBlock, XBlockWithSettingsMixin, ThemableXBlockMixin):
grade_feedback_template = FeedbackMessages.FINAL_ATTEMPT_TPL
feedback_msgs.append(
FeedbackMessage(grade_feedback_template.format(score=self.grade), grade_feedback_class)
FeedbackMessage(grade_feedback_template.format(score=self.weighted_grade()), grade_feedback_class)
)
return feedback_msgs, misplaced_ids
......@@ -633,7 +682,7 @@ class DragAndDropBlock(XBlock, XBlockWithSettingsMixin, ThemableXBlockMixin):
return {
'correct': is_correct,
'grade': self._get_grade_if_set(),
'grade': self._get_raw_earned_if_set(),
'finished': self._is_answer_correct(),
'overall_feedback': self._present_feedback(overall_feedback),
'feedback': self._present_feedback([item_feedback])
......@@ -680,38 +729,25 @@ class DragAndDropBlock(XBlock, XBlockWithSettingsMixin, ThemableXBlockMixin):
"""
# pylint: disable=fixme
# TODO: (arguable) split this method into "clean" functions (with no side effects and implicit state)
# This method implicitly depends on self.item_state (via _is_answer_correct and _calculate_grade)
# and also updates self.grade if some conditions are met. As a result this method implies some order of
# This method implicitly depends on self.item_state (via _is_answer_correct and _learner_raw_score)
# and also updates self.raw_earned if some conditions are met. As a result this method implies some order of
# invocation:
# * it should be called after learner-caused updates to self.item_state is applied
# * it should be called before self.item_state cleanup is applied (i.e. returning misplaced items to item bank)
# * it should be called before any method that depends on self.grade (i.e. self._get_feedback)
# * it should be called before any method that depends on self.raw_earned (i.e. self._get_feedback)
# Splitting it into a "clean" functions will allow to capture this implicit invocation order in caller method
# and help avoid bugs caused by invocation order violation in future.
# There's no going back from "completed" status to "incomplete"
self.completed = self.completed or self._is_answer_correct() or not self.attempts_remain
grade = self._calculate_grade()
current_raw_earned = self._learner_raw_score()
# ... and from higher grade to lower
current_grade = self._get_grade_if_set()
if current_grade is None or grade > current_grade:
self.grade = grade
self._publish_grade()
def _publish_grade(self):
"""
Publishes grade
"""
try:
self.runtime.publish(self, 'grade', {
'value': self.grade,
'max_value': self.weight,
})
except NotImplementedError:
# Note, this publish method is unimplemented in Studio runtimes,
# so we have to figure that we're running in Studio for now
pass
# if we have an old-style (i.e. unreliable) grade, override no matter what
saved_raw_earned = self._get_raw_earned_if_set()
if current_raw_earned is None or current_raw_earned > saved_raw_earned:
self.raw_earned = current_raw_earned
self._publish_grade(Score(self.raw_earned, self.max_score()))
def _publish_item_dropped_event(self, attempt, is_correct):
"""
......@@ -778,7 +814,7 @@ class DragAndDropBlock(XBlock, XBlockWithSettingsMixin, ThemableXBlockMixin):
'items': item_state,
'finished': is_finished,
'attempts': self.attempts,
'grade': self._get_grade_if_set(),
'grade': self._get_raw_earned_if_set(),
'overall_feedback': self._present_feedback(overall_feedback_msgs)
}
......@@ -900,19 +936,13 @@ class DragAndDropBlock(XBlock, XBlockWithSettingsMixin, ThemableXBlockMixin):
return ItemStats(required, placed, correctly_placed, decoy, decoy_in_bank)
def _calculate_grade(self):
"""
Calculates the student's grade for this block based on current item state.
"""
return self._learner_raw_score() * self.weight
def _get_grade_if_set(self):
def _get_raw_earned_if_set(self):
"""
Returns student's grade if already explicitly set, otherwise returns None.
This is different from self.grade which returns 0 by default.
This is different from self.raw_earned which returns 0 by default.
"""
if self.fields['grade'].is_set_on(self):
return self.grade
if self.fields['raw_earned'].is_set_on(self):
return self.raw_earned
else:
return None
......
# Installs xblock-sdk and dependencies needed to run the tests suite.
# Run this script inside a fresh virtual environment.
pip install -e git://github.com/edx/xblock-sdk.git@v0.1.2#egg=xblock-sdk==v0.1.2
pip install -e git://github.com/edx/xblock-sdk.git@v0.1.3#egg=xblock-sdk==v0.1.3
cd $VIRTUAL_ENV/src/xblock-sdk/ && pip install -r requirements/base.txt \
&& pip install -r requirements/test.txt && cd -
pip install -r requirements.txt
git+https://github.com/edx/xblock-utils.git@v1.0.2#egg=xblock-utils==1.0.2
git+https://github.com/edx/xblock-utils.git@v1.0.4#egg=xblock-utils==1.0.4
-e .
......@@ -23,7 +23,7 @@ def package_data(pkg, root_list):
setup(
name='xblock-drag-and-drop-v2',
version='2.0.15',
version='2.0.16',
description='XBlock - Drag-and-Drop v2',
packages=['drag_and_drop_v2'],
install_requires=[
......
......@@ -41,7 +41,7 @@ class EventsFiredTest(DefaultDataTestMixin, ParameterizedTestsMixin, BaseEventsT
},
{
'name': 'grade',
'data': {'max_value': 1, 'value': (2.0 / 5)},
'data': {'max_value': 1, 'value': (2.0 / 5), 'only_if_higher': None},
},
{
'name': 'edx.drag_and_drop_v2.item.dropped',
......@@ -77,7 +77,7 @@ class EventsFiredTest(DefaultDataTestMixin, ParameterizedTestsMixin, BaseEventsT
@unpack
def test_event(self, index, event):
self.parameterized_item_positive_feedback_on_good_move_standard(self.items_map)
dummy, name, published_data = self.publish.call_args_list[index][0]
_, name, published_data = self.publish.call_args_list[index][0]
self.assertEqual(name, event['name'])
self.assertEqual(published_data, event['data'])
......@@ -121,7 +121,7 @@ class AssessmentEventsFiredTest(
},
{
'name': 'grade',
'data': {'max_value': 1, 'value': (1.0 / 5)},
'data': {'max_value': 1, 'value': (1.0 / 5), 'only_if_higher': None},
},
{
'name': 'edx.drag_and_drop_v2.feedback.opened',
......@@ -143,7 +143,7 @@ class AssessmentEventsFiredTest(
self.click_submit()
self.wait_for_ajax()
for index, event in enumerate(self.scenarios):
dummy, name, published_data = self.publish.call_args_list[index][0]
_, name, published_data = self.publish.call_args_list[index][0]
self.assertEqual(name, event['name'])
self.assertEqual(published_data, event['data'])
......@@ -158,7 +158,7 @@ class AssessmentEventsFiredTest(
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)}
expected_grade = {'max_value': 1, 'value': (1.0 / 5.0), 'only_if_higher': None}
self.assertEqual(published_grade, expected_grade)
......
......@@ -223,6 +223,10 @@ class TestDragAndDropRender(BaseIntegrationTest):
# usually is not the case when running integration tests.
# See: https://github.com/seleniumhq/selenium-google-code-issue-archive/issues/7346
self.browser.execute_script('$("button.go-to-beginning-button").focus()')
# For unknown reasons the element only becomes visible when focus() is called twice.
# See: https://openedx.atlassian.net/browse/TNL-6736
self.browser.execute_script('$("button.go-to-beginning-button").focus()')
self.assertFocused(button)
# Button should be visible when focused.
self.assertNotIn('sr', button.get_attribute('class').split())
......
......@@ -156,7 +156,7 @@ class StandardModeFixture(BaseDragAndDropAjaxFixture):
self.block.runtime.publish = mock_publish
# Before the user starts working on the problem, grade should equal zero.
self.assertEqual(0, self.block.grade)
self.assertEqual(0, self.block.raw_earned)
# Drag the first item into the correct zone.
self.call_handler(self.DROP_ITEM_HANDLER, {"val": 0, "zone": self.ZONE_1})
......@@ -166,16 +166,68 @@ class StandardModeFixture(BaseDragAndDropAjaxFixture):
# 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, 3 out of 4 items are already in correct positions
# (1st item in ZONE_1 and two decoy items left in the bank). The grade at this point is therefore 3/4 * weight.
self.assertEqual(0.75 * weight, self.block.grade)
self.assertEqual({'value': 0.75 * weight, 'max_value': weight}, published_grades[-1])
self.assertEqual(0.75, self.block.raw_earned)
self.assertEqual(0.75 * self.block.weight, self.block.weighted_grade())
self.assertEqual({'value': 0.75, '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(2, len(published_grades))
# All items are now placed in the right place, the user therefore gets the full grade.
self.assertEqual(weight, self.block.grade)
self.assertEqual({'value': weight, 'max_value': weight}, published_grades[-1])
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())
if grade_below_one:
self.block.weight = 1.2
self.block.grade = 0.96
else:
self.block.weight = 50
self.block.grade = 40
published_grades = []
# for rescoring purposes has_submitted_answer should be true even if the block
# only has a deprecated weighted grade
self.assertTrue(self.block.has_submitted_answer())
self.assertIsNone(self.block._get_raw_earned_if_set()) # pylint: disable=protected-access
def mock_publish(_, event, params):
if event == 'grade':
published_grades.append(params)
self.block.runtime.publish = mock_publish
# Drag the first item into the correct zone.
self.call_handler(self.DROP_ITEM_HANDLER, {"val": 0, "zone": self.ZONE_1})
# The grade should be overridden even though self.grade will go down, since the block is at version 0
self.assertEqual(1, len(published_grades))
self.assertEqual(0.75, self.block.raw_earned)
self.assertEqual({'value': 0.75, 'max_value': 1, 'only_if_higher': None}, published_grades[-1])
# Drag the first item into the incorrect zone.
self.call_handler(self.DROP_ITEM_HANDLER, {"val": 0, "zone": self.ZONE_2})
# The grade should not be updated now that the block has a raw value in self.grade
self.assertEqual(1, len(published_grades))
self.assertEqual(0.75, self.block.raw_earned)
# Drag the first item back into the correct zone.
self.call_handler(self.DROP_ITEM_HANDLER, {"val": 0, "zone": self.ZONE_1})
# The grade should not be updated because user has already achieved a 0.75 raw score
self.assertEqual(1, len(published_grades))
self.assertEqual(0.75, self.block.raw_earned)
# Drag the second item into correct zone.
self.call_handler(self.DROP_ITEM_HANDLER, {"val": 1, "zone": self.ZONE_2})
self.assertEqual(2, 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])
def test_drop_item_final(self):
data = {"val": 0, "zone": self.ZONE_1}
......@@ -306,7 +358,7 @@ class AssessmentModeFixture(BaseDragAndDropAjaxFixture):
expected_overall_feedback = [
self._make_feedback_message(message=self.INITIAL_FEEDBACK),
self._make_feedback_message(
FeedbackMessages.GRADE_FEEDBACK_TPL.format(score=self.block.grade),
FeedbackMessages.GRADE_FEEDBACK_TPL.format(score=self.block.raw_earned),
FeedbackMessages.MessageClasses.PARTIAL_SOLUTION
)
]
......@@ -349,11 +401,12 @@ class AssessmentModeFixture(BaseDragAndDropAjaxFixture):
self.assertTrue(self.block.completed)
patched_publish.assert_called_once_with(self.block, 'grade', {
'value': weight,
'max_value': weight,
'value': 1,
'max_value': 1,
'only_if_higher': None,
})
self.assertTrue(res['correct'])
self.assertEqual(res['grade'], weight)
self.assertEqual(res['grade'], 1)
@ddt.data(*[random.randint(1, 50) for _ in xrange(5)]) # pylint: disable=star-args
def test_do_attempt_incorrect_publish_grade(self, weight):
......@@ -366,11 +419,12 @@ class AssessmentModeFixture(BaseDragAndDropAjaxFixture):
self.assertFalse(self.block.completed)
patched_publish.assert_called_once_with(self.block, 'grade', {
'value': weight * correctness,
'max_value': weight,
'value': correctness,
'max_value': 1,
'only_if_higher': None,
})
self.assertFalse(res['correct'])
self.assertEqual(res['grade'], weight * correctness)
self.assertEqual(res['grade'], correctness)
@ddt.data(*[random.randint(1, 50) for _ in xrange(5)]) # pylint: disable=star-args
def test_do_attempt_post_correct_no_publish_grade(self, weight):
......@@ -384,7 +438,7 @@ class AssessmentModeFixture(BaseDragAndDropAjaxFixture):
self.call_handler(self.DO_ATTEMPT_HANDLER, data={})
self.assertTrue(self.block.completed)
self.assertEqual(self.block.grade, weight)
self.assertEqual(self.block.raw_earned, 1)
self.assertFalse(patched_publish.called)
def test_get_user_state_finished_after_final_attempt(self):
......@@ -403,8 +457,7 @@ class AssessmentModeFixture(BaseDragAndDropAjaxFixture):
self._set_final_attempt()
correctness = self._submit_partial_solution()
expected_grade = weight * correctness
expected_grade = self._submit_partial_solution()
with mock.patch('workbench.runtime.WorkbenchRuntime.publish', mock.Mock()) as patched_publish:
res = self.call_handler(self.DO_ATTEMPT_HANDLER, data={})
......@@ -412,11 +465,12 @@ class AssessmentModeFixture(BaseDragAndDropAjaxFixture):
self.assertTrue(self.block.completed)
patched_publish.assert_called_once_with(self.block, 'grade', {
'value': expected_grade,
'max_value': weight,
'max_value': 1,
'only_if_higher': None,
})
expected_grade_feedback = self._make_feedback_message(
FeedbackMessages.FINAL_ATTEMPT_TPL.format(score=expected_grade),
FeedbackMessages.FINAL_ATTEMPT_TPL.format(score=expected_grade * self.block.weight),
FeedbackMessages.MessageClasses.PARTIAL_SOLUTION
)
self.assertIn(expected_grade_feedback, res[self.OVERALL_FEEDBACK_KEY])
......@@ -430,7 +484,7 @@ class AssessmentModeFixture(BaseDragAndDropAjaxFixture):
self.call_handler(self.DO_ATTEMPT_HANDLER, data={})
self.assertTrue(self.block.completed) # precondition check
self.assertEqual(self.block.grade, weight) # precondition check
self.assertEqual(self.block.raw_earned, 1) # precondition check
self._reset_problem()
......@@ -447,7 +501,7 @@ class AssessmentModeFixture(BaseDragAndDropAjaxFixture):
)
self.assertFalse(patched_publish.called)
self.assertIn(expected_grade_feedback, res[self.OVERALL_FEEDBACK_KEY])
self.assertEqual(self.block.grade, weight)
self.assertEqual(self.block.raw_earned, 1)
def test_do_attempt_misplaced_ids(self):
misplaced_ids = self._submit_incorrect_solution()
......@@ -641,7 +695,7 @@ class TestDragAndDropAssessmentData(AssessmentModeFixture, unittest.TestCase):
None
),
self._make_feedback_message(
FeedbackMessages.GRADE_FEEDBACK_TPL.format(score=self.block.grade),
FeedbackMessages.GRADE_FEEDBACK_TPL.format(score=self.block.weighted_grade()),
FeedbackMessages.MessageClasses.PARTIAL_SOLUTION
),
]
......@@ -670,7 +724,7 @@ class TestDragAndDropAssessmentData(AssessmentModeFixture, unittest.TestCase):
self._make_feedback_message(FeedbackMessages.not_placed(3), FeedbackMessages.MessageClasses.NOT_PLACED),
self._make_feedback_message(self.INITIAL_FEEDBACK, None),
self._make_feedback_message(
FeedbackMessages.GRADE_FEEDBACK_TPL.format(score=self.block.grade),
FeedbackMessages.GRADE_FEEDBACK_TPL.format(score=self.block.weighted_grade()),
FeedbackMessages.MessageClasses.PARTIAL_SOLUTION
)
]
......@@ -700,7 +754,7 @@ class TestDragAndDropAssessmentData(AssessmentModeFixture, unittest.TestCase):
None
),
self._make_feedback_message(
FeedbackMessages.GRADE_FEEDBACK_TPL.format(score=self.block.grade),
FeedbackMessages.GRADE_FEEDBACK_TPL.format(score=self.block.weighted_grade()),
FeedbackMessages.MessageClasses.PARTIAL_SOLUTION
),
]
......@@ -718,7 +772,7 @@ class TestDragAndDropAssessmentData(AssessmentModeFixture, unittest.TestCase):
),
self._make_feedback_message(self.FINAL_FEEDBACK, FeedbackMessages.MessageClasses.CORRECT_SOLUTION),
self._make_feedback_message(
FeedbackMessages.GRADE_FEEDBACK_TPL.format(score=self.block.grade),
FeedbackMessages.GRADE_FEEDBACK_TPL.format(score=self.block.weighted_grade()),
FeedbackMessages.MessageClasses.CORRECT_SOLUTION
),
]
......@@ -757,7 +811,7 @@ class TestDragAndDropAssessmentData(AssessmentModeFixture, unittest.TestCase):
self._make_feedback_message(FeedbackMessages.not_placed(2), FeedbackMessages.MessageClasses.NOT_PLACED),
self._make_feedback_message(self.INITIAL_FEEDBACK, None),
self._make_feedback_message(
FeedbackMessages.GRADE_FEEDBACK_TPL.format(score=self.block.grade),
FeedbackMessages.GRADE_FEEDBACK_TPL.format(score=self.block.weighted_grade()),
FeedbackMessages.MessageClasses.PARTIAL_SOLUTION
),
]
......@@ -766,11 +820,11 @@ class TestDragAndDropAssessmentData(AssessmentModeFixture, unittest.TestCase):
def test_do_attempt_keeps_highest_score(self):
self.assertFalse(self.block.completed) # precondition check
expected_score = 4.0 / 5.0 * self.block.weight
expected_score = 4.0 / 5.0
self._submit_solution({0: self.ZONE_1, 1: self.ZONE_2}) # partial solution, 0.8 score
self._do_attempt()
self.assertEqual(self.block.grade, expected_score)
self.assertEqual(self.block.raw_earned, expected_score)
self._reset_problem()
# make it a last attempt so we can check feedback
......@@ -778,10 +832,10 @@ class TestDragAndDropAssessmentData(AssessmentModeFixture, unittest.TestCase):
self._submit_solution({0: self.ZONE_1}) # partial solution, 0.6 score
res = self._do_attempt()
self.assertEqual(self.block.grade, expected_score)
self.assertEqual(self.block.raw_earned, expected_score)
expected_feedback = self._make_feedback_message(
FeedbackMessages.FINAL_ATTEMPT_TPL.format(score=expected_score),
FeedbackMessages.FINAL_ATTEMPT_TPL.format(score=expected_score * self.block.weight),
FeedbackMessages.MessageClasses.PARTIAL_SOLUTION
)
......@@ -789,7 +843,7 @@ class TestDragAndDropAssessmentData(AssessmentModeFixture, unittest.TestCase):
def test_do_attempt_check_score_with_decoy(self):
self.assertFalse(self.block.completed) # precondition check
expected_score = 4.0 / 5.0 * self.block.weight
expected_score = 4.0 / 5.0
self._submit_solution({
0: self.ZONE_1,
1: self.ZONE_2,
......@@ -797,7 +851,7 @@ class TestDragAndDropAssessmentData(AssessmentModeFixture, unittest.TestCase):
3: self.ZONE_1,
}) # incorrect solution, 0.8 score
self._do_attempt()
self.assertEqual(self.block.grade, expected_score)
self.assertEqual(self.block.raw_earned, expected_score)
def test_do_attempt_zero_score_with_all_decoys(self):
published_grades = []
......@@ -815,9 +869,9 @@ class TestDragAndDropAssessmentData(AssessmentModeFixture, unittest.TestCase):
res = self._do_attempt()
self.assertEqual(res['grade'], 0)
self.assertEqual(self.block.grade, 0)
self.assertEqual(self.block.raw_earned, 0)
self.assertEqual(1, len(published_grades))
self.assertEqual({'value': 0, 'max_value': self.block.weight}, published_grades[-1])
self.assertEqual({'value': 0, 'max_value': 1, 'only_if_higher': None}, published_grades[-1])
user_state = self.call_handler('get_user_state', method="GET")
self.assertEqual(user_state['grade'], 0)
......
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