Commit 1105c899 by Alan Boudreault

Mentoring assessment mode

parent b6a23798
...@@ -19,12 +19,12 @@ Examples ...@@ -19,12 +19,12 @@ Examples
First XBlock instance: First XBlock instance:
```xml ```xml
<mentoring url_name="goal_definition" followed_by="getting_feedback"> <mentoring url_name="goal_definition" followed_by="getting_feedback" weight="20">
<html> <html>
<p>What is your goal?</p> <p>What is your goal?</p>
</html> </html>
<answer name="goal" /> <answer name="goal" weight="10"/>
</mentoring> </mentoring>
``` ```
...@@ -40,15 +40,18 @@ Second XBlock instance: ...@@ -40,15 +40,18 @@ Second XBlock instance:
<html> <html>
<p>Ask feedback from friends about this goal - what did they think?</p> <p>Ask feedback from friends about this goal - what did they think?</p>
</html> </html>
<answer name="goal_feedback" /> <answer name="goal_feedback" weight="5"/>
</mentoring> </mentoring>
``` ```
You can specify the weight of a free form answer. It will be considered during the
grade/score computation.
### Self-assessment MCQs ### Self-assessment MCQs
```xml ```xml
<mentoring url_name="mcq_1" enforce_dependency="false"> <mentoring url_name="mcq_1" enforce_dependency="false">
<mcq name="mcq_1_1" type="choices"> <mcq name="mcq_1_1" type="choices" weight="10">
<question>Do you like this MCQ?</question> <question>Do you like this MCQ?</question>
<choice value="yes">Yes</choice> <choice value="yes">Yes</choice>
<choice value="maybenot">Maybe not</choice> <choice value="maybenot">Maybe not</choice>
...@@ -59,7 +62,7 @@ Second XBlock instance: ...@@ -59,7 +62,7 @@ Second XBlock instance:
<tip reject="understand"><html><div id="test-custom-html">Really?</div></html></tip> <tip reject="understand"><html><div id="test-custom-html">Really?</div></html></tip>
</mcq> </mcq>
<mcq name="mcq_1_2" type="rating" low="Not good at all" high="Extremely good"> <mcq name="mcq_1_2" type="rating" low="Not good at all" high="Extremely good" weight="5">
<question>How much do you rate this MCQ?</question> <question>How much do you rate this MCQ?</question>
<choice value="notwant">I don't want to rate it</choice> <choice value="notwant">I don't want to rate it</choice>
...@@ -78,10 +81,13 @@ Second XBlock instance: ...@@ -78,10 +81,13 @@ Second XBlock instance:
</mentoring> </mentoring>
``` ```
You can specify the weight of a self-assessment MCQ. It will be considered during the
grade/score computation.
### Self-assessment MRQs ### Self-assessment MRQs
```xml ```xml
<mentoring url_name="mcq_1" enforce_dependency="false"> <mentoring url_name="mrq_1" enforce_dependency="false">
<mrq name="mrq_1_1" type="choices" hide_results="true"> <mrq name="mrq_1_1" type="choices" hide_results="true" weight="10">
<question>What do you like in this MRQ?</question> <question>What do you like in this MRQ?</question>
<choice value="elegance">Its elegance</choice> <choice value="elegance">Its elegance</choice>
<choice value="beauty">Its beauty</choice> <choice value="beauty">Its beauty</choice>
...@@ -104,6 +110,9 @@ Second XBlock instance: ...@@ -104,6 +110,9 @@ Second XBlock instance:
</mentoring> </mentoring>
``` ```
You can specify the weight of a self-assessment MRQ. It will be considered during the
grade/score computation.
### Tables ### Tables
```xml ```xml
...@@ -128,6 +137,24 @@ Second XBlock instance: ...@@ -128,6 +137,24 @@ Second XBlock instance:
</vertical> </vertical>
``` ```
### Modes
There are 2 mentoring modes available:
* standard: Traditional mentoring. All questions are displayed in the page and submitted at the
same time. The student get some tips and feedback about their answers. (default mode)
* assessment: Questions are displayed and submitted one after one. The student dont get tips or
feedback but only know if their answer was correct. Assessment mode comes with a default
max_attempts of 2.
To set the *assessment* mode, set the mode attribute in the settings:
```xml
<mentoring url_name="mentoring_1" mode="assesment">
...
</mentoring>
```
### Maximum Attempts ### Maximum Attempts
You can set the number of maximum attempts for the unit completion, as well as You can set the number of maximum attempts for the unit completion, as well as
......
...@@ -29,9 +29,9 @@ from lazy import lazy ...@@ -29,9 +29,9 @@ from lazy import lazy
from xblock.fragment import Fragment from xblock.fragment import Fragment
from .light_children import LightChild, Boolean, Scope, String, Integer from .light_children import LightChild, Boolean, Scope, String, Integer, Float
from .models import Answer from .models import Answer
from .utils import render_template, serialize_opaque_key from .utils import render_js_template, serialize_opaque_key
# Globals ########################################################### # Globals ###########################################################
...@@ -53,6 +53,8 @@ class AnswerBlock(LightChild): ...@@ -53,6 +53,8 @@ class AnswerBlock(LightChild):
default=None, scope=Scope.content) default=None, scope=Scope.content)
min_characters = Integer(help="Minimum number of characters allowed for the answer", min_characters = Integer(help="Minimum number of characters allowed for the answer",
default=0, scope=Scope.content) default=0, scope=Scope.content)
weight = Float(help="Defines the maximum total grade of the light child block.",
default=1, scope=Scope.content, enforce_type=True)
@lazy @lazy
def student_input(self): def student_input(self):
...@@ -74,11 +76,11 @@ class AnswerBlock(LightChild): ...@@ -74,11 +76,11 @@ class AnswerBlock(LightChild):
def mentoring_view(self, context=None): def mentoring_view(self, context=None):
if not self.read_only: if not self.read_only:
html = render_template('templates/html/answer_editable.html', { html = render_js_template('templates/html/answer_editable.html', {
'self': self, 'self': self,
}) })
else: else:
html = render_template('templates/html/answer_read_only.html', { html = render_js_template('templates/html/answer_read_only.html', {
'self': self, 'self': self,
}) })
...@@ -90,7 +92,7 @@ class AnswerBlock(LightChild): ...@@ -90,7 +92,7 @@ class AnswerBlock(LightChild):
return fragment return fragment
def mentoring_table_view(self, context=None): def mentoring_table_view(self, context=None):
html = render_template('templates/html/answer_table.html', { html = render_js_template('templates/html/answer_table.html', {
'self': self, 'self': self,
}) })
fragment = Fragment(html) fragment = Fragment(html)
...@@ -104,6 +106,7 @@ class AnswerBlock(LightChild): ...@@ -104,6 +106,7 @@ class AnswerBlock(LightChild):
return { return {
'student_input': self.student_input, 'student_input': self.student_input,
'completed': self.completed, 'completed': self.completed,
'weight': self.weight,
'score': 1 if self.completed else 0, 'score': 1 if self.completed else 0,
} }
......
...@@ -56,7 +56,10 @@ class HTMLBlock(LightChild): ...@@ -56,7 +56,10 @@ class HTMLBlock(LightChild):
return block return block
def student_view(self, context=None): def student_view(self, context=None):
return Fragment(self.content) return Fragment(u"<script type='text/template' id='{}'>\n{}\n</script>".format(
'light-child-template',
self.content
))
def mentoring_view(self, context=None): def mentoring_view(self, context=None):
return self.student_view(context) return self.student_view(context)
......
...@@ -365,6 +365,18 @@ class Boolean(LightChildField): ...@@ -365,6 +365,18 @@ class Boolean(LightChildField):
self.data[instance] = value self.data[instance] = value
class Float(LightChildField):
def __init__(self, *args, **kwargs):
super(Float, self).__init__(*args, **kwargs)
self.default = kwargs.get('default', 0)
def __set__(self, instance, value):
try:
self.data[instance] = float(value)
except (TypeError, ValueError): # not an integer
self.data[instance] = 0
class List(LightChildField): class List(LightChildField):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(List, self).__init__(*args, **kwargs) super(List, self).__init__(*args, **kwargs)
......
...@@ -81,6 +81,7 @@ class MCQBlock(QuestionnaireAbstractBlock): ...@@ -81,6 +81,7 @@ class MCQBlock(QuestionnaireAbstractBlock):
'submission': submission, 'submission': submission,
'completed': completed, 'completed': completed,
'tips': tips, 'tips': tips,
'weight': self.weight,
'score': 1 if completed else 0, 'score': 1 if completed else 0,
} }
log.debug(u'MCQ submission result: %s', result) log.debug(u'MCQ submission result: %s', result)
......
...@@ -30,11 +30,12 @@ from lxml import etree ...@@ -30,11 +30,12 @@ from lxml import etree
from StringIO import StringIO from StringIO import StringIO
from xblock.core import XBlock from xblock.core import XBlock
from xblock.fields import Boolean, Scope, String, Integer, Float from xblock.fields import Boolean, Scope, String, Integer, Float, List
from xblock.fragment import Fragment from xblock.fragment import Fragment
from .light_children import XBlockWithLightChildren from .light_children import XBlockWithLightChildren
from .title import TitleBlock from .title import TitleBlock
from .html import HTMLBlock
from .message import MentoringMessageBlock from .message import MentoringMessageBlock
from .utils import get_scenarios_from_path, load_resource, render_template from .utils import get_scenarios_from_path, load_resource, render_template
...@@ -75,9 +76,40 @@ class MentoringBlock(XBlockWithLightChildren): ...@@ -75,9 +76,40 @@ class MentoringBlock(XBlockWithLightChildren):
default=0, scope=Scope.user_state, enforce_type=True) default=0, scope=Scope.user_state, enforce_type=True)
max_attempts = Integer(help="Number of max attempts for this questions", default=0, max_attempts = Integer(help="Number of max attempts for this questions", default=0,
scope=Scope.content, enforce_type=True) scope=Scope.content, enforce_type=True)
mode = String(help="Mode of the mentoring. 'standard' or 'accessment'",
default='standard', scope=Scope.content)
step = Integer(help="Keep track of the student assessment progress.",
default=0, scope=Scope.user_state, enforce_type=True)
student_results = List(help="Store results of student choices.", default=[],
scope=Scope.user_state)
icon_class = 'problem' icon_class = 'problem'
has_score = True has_score = True
MENTORING_MODES = ('standard', 'assessment')
@property
def is_assessment(self):
return self.mode == 'assessment'
@property
def steps(self):
return [child for child in self.get_children_objects() if
not isinstance(child, (HTMLBlock, TitleBlock, MentoringMessageBlock))]
@property
def score(self):
"""Compute the student score taking into account the light child weight."""
total_child_weight = sum(float(step.weight) for step in self.steps)
if total_child_weight == 0:
return (0, 0, 0)
score = sum(r[1]['score']*r[1]['weight'] \
for r in self.student_results) / total_child_weight
correct = sum(1 for r in self.student_results if r[1]['completed'] == True)
incorrect = sum(1 for r in self.student_results if r[1]['completed'] == False)
return (score, float('%0.2f' % (score*100,)), correct, incorrect)
def student_view(self, context): def student_view(self, context):
fragment, named_children = self.get_children_fragment( fragment, named_children = self.get_children_fragment(
context, view_name='mentoring_view', context, view_name='mentoring_view',
...@@ -92,8 +124,18 @@ class MentoringBlock(XBlockWithLightChildren): ...@@ -92,8 +124,18 @@ class MentoringBlock(XBlockWithLightChildren):
fragment.add_css_url(self.runtime.local_resource_url(self, 'public/css/mentoring.css')) fragment.add_css_url(self.runtime.local_resource_url(self, 'public/css/mentoring.css'))
fragment.add_javascript_url( fragment.add_javascript_url(
self.runtime.local_resource_url(self, 'public/js/vendor/underscore-min.js')) self.runtime.local_resource_url(self, 'public/js/vendor/underscore-min.js'))
if self.is_assessment:
fragment.add_javascript_url(
self.runtime.local_resource_url(self, 'public/js/mentoring_assessment_view.js')
)
else:
fragment.add_javascript_url(
self.runtime.local_resource_url(self, 'public/js/mentoring_standard_view.js')
)
fragment.add_javascript_url(self.runtime.local_resource_url(self, 'public/js/mentoring.js')) fragment.add_javascript_url(self.runtime.local_resource_url(self, 'public/js/mentoring.js'))
fragment.add_resource(load_resource('templates/html/mentoring_attempts.html'), "text/html") fragment.add_resource(load_resource('templates/html/mentoring_attempts.html'), "text/html")
fragment.add_resource(load_resource('templates/html/mentoring_grade.html'), "text/html")
fragment.initialize_js('MentoringBlock') fragment.initialize_js('MentoringBlock')
...@@ -129,6 +171,9 @@ class MentoringBlock(XBlockWithLightChildren): ...@@ -129,6 +171,9 @@ class MentoringBlock(XBlockWithLightChildren):
log.info(u'Received submissions: {}'.format(submissions)) log.info(u'Received submissions: {}'.format(submissions))
self.attempted = True self.attempted = True
if self.is_assessment:
return self.handleAssessmentSubmit(submissions, suffix)
submit_results = [] submit_results = []
completed = True completed = True
for child in self.get_children_objects(): for child in self.get_children_objects():
...@@ -163,9 +208,15 @@ class MentoringBlock(XBlockWithLightChildren): ...@@ -163,9 +208,15 @@ class MentoringBlock(XBlockWithLightChildren):
# Once it was completed, lock score # Once it was completed, lock score
if not self.completed: if not self.completed:
score = sum(r[1]['score'] for r in submit_results) / float(len(submit_results)) # save user score and results
while self.student_results:
self.student_results.pop()
for result in submit_results:
self.student_results.append(result)
(raw_score, score, correct, incorrect) = self.score
self.runtime.publish(self, 'grade', { self.runtime.publish(self, 'grade', {
'value': score, 'value': raw_score,
'max_value': 1, 'max_value': 1,
}) })
...@@ -183,6 +234,77 @@ class MentoringBlock(XBlockWithLightChildren): ...@@ -183,6 +234,77 @@ class MentoringBlock(XBlockWithLightChildren):
'num_attempts': self.num_attempts 'num_attempts': self.num_attempts
} }
def handleAssessmentSubmit(self, submissions, suffix):
completed = False
step = 0
children = [child for child in self.get_children_objects() \
if not isinstance(child, TitleBlock)]
for child in children:
if child.name and child.name in submissions:
submission = submissions[child.name]
# Assessment mode doesn't allow to modify answers
# This will get the student back at the step he should be
step = children.index(child)
if self.step > step or self.max_attempts_reached:
step = self.step
completed = False
break
self.step = step+1
child_result = child.submit(submission)
if 'tips' in child_result:
del child_result['tips']
self.student_results.append([child.name, child_result])
child.save()
completed = child_result['completed']
(raw_score, score, correct, incorrect) = self.score
if step == len(self.steps):
log.info(u'Last assessment step submitted: {}'.format(submissions))
if not self.max_attempts_reached:
self.runtime.publish(self, 'grade', {
'value': raw_score,
'max_value': 1,
})
self.num_attempts += 1
self.completed = True
return {
'completed': completed,
'attempted': self.attempted,
'max_attempts': self.max_attempts,
'num_attempts': self.num_attempts,
'step': self.step,
'score': score,
'correct_answer': correct,
'incorrect_answer': incorrect
}
@XBlock.json_handler
def try_again(self, data, suffix=''):
if self.max_attempts_reached:
return {
'result': 'error',
'message': 'max attempts reached'
}
# reset
self.step = 0
self.completed = False
while self.student_results:
self.student_results.pop()
return {
'result': 'success'
}
@property @property
def max_attempts_reached(self): def max_attempts_reached(self):
return self.max_attempts > 0 and self.num_attempts >= self.max_attempts return self.max_attempts > 0 and self.num_attempts >= self.max_attempts
...@@ -222,19 +344,34 @@ class MentoringBlock(XBlockWithLightChildren): ...@@ -222,19 +344,34 @@ class MentoringBlock(XBlockWithLightChildren):
def studio_submit(self, submissions, suffix=''): def studio_submit(self, submissions, suffix=''):
log.info(u'Received studio submissions: {}'.format(submissions)) log.info(u'Received studio submissions: {}'.format(submissions))
success = True
xml_content = submissions['xml_content'] xml_content = submissions['xml_content']
try: try:
etree.parse(StringIO(xml_content)) content = etree.parse(StringIO(xml_content))
except etree.XMLSyntaxError as e: except etree.XMLSyntaxError as e:
response = { response = {
'result': 'error', 'result': 'error',
'message': e.message 'message': e.message
} }
success = False
else: else:
root = content.getroot()
if 'mode' in root.attrib:
if root.attrib['mode'] not in self.MENTORING_MODES:
response = {
'result': 'error',
'message': "Invalid mentoring mode: should be 'standard' or 'assessment'"
}
success = False
elif root.attrib['mode'] == 'assessment' and 'max_attempts' not in root.attrib:
# assessment has a default of 2 max_attempts
root.attrib['max_attempts'] = '2'
if success:
response = { response = {
'result': 'success', 'result': 'success',
} }
self.xml_content = xml_content self.xml_content = etree.tostring(content, pretty_print=True)
log.debug(u'Response from Studio: {}'.format(response)) log.debug(u'Response from Studio: {}'.format(response))
return response return response
......
...@@ -82,6 +82,7 @@ class MRQBlock(QuestionnaireAbstractBlock): ...@@ -82,6 +82,7 @@ class MRQBlock(QuestionnaireAbstractBlock):
'completed': completed, 'completed': completed,
'choices': results, 'choices': results,
'message': self.message, 'message': self.message,
'weight': self.weight,
'score': sum(1.0 for r in results if r['completed']) / len(results) 'score': sum(1.0 for r in results if r['completed']) / len(results)
} }
......
...@@ -53,6 +53,10 @@ ...@@ -53,6 +53,10 @@
margin-top: 20px; margin-top: 20px;
} }
.mentoring .submit input {
display: none;
}
.mentoring .attempts { .mentoring .attempts {
margin-left: 10px; margin-left: 10px;
display: inline-block; display: inline-block;
...@@ -86,6 +90,15 @@ ...@@ -86,6 +90,15 @@
height:33.33px; height:33.33px;
} }
.mentoring .assessment-checkmark {
margin-right: 10px;
}
.mentoring .grade .checkmark-incorrect {
margin-left: 10px;
margin-right: 20px;
}
.mentoring input[type=button], .mentoring input[type=button],
.mentoring input[type=button]:focus { .mentoring input[type=button]:focus {
background-color: #3384ca; background-color: #3384ca;
......
function AnswerBlock(runtime, element) { function AnswerBlock(runtime, element) {
return { return {
mode: null,
init: function(options) { init: function(options) {
// register the child validator // register the child validator
$(':input', element).on('keyup', options.onChange); $(':input', element).on('keyup', options.onChange);
this.mode = options.mode;
var checkmark = $('.answer-checkmark', element); var checkmark = $('.answer-checkmark', element);
var completed = $('.xblock-answer', element).data('completed'); var completed = $('.xblock-answer', element).data('completed');
if (completed === 'True') { if (completed === 'True' && this.mode === 'standard') {
checkmark.addClass('checkmark-correct icon-ok fa-check'); checkmark.addClass('checkmark-correct icon-ok fa-check');
} }
}, },
...@@ -17,6 +18,9 @@ function AnswerBlock(runtime, element) { ...@@ -17,6 +18,9 @@ function AnswerBlock(runtime, element) {
}, },
handleSubmit: function(result) { handleSubmit: function(result) {
if (this.mode === 'assessment')
return;
var checkmark = $('.answer-checkmark', element); var checkmark = $('.answer-checkmark', element);
$(element).find('.message').text((result || {}).error || ''); $(element).find('.message').text((result || {}).error || '');
......
function MentoringBlock(runtime, element) { function MentoringBlock(runtime, element) {
var attemptsTemplate = _.template($('#xblock-attempts-template').html()); var attemptsTemplate = _.template($('#xblock-attempts-template').html());
var children; // Keep track of children. A Child need a single object scope for its data. var data = $('.mentoring', element).data();
var submitXHR; var children_dom = []; // Keep track of children. A Child need a single object scope for its data.
var children = [];
function renderAttempts() { var step = data.step;
var data = $('.attempts', element).data();
$('.attempts', element).html(attemptsTemplate(data));
}
function renderDependency() {
var warning_dom = $('.missing-dependency', element),
data = warning_dom.data();
if (data.missing === 'True') {
warning_dom.show();
}
}
function callIfExists(obj, fn) { function callIfExists(obj, fn) {
if (typeof obj !== 'undefined' && typeof obj[fn] == 'function') { if (typeof obj !== 'undefined' && typeof obj[fn] == 'function') {
...@@ -25,155 +13,83 @@ function MentoringBlock(runtime, element) { ...@@ -25,155 +13,83 @@ function MentoringBlock(runtime, element) {
} }
} }
function handleSubmitResults(results) { function renderAttempts() {
messagesDOM.empty().hide(); var data = $('.attempts', element).data();
$('.attempts', element).html(attemptsTemplate(data));
$.each(results.submitResults || [], function(index, submitResult) {
var input = submitResult[0],
result = submitResult[1],
child = getChildByName(element, input);
var options = {
max_attempts: results.max_attempts,
num_attempts: results.num_attempts
} }
callIfExists(child, 'handleSubmit', result, options);
});
$('.attempts', element).data('max_attempts', results.max_attempts); function renderDependency() {
$('.attempts', element).data('num_attempts', results.num_attempts); var warning_dom = $('.missing-dependency', element),
renderAttempts(); data = warning_dom.data();
// Messages should only be displayed upon hitting 'submit', not on page reload if (data.missing === 'True') {
messagesDOM.append(results.message); warning_dom.show();
if (messagesDOM.html().trim()) {
messagesDOM.prepend('<div class="title1">Feedback</div>');
messagesDOM.show();
} }
submitDOM.attr('disabled', 'disabled');
} }
function getChildren(element) { function readChildren(element) {
if (!_.isUndefined(children)) var doms = $('.xblock-light-child', element);
return children;
var children_dom = $('.xblock-light-child', element); $.each(doms, function(index, child_dom) {
children = [];
$.each(children_dom, function(index, child_dom) {
var child_type = $(child_dom).attr('data-type'), var child_type = $(child_dom).attr('data-type'),
child = window[child_type]; child = window[child_type];
children_dom.push(child_dom);
children.push(child);
if (typeof child !== 'undefined') { if (typeof child !== 'undefined') {
child = child(runtime, child_dom); child = child(runtime, child_dom);
child.name = $(child_dom).attr('name'); child.name = $(child_dom).attr('name');
children.push(child); children[children.length-1] = child;
} }
}); });
return children;
} }
function getChildByName(element, name) { /* Init and display a child. */
var children = getChildren(element); function displayChild(index, options) {
var options = options || {};
options.mode = data.mode;
if (index >= children.length)
return children.length;
for (var i = 0; i < children.length; i++) { var template = $('#light-child-template', children_dom[index]).html();
var child = children[i]; $(children_dom[index]).append(template);
if (child.name === name) { $(children_dom[index]).show();
var child = children[index];
callIfExists(child, 'init', options);
return child; return child;
} }
}
}
function submit() {
var success = true;
var data = {};
var children = getChildren(element);
for (var i = 0; i < children.length; i++) {
var child = children[i];
if (child.name !== undefined) {
data[child.name] = callIfExists(child, 'submit');
}
}
var handlerUrl = runtime.handlerUrl(element, 'submit');
if (submitXHR) {
submitXHR.abort();
}
submitXHR = $.post(handlerUrl, JSON.stringify(data)).success(handleSubmitResults);
}
function clearResults() {
messagesDOM.empty().hide();
var children = getChildren(element);
for (var i = 0; i < children.length; i++) {
callIfExists(children[i], 'clearResult');
}
}
function onChange() {
clearResults();
validateXBlock();
}
function initXBlock() {
messagesDOM = $(element).find('.messages');
submitDOM = $(element).find('.submit .input-main');
submitDOM.bind('click', submit);
// init children (especially mrq blocks) function displayChildren(options) {
var children = getChildren(element); $.each(children_dom, function(index) {
var options = { displayChild(index, options);
onChange: onChange
};
_.each(children, function(child) {
callIfExists(child, 'init', options);
}); });
renderAttempts();
renderDependency();
validateXBlock();
}
function handleRefreshResults(results) {
$(element).html(results.html);
initXBlock();
}
function refreshXBlock() {
var handlerUrl = runtime.handlerUrl(element, 'view');
$.post(handlerUrl, '{}').success(handleRefreshResults);
} }
// validate all children function getChildByName(element, name) {
function validateXBlock() {
var is_valid = true;
var data = $('.attempts', element).data();
var children = getChildren(element);
if ((data.max_attempts > 0) && (data.num_attempts >= data.max_attempts)) {
is_valid = false;
}
else {
for (var i = 0; i < children.length; i++) { for (var i = 0; i < children.length; i++) {
var child = children[i]; var child = children[i];
if (child.name !== undefined) { if (child && child.name === name) {
var child_validation = callIfExists(child, 'validate'); return child;
if (_.isBoolean(child_validation)) {
is_valid = is_valid && child_validation;
}
} }
} }
} }
if (!is_valid) { var mentoring = {
submitDOM.attr('disabled','disabled'); callIfExists: callIfExists,
renderAttempts: renderAttempts,
renderDependency: renderDependency,
readChildren: readChildren,
children_dom: children_dom,
children: children,
displayChild: displayChild,
displayChildren: displayChildren,
getChildByName: getChildByName,
step: step
} }
else {
submitDOM.removeAttr("disabled"); if (data.mode === 'standard') {
MentoringStandardView(runtime, element, mentoring);
} }
else if (data.mode === 'assessment') {
MentoringAssessmentView(runtime, element, mentoring);
} }
// We need to manually refresh, XBlocks are currently loaded together with the section
refreshXBlock(element);
} }
function MentoringAssessmentView(runtime, element, mentoring) {
var gradeTemplate = _.template($('#xblock-grade-template').html());
var submitDOM, nextDOM, reviewDOM, tryAgainDOM;
var submitXHR;
var checkmark;
var active_child;
var callIfExists = mentoring.callIfExists;
function cleanAll() {
// clean checkmark state
checkmark.removeClass('checkmark-correct icon-ok fa-check');
checkmark.removeClass('checkmark-incorrect icon-exclamation fa-exclamation');
/* hide all children */
$(':nth-child(2)', mentoring.children_dom).remove();
$('.grade').html('');
$('.attempts').html('');
}
function renderGrade() {
var data = $('.grade', element).data();
cleanAll();
$('.grade', element).html(gradeTemplate(data));
reviewDOM.hide()
submitDOM.hide()
nextDOM.hide();
tryAgainDOM.show();
var attempts_data = $('.attempts', element).data();
if (attempts_data.num_attempts >= attempts_data.max_attempts) {
tryAgainDOM.attr("disabled", "disabled");
}
else {
tryAgainDOM.removeAttr("disabled");
}
mentoring.renderAttempts();
}
function handleTryAgain(result) {
if (result.result !== 'success')
return;
active_child = 0;
displayNextChild();
tryAgainDOM.hide();
submitDOM.show().removeAttr('disabled');
nextDOM.show();
}
function tryAgain() {
var success = true;
var handlerUrl = runtime.handlerUrl(element, 'try_again');
if (submitXHR) {
submitXHR.abort();
}
submitXHR = $.post(handlerUrl, JSON.stringify({})).success(handleTryAgain);
}
function initXBlockView() {
submitDOM = $(element).find('.submit .input-main');
nextDOM = $(element).find('.submit .input-next');
reviewDOM = $(element).find('.submit .input-review');
tryAgainDOM = $(element).find('.submit .input-try-again');
checkmark = $('.assessment-checkmark', element);
submitDOM.show();
submitDOM.bind('click', submit);
nextDOM.bind('click', displayNextChild);
nextDOM.show();
reviewDOM.bind('click', renderGrade);
tryAgainDOM.bind('click', tryAgain);
active_child = mentoring.step-1;
mentoring.readChildren();
displayNextChild();
mentoring.renderDependency();
}
function isLastChild() {
return (active_child == mentoring.children.length-1);
}
function isDone() {
return (active_child == mentoring.children.length);
}
function displayNextChild() {
var options = {
onChange: onChange
};
cleanAll();
// find the next real child block to display. HTMLBlock are always displayed
++active_child;
while (1) {
var child = mentoring.displayChild(active_child, options);
if ((typeof child !== 'undefined') || active_child == mentoring.children.length-1)
break;
++active_child;
}
if (isDone())
renderGrade();
nextDOM.attr('disabled', 'disabled');
reviewDOM.attr('disabled', 'disabled');
validateXBlock();
}
function onChange() {
validateXBlock();
}
function handleSubmitResults(result) {
$('.grade', element).data('score', result.score);
$('.grade', element).data('correct_answer', result.correct_answer);
$('.grade', element).data('incorrect_answer', result.incorrect_answer);
$('.grade', element).data('max_attempts', result.max_attempts);
$('.grade', element).data('num_attempts', result.num_attempts);
$('.attempts', element).data('max_attempts', result.max_attempts);
$('.attempts', element).data('num_attempts', result.num_attempts);
if (result.completed) {
checkmark.addClass('checkmark-correct icon-ok fa-check');
}
else {
checkmark.addClass('checkmark-incorrect icon-exclamation fa-exclamation');
}
submitDOM.attr('disabled', 'disabled');
/* Something went wrong with student submission, denied next question */
if (result.step != active_child+1) {
active_child = result.step-1;
displayNextChild();
}
else {
nextDOM.removeAttr("disabled");
reviewDOM.removeAttr("disabled");
}
}
function submit() {
var success = true;
var data = {};
var child = mentoring.children[active_child];
if (child && child.name !== undefined) {
data[child.name] = callIfExists(child, 'submit');
}
var handlerUrl = runtime.handlerUrl(element, 'submit');
if (submitXHR) {
submitXHR.abort();
}
submitXHR = $.post(handlerUrl, JSON.stringify(data)).success(handleSubmitResults);
}
function validateXBlock() {
var is_valid = true;
var data = $('.attempts', element).data();
var children = mentoring.children;
// if ((data.max_attempts > 0) && (data.num_attempts >= data.max_attempts)) {
// is_valid = false;
// }
var child = mentoring.children[active_child];
if (child && child.name !== undefined) {
var child_validation = callIfExists(child, 'validate');
if (_.isBoolean(child_validation)) {
is_valid = is_valid && child_validation;
}
}
if (!is_valid) {
submitDOM.attr('disabled','disabled');
}
else {
submitDOM.removeAttr("disabled");
}
if (isLastChild()) {
nextDOM.hide()
reviewDOM.show();
}
}
initXBlockView();
}
function MentoringStandardView(runtime, element, mentoring) {
var submitXHR;
var callIfExists = mentoring.callIfExists;
function handleSubmitResults(results) {
messagesDOM.empty().hide();
$.each(results.submitResults || [], function(index, submitResult) {
var input = submitResult[0],
result = submitResult[1],
child = mentoring.getChildByName(element, input);
var options = {
max_attempts: results.max_attempts,
num_attempts: results.num_attempts
}
callIfExists(child, 'handleSubmit', result, options);
});
$('.attempts', element).data('max_attempts', results.max_attempts);
$('.attempts', element).data('num_attempts', results.num_attempts);
mentoring.renderAttempts();
// Messages should only be displayed upon hitting 'submit', not on page reload
messagesDOM.append(results.message);
if (messagesDOM.html().trim()) {
messagesDOM.prepend('<div class="title1">Feedback</div>');
messagesDOM.show();
}
submitDOM.attr('disabled', 'disabled');
}
function submit() {
var success = true;
var data = {};
var children = mentoring.children;
for (var i = 0; i < children.length; i++) {
var child = children[i];
if (child && child.name !== undefined) {
data[child.name] = callIfExists(child, 'submit');
}
}
var handlerUrl = runtime.handlerUrl(element, 'submit');
if (submitXHR) {
submitXHR.abort();
}
submitXHR = $.post(handlerUrl, JSON.stringify(data)).success(handleSubmitResults);
}
function clearResults() {
messagesDOM.empty().hide();
var children = mentoring.children;
for (var i = 0; i < children.length; i++) {
callIfExists(children[i], 'clearResult');
}
}
function onChange() {
clearResults();
validateXBlock();
}
function initXBlockView() {
messagesDOM = $(element).find('.messages');
submitDOM = $(element).find('.submit .input-main');
submitDOM.bind('click', submit);
submitDOM.show();
var options = {
onChange: onChange
};
mentoring.displayChildren(options);
mentoring.renderAttempts();
mentoring.renderDependency();
validateXBlock();
}
function handleRefreshResults(results) {
$(element).html(results.html);
mentoring.readChildren();
initXBlockView();
}
function refreshXBlock() {
var handlerUrl = runtime.handlerUrl(element, 'view');
$.post(handlerUrl, '{}').success(handleRefreshResults);
}
// validate all children
function validateXBlock() {
var is_valid = true;
var data = $('.attempts', element).data();
var children = mentoring.children;
if ((data.max_attempts > 0) && (data.num_attempts >= data.max_attempts)) {
is_valid = false;
}
else {
for (var i = 0; i < children.length; i++) {
var child = children[i];
if (child && child.name !== undefined) {
var child_validation = callIfExists(child, 'validate');
if (_.isBoolean(child_validation)) {
is_valid = is_valid && child_validation;
}
}
}
}
if (!is_valid) {
submitDOM.attr('disabled','disabled');
}
else {
submitDOM.removeAttr("disabled");
}
}
// We need to manually refresh, XBlocks are currently loaded together with the section
refreshXBlock(element);
}
...@@ -52,7 +52,9 @@ function MessageView(element) { ...@@ -52,7 +52,9 @@ function MessageView(element) {
function MCQBlock(runtime, element) { function MCQBlock(runtime, element) {
return { return {
mode: null,
init: function(options) { init: function(options) {
this.mode = options.mode;
$('input[type=radio]', element).on('change', options.onChange); $('input[type=radio]', element).on('change', options.onChange);
}, },
...@@ -67,6 +69,9 @@ function MCQBlock(runtime, element) { ...@@ -67,6 +69,9 @@ function MCQBlock(runtime, element) {
}, },
handleSubmit: function(result) { handleSubmit: function(result) {
if (this.mode === 'assessment')
return;
var messageView = MessageView(element); var messageView = MessageView(element);
messageView.clearResult(); messageView.clearResult();
...@@ -132,7 +137,9 @@ function MCQBlock(runtime, element) { ...@@ -132,7 +137,9 @@ function MCQBlock(runtime, element) {
function MRQBlock(runtime, element) { function MRQBlock(runtime, element) {
return { return {
mode: null,
init: function(options) { init: function(options) {
this.mode = options.mode;
$('input[type=checkbox]', element).on('change', options.onChange); $('input[type=checkbox]', element).on('change', options.onChange);
}, },
...@@ -147,6 +154,9 @@ function MRQBlock(runtime, element) { ...@@ -147,6 +154,9 @@ function MRQBlock(runtime, element) {
}, },
handleSubmit: function(result, options) { handleSubmit: function(result, options) {
if (this.mode === 'assessment')
return;
var messageView = MessageView(element); var messageView = MessageView(element);
if (result.message) { if (result.message) {
......
...@@ -28,9 +28,9 @@ import logging ...@@ -28,9 +28,9 @@ import logging
from xblock.fragment import Fragment from xblock.fragment import Fragment
from .choice import ChoiceBlock from .choice import ChoiceBlock
from .light_children import LightChild, Scope, String from .light_children import LightChild, Scope, String, Float
from .tip import TipBlock from .tip import TipBlock
from .utils import render_template from .utils import render_template, render_js_template
# Globals ########################################################### # Globals ###########################################################
...@@ -51,6 +51,8 @@ class QuestionnaireAbstractBlock(LightChild): ...@@ -51,6 +51,8 @@ class QuestionnaireAbstractBlock(LightChild):
type = String(help="Type of questionnaire", scope=Scope.content, default="choices") 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="")
message = String(help="General feedback provided when submiting", scope=Scope.content, default="") message = String(help="General feedback provided when submiting", scope=Scope.content, default="")
weight = Float(help="Defines the maximum total grade of the light child block.",
default=1, scope=Scope.content, enforce_type=True)
valid_types = ('choices') valid_types = ('choices')
...@@ -77,7 +79,7 @@ class QuestionnaireAbstractBlock(LightChild): ...@@ -77,7 +79,7 @@ class QuestionnaireAbstractBlock(LightChild):
raise ValueError, u'Invalid value for {}.type: `{}`'.format(name, self.type) raise ValueError, u'Invalid value for {}.type: `{}`'.format(name, self.type)
template_path = 'templates/html/{}_{}.html'.format(name.lower(), self.type) template_path = 'templates/html/{}_{}.html'.format(name.lower(), self.type)
html = render_template(template_path, { html = render_js_template(template_path, {
'self': self, 'self': self,
'custom_choices': self.custom_choices 'custom_choices': self.custom_choices
}) })
......
...@@ -29,7 +29,7 @@ import logging ...@@ -29,7 +29,7 @@ import logging
from xblock.fields import Scope from xblock.fields import Scope
from .light_children import LightChild, String from .light_children import LightChild, String
from .utils import load_resource, render_template from .utils import load_resource, render_js_template
# Globals ########################################################### # Globals ###########################################################
...@@ -66,7 +66,7 @@ class MentoringTableBlock(LightChild): ...@@ -66,7 +66,7 @@ class MentoringTableBlock(LightChild):
else: else:
raise raise
fragment.add_content(render_template('templates/html/mentoring-table.html', { fragment.add_content(render_js_template('templates/html/mentoring-table.html', {
'self': self, 'self': self,
'columns_frags': columns_frags, 'columns_frags': columns_frags,
'header_frags': header_frags, 'header_frags': header_frags,
...@@ -102,7 +102,7 @@ class MentoringTableColumnBlock(LightChild): ...@@ -102,7 +102,7 @@ class MentoringTableColumnBlock(LightChild):
fragment, named_children = self.get_children_fragment(context, fragment, named_children = self.get_children_fragment(context,
view_name='mentoring_table_view', view_name='mentoring_table_view',
not_instance_of=MentoringTableColumnHeaderBlock) not_instance_of=MentoringTableColumnHeaderBlock)
fragment.add_content(render_template('templates/html/mentoring-table-column.html', { fragment.add_content(render_js_template('templates/html/mentoring-table-column.html', {
'self': self, 'self': self,
'named_children': named_children, 'named_children': named_children,
})) }))
...@@ -115,7 +115,7 @@ class MentoringTableColumnBlock(LightChild): ...@@ -115,7 +115,7 @@ class MentoringTableColumnBlock(LightChild):
fragment, named_children = self.get_children_fragment(context, fragment, named_children = self.get_children_fragment(context,
view_name='mentoring_table_header_view', view_name='mentoring_table_header_view',
instance_of=MentoringTableColumnHeaderBlock) instance_of=MentoringTableColumnHeaderBlock)
fragment.add_content(render_template('templates/html/mentoring-table-header.html', { fragment.add_content(render_js_template('templates/html/mentoring-table-header.html', {
'self': self, 'self': self,
'named_children': named_children, 'named_children': named_children,
})) }))
......
<div class="mentoring"> <div class="mentoring" data-mode="{{ self.mode }}" data-step="{{ self.step }}">
<div class="missing-dependency warning" data-missing="{{ self.has_missing_dependency }}"> <div class="missing-dependency warning" data-missing="{{ self.has_missing_dependency }}">
You need to complete <a href="{{ missing_dependency_url }}">the previous step</a> before You need to complete <a href="{{ missing_dependency_url }}">the previous step</a> before
attempting this step. attempting this step.
...@@ -13,8 +13,27 @@ ...@@ -13,8 +13,27 @@
{{c.body_html|safe}} {{c.body_html|safe}}
{% endfor %} {% endfor %}
{% if self.display_submit %} {% if self.display_submit %}
<div class="grade" data-score="{{ self.score.1 }}"
data-correct_answer="{{ self.score.2 }}"
data-incorrect_answer="{{ self.score.3 }}"
data-max_attempts="{{ self.max_attempts }}"
data-num_attempts="{{ self.num_attempts }}">
</div>
<div class="submit"> <div class="submit">
{% if self.mode == 'assessment' %}
<span class="assessment-checkmark icon-2x"></span>
{% endif %}
<input type="button" class="input-main" value="Submit" disabled="disabled"></input> <input type="button" class="input-main" value="Submit" disabled="disabled"></input>
{% if self.mode == 'assessment' %}
<input type="button" class="input-next" value="Next Question" disabled="disabled"></input>
<input type="button" class="input-review" value="Review grade" disabled="disabled"></input>
<input type="button" class="input-try-again" value="Try again" disabled="disabled"></input>
{% endif %}
<div class="attempts" data-max_attempts="{{ self.max_attempts }}" data-num_attempts="{{ self.num_attempts }}"></div> <div class="attempts" data-max_attempts="{{ self.max_attempts }}" data-num_attempts="{{ self.num_attempts }}"></div>
</div> </div>
{% endif %} {% endif %}
......
<script type="text/template" id="xblock-grade-template">
<% if (_.isNumber(max_attempts) && max_attempts > 0 && num_attempts >= max_attempts) {{ %>
<p>Note: you have used all attempts. Continue to the next unit.</p>
<% }} else {{ %>
<p>Note: if you retake this assessment, only your final score counts.</p>
<% }} %>
<h2>You scored <%= score %>% on this assessment.</h2>
<hr/>
<span class="assessment-checkmark icon-2x checkmark-correct icon-ok fa-check"></span>
<p>You answered <%= correct_answer %> questions correctly.</p>
<span class="assessment-checkmark icon-2x checkmark-incorrect icon-exclamation fa-exclamation"></span>
<p>You answered <%= incorrect_answer %> questions incorrectly.</p>
</script>
<mentoring url_name="{{ url_name }}" display_name="Nav tooltip title" weight="1"> <mentoring url_name="{{ url_name }}" display_name="Nav tooltip title" weight="1" mode="standard">
<title>Default Title</title> <title>Default Title</title>
<html> <html>
<p>What is your goal?</p> <p>What is your goal?</p>
......
...@@ -57,6 +57,16 @@ def render_template(template_path, context={}): ...@@ -57,6 +57,16 @@ def render_template(template_path, context={}):
return template.render(Context(context)) return template.render(Context(context))
def render_js_template(template_path, context={}, id='light-child-template'):
"""
Render a js template.
"""
return u"<script type='text/template' id='{}'>\n{}\n</script>".format(
id,
render_template(template_path, context)
)
def list2csv(row): def list2csv(row):
""" """
Convert a list to a CSV string (single row) Convert a list to a CSV string (single row)
......
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