Commit a88e37be by bradmerlin Committed by GitHub

Merge pull request #162 from open-craft/bradmerlin/upgrade-student_view_user_state

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