Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,13 @@ before_install:
- "export DISPLAY=:99"
- "sh -e /etc/init.d/xvfb start"
install:
- "pip install -e git://github.com/edx/xblock-sdk.git#egg=xblock-sdk"
- "pip install -e git://github.com/edx/xblock-sdk.git@22c1b2f173919bef22f2d9d9295ec5396d02dffd#egg=xblock-sdk"
- "pip install -r requirements.txt"
- "pip install -r $VIRTUAL_ENV/src/xblock-sdk/requirements.txt"
- "pip install -r $VIRTUAL_ENV/src/xblock-sdk/test-requirements.txt"
- "pip install -r $VIRTUAL_ENV/src/xblock-sdk/requirements/base.txt"
- "pip install -r $VIRTUAL_ENV/src/xblock-sdk/requirements/test.txt"
- "pip uninstall -y xblock-problem-builder && python setup.py sdist && pip install dist/xblock-problem-builder-2.0.tar.gz"
- "pip install -r test_requirements.txt"
- "mkdir var"
script:
- pep8 problem_builder --max-line-length=120
- pylint problem_builder --disable=all --enable=function-redefined,undefined-variable,unused-variable
Expand Down
7 changes: 0 additions & 7 deletions problem_builder/answer.py
Original file line number Diff line number Diff line change
Expand Up @@ -152,13 +152,6 @@ class AnswerBlock(SubmittingXBlockMixin, AnswerMixin, QuestionMixin, StudioEdita
default="",
multiline_editor=True,
)
weight = Float(
display_name=_("Weight"),
help=_("Defines the maximum total grade of the answer block."),
default=1,
scope=Scope.settings,
enforce_type=True
)

editable_fields = ('question', 'name', 'min_characters', 'weight', 'default_from', 'display_name', 'show_title')

Expand Down
15 changes: 14 additions & 1 deletion problem_builder/mixins.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from lazy import lazy
from xblock.fields import String, Boolean, Scope
from xblock.fields import String, Boolean, Float, Scope, UNIQUE_ID
from xblockutils.helpers import child_isinstance
from xblockutils.resources import ResourceLoader

Expand Down Expand Up @@ -125,12 +125,25 @@ class QuestionMixin(EnumerableChildMixin):
has_author_view = True

# Fields:
name = String(
display_name=_("Question ID (name)"),
help=_("The ID of this question (required). Should be unique within this mentoring component."),
default=UNIQUE_ID,
scope=Scope.settings, # Must be scope.settings, or the unique ID will change every time this block is edited
)
display_name = String(
display_name=_("Question title"),
help=_('Leave blank to use the default ("Question 1", "Question 2", etc.)'),
default="", # Blank will use 'Question x' - see display_name_with_default
scope=Scope.content
)
weight = Float(
display_name=_("Weight"),
help=_("Defines the maximum total grade of this question."),
default=1,
scope=Scope.content,
enforce_type=True
)

@lazy
def siblings(self):
Expand Down
4 changes: 4 additions & 0 deletions problem_builder/public/css/problem-builder-edit.css
Original file line number Diff line number Diff line change
Expand Up @@ -49,3 +49,7 @@
margin-top: 1em;
padding-top: 0.3em;
}

.xblock-author_view-pb-slider .url-name-footer {
margin: 0 -20px -20px -20px; /* Counteract spacing from xblock-render wrapper. */
}
26 changes: 26 additions & 0 deletions problem_builder/public/css/problem-builder.css
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,32 @@
margin-bottom: 0;
}

.mentoring .xblock-pb-slider p label {
font-size: inherit;
}

.mentoring .pb-slider-box {
max-width: 400px;
}

.mentoring .pb-slider-range {
width: 100%;
}

.mentoring .pb-slider-min-label {
float: left;
}

.mentoring .pb-slider-max-label {
float: right;
}

.mentoring .clearfix::after {
clear: both;
display: block;
content: " ";
}

.mentoring .attempts {
margin-left: 10px;
display: inline-block;
Expand Down
10 changes: 6 additions & 4 deletions problem_builder/public/js/mentoring_standard_view.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ function MentoringStandardView(runtime, element, mentoring) {
function handleSubmitResults(response, disable_submit) {
messagesDOM.empty().hide();

var all_have_results = response.results.length > 0;
$.each(response.results || [], function(index, result_spec) {
var input = result_spec[0];
var result = result_spec[1];
Expand All @@ -16,6 +17,7 @@ function MentoringStandardView(runtime, element, mentoring) {
num_attempts: response.num_attempts
};
callIfExists(child, 'handleSubmit', result, options);
all_have_results = all_have_results && !$.isEmptyObject(result);
});

$('.attempts', element).data('max_attempts', response.max_attempts);
Expand All @@ -29,10 +31,10 @@ function MentoringStandardView(runtime, element, mentoring) {
messagesDOM.show();
}

// this method is called on successful submission and on page load
// results will be empty only for initial load if no submissions was made
// in such case we must allow submission to support submitting empty read-only long answer recaps
if (disable_submit || response.results.length > 0) {
// Disable the submit button if we have just submitted new answers,
// or if we have just [re]loaded the page and are showing a complete set
// of old answers.
if (disable_submit || all_have_results) {
submitDOM.attr('disabled', 'disabled');
}
}
Expand Down
41 changes: 41 additions & 0 deletions problem_builder/public/js/slider.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
function SliderBlock(runtime, element) {
var $slider = $('.pb-slider-range', element);
return {
mode: null,
mentoring: null,

value: function() {
return parseInt($slider.val());
},

init: function(options) {
this.mentoring = options.mentoring;
this.mode = options.mode;
$slider.on('change', options.onChange);
},

submit: function() {
return this.value() / 100.0;
},

handleReview: function(result){
$slider.val(result.submission * 100.0);
$slider.prop('disabled', true);
},

handleSubmit: function(result) {
// Show a green check if the user has submitted a valid value:
if (typeof result.submission !== "undefined") {
$('.submit-result', element).css('visibility', 'visible');
}
},

clearResult: function() {
$('.submit-result', element).css('visibility', 'hidden');
},

validate: function(){
return Boolean(this.value() >= 0 && this.value() <= 100);
}
};
}
16 changes: 1 addition & 15 deletions problem_builder/questionnaire.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
from lazy import lazy
import uuid
from xblock.core import XBlock
from xblock.fields import Scope, String, Float, UNIQUE_ID
from xblock.fields import Scope, String
from xblock.fragment import Fragment
from xblock.validation import ValidationMessage
from xblockutils.helpers import child_isinstance
Expand Down Expand Up @@ -61,13 +61,6 @@ class QuestionnaireAbstractBlock(
values entered by the student, and supports multiple types of multiple-choice
set, with preset choices and author-defined values.
"""
name = String(
# This doesn't need to be a field but is kept for backwards compatibility with v1 student data
display_name=_("Question ID (name)"),
help=_("The ID of this question (required). Should be unique within this mentoring component."),
default=UNIQUE_ID,
scope=Scope.settings, # Must be scope.settings, or the unique ID will change every time this block is edited
)
question = String(
display_name=_("Question"),
help=_("Question to ask the student"),
Expand All @@ -81,13 +74,6 @@ class QuestionnaireAbstractBlock(
scope=Scope.content,
default=""
)
weight = Float(
display_name=_("Weight"),
help=_("Defines the maximum total grade of this question."),
default=1,
scope=Scope.content,
enforce_type=True
)
editable_fields = ('question', 'message', 'weight', 'display_name', 'show_title')
has_children = True
answerable = True
Expand Down
157 changes: 157 additions & 0 deletions problem_builder/slider.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
# -*- 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
import uuid

from xblock.core import XBlock
from xblock.fields import Scope, String, Float
from xblock.fragment import Fragment
from xblockutils.studio_editable import StudioEditableXBlockMixin
from xblockutils.resources import ResourceLoader

from .mixins import QuestionMixin, XBlockWithTranslationServiceMixin
from .sub_api import sub_api, SubmittingXBlockMixin


# Globals ###########################################################

log = logging.getLogger(__name__)
loader = ResourceLoader(__name__)


# Make '_' a no-op so we can scrape strings
def _(text):
return text

# Classes ###########################################################


@XBlock.needs("i18n")
class SliderBlock(
SubmittingXBlockMixin, QuestionMixin, StudioEditableXBlockMixin, XBlockWithTranslationServiceMixin, XBlock,
):
"""
An XBlock used by students to indicate a numeric value on a sliding scale.
The student's answer is always considered "correct".
"""
CATEGORY = 'pb-slider'
STUDIO_LABEL = _(u"Ranged Value Slider")
answerable = True

min_label = String(
display_name=_("Low"),
help=_("Label for low end of the range"),
scope=Scope.content,
default=_("0%"),
)
max_label = String(
display_name=_("High"),
help=_("Label for high end of the range"),
scope=Scope.content,
default=_("100%"),
)

question = String(
display_name=_("Question"),
help=_("Question to ask the student (optional)"),
scope=Scope.content,
default="",
multiline_editor=True,
)

student_value = Float(
# The value selected by the student
default=None,
scope=Scope.user_state,
)

editable_fields = ('min_label', 'max_label', 'display_name', 'question', 'show_title')

def mentoring_view(self, context):
""" Main view of this block """
context = context.copy() if context else {}
context['question'] = self.question
context['slider_id'] = 'pb-slider-{}'.format(uuid.uuid4().hex[:20])
context['initial_value'] = int(self.student_value*100) if self.student_value is not None else 50
context['min_label'] = self.min_label
context['max_label'] = self.max_label
context['title'] = self.display_name_with_default
context['hide_header'] = context.get('hide_header', False) or not self.show_title
context['instructions_string'] = self._("Select a value from {min_label} to {max_label}").format(
min_label=self.min_label, max_label=self.max_label
)
html = loader.render_template('templates/html/slider.html', context)

fragment = Fragment(html)
fragment.add_javascript_url(self.runtime.local_resource_url(self, 'public/js/slider.js'))
fragment.initialize_js('SliderBlock')
return fragment

student_view = mentoring_view
preview_view = mentoring_view

def author_view(self, context):
"""
Add some HTML to the author view that allows authors to see the ID of the block, so they
can refer to it in other blocks such as Plot blocks.
"""
context['hide_header'] = True # Header is already shown in the Studio wrapper
fragment = self.student_view(context)
fragment.add_content(loader.render_template('templates/html/slider_edit_footer.html', {
"url_name": self.url_name
}))
return fragment

def get_last_result(self):
""" Return the current/last result in the required format """
if self.student_value is None:
return {}
return {
'submission': self.student_value,
'status': 'correct',
'tips': [],
'weight': self.weight,
'score': 1,
}

def get_results(self, _previous_result_unused=None):

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@itsjeyd Do you know why we have get_results and get_last_result? The API is a little confusing. Would be nice to combined them.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@bradenmacdonald I'm not sure why we have get_result and get_last_result, that part of the API predates me :) But I agree, if possible it would be good to combine them.

""" Alias for get_last_result() """
return self.get_last_result()

def submit(self, value):
log.debug(u'Received Slider submission: "%s"', value)
if value < 0 or value > 1:
return {} # Invalid
self.student_value = value
if sub_api:
# Also send to the submissions API:
sub_api.create_submission(self.student_item_key, {'value': value})
result = self.get_last_result()
log.debug(u'Slider submission result: %s', result)
return result

def validate_field_data(self, validation, data):
"""
Validate this block's field data.
"""
super(SliderBlock, self).validate_field_data(validation, data)
3 changes: 2 additions & 1 deletion problem_builder/step.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
from problem_builder.mixins import EnumerableChildMixin, MessageParentMixin, StepParentMixin
from problem_builder.mrq import MRQBlock
from problem_builder.plot import PlotBlock
from problem_builder.slider import SliderBlock
from problem_builder.table import MentoringTableBlock


Expand Down Expand Up @@ -147,7 +148,7 @@ def allowed_nested_blocks(self):
return [
NestedXBlockSpec(AnswerBlock, boilerplate='studio_default'),
MCQBlock, RatingBlock, MRQBlock, HtmlBlockShim,
AnswerRecapBlock, MentoringTableBlock, PlotBlock
AnswerRecapBlock, MentoringTableBlock, PlotBlock, SliderBlock
] + additional_blocks

@property
Expand Down
1 change: 1 addition & 0 deletions problem_builder/templates/html/mentoring_add_buttons.html
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ <h5>{% trans "Add New Component" %}</h5>
<li><a href="#" class="single-template add-xblock-component-button" data-category="pb-mcq">{% trans "Multiple Choice Question" %}</a></li>
<li><a href="#" class="single-template add-xblock-component-button" data-category="pb-rating">{% trans "Rating Question" %}</a></li>
<li><a href="#" class="single-template add-xblock-component-button" data-category="pb-mrq">{% trans "Multiple Response Question" %}</a></li>
<li><a href="#" class="single-template add-xblock-component-button" data-category="pb-slider">{% trans "Ranged Value Slider" %}</a></li>
<li><a href="#" class="single-template add-xblock-component-button" data-category="html">{% trans "HTML" %}</a></li>
<li><a href="#" class="single-template add-xblock-component-button" data-category="pb-answer-recap">{% trans "Long Answer Recap" %}</a></li>
<li><a href="#" class="single-template add-xblock-component-button" data-category="pb-table">{% trans "Answer Recap Table" %}</a></li>
Expand Down
Loading