Commit 9b9b2aa1 by Xavier Antoviaque

Add base support for MRQ, with tips displayed next to the answers

parent f03bf074
...@@ -3,6 +3,7 @@ from .choice import ChoiceBlock ...@@ -3,6 +3,7 @@ from .choice import ChoiceBlock
from .dataexport import MentoringDataExportBlock from .dataexport import MentoringDataExportBlock
from .html import HTMLBlock from .html import HTMLBlock
from .mcq import MCQBlock from .mcq import MCQBlock
from .mrq import MRQBlock
from .mentoring import MentoringBlock from .mentoring import MentoringBlock
from .message import MentoringMessageBlock from .message import MentoringMessageBlock
from .table import MentoringTableBlock, MentoringTableColumnBlock, MentoringTableColumnHeaderBlock from .table import MentoringTableBlock, MentoringTableColumnBlock, MentoringTableColumnHeaderBlock
......
...@@ -249,6 +249,12 @@ class String(LightChildField): ...@@ -249,6 +249,12 @@ class String(LightChildField):
class Boolean(LightChildField): class Boolean(LightChildField):
pass pass
class List(LightChildField):
def __init__(self, *args, **kwargs):
self.value = kwargs.get('default', [])
class Scope(object): class Scope(object):
content = None content = None
user_state = None user_state = None
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
# Copyright (C) 2014 Harvard # Copyright (C) 2014 Harvard
...@@ -56,11 +55,11 @@ class MCQBlock(QuestionnaireAbstractBlock): ...@@ -56,11 +55,11 @@ class MCQBlock(QuestionnaireAbstractBlock):
completed = True completed = True
tips_fragments = [] tips_fragments = []
for tip in self.get_tips(): for tip in self.get_tips():
completed = completed and tip.is_completed([submission]) completed = completed and self.is_tip_completed(tip, submission)
if tip.is_tip_displayed([submission]): if submission in tip.display_with_defaults:
tips_fragments.append(tip.render(submission)) tips_fragments.append(tip.render())
formatted_tips = render_template('templates/html/tip_group.html', { formatted_tips = render_template('templates/html/tip_question_group.html', {
'self': self, 'self': self,
'tips_fragments': tips_fragments, 'tips_fragments': tips_fragments,
'submission': submission, 'submission': submission,
...@@ -75,3 +74,12 @@ class MCQBlock(QuestionnaireAbstractBlock): ...@@ -75,3 +74,12 @@ class MCQBlock(QuestionnaireAbstractBlock):
} }
log.debug(u'MCQ submission result: %s', result) log.debug(u'MCQ submission result: %s', result)
return result return result
def is_tip_completed(self, tip, submission):
if not submission:
return False
if submission in tip.reject_with_defaults:
return False
return True
# -*- coding: utf-8 -*-
#
# Copyright (C) 2014 edX
#
# Authors:
# Xavier Antoviaque <xavier@antoviaque.org>
#
# 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 .light_children import List, Scope
from .questionnaire import QuestionnaireAbstractBlock
from .utils import render_template
# Globals ###########################################################
log = logging.getLogger(__name__)
# Classes ###########################################################
class MRQBlock(QuestionnaireAbstractBlock):
"""
An XBlock used to ask multiple-response questions
"""
student_choices = List(help="Last submissions by the student", default=[], scope=Scope.user_state)
def submit(self, submissions):
log.debug(u'Received MRQ submissions: "%s"', submissions)
completed = True
results = []
for choice in self.custom_choices:
choice_completed = True
choice_tips_fragments = []
choice_selected = choice.value in submissions
for tip in self.get_tips():
if choice.value in tip.display_with_defaults:
choice_tips_fragments.append(tip.render())
if ((not choice_selected and choice.value in tip.require_with_defaults) or
(choice_selected and choice.value in tip.reject_with_defaults)):
choice_completed = False
completed = completed and choice_completed
results.append({
'value': choice.value,
'selected': choice_selected,
'completed': choice_completed,
'tips': render_template('templates/html/tip_choice_group.html', {
'self': self,
'tips_fragments': choice_tips_fragments,
'completed': choice_completed,
}),
})
self.student_choices = submissions
result = {
'submissions': submissions,
'completed': completed,
'choices': results,
}
log.debug(u'MRQ submissions result: %s', result)
return result
...@@ -8,7 +8,26 @@ ...@@ -8,7 +8,26 @@
margin-bottom: 5px; margin-bottom: 5px;
} }
.mentoring .choices .choice { .mentoring .choices .choices .choice {
margin: 10px 0;
}
.mentoring .choices .choices .choice-result {
padding-right: 40px;
background-position: center;
background-repeat: no-repeat;
}
.mentoring .choices .choices .choice-result.correct {
background-image: url({{ correct_icon_url }});
cursor: pointer;
}
.mentoring .choices .choices .choice-result.incorrect {
background-image: url({{ incorrect_icon_url }});
cursor: pointer;
}
.mentoring .rating .choices .choice {
margin-right: 10px; margin-right: 10px;
} }
......
...@@ -2,7 +2,7 @@ function MentoringEditBlock(runtime, element) { ...@@ -2,7 +2,7 @@ function MentoringEditBlock(runtime, element) {
var xmlEditorTextarea = $('.block-xml-editor', element), var xmlEditorTextarea = $('.block-xml-editor', element),
xmlEditor = CodeMirror.fromTextArea(xmlEditorTextarea[0], { mode: 'xml' }); xmlEditor = CodeMirror.fromTextArea(xmlEditorTextarea[0], { mode: 'xml' });
$('.save-button').bind('click', function() { $('.save-button', element).bind('click', function() {
var handlerUrl = runtime.handlerUrl(element, 'studio_submit'), var handlerUrl = runtime.handlerUrl(element, 'studio_submit'),
data = { data = {
'xml_content': xmlEditor.getValue(), 'xml_content': xmlEditor.getValue(),
......
function QuestionnaireBlock(runtime, element) { // TODO: Split in two files
function MCQBlock(runtime, element) {
return { return {
submit: function() {
var checkedRadio = $('input[type=radio]:checked', element);
if(checkedRadio.length) {
return checkedRadio.val();
} else {
return null;
}
},
handleSubmit: function(result) { handleSubmit: function(result) {
var tipsDom = $(element).parent().find('.messages'), var tipsDom = $(element).parent().find('.messages'),
tipHtml = (result || {}).tips || ''; tipHtml = (result || {}).tips || '';
...@@ -8,21 +20,36 @@ function QuestionnaireBlock(runtime, element) { ...@@ -8,21 +20,36 @@ function QuestionnaireBlock(runtime, element) {
tipsDom.append(tipHtml); tipsDom.append(tipHtml);
} }
} }
} };
} }
function MCQBlock(runtime, element) { function MRQBlock(runtime, element) {
var init = QuestionnaireBlock(runtime, element); return {
submit: function() {
var checkedCheckboxes = $('input[type=checkbox]:checked', element),
checkedValues = [];
init.submit = function() { $.each(checkedCheckboxes, function(index, checkedCheckbox) {
var checkedRadio = $('input[type=radio]:checked', element); checkedValues.push($(checkedCheckbox).val());
});
return checkedValues;
},
if(checkedRadio.length) { handleSubmit: function(result) {
return checkedRadio.val(); $.each(result.choices, function(index, choice) {
var choice_input_dom = $('.choice input[value='+choice.value+']', element),
choice_dom = choice_input_dom.closest('.choice'),
choice_result_dom = $('.choice-result', choice_dom),
choice_tips_dom = $('.choice-tips', choice_dom);
if (choice.completed) {
choice_result_dom.removeClass('incorrect').addClass('correct');
} else { } else {
return null; choice_result_dom.removeClass('correct').addClass('incorrect');
} }
};
return init; choice_tips_dom.html(choice.tips);
});
}
};
} }
...@@ -48,8 +48,11 @@ class QuestionnaireAbstractBlock(LightChild): ...@@ -48,8 +48,11 @@ class QuestionnaireAbstractBlock(LightChild):
values entered by the student, and supports multiple types of multiple-choice values entered by the student, and supports multiple types of multiple-choice
set, with preset choices and author-defined values. set, with preset choices and author-defined values.
""" """
type = String(help="Type of questionnaire", scope=Scope.content, default="choices")
question = String(help="Question to ask the student", scope=Scope.content, default="") question = String(help="Question to ask the student", scope=Scope.content, default="")
valid_types = ('choices')
@classmethod @classmethod
def init_block_from_node(cls, block, node, attr): def init_block_from_node(cls, block, node, attr):
block.light_children = [] block.light_children = []
...@@ -77,11 +80,16 @@ class QuestionnaireAbstractBlock(LightChild): ...@@ -77,11 +80,16 @@ class QuestionnaireAbstractBlock(LightChild):
}) })
fragment = Fragment(html) fragment = Fragment(html)
fragment.add_css_url(self.runtime.local_resource_url(self.xblock_container, fragment.add_css(render_template('public/css/questionnaire.css', {
'public/css/questionnaire.css')) 'self': self,
'correct_icon_url': self.runtime.local_resource_url(self.xblock_container,
'public/img/correct-icon.png'),
'incorrect_icon_url': self.runtime.local_resource_url(self.xblock_container,
'public/img/incorrect-icon.png'),
}))
fragment.add_javascript_url(self.runtime.local_resource_url(self.xblock_container, fragment.add_javascript_url(self.runtime.local_resource_url(self.xblock_container,
'public/js/questionnaire.js')) 'public/js/questionnaire.js'))
fragment.initialize_js('QuestionnaireBlock') fragment.initialize_js(name)
return fragment return fragment
@property @property
......
...@@ -2,9 +2,12 @@ ...@@ -2,9 +2,12 @@
<legend class="question">{{ self.question }}</legend> <legend class="question">{{ self.question }}</legend>
<div class="choices"> <div class="choices">
{% for choice in custom_choices %} {% for choice in custom_choices %}
<span class="choice"> <div class="choice">
<label><input class="choice-selector" type="radio" name="{{ self.name }}" value="{{ choice.value }}"{% if self.student_choice == choice.value %} checked{% endif %}> {{ choice.content }}</label> <span class="choice-result"></span>
</span> <label class="choice-label">
<input class="choice-selector" type="radio" name="{{ self.name }}" value="{{ choice.value }}"{% if self.student_choice == choice.value %} checked{% endif %}> {{ choice.content }}
</label>
</div>
{% endfor %} {% endfor %}
</div> </div>
</fieldset> </fieldset>
<fieldset class="choices">
<legend class="question">{{ self.question }}</legend>
<div class="choices">
{% for choice in custom_choices %}
<div class="choice">
<span class="choice-result"></span>
<label class="choice-label">
<input class="choice-selector" type="checkbox" name="{{ self.name }}" value="{{ choice.value }}"{% if choice.value in self.student_choices %} checked{% endif %}> {{ choice.content }}
</label>
<div class="choice-tips"></div>
</div>
{% endfor %}
</div>
</fieldset>
<div class="tip-choice-group">
<strong>
{% if completed %}
Correct! You have made the right choice.
{% else %}
This is not the right choice.
{% endif %}
</strong>
{% for tip_fragment in tips_fragments %}
{{ tip_fragment.body_html|safe }}
{% endfor %}
</div>
<div class="mcq-tip"> <div class="tip-question-group">
<strong> <strong>
To the question <span class="italic">"{{ self.question }}"</span>, To the question <span class="italic">"{{ self.question }}"</span>,
{% if submission %} {% if submission %}
......
...@@ -36,16 +36,14 @@ log = logging.getLogger(__name__) ...@@ -36,16 +36,14 @@ log = logging.getLogger(__name__)
# Functions ######################################################### # Functions #########################################################
def commas_to_list(commas_str): def commas_to_set(commas_str):
""" """
Converts a comma-separated string to a list Converts a comma-separated string to a set
""" """
if commas_str is None: if not commas_str:
return None # Means default value (which can be non-empty) return set()
elif commas_str == '':
return [] # Means empty list
else: else:
return commas_str.split(',') return set(commas_str.split(','))
...@@ -58,8 +56,9 @@ class TipBlock(LightChild): ...@@ -58,8 +56,9 @@ class TipBlock(LightChild):
content = String(help="Text of the tip to provide if needed", scope=Scope.content, default="") content = String(help="Text of the tip to provide if needed", scope=Scope.content, default="")
display = String(help="List of choices to display the tip for", scope=Scope.content, default=None) display = String(help="List of choices to display the tip for", scope=Scope.content, default=None)
reject = String(help="List of choices to reject", scope=Scope.content, default=None) reject = String(help="List of choices to reject", scope=Scope.content, default=None)
require = String(help="List of choices to require", scope=Scope.content, default=None)
def render(self, submission): def render(self):
""" """
Returns a fragment containing the formatted tip Returns a fragment containing the formatted tip
""" """
...@@ -70,32 +69,15 @@ class TipBlock(LightChild): ...@@ -70,32 +69,15 @@ class TipBlock(LightChild):
})) }))
return self.xblock_container.fragment_text_rewriting(fragment) return self.xblock_container.fragment_text_rewriting(fragment)
def is_completed(self, submissions):
if not submissions:
return False
for submission in submissions:
if submission in self.reject_with_defaults:
return False
return True
def is_tip_displayed(self, submissions):
for submission in submissions:
if submission in self.display_with_defaults:
return True
return False
@property @property
def display_with_defaults(self): def display_with_defaults(self):
display = commas_to_list(self.display) display = commas_to_set(self.display)
if display is None: return display | self.reject_with_defaults | self.require_with_defaults
display = self.reject_with_defaults
else:
display += [choice for choice in self.reject_with_defaults
if choice not in display]
return display
@property @property
def reject_with_defaults(self): def reject_with_defaults(self):
reject = commas_to_list(self.reject) return commas_to_set(self.reject)
return reject or []
@property
def require_with_defaults(self):
return commas_to_set(self.require)
...@@ -73,7 +73,6 @@ def get_scenarios_from_path(scenarios_path, include_identifier=False): ...@@ -73,7 +73,6 @@ def get_scenarios_from_path(scenarios_path, include_identifier=False):
""" """
base_fullpath = os.path.dirname(os.path.realpath(__file__)) base_fullpath = os.path.dirname(os.path.realpath(__file__))
scenarios_fullpath = os.path.join(base_fullpath, scenarios_path) scenarios_fullpath = os.path.join(base_fullpath, scenarios_path)
print scenarios_fullpath
scenarios = [] scenarios = []
if os.path.isdir(scenarios_fullpath): if os.path.isdir(scenarios_fullpath):
......
...@@ -54,6 +54,7 @@ BLOCKS_CHILDREN = [ ...@@ -54,6 +54,7 @@ BLOCKS_CHILDREN = [
'answer = mentoring:AnswerBlock', 'answer = mentoring:AnswerBlock',
'quizz = mentoring:MCQBlock', 'quizz = mentoring:MCQBlock',
'mcq = mentoring:MCQBlock', 'mcq = mentoring:MCQBlock',
'mrq = mentoring:MRQBlock',
'message = mentoring:MentoringMessageBlock', 'message = mentoring:MentoringMessageBlock',
'tip = mentoring:TipBlock', 'tip = mentoring:TipBlock',
'choice = mentoring:ChoiceBlock', 'choice = mentoring:ChoiceBlock',
......
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