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`)
### `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()],
}
......@@ -229,7 +228,7 @@ class RatingBlock(MCQBlock):
display_names = ["1 - {}".format(self.low), "2", "3", "4", "5 - {}".format(self.high)]
return [
{"display_name": dn, "value": val} for val, dn in zip(self.FIXED_VALUES, display_names)
] + super(RatingBlock, self).human_readable_choices
] + super(RatingBlock, self).human_readable_choices
def get_author_edit_view_fragment(self, context):
"""
......
......@@ -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
......@@ -139,14 +140,14 @@ class MRQBlock(StudentViewUserStateMixin, QuestionnaireAbstractBlock):
choice_result = {
'value': choice.value,
'selected': choice_selected,
}
}
# Only include tips/results in returned response if we want to display them
if not self.hide_results:
loader = ResourceLoader(__name__)
choice_result['completed'] = choice_completed
choice_result['tips'] = loader.render_template('templates/html/tip_choice_group.html', {
'tips_html': choice_tips_html,
})
})
results.append(choice_result)
......@@ -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
......@@ -69,8 +70,9 @@ class Correctness(object):
@XBlock.needs('i18n')
class MentoringStepBlock(
StudioEditableXBlockMixin, StudioContainerWithNestedXBlocksMixin, XBlockWithPreviewMixin,
EnumerableChildMixin, StepParentMixin, StudentViewUserStateMixin, XBlock
StudioEditableXBlockMixin, StudioContainerWithNestedXBlocksMixin, XBlockWithPreviewMixin,
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,
}
"student_input": existing_model.student_input,
}
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(),
}
"student_input": existing_model.student_input,
}
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