Commit 1dc9578a by Matjaz Gregoric Committed by GitHub

Merge pull request #172 from open-craft/josh/swipeable-questions

Add SwipeBlock for swipeable binary choice questions
parents 673a7533 60c46f35
......@@ -362,6 +362,50 @@ Identical to [MCQ questions](#multiple-choice-question-pb-mcq).
When submitting the problem the data should be equal to the string value of the
selected choice. Example: `"3"`.
Swipeable Binary Response (`pb-swipe`)
--------------------------------------
### `student_view_data`
- `id`: (string) The XBlock's ID
- `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
### `student_view_user_state`
- `student_choice`: (string) 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"}`
Multiple Response Question (`pb-mrq`)
-------------------------------------
......
......@@ -50,6 +50,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.swipe import SwipeBlock
from problem_builder.mrq import MRQBlock
from problem_builder.plot import PlotBlock
from problem_builder.slider import SliderBlock
......@@ -361,6 +362,14 @@ class MentoringBlock(
except ImportError:
pass
try:
from xblock_django.models import XBlockConfiguration
opt = XBlockConfiguration.objects.filter(name="pb-swipe")
if opt.count() and opt.first().enabled:
additional_blocks.append(SwipeBlock)
except ImportError:
pass
message_block_shims = [
NestedXBlockSpec(
MentoringMessageBlock,
......
......@@ -260,3 +260,7 @@
.mentoring .copyright a {
color: #69C0E8;
}
.swipe-img {
max-width: 100%;
}
......@@ -176,6 +176,10 @@ function RatingBlock(runtime, element) {
return MCQBlock(runtime, element);
}
function SwipeBlock(runtime, element) {
return MCQBlock(runtime, element);
}
function MRQBlock(runtime, element) {
return {
mode: null,
......
# -*- coding: utf-8 -*-
#
# Copyright (c) 2014-2015 Harvard, edX & OpenCraft
#
# This software's license gives you freedom; you can copy, convey,
# propagate, redistribute and/or modify this program under the terms of
# the GNU Affero General Public License (AGPL) as published by the Free
# Software Foundation (FSF), either version 3 of the License, or (at your
# option) any later version of the AGPL published by the FSF.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero
# General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program in a file in the toplevel directory called
# "AGPLv3". If not, see <http://www.gnu.org/licenses/>.
#
# Imports ###########################################################
import logging
from xblock.fields import Scope, String
from xblock.validation import ValidationMessage
from xblockutils.resources import ResourceLoader
from .mixins import StudentViewUserStateMixin
from .questionnaire import QuestionnaireAbstractBlock
from .sub_api import SubmittingXBlockMixin
# Globals ###########################################################
log = logging.getLogger(__name__)
loader = ResourceLoader(__name__)
# Make '_' a no-op so we can scrape strings
def _(text):
return text
# Classes ###########################################################
class SwipeBlock(SubmittingXBlockMixin, StudentViewUserStateMixin, QuestionnaireAbstractBlock):
"""
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=""
)
student_choice = String(
# {Last input submitted by the student
default="",
scope=Scope.user_state,
)
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."
),
scope=Scope.content,
default=""
)
editable_fields = QuestionnaireAbstractBlock.editable_fields + ('message', 'correct_choice', 'img_url',)
def calculate_results(self, submission):
correct = self.correct_choice == submission
return {
'submission': submission,
'message': self.message_formatted,
'status': 'correct' if correct else 'incorrect',
'weight': self.weight,
'score': 1 if correct else 0,
}
def get_results(self, previous_result):
return self.calculate_results(previous_result['submission'])
def get_last_result(self):
return self.get_results({'submission': self.student_choice}) if self.student_choice else {}
def submit(self, submission):
log.debug(u'Received Swipe submission: "%s"', submission)
result = self.calculate_results(submission['value'])
self.student_choice = submission['value']
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,
retrievable from the Course Block API.
"""
return {
'id': self.name,
'block_id': unicode(self.scope_ids.usage_id),
'display_name': self.display_name_with_default,
'type': self.CATEGORY,
'question': self.question,
'message': self.message,
'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()],
}
def expand_static_url(self, url):
"""
This is required to make URLs like '/static/dnd-test-image.png' work (note: that is the
only portable URL format for static files that works across export/import and reruns).
This method is unfortunately a bit hackish since XBlock does not provide a low-level API
for this.
"""
if hasattr(self.runtime, 'replace_urls'):
url = self.runtime.replace_urls('"{}"'.format(url))[1:-1]
elif hasattr(self.runtime, 'course_id'):
# edX Studio uses a different runtime for 'studio_view' than 'student_view',
# and the 'studio_view' runtime doesn't provide the replace_urls API.
try:
from static_replace import replace_static_urls # pylint: disable=import-error
url = replace_static_urls('"{}"'.format(url), None, course_id=self.runtime.course_id)[1:-1]
except ImportError:
pass
return url
@property
def expanded_img_url(self):
return self.expand_static_url(self.img_url)
{% 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>
......@@ -56,6 +56,7 @@ BLOCKS = [
'pb-answer = problem_builder.answer:AnswerBlock',
'pb-answer-recap = problem_builder.answer:AnswerRecapBlock',
'pb-mcq = problem_builder.mcq:MCQBlock',
'pb-swipe = problem_builder.swipe:SwipeBlock',
'pb-rating = problem_builder.mcq:RatingBlock',
'pb-mrq = problem_builder.mrq:MRQBlock',
'pb-slider = problem_builder.slider:SliderBlock',
......@@ -71,7 +72,7 @@ BLOCKS = [
setup(
name='xblock-problem-builder',
version='2.7.8',
version='2.7.9',
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