Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
P
problem-builder
Overview
Overview
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
OpenEdx
problem-builder
Commits
e8a106ff
Commit
e8a106ff
authored
Oct 06, 2017
by
Josh McLaughlin
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Add SwipeBlock for swipeable binary choice questions
parent
673a7533
Show whitespace changes
Inline
Side-by-side
Showing
7 changed files
with
266 additions
and
0 deletions
+266
-0
doc/Native APIs.md
+44
-0
problem_builder/mentoring.py
+9
-0
problem_builder/public/css/problem-builder.css
+4
-0
problem_builder/public/js/questionnaire.js
+4
-0
problem_builder/swipe.py
+171
-0
problem_builder/templates/html/swipeblock.html
+33
-0
setup.py
+1
-0
No files found.
doc/Native APIs.md
View file @
e8a106ff
...
...
@@ -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`)
-------------------------------------
...
...
problem_builder/mentoring.py
View file @
e8a106ff
...
...
@@ -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
,
...
...
problem_builder/public/css/problem-builder.css
View file @
e8a106ff
...
...
@@ -260,3 +260,7 @@
.mentoring
.copyright
a
{
color
:
#69C0E8
;
}
.swipe-img
{
max-width
:
100%
;
}
problem_builder/public/js/questionnaire.js
View file @
e8a106ff
...
...
@@ -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
,
...
...
problem_builder/swipe.py
0 → 100644
View file @
e8a106ff
# -*- 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
)
problem_builder/templates/html/swipeblock.html
0 → 100644
View file @
e8a106ff
{% 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>
setup.py
View file @
e8a106ff
...
...
@@ -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'
,
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment