Commit 318d8c04 by Braden MacDonald

Fix: Step Builder grades did not appear

parent 127c50db
...@@ -99,6 +99,13 @@ class BaseMentoringBlock( ...@@ -99,6 +99,13 @@ class BaseMentoringBlock(
scope=Scope.content, scope=Scope.content,
enforce_type=True enforce_type=True
) )
weight = Float(
display_name=_("Weight"),
help=_("Defines the maximum total grade of the block."),
default=1,
scope=Scope.settings,
enforce_type=True
)
# User state # User state
num_attempts = Integer( num_attempts = Integer(
...@@ -109,6 +116,7 @@ class BaseMentoringBlock( ...@@ -109,6 +116,7 @@ class BaseMentoringBlock(
) )
has_children = True has_children = True
has_score = True # The Problem/Step Builder XBlocks produce scores. (Their children do not send scores to the LMS.)
icon_class = 'problem' icon_class = 'problem'
block_settings_key = 'mentoring' block_settings_key = 'mentoring'
...@@ -197,8 +205,11 @@ class BaseMentoringBlock( ...@@ -197,8 +205,11 @@ class BaseMentoringBlock(
Publish data for analytics purposes Publish data for analytics purposes
""" """
event_type = data.pop('event_type') event_type = data.pop('event_type')
self.runtime.publish(self, event_type, data) if (event_type == 'grade'):
# This handler can be called from the browser. Don't allow the browser to submit arbitrary grades ;-)
raise JsonHandlerError(403, "Posting grade events from the browser is forbidden.")
self.runtime.publish(self, event_type, data)
return {'result': 'ok'} return {'result': 'ok'}
def author_preview_view(self, context): def author_preview_view(self, context):
...@@ -214,6 +225,10 @@ class BaseMentoringBlock( ...@@ -214,6 +225,10 @@ class BaseMentoringBlock(
self.include_theme_files(fragment) self.include_theme_files(fragment)
return fragment return fragment
def max_score(self):
""" Maximum score. We scale all scores to a maximum of 1.0 so this is always 1.0 """
return 1.0
class MentoringBlock(BaseMentoringBlock, StudioContainerXBlockMixin, StepParentMixin): class MentoringBlock(BaseMentoringBlock, StudioContainerXBlockMixin, StepParentMixin):
""" """
...@@ -262,13 +277,6 @@ class MentoringBlock(BaseMentoringBlock, StudioContainerXBlockMixin, StepParentM ...@@ -262,13 +277,6 @@ class MentoringBlock(BaseMentoringBlock, StudioContainerXBlockMixin, StepParentM
) )
# Settings # Settings
weight = Float(
display_name=_("Weight"),
help=_("Defines the maximum total grade of the block."),
default=1,
scope=Scope.settings,
enforce_type=True
)
display_name = String( display_name = String(
display_name=_("Title (Display name)"), display_name=_("Title (Display name)"),
help=_("Title to display"), help=_("Title to display"),
...@@ -323,8 +331,6 @@ class MentoringBlock(BaseMentoringBlock, StudioContainerXBlockMixin, StepParentM ...@@ -323,8 +331,6 @@ class MentoringBlock(BaseMentoringBlock, StudioContainerXBlockMixin, StepParentM
'display_submit', 'feedback_label', 'weight', 'extended_feedback' 'display_submit', 'feedback_label', 'weight', 'extended_feedback'
) )
has_score = True
@property @property
def is_assessment(self): def is_assessment(self):
""" Checks if mentoring XBlock is in assessment mode """ """ Checks if mentoring XBlock is in assessment mode """
...@@ -377,10 +383,6 @@ class MentoringBlock(BaseMentoringBlock, StudioContainerXBlockMixin, StepParentM ...@@ -377,10 +383,6 @@ class MentoringBlock(BaseMentoringBlock, StudioContainerXBlockMixin, StepParentM
return Score(score, int(round(score * 100)), correct, incorrect, partially_correct) return Score(score, int(round(score * 100)), correct, incorrect, partially_correct)
def max_score(self):
""" Maximum score. We scale all scores to a maximum of 1.0 so this is always 1.0 """
return 1.0
def student_view(self, context): def student_view(self, context):
# Migrate stored data if necessary # Migrate stored data if necessary
self.migrate_fields() self.migrate_fields()
...@@ -848,7 +850,7 @@ class MentoringWithExplicitStepsBlock(BaseMentoringBlock, StudioContainerWithNes ...@@ -848,7 +850,7 @@ class MentoringWithExplicitStepsBlock(BaseMentoringBlock, StudioContainerWithNes
enforce_type=True enforce_type=True
) )
editable_fields = ('display_name', 'max_attempts', 'extended_feedback') editable_fields = ('display_name', 'max_attempts', 'extended_feedback', 'weight')
@lazy @lazy
def question_ids(self): def question_ids(self):
......
"""
Tests common to Problem Builder and Step Builder
"""
import ddt
import unittest
from problem_builder.mentoring import MentoringBlock, MentoringWithExplicitStepsBlock
from xblock.core import XBlock
from .utils import ScoresTestMixin, instantiate_block
@ddt.ddt
class TestBuilderBlocks(ScoresTestMixin, unittest.TestCase):
""" Unit tests for Problem Builder and Step Builder """
@ddt.data(MentoringBlock, MentoringWithExplicitStepsBlock)
def test_interface(self, block_cls):
"""
Basic tests of the block's public interface.
"""
self.assertTrue(issubclass(block_cls, XBlock))
self.assertTrue(block_cls.has_children)
block = instantiate_block(block_cls)
self.assertTrue(block.has_children)
self.assert_produces_scores(block)
import unittest
import ddt
from mock import Mock
from problem_builder.mentoring import MentoringWithExplicitStepsBlock
from .utils import ScoresTestMixin, instantiate_block
@ddt.ddt
class TestStepBuilder(ScoresTestMixin, unittest.TestCase):
""" Unit tests for Step Builder (MentoringWithExplicitStepsBlock) """
def test_scores(self):
"""
Test that scores are emitted correctly.
"""
# Submit an empty block - score should be 0:
block = instantiate_block(MentoringWithExplicitStepsBlock)
with self.expect_score_event(block, score=0.0, max_score=1.0):
request = Mock(method="POST", body="{}")
block.publish_attempt(request, suffix=None)
# Mock a block to contain an MCQ question, then submit it. Score should be 1:
block = instantiate_block(MentoringWithExplicitStepsBlock)
block.questions = [Mock(weight=1.0)]
block.questions[0].name = 'mcq1'
block.steps = [Mock(
student_results=[('mcq1', {'score': 1, 'status': 'correct'})]
)]
block.answer_mapper = lambda _status: None
with self.expect_score_event(block, score=1.0, max_score=1.0):
request = Mock(method="POST", body="{}")
block.publish_attempt(request, suffix=None)
"""
Helper methods for testing Problem Builder / Step Builder blocks
"""
from contextlib import contextmanager
from mock import MagicMock, Mock, patch
from xblock.field_data import DictFieldData
class ScoresTestMixin(object):
"""
Mixin for tests that involve scores (grades)
"""
def assert_produces_scores(self, block):
"""
Test that the given XBlock instance meets the requirements of being able to report
scores to the edX LMS, and have them appear on the student's progress page.
"""
self.assertTrue(block.has_score)
self.assertTrue(type(block).has_score)
self.assertEqual(block.weight, 1.0) # Default weight should be 1
self.assertIsInstance(block.max_score(), (int, float))
@contextmanager
def expect_score_event(self, block, score, max_score):
"""
Context manager. Expect that the given block instance will publish the given score.
"""
with patch.object(block.runtime, 'publish') as mocked_publish:
yield
mocked_publish.assert_called_once_with(block, 'grade', {'value': score, 'max_value': max_score})
def instantiate_block(cls, fields=None):
"""
Instantiate the given XBlock in a mock runtime.
"""
fields = fields or {}
children = fields.pop('children', {})
field_data = DictFieldData(fields or {})
block = cls(
runtime=Mock(),
field_data=field_data,
scope_ids=MagicMock()
)
block.children = children
block.runtime.get_block = lambda child_id: children[child_id]
return block
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