Commit 0931ca2c by Uman Shahzad Committed by Braden MacDonald

Fix Swipe component to match desired specs and API behavior

parent 1dc9578a
......@@ -371,40 +371,29 @@ Swipeable Binary Response (`pb-swipe`)
- `block_id`: (string) The XBlock's usage ID
- `display_name`: (string) The XBlock's display name
- `type`: (string): The XBlock's identifier, "pb-swipe"
- `question`: (string) The question contents
- `message`: (string) Feedback provided when submitting
- `img_url`: (string) URL to an associated image
- `weight`: (float) Overall value of the question
- `choices`: (array) A list of objects providing info about available
choices. See below for more info.
- `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.
#### `choices`
Each item in the `choices` array contains these fields:
- `value`: (string) The value of the choice.
- `content`: (string) The description of the choice
- `text`: (string) The text to display on the card
- `img_url`: (string) URL of the image to display as the background of the card
- `correct`: (boolean) Whether the student's swipe is correct or not
### `student_view_user_state`
- `student_choice`: (string) The value of the last submitted choice.
- `student_choice`: (boolean) The value of the last submitted choice.
### POST Submit Data
When submitting the problem the data should be a single object containing the
`"value"` property which has the value of the selected choice.
Example: `{"value": "blue"}`
`"value"` property which has a boolean value indicating which swipe the student took:
- Right -> True
- Left -> False
Example: `{"value": true}` if the student made a *rightwards* swipe.
The returned result will contain the following:
- `submission`: (boolean) The value just POST'd.
- `status`: (string) `"correct"` if the swipe is correct, `"incorrect"` otherwise.
- `score`: (integer) 1 if the swipe is correct, otherwise 0.
Multiple Response Question (`pb-mrq`)
-------------------------------------
......
......@@ -260,7 +260,3 @@
.mentoring .copyright a {
color: #69C0E8;
}
.swipe-img {
max-width: 100%;
}
......@@ -19,18 +19,20 @@
#
# Imports ###########################################################
import logging
from xblock.fields import Scope, String
from xblock.validation import ValidationMessage
from xblock.core import XBlock
from xblock.fields import Boolean, Scope, String
from xblock.fragment import Fragment
from xblockutils.resources import ResourceLoader
from xblockutils.studio_editable import StudioEditableXBlockMixin, XBlockWithPreviewMixin
from .mixins import StudentViewUserStateMixin
from .questionnaire import QuestionnaireAbstractBlock
from .sub_api import SubmittingXBlockMixin
from .mixins import QuestionMixin, StudentViewUserStateMixin
# Globals ###########################################################
log = logging.getLogger(__name__)
loader = ResourceLoader(__name__)
......@@ -41,53 +43,51 @@ def _(text):
# Classes ###########################################################
class SwipeBlock(SubmittingXBlockMixin, StudentViewUserStateMixin, QuestionnaireAbstractBlock):
@XBlock.needs("i18n")
class SwipeBlock(
QuestionMixin, StudioEditableXBlockMixin, StudentViewUserStateMixin, XBlockWithPreviewMixin, XBlock,
):
"""
An XBlock used to ask binary-choice questions with a swiping interface
"""
CATEGORY = 'pb-swipe'
STUDIO_LABEL = _(u"Swipeable Binary Choice Question")
USER_STATE_FIELDS = ['num_attempts', 'student_choice']
message = String(
display_name=_("Message"),
help=_(
"General feedback provided when submitting. "
"(This is not shown if there is a more specific feedback tip for the choice selected by the learner.)"
),
scope=Scope.content,
default=""
)
USER_STATE_FIELDS = ['student_choice']
student_choice = String(
# {Last input submitted by the student
text = String(
display_name=_("Text"),
help=_("Text to display on this card. The student must determine if this statement is true or false."),
scope=Scope.content,
default="",
scope=Scope.user_state,
multiline_editor=True,
)
correct_choice = String(
display_name=_("Correct Choice"),
help=_("Specify the value that students may select for this question to be considered correct."),
scope=Scope.content,
values_provider=QuestionnaireAbstractBlock.choice_values_provider,
)
img_url = String(
display_name=_("Image"),
help=_(
"Specify the URL of an image associated with this question."
),
help=_("Specify the URL of an image associated with this question."),
scope=Scope.content,
default=""
)
editable_fields = QuestionnaireAbstractBlock.editable_fields + ('message', 'correct_choice', 'img_url',)
correct = Boolean(
display_name=_("Correct Choice"),
help=_("Specifies whether the card is correct."),
scope=Scope.content,
)
student_choice = Boolean(
scope=Scope.user_state,
help=_("Last input submitted by the student.")
)
editable_fields = ('display_name', 'text', 'img_url', 'correct')
def calculate_results(self, submission):
correct = self.correct_choice == submission
correct = submission == self.correct
return {
'submission': submission,
'message': self.message_formatted,
'status': 'correct' if correct else 'incorrect',
'weight': self.weight,
'score': 1 if correct else 0,
}
......@@ -99,33 +99,12 @@ class SwipeBlock(SubmittingXBlockMixin, StudentViewUserStateMixin, Questionnaire
def submit(self, submission):
log.debug(u'Received Swipe submission: "%s"', submission)
result = self.calculate_results(submission['value'])
# We expect to receive a boolean indicating the swipe the student made (left -> false, right -> true)
self.student_choice = submission['value']
result = self.calculate_results(self.student_choice)
log.debug(u'Swipe submission result: %s', result)
return result
def validate_field_data(self, validation, data):
"""
Validate this block's field data.
"""
super(SwipeBlock, self).validate_field_data(validation, data)
def add_error(msg):
validation.add(ValidationMessage(ValidationMessage.ERROR, msg))
if len(self.all_choice_values) == 0:
# Let's not set an error until at least one choice is added
return
if len(self.all_choice_values) != 2:
add_error(
self._(u"You must have exactly two choices.")
)
if not data.correct_choice:
add_error(
self._(u"You must indicate the correct answer, or the student will always get this question wrong.")
)
def student_view_data(self, context=None):
"""
Returns a JSON representation of the student_view of this XBlock,
......@@ -136,15 +115,9 @@ class SwipeBlock(SubmittingXBlockMixin, StudentViewUserStateMixin, Questionnaire
'block_id': unicode(self.scope_ids.usage_id),
'display_name': self.display_name_with_default,
'type': self.CATEGORY,
'question': self.question,
'message': self.message,
'text': self.text,
'img_url': self.expand_static_url(self.img_url),
'choices': [
{'value': choice['value'], 'content': choice['display_name']}
for choice in self.human_readable_choices
],
'weight': self.weight,
'tips': [tip.student_view_data() for tip in self.get_tips()],
'correct': self.correct,
}
def expand_static_url(self, url):
......@@ -166,6 +139,18 @@ class SwipeBlock(SubmittingXBlockMixin, StudentViewUserStateMixin, Questionnaire
pass
return url
@property
def expanded_img_url(self):
return self.expand_static_url(self.img_url)
def mentoring_view(self, context=None):
""" Render the swipe image, text & whether it's correct within a mentoring block question. """
return Fragment(
(
u'<img src="{img_url}" style="max-width: 100%;" />'
u'<p class="swipe-text">"{text}"</p>'
).format(
img_url=self.expand_static_url(self.img_url),
text=self.text,
)
)
def student_view(self, context=None):
""" Normal view of this XBlock, identical to mentoring_view """
return self.mentoring_view(context)
{% load i18n %}
{% if not hide_header %}
<h4 class="question-title" id="heading_{{ self.html_id }}">{{ self.display_name_with_default }}</h4>
{% endif %}
{% if self.img_url.strip %}
<img class="swipe-img" src="{{ self.expanded_img_url }}" alt="" />
{% endif %}
<fieldset class="choices questionnaire" id="{{ self.html_id }}">
<legend class="question field-group-hd">{{ self.question|safe }}</legend>
<div class="choices-list">
{% for choice in custom_choices %}
<div class="choice" aria-live="polite" aria-atomic="true">
<label class="choice-label"
aria-describedby="feedback_{{ self.html_id }} choice_tips_{{ self.html_id }}-{{ forloop.counter }}">
<span class="choice-result fa icon-2x"
aria-label=""
data-label_correct="{% trans "Correct" %}"
data-label_incorrect="{% trans "Incorrect" %}"></span>
<span class="choice-selector">
<input type="radio" name="{{ self.name }}" value="{{ choice.value }}"
{% if self.student_choice == choice.value and not hide_prev_answer %} checked{% endif %}
/>
</span>
<span class="choice-label-text">{{ choice.content|safe }}</span>
</label>
<div class="choice-tips-container">
<div class="choice-tips" id="choice_tips_{{ self.html_id }}-{{ forloop.counter }}"></div>
</div>
</div>
{% endfor %}
<div class="feedback" id="feedback_{{ self.html_id }}"></div>
</div>
</fieldset>
......@@ -72,7 +72,7 @@ BLOCKS = [
setup(
name='xblock-problem-builder',
version='2.7.9',
version='2.7.10',
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