Commit efc6083b by Brad Melin

define ways to filter and map xblock features into build_user_state_data

upgrade how tips are retrieved for blocks that currently implement them

updated docs to reflect changes; consolidated tip output; slight tidy

fixed tests and style errors

tips is a list on read, not write

cleaned up tip transformer

added tests for new functionality

minimise diff

more style errors

version bump and simplify transform loop

remove unnecessary tips for mcq

remove tips from student_view_user_state
parent 7665063a
......@@ -25,6 +25,7 @@ Problem Builder (`problem-builder`)
### `student_view_data`
- `block_id`: (string) The XBlock's usage ID.
- `max_attempts`: (integer) Max number of allowed attempts.
- `extended_feedback`: (boolean) `true` if extended feedback is enabled for this
block.
......@@ -101,6 +102,7 @@ Step Builder (`step-builder`)
### `student_view_data`
- `block_id`: (string) The XBlock's usage ID.
- `title`: (string) The display name of the component.
- `show_title`: (boolean) `true` if the title should be displayed.
- `weight`: (float) The weight of the problem.
......@@ -158,6 +160,7 @@ Mentoring Step (`sb-step`)
### `student_view_data`
- `block_id`: (string) The XBlock's usage ID.
- `type`: (string) Always equals `"sb-step"` for Mentoring Step components.
- `title`: (string) Step component's display name.
- `show_title`: (boolean) `true` if the title should be displayed.
......@@ -180,6 +183,7 @@ Review Step (`sb-review-step`)
### `student_view_data`
- `block_id`: (string) The XBlock's usage ID.
- `type`: (string) Always equals `"sb-review-step`" for Review Step components.
- `title`: (string) Display name of the component.
- `components`: (array) A list of `student_view_data` output of all immediate
......@@ -193,6 +197,7 @@ Conditional Message component is always child of a Review Step component.
### `student_view_data`
- `block_id`: (string) The XBlock's usage ID.
- `type`: (string) Always equals `"sb-conditional-message"` for Conditional
Message components.
- `content`: (string) Content of the message. May contain HTML.
......@@ -205,6 +210,7 @@ Score Summary (`sb-review-score`)
### `student_view_data`
- `block_id`: (string) The XBlock's usage ID.
- `type`: (string) Always equals `"sb-review-score"` for Score Summary
components.
......@@ -213,6 +219,7 @@ Per-Question Feedback (`sb-review-per-question-feedback`)
### `student_view_data`
- `block_id`: (string) The XBlock's usage ID.
- `type`: (string) Always equals `"sb-review-per-question-feedback"` for Score
Summary components.
......@@ -221,6 +228,7 @@ Long Answer (`pb-answer`)
### `student_view_data`
- `block_id`: (string) The XBlock's usage ID.
- `type`: (string) Always equals `"pb-answer"` for Long Answer components.
- `id`: (string) Unique ID (name) of the component.
- `weight`: (float) The weight of this component.
......@@ -257,6 +265,7 @@ Multiple Choice Question (`pb-mcq`)
### `student_view_data`
- `block_id`: (string) The XBlock's usage ID.
- `type`: (string) Always equals `"pb-mcq"` for MCQ components.
- `id`: (string) Unique ID (name) of the component.
- `question`: (string) The content of the question.
......@@ -294,7 +303,6 @@ Each entry in the `tips` array contains these values:
- `weight`: (float) Child component's weight attribute.
- `submission`: (string) The value of the choice that the user selected.
- `message`: (string) General feedback. May contain HTML.
- `tips`: (string) HTML representation of tips. May be `null`.
### POST Submit Data
......@@ -308,6 +316,7 @@ Rating Question (`pb-rating`)
### `student_view_data`
- `block_id`: (string) The XBlock's usage ID.
Identical to [MCQ questions](#multiple-choice-question-pb-mcq) except that the
`type` field always equals `"pb-rating"`.
......@@ -329,6 +338,7 @@ Multiple Response Question (`pb-mrq`)
### `student_view_data`
- `block_id`: (string) The XBlock's usage ID.
- `type`: (string) Always equals `"pb-mrq"` for Multiple Response Question
components.
- `id`: (string) Unique ID (name) of the component.
......@@ -342,6 +352,14 @@ Multiple Response Question (`pb-mrq`)
- `tips`: (array) A list of objects providing info about tips defined for the
problem. See below for more info.
#### `tips`
Each entry in the `tips` array contains these values:
- `content`: (string) The text content of the tip.
- `for_choices`: (array) A list of string values corresponding to choices to
which this tip applies to.
### `student_view_user_state`
- `student_choices`: (array) A list of string values corresponding to choices
......@@ -365,7 +383,6 @@ Each item in the `choices` array contains these fields:
- `completed`: (boolean) Boolean indicating whether the state of the choice is
correct.
- `selected`: (boolean) `true` if the user selected this choice.
- `tips`: (string) Tips formatted as a string of HTML.
- `value`: (string) The value of the choice.
### POST Submit Data
......@@ -378,6 +395,7 @@ Ranged Value Slider (`pb-slider`)
### `student_view_data`
- `block_id`: (string) The XBlock's usage ID.
- `type`: (string) Always equals `"pb-slider"` for Ranged Value Slider
components.
- `id`: (string) Unique ID (name) of the component.
......@@ -409,6 +427,7 @@ Completion (`pb-completion`)
### `student_view_data`
- `block_id`: (string) The XBlock's usage ID.
- `type`: (string) Always equals `"pb-completion"` for Completion components.
- `id`: (string) Unique ID (name) of the component.
- `title`: (string) Display name of the problem.
......@@ -440,6 +459,7 @@ Plot (`sb-plot`)
### `student_view_data`
- `block_id`: (string) The XBlock's usage ID.
- `type`: (string) Always equals `"sb-plot"` for Plot components.
- `title`: (string) Display name of the component.
- `q1_label`: (string) Quadrant I label.
......@@ -452,3 +472,8 @@ Plot (`sb-plot`)
- `point_color_average`: (string) Point color to use for the average overlay.
- `overlay_data`: (string) JSON data representing points on overlays.
- `hide_header`: (boolean) Always `true` for Plot components.
### `student_view_user_state`
- `average_claims`: (array) Averaged claim data
- `default_claims`: (array) Default claim data
......@@ -19,7 +19,6 @@
#
# Imports ###########################################################
import logging
from lazy import lazy
......@@ -53,6 +52,11 @@ class AnswerMixin(XBlockWithPreviewMixin, XBlockWithTranslationServiceMixin, Stu
"""
Mixin to give an XBlock the ability to read/write data to the Answers DB table.
"""
def build_user_state_data(self, context=None):
result = super(AnswerMixin, self).build_user_state_data()
result['student_input'] = self.student_input
return result
def _get_course_id(self):
""" Get a course ID if available """
return getattr(self.runtime, 'course_id', 'all')
......@@ -86,12 +90,6 @@ class AnswerMixin(XBlockWithPreviewMixin, XBlockWithTranslationServiceMixin, Stu
return answer_data
@property
def student_input(self):
if self.name:
return self.get_model_object().student_input
return ''
@XBlock.json_handler
def answer_value(self, data, suffix=''):
""" Current value of the answer, for refresh by client """
......@@ -103,6 +101,25 @@ class AnswerMixin(XBlockWithPreviewMixin, XBlockWithTranslationServiceMixin, Stu
frag = self.mentoring_view({})
return {'html': frag.content}
@lazy
def student_input(self):
"""
The student input value, or a default which may come from another block.
Read from a Django model, since XBlock API doesn't yet support course-scoped
fields or generating instructor reports across many student responses.
"""
# Only attempt to locate a model object for this block when the answer has a name
if not self.name:
return ''
student_input = self.get_model_object().student_input
# Default value can be set from another answer's current value
if not student_input and hasattr(self, 'default_from') and self.default_from:
student_input = self.get_model_object(name=self.default_from).student_input
return student_input
def validate_field_data(self, validation, data):
"""
Validate this block's field data.
......@@ -115,20 +132,6 @@ class AnswerMixin(XBlockWithPreviewMixin, XBlockWithTranslationServiceMixin, Stu
if not data.name:
add_error(u"A Question ID is required.")
def build_user_state_data(self, context=None):
"""
Returns a JSON representation of the student data of this XBlock,
retrievable from the Course Block API.
"""
result = super(AnswerMixin, self).build_user_state_data(context)
answer_data = self.get_model_object()
result["answer_data"] = {
"student_input": answer_data.student_input,
"created_on": answer_data.created_on,
"modified_on": answer_data.modified_on,
}
return result
@XBlock.needs("i18n")
class AnswerBlock(SubmittingXBlockMixin, AnswerMixin, QuestionMixin, StudioEditableXBlockMixin, XBlock):
......@@ -170,25 +173,6 @@ class AnswerBlock(SubmittingXBlockMixin, AnswerMixin, QuestionMixin, StudioEdita
editable_fields = ('question', 'name', 'min_characters', 'weight', 'default_from', 'display_name', 'show_title')
@lazy
def student_input(self):
"""
The student input value, or a default which may come from another block.
Read from a Django model, since XBlock API doesn't yet support course-scoped
fields or generating instructor reports across many student responses.
"""
# Only attempt to locate a model object for this block when the answer has a name
if not self.name:
return ''
student_input = self.get_model_object().student_input
# Default value can be set from another answer's current value
if not student_input and self.default_from:
student_input = self.get_model_object(name=self.default_from).student_input
return student_input
def mentoring_view(self, context=None):
""" Render this XBlock within a mentoring block. """
context = context.copy() if context else {}
......@@ -282,6 +266,7 @@ class AnswerBlock(SubmittingXBlockMixin, AnswerMixin, QuestionMixin, StudioEdita
"""
return {
'id': self.name,
'block_id': unicode(self.scope_ids.usage_id),
'type': self.CATEGORY,
'weight': self.weight,
'question': self.question,
......
......@@ -75,6 +75,7 @@ class ChoiceBlock(
retrievable from the Course Block API.
"""
return {
'block_id': unicode(self.scope_ids.usage_id),
'value': self.value,
'content': self.content,
}
......
......@@ -56,6 +56,8 @@ class CompletionBlock(
"""
CATEGORY = 'pb-completion'
STUDIO_LABEL = _(u'Completion')
USER_STATE_FIELDS = ['student_value']
answerable = True
question = String(
......@@ -113,6 +115,7 @@ class CompletionBlock(
"""
return {
'id': self.name,
'block_id': unicode(self.scope_ids.usage_id),
'type': self.CATEGORY,
'question': self.question,
'answer': self.answer,
......
......@@ -51,6 +51,7 @@ class MCQBlock(SubmittingXBlockMixin, StudentViewUserStateMixin, QuestionnaireAb
"""
CATEGORY = 'pb-mcq'
STUDIO_LABEL = _(u"Multiple Choice Question")
USER_STATE_FIELDS = ['num_attempts', 'student_choice']
message = String(
display_name=_("Message"),
......@@ -75,7 +76,7 @@ class MCQBlock(SubmittingXBlockMixin, StudentViewUserStateMixin, QuestionnaireAb
list_values_provider=QuestionnaireAbstractBlock.choice_values_provider,
list_style='set', # Underered, unique items. Affects the UI editor.
)
editable_fields = QuestionnaireAbstractBlock.editable_fields + ('message', 'correct_choices', )
editable_fields = QuestionnaireAbstractBlock.editable_fields + ('message', 'correct_choices',)
def describe_choice_correctness(self, choice_value):
if choice_value in self.correct_choices:
......@@ -175,6 +176,7 @@ class MCQBlock(SubmittingXBlockMixin, StudentViewUserStateMixin, QuestionnaireAb
"""
return {
'id': self.name,
'block_id': unicode(self.scope_ids.usage_id),
'type': self.CATEGORY,
'question': self.question,
'message': self.message,
......@@ -183,10 +185,7 @@ class MCQBlock(SubmittingXBlockMixin, StudentViewUserStateMixin, QuestionnaireAb
for choice in self.human_readable_choices
],
'weight': self.weight,
'tips': [
{'content': tip.content, 'for_choices': tip.values}
for tip in self.get_tips()
],
'tips': [tip.student_view_data() for tip in self.get_tips()],
}
......
......@@ -37,7 +37,7 @@ from xblock.validation import ValidationMessage
from .message import MentoringMessageBlock, get_message_label
from .mixins import (
_normalize_id, QuestionMixin, MessageParentMixin, StepParentMixin, XBlockWithTranslationServiceMixin,
StudentViewUserStateMixin)
StudentViewUserStateMixin, StudentViewUserStateResultsTransformerMixin)
from .step_review import ReviewStepBlock
from xblockutils.helpers import child_isinstance
......@@ -229,7 +229,10 @@ class BaseMentoringBlock(
return 1.0
class MentoringBlock(BaseMentoringBlock, StudioContainerWithNestedXBlocksMixin, StepParentMixin):
class MentoringBlock(
StudentViewUserStateResultsTransformerMixin,
BaseMentoringBlock, StudioContainerWithNestedXBlocksMixin, StepParentMixin,
):
"""
An XBlock providing mentoring capabilities
......@@ -239,6 +242,7 @@ class MentoringBlock(BaseMentoringBlock, StudioContainerWithNestedXBlocksMixin,
ok to continue.
"""
# Content
USER_STATE_FIELDS = ['completed', 'num_attempts', 'student_results']
MENTORING_MODES = ('standard', 'assessment')
mode = String(
display_name=_("Mode"),
......@@ -918,6 +922,7 @@ class MentoringBlock(BaseMentoringBlock, StudioContainerWithNestedXBlocksMixin,
if hasattr(block, 'student_view_data'):
components.append(block.student_view_data())
return {
'block_id': unicode(self.scope_ids.usage_id),
'max_attempts': self.max_attempts,
'extended_feedback': self.extended_feedback,
'feedback_label': self.feedback_label,
......@@ -937,6 +942,8 @@ class MentoringWithExplicitStepsBlock(BaseMentoringBlock, StudioContainerWithNes
"""
An XBlock providing mentoring capabilities with explicit steps
"""
USER_STATE_FIELDS = ['num_attempts']
# Content
extended_feedback = Boolean(
display_name=_("Extended feedback"),
......@@ -963,6 +970,11 @@ class MentoringWithExplicitStepsBlock(BaseMentoringBlock, StudioContainerWithNes
editable_fields = ('display_name', 'max_attempts', 'extended_feedback', 'weight')
def build_user_state_data(self, context=None):
user_state_data = super(MentoringWithExplicitStepsBlock, self).build_user_state_data()
user_state_data['active_step'] = self.active_step_safe
return user_state_data
@lazy
def question_ids(self):
"""
......@@ -1259,6 +1271,7 @@ class MentoringWithExplicitStepsBlock(BaseMentoringBlock, StudioContainerWithNes
return {
'title': self.display_name,
'block_id': unicode(self.scope_ids.usage_id),
'show_title': self.show_title,
'weight': self.weight,
'extended_feedback': self.extended_feedback,
......
......@@ -185,20 +185,37 @@ class NoSettingsMixin(object):
class StudentViewUserStateMixin(object):
"""
Mixin to provide student_view_user_state view
Mixin to provide student_view_user_state view.
To prevent unnecessary overloading of the build_user_state_data method,
you may specify `USER_STATE_FIELDS` to customise build_user_state_data
and student_view_user_state output.
"""
NESTED_BLOCKS_KEY = "components"
INCLUDE_SCOPES = (Scope.user_state, Scope.user_info, Scope.preferences)
USER_STATE_FIELDS = []
def transforms(self):
"""
Return a dict where keys are fields to transform, and values are
transform functions that accept a value to to transform as the
only argument.
"""
return {}
def build_user_state_data(self, context=None):
"""
Returns a JSON representation of the student data of this XBlock,
Returns a dictionary of the student data of this XBlock,
retrievable from the Course Block API.
"""
result = {}
transforms = self.transforms()
for _, field in self.fields.iteritems():
if field.scope in self.INCLUDE_SCOPES:
result[field.name] = field.read_from(self)
# Only insert fields if their scopes and field names match
if field.scope in self.INCLUDE_SCOPES and field.name in self.USER_STATE_FIELDS:
transformer = transforms.get(field.name, lambda value: value)
result[field.name] = transformer(field.read_from(self))
if getattr(self, "has_children", False):
components = {}
......@@ -221,3 +238,36 @@ class StudentViewUserStateMixin(object):
json_result = json.dumps(result, cls=DateTimeEncoder)
return webob.response.Response(body=json_result, content_type='application/json')
class StudentViewUserStateResultsTransformerMixin(object):
"""
A convenient way for MentoringBlock and MentoringStepBlock to share
student_results transform code.
Needs to be used alongside StudentViewUserStateMixin.
"""
def transforms(self):
return {
'student_results': self.transform_student_results
}
def transform_student_results(self, student_results):
"""
Remove tips, since they are already in student_view_data.
"""
for _name, current_student_results in student_results:
for choice in current_student_results.get('choices', []):
self.delete_key(choice, 'tips')
self.delete_key(current_student_results, 'tips')
return student_results
def delete_key(self, dictionary, key):
"""
Safely delete `key` from `dictionary`.
"""
try:
del dictionary[key]
except KeyError:
pass
return dictionary
......@@ -24,10 +24,10 @@ import logging
from xblock.fields import List, Scope, Boolean, String
from xblock.validation import ValidationMessage
from xblockutils.resources import ResourceLoader
from problem_builder.mixins import StudentViewUserStateMixin
from .questionnaire import QuestionnaireAbstractBlock
from xblockutils.resources import ResourceLoader
# Globals ###########################################################
......@@ -48,6 +48,7 @@ class MRQBlock(StudentViewUserStateMixin, QuestionnaireAbstractBlock):
"""
CATEGORY = 'pb-mrq'
STUDIO_LABEL = _(u"Multiple Response Question")
USER_STATE_FIELDS = ['student_choices', ]
student_choices = List(
# Last submissions by the student
......@@ -198,6 +199,7 @@ class MRQBlock(StudentViewUserStateMixin, QuestionnaireAbstractBlock):
"""
return {
'id': self.name,
'block_id': unicode(self.scope_ids.usage_id),
'title': self.display_name,
'type': self.CATEGORY,
'weight': self.weight,
......@@ -208,8 +210,5 @@ class MRQBlock(StudentViewUserStateMixin, QuestionnaireAbstractBlock):
for choice in self.human_readable_choices
],
'hide_results': self.hide_results,
'tips': [
{'content': tip.content, 'for_choices': tip.values}
for tip in self.get_tips()
],
'tips': [tip.student_view_data() for tip in self.get_tips()],
}
......@@ -32,6 +32,7 @@ from xblockutils.resources import ResourceLoader
from xblockutils.studio_editable import (
StudioEditableXBlockMixin, StudioContainerWithNestedXBlocksMixin, XBlockWithPreviewMixin
)
from .mixins import StudentViewUserStateMixin
from .sub_api import sub_api
......@@ -59,7 +60,10 @@ def _normalize_id(key):
@XBlock.needs('i18n')
@XBlock.wants('user')
class PlotBlock(StudioEditableXBlockMixin, StudioContainerWithNestedXBlocksMixin, XBlockWithPreviewMixin, XBlock):
class PlotBlock(
StudioEditableXBlockMixin, StudioContainerWithNestedXBlocksMixin, XBlockWithPreviewMixin, XBlock,
StudentViewUserStateMixin,
):
"""
XBlock that displays plot that summarizes answers to scale and/or rating questions.
"""
......@@ -239,6 +243,12 @@ class PlotBlock(StudioEditableXBlockMixin, StudioContainerWithNestedXBlocksMixin
def average_claims_json(self):
return json.dumps(self.average_claims)
def build_user_state_data(self, context=None):
user_state_data = super(PlotBlock, self).build_user_state_data()
user_state_data['default_claims'] = self.default_claims
user_state_data['average_claims'] = self.average_claims
return user_state_data
@XBlock.json_handler
def get_data(self, data, suffix):
return {
......
......@@ -57,6 +57,8 @@ class SliderBlock(
"""
CATEGORY = 'pb-slider'
STUDIO_LABEL = _(u"Ranged Value Slider")
USER_STATE_FIELDS = ['student_value']
answerable = True
min_label = String(
......@@ -125,6 +127,7 @@ class SliderBlock(
def student_view_data(self, context=None):
return {
'id': self.name,
'block_id': unicode(self.scope_ids.usage_id),
'type': self.CATEGORY,
'question': self.question,
'min_label': self.min_label,
......
......@@ -33,7 +33,8 @@ from xblockutils.studio_editable import (
from problem_builder.answer import AnswerBlock, AnswerRecapBlock
from problem_builder.completion import CompletionBlock
from problem_builder.mcq import MCQBlock, RatingBlock
from problem_builder.mixins import EnumerableChildMixin, StepParentMixin, StudentViewUserStateMixin
from problem_builder.mixins import (
EnumerableChildMixin, StepParentMixin, StudentViewUserStateMixin, StudentViewUserStateResultsTransformerMixin)
from problem_builder.mrq import MRQBlock
from problem_builder.plot import PlotBlock
from problem_builder.slider import SliderBlock
......@@ -70,7 +71,8 @@ class Correctness(object):
@XBlock.needs('i18n')
class MentoringStepBlock(
StudioEditableXBlockMixin, StudioContainerWithNestedXBlocksMixin, XBlockWithPreviewMixin,
EnumerableChildMixin, StepParentMixin, StudentViewUserStateMixin, XBlock
EnumerableChildMixin, StepParentMixin, StudentViewUserStateResultsTransformerMixin,
StudentViewUserStateMixin, XBlock,
):
"""
An XBlock for a step.
......@@ -78,6 +80,7 @@ class MentoringStepBlock(
CAPTION = _(u"Step")
STUDIO_LABEL = _(u"Mentoring Step")
CATEGORY = 'sb-step'
USER_STATE_FIELDS = ['student_results']
# Settings
display_name = String(
......@@ -279,6 +282,7 @@ class MentoringStepBlock(
components.append(child.student_view_data(context))
return {
'block_id': unicode(self.scope_ids.usage_id),
'type': self.CATEGORY,
'title': self.display_name_with_default,
'show_title': self.show_title,
......
......@@ -111,6 +111,7 @@ class ConditionalMessageBlock(
def student_view_data(self, context=None):
return {
'block_id': unicode(self.scope_ids.usage_id),
'type': self.CATEGORY,
'content': self.content,
'score_condition': self.score_condition,
......@@ -160,9 +161,8 @@ class ScoreSummaryBlock(XBlockWithTranslationServiceMixin, XBlockWithPreviewMixi
return Fragment(html)
def student_view_data(self, context=None):
context = context or {}
return {
'block_id': unicode(self.scope_ids.usage_id),
'type': self.CATEGORY,
}
......@@ -216,6 +216,7 @@ class PerQuestionFeedbackBlock(XBlockWithTranslationServiceMixin, XBlockWithPrev
def student_view_data(self, context=None):
return {
'block_id': unicode(self.scope_ids.usage_id),
'type': self.CATEGORY,
}
......@@ -312,6 +313,7 @@ class ReviewStepBlock(
components.append(child.student_view_data(context))
return {
'block_id': unicode(self.scope_ids.usage_id),
'type': self.CATEGORY,
'title': self.display_name,
'components': components,
......
......@@ -78,11 +78,7 @@ class TestAnswerMixin(unittest.TestCase):
student_view_user_state = answer_mixin.build_user_state_data()
expected_user_state_data = {
"answer_data": {
"student_input": existing_model.student_input,
"created_on": existing_model.created_on,
"modified_on": existing_model.modified_on,
}
}
self.assertEqual(student_view_user_state, expected_user_state_data)
......@@ -103,10 +99,6 @@ class TestAnswerMixin(unittest.TestCase):
parsed_student_state = json.loads(student_view_user_state.body)
expected_user_state_data = {
"answer_data": {
"student_input": existing_model.student_input,
"created_on": existing_model.created_on.isoformat(),
"modified_on": existing_model.modified_on.isoformat(),
}
}
self.assertEqual(parsed_student_state, expected_user_state_data)
......@@ -93,9 +93,56 @@ class TestStudentViewUserStateMixin(unittest.TestCase):
other_fields = {"setting": "setting", "content": "content", "user_state_summary": "Something"}
block_fields = self._merge_dicts(user_fields, other_fields)
block = self._build_block(XBlockNoChildrenWithUserState, block_fields)
block.USER_STATE_FIELDS = user_fields.keys()
self.assertEqual(block.build_user_state_data(), user_fields)
def test_only_shows_whitelisted_fields(self):
user_fields = {
"answer_1": "AAAA",
"answer_2": False,
}
other_fields = {"setting": "setting", "content": "content", "user_state_summary": "Something"}
block_fields = self._merge_dicts(user_fields, other_fields)
block = self._build_block(XBlockNoChildrenWithUserState, block_fields)
block.USER_STATE_FIELDS = ['answer_1']
self.assertEqual(block.build_user_state_data(), {
'answer_1': 'AAAA'
})
def test_transform(self):
"""
Transform should only affect fields listed in USER_STATE_FIELDS,
and should only return functions that accept one parameter.
"""
block_fields = {
"answer_1": "AAAA",
"answer_2": False,
}
block = self._build_block(XBlockNoChildrenWithUserState, block_fields)
block.USER_STATE_FIELDS = ['answer_1']
def transforms():
return {
'answer_1': lambda value: value.replace('A', 'B'),
'answer_2': lambda value: True
}
block.transforms = transforms
self.assertEqual(block.build_user_state_data(), {
'answer_1': 'BBBB'
})
def bad_transforms():
return {
'answer_1': lambda: 'Fail'
}
block.transforms = bad_transforms
with self.assertRaises(TypeError):
block.build_user_state_data()
def test_children_empty_no_user_state(self):
block = self._build_block(XBlockChildrenNoUserState, {"scope_settings": "qwe", "scope_content": "ASD"})
self.assertEqual(block.children, []) # precondition
......@@ -131,6 +178,8 @@ class TestStudentViewUserStateMixin(unittest.TestCase):
"user_info_1": "John",
"user_info_2": datetime(2017, 1, 2, 3, 4, 5, tzinfo=pytz.UTC)
}
block.USER_STATE_FIELDS = user_fields1.keys()
user_fields2 = {
"answer_1": "BBBB",
"answer_2": True,
......@@ -151,7 +200,7 @@ class TestStudentViewUserStateMixin(unittest.TestCase):
student_user_state = block.build_user_state_data()
expected = {"components": {"child1": user_fields1, "child2": user_fields2}}
expected = {"components": {"child1": {}, "child2": {}}}
self.assertEqual(student_user_state, expected)
def test_user_state_at_parent_and_children(self):
......@@ -165,6 +214,7 @@ class TestStudentViewUserStateMixin(unittest.TestCase):
"user_info_2": datetime(2017, 1, 2, 3, 4, 5, tzinfo=pytz.UTC)
}
block = self._build_block(XBlockChildrenUserState, self._merge_dicts(user_fields, other_fields))
block.USER_STATE_FIELDS = user_fields.keys()
nested_user_fields = {
"answer_1": "AAAA",
......@@ -187,7 +237,7 @@ class TestStudentViewUserStateMixin(unittest.TestCase):
student_user_state = block.build_user_state_data()
expected = user_fields.copy()
expected["components"] = {"child1": nested_user_fields}
expected["components"] = {"child1": {}}
self.assertEqual(student_user_state, expected)
def test_user_state_handler(self):
......@@ -201,6 +251,7 @@ class TestStudentViewUserStateMixin(unittest.TestCase):
"user_info_2": datetime(2017, 1, 2, 3, 4, 5, tzinfo=pytz.UTC)
}
block = self._build_block(XBlockChildrenUserState, self._merge_dicts(user_fields, other_fields))
block.USER_STATE_FIELDS = user_fields.keys()
nested_user_fields = {
"answer_1": "AAAA",
......@@ -220,12 +271,11 @@ class TestStudentViewUserStateMixin(unittest.TestCase):
self.assertEqual(block.children, nested.keys())
self.assertEqual(self._runtime.get_block("child1"), user_state)
block.USER_STATE_FIELDS = ['answer_1', 'answer_2', 'preference_1', 'preference_2', 'user_info_1', 'user_info_2']
student_user_state_response = block.student_view_user_state()
student_user_state = json.loads(student_user_state_response.body)
expected = user_fields.copy()
expected["user_info_2"] = expected["user_info_2"].isoformat()
nested_copy = nested_user_fields.copy()
nested_copy["user_info_2"] = nested_copy["user_info_2"].isoformat()
expected["components"] = {"child1": nested_copy}
expected["components"] = {"child1": {}}
self.assertEqual(student_user_state, expected)
......@@ -24,7 +24,7 @@ class TestMRQBlock(BlockWithChildrenTestMixin, unittest.TestCase):
self.assertListEqual(
block.student_view_data().keys(),
['hide_results', 'tips', 'weight', 'title', 'question', 'message', 'type', 'id', 'choices'])
['hide_results', 'tips', 'block_id', 'weight', 'title', 'question', 'message', 'type', 'id', 'choices'])
@ddt.ddt
......@@ -141,13 +141,14 @@ class TestMentoringBlock(BlockWithChildrenTestMixin, unittest.TestCase):
children_by_id = {child.block_id: child for child in children}
block_data = {'children': children}
block_data.update(shared_data)
block = MentoringBlock(Mock(), DictFieldData(block_data), Mock())
block = MentoringBlock(Mock(usage_id=1), DictFieldData(block_data), Mock(usage_id=1))
block.runtime = Mock(
get_block=lambda block: children_by_id[block.block_id],
load_block_type=lambda block: Mock,
id_reader=Mock(get_definition_id=lambda block: block, get_block_type=lambda block: block),
)
expected = {
'block_id': '1',
'components': [
'child_a_json',
],
......
......@@ -11,7 +11,6 @@ from .utils import BlockWithChildrenTestMixin
class TestMentoringBlock(BlockWithChildrenTestMixin, unittest.TestCase):
def test_student_view_data(self):
blocks_by_id = {}
......@@ -90,6 +89,7 @@ class TestMentoringBlock(BlockWithChildrenTestMixin, unittest.TestCase):
make_block(ConditionalMessageBlock, conditional_message_data, for_parent=review_step)
expected = {
'block_id': u'1',
'title': step_builder_data['display_name'],
'show_title': step_builder_data['show_title'],
'weight': step_builder_data['weight'],
......@@ -97,6 +97,7 @@ class TestMentoringBlock(BlockWithChildrenTestMixin, unittest.TestCase):
'extended_feedback': step_builder_data['extended_feedback'],
'components': [
{
'block_id': '2',
'type': 'sb-step',
'title': step_data['display_name'],
'show_title': step_data['show_title'],
......@@ -105,13 +106,16 @@ class TestMentoringBlock(BlockWithChildrenTestMixin, unittest.TestCase):
'components': ['child_a_json'],
},
{
'block_id': '3',
'type': 'sb-review-step',
'title': review_step_data['display_name'],
'components': [
{
'block_id': '4',
'type': 'sb-review-score',
},
{
'block_id': '5',
'type': 'sb-conditional-message',
'content': conditional_message_data['content'],
'score_condition': conditional_message_data['score_condition'],
......
......@@ -95,6 +95,12 @@ class TipBlock(StudioEditableXBlockMixin, XBlockWithTranslationServiceMixin, XBl
})
return Fragment(html)
def student_view_data(self, context=None):
return {
'content': self.content,
'for_choices': self.values,
}
def student_view(self, context=None):
""" Normal view of this XBlock, identical to mentoring_view """
return self.mentoring_view(context)
......
......@@ -71,7 +71,7 @@ BLOCKS = [
setup(
name='xblock-problem-builder',
version='2.7.2',
version='2.7.3',
description='XBlock - Problem Builder',
packages=find_packages(),
install_requires=[
......
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