Commit 99ad1917 by Eugeny Kolpakov Committed by GitHub

[MCKIN-5253] - Student_view_user_data (#153)

[MCKIN-5253] Added student_view_user_state handler
parents b46c2c80 7dced20b
......@@ -32,7 +32,7 @@ from xblock.validation import ValidationMessage
from xblockutils.resources import ResourceLoader
from xblockutils.studio_editable import StudioEditableXBlockMixin, XBlockWithPreviewMixin
from problem_builder.sub_api import SubmittingXBlockMixin, sub_api
from .mixins import QuestionMixin, XBlockWithTranslationServiceMixin
from .mixins import QuestionMixin, XBlockWithTranslationServiceMixin, StudentViewUserStateMixin
import uuid
......@@ -49,7 +49,7 @@ def _(text):
# Classes ###########################################################
class AnswerMixin(XBlockWithPreviewMixin, XBlockWithTranslationServiceMixin):
class AnswerMixin(XBlockWithPreviewMixin, XBlockWithTranslationServiceMixin, StudentViewUserStateMixin):
"""
Mixin to give an XBlock the ability to read/write data to the Answers DB table.
"""
......@@ -115,6 +115,20 @@ class AnswerMixin(XBlockWithPreviewMixin, XBlockWithTranslationServiceMixin):
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):
......
......@@ -29,7 +29,7 @@ from xblock.fragment import Fragment
from xblock.validation import ValidationMessage
from xblockutils.studio_editable import StudioEditableXBlockMixin, XBlockWithPreviewMixin
from problem_builder.mixins import XBlockWithTranslationServiceMixin
from problem_builder.mixins import XBlockWithTranslationServiceMixin, StudentViewUserStateMixin
# Make '_' a no-op so we can scrape strings
......@@ -40,7 +40,10 @@ def _(text):
@XBlock.needs("i18n")
class ChoiceBlock(StudioEditableXBlockMixin, XBlockWithPreviewMixin, XBlockWithTranslationServiceMixin, XBlock):
class ChoiceBlock(
StudioEditableXBlockMixin, XBlockWithPreviewMixin, XBlockWithTranslationServiceMixin, StudentViewUserStateMixin,
XBlock
):
"""
Custom choice of an answer for a MCQ/MRQ
"""
......
......@@ -28,7 +28,7 @@ from xblock.fragment import Fragment
from xblockutils.studio_editable import StudioEditableXBlockMixin
from xblockutils.resources import ResourceLoader
from .mixins import QuestionMixin, XBlockWithTranslationServiceMixin
from .mixins import QuestionMixin, XBlockWithTranslationServiceMixin, StudentViewUserStateMixin
from .sub_api import sub_api, SubmittingXBlockMixin
......@@ -47,7 +47,8 @@ def _(text):
@XBlock.needs('i18n')
class CompletionBlock(
SubmittingXBlockMixin, QuestionMixin, StudioEditableXBlockMixin, XBlockWithTranslationServiceMixin, XBlock
SubmittingXBlockMixin, QuestionMixin, StudioEditableXBlockMixin, XBlockWithTranslationServiceMixin,
StudentViewUserStateMixin, XBlock
):
"""
An XBlock used by students to indicate that they completed a given task.
......
......@@ -27,6 +27,7 @@ from xblock.fragment import Fragment
from xblock.validation import ValidationMessage
from xblockutils.resources import ResourceLoader
from problem_builder.mixins import StudentViewUserStateMixin
from .questionnaire import QuestionnaireAbstractBlock
from .sub_api import sub_api, SubmittingXBlockMixin
......@@ -44,7 +45,7 @@ def _(text):
# Classes ###########################################################
class MCQBlock(SubmittingXBlockMixin, QuestionnaireAbstractBlock):
class MCQBlock(SubmittingXBlockMixin, StudentViewUserStateMixin, QuestionnaireAbstractBlock):
"""
An XBlock used to ask multiple-choice questions
"""
......
......@@ -36,8 +36,8 @@ from xblock.validation import ValidationMessage
from .message import MentoringMessageBlock, get_message_label
from .mixins import (
_normalize_id, QuestionMixin, MessageParentMixin, StepParentMixin, XBlockWithTranslationServiceMixin
)
_normalize_id, QuestionMixin, MessageParentMixin, StepParentMixin, XBlockWithTranslationServiceMixin,
StudentViewUserStateMixin)
from .step_review import ReviewStepBlock
from xblockutils.helpers import child_isinstance
......@@ -91,6 +91,7 @@ PARTIAL = 'partial'
class BaseMentoringBlock(
XBlock, XBlockWithTranslationServiceMixin, XBlockWithSettingsMixin,
StudioEditableXBlockMixin, ThemableXBlockMixin, MessageParentMixin,
StudentViewUserStateMixin,
):
"""
An XBlock that defines functionality shared by mentoring blocks.
......
import json
import webob
from lazy import lazy
from problem_builder.tests.unit.utils import DateTimeEncoder
from xblock.core import XBlock
from xblock.fields import String, Boolean, Float, Scope, UNIQUE_ID
from xblock.fragment import Fragment
from xblockutils.helpers import child_isinstance
......@@ -176,3 +181,43 @@ class NoSettingsMixin(object):
def studio_view(self, _context=None):
""" Studio View """
return Fragment(u'<p>{}</p>'.format(self._("This XBlock does not have any settings.")))
class StudentViewUserStateMixin(object):
"""
Mixin to provide student_view_user_state view
"""
NESTED_BLOCKS_KEY = "components"
INCLUDE_SCOPES = (Scope.user_state, Scope.user_info, Scope.preferences)
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 = {}
for _, field in self.fields.iteritems():
if field.scope in self.INCLUDE_SCOPES:
result[field.name] = field.read_from(self)
if getattr(self, "has_children", False):
components = {}
for child_id in self.children:
child = self.runtime.get_block(child_id)
if hasattr(child, 'build_user_state_data'):
components[str(child_id)] = child.build_user_state_data(context)
result[self.NESTED_BLOCKS_KEY] = components
return result
@XBlock.handler
def student_view_user_state(self, context=None, suffix=''):
"""
Returns a JSON representation of the student data of this XBlock,
retrievable from the Course Block API.
"""
result = self.build_user_state_data(context)
json_result = json.dumps(result, cls=DateTimeEncoder)
return webob.response.Response(body=json_result, content_type='application/json')
......@@ -24,6 +24,8 @@ import logging
from xblock.fields import List, Scope, Boolean, String
from xblock.validation import ValidationMessage
from problem_builder.mixins import StudentViewUserStateMixin
from .questionnaire import QuestionnaireAbstractBlock
from xblockutils.resources import ResourceLoader
......@@ -40,7 +42,7 @@ def _(text):
# Classes ###########################################################
class MRQBlock(QuestionnaireAbstractBlock):
class MRQBlock(StudentViewUserStateMixin, QuestionnaireAbstractBlock):
"""
An XBlock used to ask multiple-response questions
"""
......
......@@ -29,7 +29,7 @@ from xblock.fragment import Fragment
from xblockutils.studio_editable import StudioEditableXBlockMixin
from xblockutils.resources import ResourceLoader
from .mixins import QuestionMixin, XBlockWithTranslationServiceMixin
from .mixins import QuestionMixin, XBlockWithTranslationServiceMixin, StudentViewUserStateMixin
from .sub_api import sub_api, SubmittingXBlockMixin
......@@ -48,7 +48,8 @@ def _(text):
@XBlock.needs("i18n")
class SliderBlock(
SubmittingXBlockMixin, QuestionMixin, StudioEditableXBlockMixin, XBlockWithTranslationServiceMixin, XBlock,
SubmittingXBlockMixin, QuestionMixin, StudioEditableXBlockMixin, XBlockWithTranslationServiceMixin,
StudentViewUserStateMixin, XBlock,
):
"""
An XBlock used by students to indicate a numeric value on a sliding scale.
......
......@@ -33,7 +33,7 @@ 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
from problem_builder.mixins import EnumerableChildMixin, StepParentMixin, StudentViewUserStateMixin
from problem_builder.mrq import MRQBlock
from problem_builder.plot import PlotBlock
from problem_builder.slider import SliderBlock
......@@ -70,7 +70,7 @@ class Correctness(object):
@XBlock.needs('i18n')
class MentoringStepBlock(
StudioEditableXBlockMixin, StudioContainerWithNestedXBlocksMixin, XBlockWithPreviewMixin,
EnumerableChildMixin, StepParentMixin, XBlock
EnumerableChildMixin, StepParentMixin, StudentViewUserStateMixin, XBlock
):
"""
An XBlock for a step.
......
"""
Unit tests for AnswerMixin.
"""
import json
import unittest
from collections import namedtuple
from datetime import datetime
from django.utils.crypto import get_random_string
from problem_builder.answer import AnswerMixin
......@@ -28,6 +31,8 @@ class TestAnswerMixin(unittest.TestCase):
answer_mixin = AnswerMixin()
answer_mixin.name = name
answer_mixin.runtime = self.FakeRuntime(course_id, student_id)
answer_mixin.fields = {}
answer_mixin.has_children = False
return answer_mixin
def test_creates_model_instance(self):
......@@ -57,3 +62,51 @@ class TestAnswerMixin(unittest.TestCase):
answer_mixin = self.make_answer_mixin(course_id=course_id)
model = answer_mixin.get_model_object()
self.assertEqual(model.course_key, course_id)
def test_build_user_state_data(self):
name = 'test-course-key-2'
existing_model = Answer(
name=name,
student_id=self.anonymous_student_id,
course_key=self.course_id,
student_input="Test",
created_on=datetime(2017, 1, 2, 3, 4, 5),
modified_on=datetime(2017, 7, 8, 9, 10, 11),
)
existing_model.save()
answer_mixin = self.make_answer_mixin(name=name)
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)
def test_student_view_user_state(self):
name = 'test-course-key-3'
existing_model = Answer(
name=name,
student_id=self.anonymous_student_id,
course_key=self.course_id,
student_input="Test",
created_on=datetime(2017, 1, 2, 3, 4, 5),
modified_on=datetime(2017, 7, 8, 9, 10, 11),
)
existing_model.save()
answer_mixin = self.make_answer_mixin(name=name)
student_view_user_state = answer_mixin.student_view_user_state()
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)
import json
import unittest
from datetime import datetime
import pytz
from mock import MagicMock, Mock
from xblock.core import XBlock
from xblock.field_data import DictFieldData
from xblock.fields import String, Scope, Boolean, Integer, DateTime
from problem_builder.mixins import StudentViewUserStateMixin
class NoUserStateFieldsMixin(object):
scope_settings = String(name="Field1", scope=Scope.settings)
scope_content = String(name="Field1", scope=Scope.content)
user_state_summary = String(name="Not in the output", scope=Scope.user_state_summary)
class UserStateFieldsMixin(object):
answer_1 = String(name="state1", scope=Scope.user_state)
answer_2 = Boolean(name="state2", scope=Scope.user_state)
preference_1 = String(name="pref1", scope=Scope.preferences)
preference_2 = Integer(name="pref2", scope=Scope.preferences)
user_info_1 = String(name="info1", scope=Scope.user_info)
user_info_2 = DateTime(name="info2", scope=Scope.user_info)
class ChildrenMixin(object):
# overriding children for ease of testing
_children = []
has_children = True
@property
def children(self):
return self._children
@children.setter
def children(self, value):
self._children = value
class XBlockWithNoUserState(XBlock, NoUserStateFieldsMixin, StudentViewUserStateMixin):
pass
class XBlockNoChildrenWithUserState(XBlock, NoUserStateFieldsMixin, UserStateFieldsMixin, StudentViewUserStateMixin):
pass
class XBlockChildrenNoUserState(XBlock, NoUserStateFieldsMixin, ChildrenMixin, StudentViewUserStateMixin):
has_children = True
class XBlockChildrenUserState(
XBlock, NoUserStateFieldsMixin, UserStateFieldsMixin, ChildrenMixin, StudentViewUserStateMixin
):
has_children = True
class TestStudentViewUserStateMixin(unittest.TestCase):
def setUp(self):
self._runtime = MagicMock()
def _build_block(self, block_type, fields):
return block_type(self._runtime, DictFieldData(fields), Mock())
def _set_children(self, block, children):
block.children = children.keys()
self._runtime.get_block.side_effect = children.get
def _merge_dicts(self, dict1, dict2):
result = dict1.copy()
result.update(dict2)
return result
def test_no_user_state_returns_empty(self):
block = self._build_block(XBlockWithNoUserState, {"scope_settings": "qwe", "scope_content": "ASD"})
self.assertEqual(block.build_user_state_data(), {})
def test_no_child_blocks_with_user_state(self):
user_fields = {
"answer_1": "AAAA",
"answer_2": False,
"preference_1": "Yes",
"preference_2": 12,
"user_info_1": "John",
"user_info_2": datetime(2017, 1, 2, 3, 4, 5, tzinfo=pytz.UTC)
}
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)
self.assertEqual(block.build_user_state_data(), user_fields)
def test_children_empty_no_user_state(self):
block = self._build_block(XBlockChildrenNoUserState, {"scope_settings": "qwe", "scope_content": "ASD"})
self.assertEqual(block.children, []) # precondition
self.assertEqual(block.build_user_state_data(), {"components": {}})
def test_children_no_user_state(self):
block = self._build_block(XBlockChildrenNoUserState, {"scope_settings": "qwe", "scope_content": "ASD"})
no_user_state1 = self._build_block(XBlockWithNoUserState, {"scope_settings": "qwe", "scope_content": "ASD"})
no_user_state2 = self._build_block(XBlockWithNoUserState, {"scope_settings": "ZXC", "scope_content": "VBN"})
nested = {"child1": no_user_state1, "child2": no_user_state2}
self._set_children(block, nested)
# preconditions
self.assertEqual(block.children, nested.keys())
self.assertEqual(self._runtime.get_block("child1"), no_user_state1)
self.assertEqual(self._runtime.get_block("child2"), no_user_state2)
student_user_state = block.build_user_state_data()
expected = {"components": {"child1": {}, "child2": {}}}
self.assertEqual(student_user_state, expected)
def test_children_with_user_state(self):
block = self._build_block(XBlockChildrenNoUserState, {"scope_settings": "qwe", "scope_content": "ASD"})
other_fields = {"setting": "setting", "content": "content", "user_state_summary": "Something"}
user_fields1 = {
"answer_1": "AAAA",
"answer_2": False,
"preference_1": "Yes",
"preference_2": 12,
"user_info_1": "John",
"user_info_2": datetime(2017, 1, 2, 3, 4, 5, tzinfo=pytz.UTC)
}
user_fields2 = {
"answer_1": "BBBB",
"answer_2": True,
"preference_1": "No",
"preference_2": 7,
"user_info_1": "jane",
"user_info_2": datetime(2017, 1, 2, 3, 4, 5, tzinfo=pytz.UTC)
}
user_state1 = self._build_block(XBlockNoChildrenWithUserState, self._merge_dicts(user_fields1, other_fields))
user_state2 = self._build_block(XBlockNoChildrenWithUserState, self._merge_dicts(user_fields2, other_fields))
nested = {"child1": user_state1, "child2": user_state2}
self._set_children(block, nested)
# preconditions
self.assertEqual(block.children, nested.keys())
self.assertEqual(self._runtime.get_block("child1"), user_state1)
self.assertEqual(self._runtime.get_block("child2"), user_state2)
student_user_state = block.build_user_state_data()
expected = {"components": {"child1": user_fields1, "child2": user_fields2}}
self.assertEqual(student_user_state, expected)
def test_user_state_at_parent_and_children(self):
other_fields = {"setting": "setting", "content": "content", "user_state_summary": "Something"}
user_fields = {
"answer_1": "OOOO",
"answer_2": True,
"preference_1": "IDN",
"preference_2": 42,
"user_info_1": "Douglas",
"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))
nested_user_fields = {
"answer_1": "AAAA",
"answer_2": False,
"preference_1": "Yes",
"preference_2": 12,
"user_info_1": "John",
"user_info_2": datetime(2017, 1, 2, 3, 4, 5, tzinfo=pytz.UTC)
}
user_state = self._build_block(
XBlockNoChildrenWithUserState, self._merge_dicts(nested_user_fields, other_fields)
)
nested = {"child1": user_state}
self._set_children(block, nested)
# preconditions
self.assertEqual(block.children, nested.keys())
self.assertEqual(self._runtime.get_block("child1"), user_state)
student_user_state = block.build_user_state_data()
expected = user_fields.copy()
expected["components"] = {"child1": nested_user_fields}
self.assertEqual(student_user_state, expected)
def test_user_state_handler(self):
other_fields = {"setting": "setting", "content": "content", "user_state_summary": "Something"}
user_fields = {
"answer_1": "OOOO",
"answer_2": True,
"preference_1": "IDN",
"preference_2": 42,
"user_info_1": "Douglas",
"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))
nested_user_fields = {
"answer_1": "AAAA",
"answer_2": False,
"preference_1": "Yes",
"preference_2": 12,
"user_info_1": "John",
"user_info_2": datetime(2017, 1, 2, 3, 4, 5, tzinfo=pytz.UTC)
}
user_state = self._build_block(
XBlockNoChildrenWithUserState, self._merge_dicts(nested_user_fields, other_fields)
)
nested = {"child1": user_state}
self._set_children(block, nested)
# preconditions
self.assertEqual(block.children, nested.keys())
self.assertEqual(self._runtime.get_block("child1"), user_state)
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}
self.assertEqual(student_user_state, expected)
"""
Helper methods for testing Problem Builder / Step Builder blocks
"""
import json
from datetime import datetime, date
from mock import MagicMock, Mock, patch
from xblock.field_data import DictFieldData
......@@ -87,3 +90,11 @@ def instantiate_block(cls, fields=None):
block.children = children
block.runtime.get_block = lambda child_id: children[child_id]
return block
class DateTimeEncoder(json.JSONEncoder):
def default(self, o):
if isinstance(o, (datetime, date)):
return o.isoformat()
return json.JSONEncoder.default(self, o)
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