Commit b3d47cb7 by Xavier Antoviaque

Merge pull request #5 from aboudreault/mrq-max-attempts

Mrq max attempts
parents 7d794f08 c810ef22
...@@ -75,6 +75,28 @@ Second XBlock instance: ...@@ -75,6 +75,28 @@ Second XBlock instance:
</mentoring> </mentoring>
``` ```
### Self-assessment MRQs
```xml
<mentoring url_name="mcq_1" enforce_dependency="false">
<mrq name="mrq_1_1" type="choices" max_attempts="3">
<question>What do you like in this MRQ?</question>
<choice value="elegance">Its elegance</choice>
<choice value="beauty">Its beauty</choice>
<choice value="gracefulness">Its gracefulness</choice>
<choice value="bugs">Its bugs</choice>
<tip require="gracefulness">This MRQ is indeed very graceful</tip>
<tip require="elegance,beauty">This is something everyone has to like about this MRQ</tip>
<tip reject="bugs">Nah, there isn't any!</tip>
<message type="on-submit">Thank you for answering!</message>
</mrq>
<message type="completed">
All is good now...
<html><p>Congratulations!</p></html>
</message>
</mentoring>
### Tables ### Tables
```xml ```xml
......
...@@ -55,7 +55,7 @@ class AnswerBlock(LightChild): ...@@ -55,7 +55,7 @@ class AnswerBlock(LightChild):
@lazy @lazy
def student_input(self): def student_input(self):
""" """
Use lazy property instead of XBlock field, as __init__() doesn't support Use lazy property instead of XBlock field, as __init__() doesn't support
overwriting field values overwriting field values
""" """
# Only attempt to locate a model object for this block when the answer has a name # Only attempt to locate a model object for this block when the answer has a name
...@@ -79,7 +79,7 @@ class AnswerBlock(LightChild): ...@@ -79,7 +79,7 @@ class AnswerBlock(LightChild):
html = render_template('templates/html/answer_read_only.html', { html = render_template('templates/html/answer_read_only.html', {
'self': self, 'self': self,
}) })
fragment = Fragment(html) fragment = Fragment(html)
fragment.add_css_url(self.runtime.local_resource_url(self.xblock_container, 'public/css/answer.css')) fragment.add_css_url(self.runtime.local_resource_url(self.xblock_container, 'public/css/answer.css'))
fragment.add_javascript_url(self.runtime.local_resource_url(self.xblock_container, fragment.add_javascript_url(self.runtime.local_resource_url(self.xblock_container,
......
...@@ -24,6 +24,10 @@ ...@@ -24,6 +24,10 @@
# Imports ########################################################### # Imports ###########################################################
import logging import logging
import json
from lazy import lazy
from weakref import WeakKeyDictionary
from cStringIO import StringIO from cStringIO import StringIO
from lxml import etree from lxml import etree
...@@ -34,6 +38,8 @@ from xblock.core import XBlock ...@@ -34,6 +38,8 @@ from xblock.core import XBlock
from xblock.fragment import Fragment from xblock.fragment import Fragment
from xblock.plugin import Plugin from xblock.plugin import Plugin
from .models import LightChild as LightChildModel
try: try:
from xmodule_modifiers import replace_jump_to_id_urls from xmodule_modifiers import replace_jump_to_id_urls
except: except:
...@@ -133,6 +139,7 @@ class LightChildrenMixin(XBlockWithChildrenFragmentsMixin): ...@@ -133,6 +139,7 @@ class LightChildrenMixin(XBlockWithChildrenFragmentsMixin):
""" """
Replacement for ```self.runtime.render_child()``` Replacement for ```self.runtime.render_child()```
""" """
frag = getattr(child, view_name)(context) frag = getattr(child, view_name)(context)
frag.content = u'<div class="xblock-light-child" name="{}" data-type="{}">{}</div>'.format( frag.content = u'<div class="xblock-light-child" name="{}" data-type="{}">{}</div>'.format(
child.name, child.__class__.__name__, frag.content) child.name, child.__class__.__name__, frag.content)
...@@ -167,6 +174,7 @@ class XBlockWithLightChildren(LightChildrenMixin, XBlock): ...@@ -167,6 +174,7 @@ class XBlockWithLightChildren(LightChildrenMixin, XBlock):
""" """
Current HTML view of the XBlock, for refresh by client Current HTML view of the XBlock, for refresh by client
""" """
frag = self.student_view({}) frag = self.student_view({})
frag = self.fragment_text_rewriting(frag) frag = self.fragment_text_rewriting(frag)
...@@ -194,6 +202,7 @@ class XBlockWithLightChildren(LightChildrenMixin, XBlock): ...@@ -194,6 +202,7 @@ class XBlockWithLightChildren(LightChildrenMixin, XBlock):
fragment = replace_jump_to_id_urls(course_id, jump_to_url, self, 'student_view', fragment, {}) fragment = replace_jump_to_id_urls(course_id, jump_to_url, self, 'student_view', fragment, {})
return fragment return fragment
class LightChild(Plugin, LightChildrenMixin): class LightChild(Plugin, LightChildrenMixin):
""" """
Base class for the light children Base class for the light children
...@@ -203,6 +212,7 @@ class LightChild(Plugin, LightChildrenMixin): ...@@ -203,6 +212,7 @@ class LightChild(Plugin, LightChildrenMixin):
def __init__(self, parent): def __init__(self, parent):
self.parent = parent self.parent = parent
self.xblock_container = parent.xblock_container self.xblock_container = parent.xblock_container
self._student_data_loaded = False
@property @property
def runtime(self): def runtime(self):
...@@ -220,30 +230,125 @@ class LightChild(Plugin, LightChildrenMixin): ...@@ -220,30 +230,125 @@ class LightChild(Plugin, LightChildrenMixin):
xmodule_runtime = xmodule_runtime() xmodule_runtime = xmodule_runtime()
return xmodule_runtime return xmodule_runtime
@lazy
def student_data(self):
"""
Use lazy property instead of XBlock field, as __init__() doesn't support
overwriting field values
"""
if not self.name:
return ''
student_data = self.get_lightchild_model_object().student_data
return student_data
def load_student_data(self):
"""
Load the student data from the database.
"""
if self._student_data_loaded:
return
fields = self.get_fields_to_save()
if not fields or not self.student_data:
return
student_data = json.loads(self.student_data)
for field in fields:
if field in student_data:
setattr(self, field, student_data[field])
self._student_data_loaded = True
@classmethod
def get_fields_to_save(cls):
"""
Returns a list of all LightChildField of the class. Used for saving student data.
"""
return []
def save(self): def save(self):
pass """
Replicate data changes on the related Django model used for sharing of data accross XBlocks
"""
# Save all children
for child in self.get_children_objects():
child.save()
self.student_data = {}
# Get All LightChild fields to save
for field in self.get_fields_to_save():
self.student_data[field] = getattr(self, field)
if self.name:
lightchild_data = self.get_lightchild_model_object()
if lightchild_data.student_data != self.student_data:
lightchild_data.student_data = json.dumps(self.student_data)
lightchild_data.save()
def get_lightchild_model_object(self, name=None):
"""
Fetches the LightChild model object for the lightchild named `name`
"""
if not name:
name = self.name
if not name:
raise ValueError, 'LightChild.name field need to be set to a non-null/empty value'
student_id = self.xmodule_runtime.anonymous_student_id
course_id = self.xmodule_runtime.course_id
lightchild_data, created = LightChildModel.objects.get_or_create(
student_id=student_id,
course_id=course_id,
name=name,
)
return lightchild_data
class LightChildField(object): class LightChildField(object):
""" """
Fake field with no persistence - allows to keep XBlocks fields definitions on LightChild Fake field with no persistence - allows to keep XBlocks fields definitions on LightChild
""" """
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
self.value = kwargs.get('default', '') self.default = kwargs.get('default', '')
self.data = WeakKeyDictionary()
def __get__(self, instance, name):
def __nonzero__(self): # A LightChildField can depend on student_data
return bool(self.value) instance.load_student_data()
return self.data.get(instance, self.default)
def __set__(self, instance, value):
self.data[instance] = value
class String(LightChildField): class String(LightChildField):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
self.value = kwargs.get('default', '') or '' super(String, self).__init__(*args, **kwargs)
self.default = kwargs.get('default', '') or ''
# def split(self, *args, **kwargs):
# return self.value.split(*args, **kwargs)
def __str__(self):
return self.value
def split(self, *args, **kwargs): class Integer(LightChildField):
return self.value.split(*args, **kwargs) def __init__(self, *args, **kwargs):
super(Integer, self).__init__(*args, **kwargs)
self.default = kwargs.get('default', 0)
def __set__(self, instance, value):
try:
self.data[instance] = int(value)
except (TypeError, ValueError): # not an integer
self.data[instance] = 0
class Boolean(LightChildField): class Boolean(LightChildField):
...@@ -252,7 +357,8 @@ class Boolean(LightChildField): ...@@ -252,7 +357,8 @@ class Boolean(LightChildField):
class List(LightChildField): class List(LightChildField):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
self.value = kwargs.get('default', []) super(List, self).__init__(*args, **kwargs)
self.default = kwargs.get('default', [])
class Scope(object): class Scope(object):
......
...@@ -84,6 +84,7 @@ class MentoringBlock(XBlockWithLightChildren): ...@@ -84,6 +84,7 @@ class MentoringBlock(XBlockWithLightChildren):
self.runtime.local_resource_url(self, 'public/js/vendor/underscore-min.js')) self.runtime.local_resource_url(self, 'public/js/vendor/underscore-min.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_progress.html'), "text/html") fragment.add_resource(load_resource('templates/html/mentoring_progress.html'), "text/html")
fragment.add_resource(load_resource('templates/html/mrqblock_attempts.html'), "text/html")
fragment.initialize_js('MentoringBlock') fragment.initialize_js('MentoringBlock')
......
...@@ -43,4 +43,4 @@ class Migration(SchemaMigration): ...@@ -43,4 +43,4 @@ class Migration(SchemaMigration):
} }
} }
complete_apps = ['mentoring'] complete_apps = ['mentoring']
\ No newline at end of file
# -*- coding: utf-8 -*-
import datetime
from south.db import db
from south.v2 import SchemaMigration
from django.db import models
class Migration(SchemaMigration):
def forwards(self, orm):
# Adding model 'LightChild'
db.create_table('mentoring_lightchild', (
('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
('name', self.gf('django.db.models.fields.CharField')(max_length=50, db_index=True)),
('student_id', self.gf('django.db.models.fields.CharField')(max_length=32, db_index=True)),
('course_id', self.gf('django.db.models.fields.CharField')(max_length=50, db_index=True)),
('student_data', self.gf('django.db.models.fields.TextField')(default='', blank=True)),
('created_on', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, blank=True)),
('modified_on', self.gf('django.db.models.fields.DateTimeField')(auto_now=True, blank=True)),
))
db.send_create_signal('mentoring', ['LightChild'])
# Adding unique constraint on 'LightChild', fields ['student_id', 'course_id', 'name']
db.create_unique('mentoring_lightchild', ['student_id', 'course_id', 'name'])
def backwards(self, orm):
# Removing unique constraint on 'LightChild', fields ['student_id', 'course_id', 'name']
db.delete_unique('mentoring_lightchild', ['student_id', 'course_id', 'name'])
# Deleting model 'LightChild'
db.delete_table('mentoring_lightchild')
models = {
'mentoring.answer': {
'Meta': {'unique_together': "(('student_id', 'course_id', 'name'),)", 'object_name': 'Answer'},
'course_id': ('django.db.models.fields.CharField', [], {'max_length': '50', 'db_index': 'True'}),
'created_on': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'modified_on': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '50', 'db_index': 'True'}),
'student_id': ('django.db.models.fields.CharField', [], {'max_length': '32', 'db_index': 'True'}),
'student_input': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'})
},
'mentoring.lightchild': {
'Meta': {'unique_together': "(('student_id', 'course_id', 'name'),)", 'object_name': 'LightChild'},
'course_id': ('django.db.models.fields.CharField', [], {'max_length': '50', 'db_index': 'True'}),
'created_on': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'student_data': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'modified_on': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '50', 'db_index': 'True'}),
'student_id': ('django.db.models.fields.CharField', [], {'max_length': '32', 'db_index': 'True'})
}
}
complete_apps = ['mentoring']
...@@ -49,3 +49,21 @@ class Answer(models.Model): ...@@ -49,3 +49,21 @@ class Answer(models.Model):
# Force validation of max_length # Force validation of max_length
self.full_clean() self.full_clean()
super(Answer, self).save(*args, **kwargs) super(Answer, self).save(*args, **kwargs)
class LightChild(models.Model):
"""
Django model used to store LightChild student data that need to be shared and queried accross
XBlock instances (workaround). Since this is temporary, `data` are stored in json.
"""
class Meta:
app_label = 'mentoring'
unique_together = (('student_id', 'course_id', 'name'),)
name = models.CharField(max_length=50, db_index=True)
student_id = models.CharField(max_length=32, db_index=True)
course_id = models.CharField(max_length=50, db_index=True)
student_data = models.TextField(blank=True, default='')
created_on = models.DateTimeField('created on', auto_now_add=True)
modified_on = models.DateTimeField('modified on', auto_now=True)
...@@ -26,7 +26,7 @@ ...@@ -26,7 +26,7 @@
import logging import logging
from .light_children import List, Scope from .light_children import Integer, List, Scope
from .questionnaire import QuestionnaireAbstractBlock from .questionnaire import QuestionnaireAbstractBlock
from .utils import render_template from .utils import render_template
...@@ -43,11 +43,21 @@ class MRQBlock(QuestionnaireAbstractBlock): ...@@ -43,11 +43,21 @@ class MRQBlock(QuestionnaireAbstractBlock):
An XBlock used to ask multiple-response questions An XBlock used to ask multiple-response questions
""" """
student_choices = List(help="Last submissions by the student", default=[], scope=Scope.user_state) student_choices = List(help="Last submissions by the student", default=[], scope=Scope.user_state)
max_attempts = Integer(help="Number of max attempts for this questions", scope=Scope.content)
num_attempts = Integer(help="Number of attempts a user has answered for this questions", scope=Scope.user_state)
# TODO REMOVE THIS, ONLY NEEDED FOR LIGHTCHILDREN
@classmethod
def get_fields_to_save(cls):
return [
'num_attempts'
]
def submit(self, submissions): def submit(self, submissions):
log.debug(u'Received MRQ submissions: "%s"', submissions) log.debug(u'Received MRQ submissions: "%s"', submissions)
completed = True completed = True
results = [] results = []
for choice in self.custom_choices: for choice in self.custom_choices:
choice_completed = True choice_completed = True
...@@ -73,12 +83,26 @@ class MRQBlock(QuestionnaireAbstractBlock): ...@@ -73,12 +83,26 @@ class MRQBlock(QuestionnaireAbstractBlock):
}), }),
}) })
self.student_choices = submissions self.message = u'Your answer is correct!' if completed else u'Your answer is incorrect.'
# Do not increase the counter is the answer is correct
if not completed:
setattr(self, 'num_attempts', self.num_attempts + 1)
if self.max_attempts > 0 and self.num_attempts >= self.max_attempts:
completed = True
self.message += u' You have reached the maximum number of attempts for this question. ' \
u'Your next answers won''t be saved. You can check the answer(s) using the "Show Answer(s)" button.'
else:
self.student_choices = submissions
result = { result = {
'submissions': submissions, 'submissions': submissions,
'completed': completed, 'completed': completed,
'choices': results, 'choices': results,
'message': self.message, 'message': self.message,
'max_attempts': self.max_attempts,
'num_attempts': self.num_attempts
} }
log.debug(u'MRQ submissions result: %s', result) log.debug(u'MRQ submissions result: %s', result)
return result return result
...@@ -51,7 +51,7 @@ ...@@ -51,7 +51,7 @@
.mentoring .progress .indicator { .mentoring .progress .indicator {
display: inline-block; display: inline-block;
margin-top: 5px; vertical-align: middle;
} }
.mentoring .progress .indicator .checkmark-correct { .mentoring .progress .indicator .checkmark-correct {
......
...@@ -11,21 +11,28 @@ ...@@ -11,21 +11,28 @@
margin: 10px 0; margin: 10px 0;
} }
.mentoring .choices .choice-checkbox {
display: inline-block;
margin-top: 5px;
margin-bottom: 5px;
}
.mentoring .choices .choice-result { .mentoring .choices .choice-result {
padding-right: 10px; display: inline-block;
width: 40px;
vertical-align: middle; vertical-align: middle;
cursor: pointer;
} }
.mentoring .choices .choice-result.correct { .mentoring .choices .choice-result.correct, .choice-answer.correct {
cursor: pointer;
color: #006600; color: #006600;
position: relative; position: relative;
top: -3px; top: -3px;
} }
.mentoring .choices .choice-result.incorrect { .mentoring .choices .choice-result.incorrect {
margin-right: 10px; text-align:center;
padding-left: 10px;
padding-right: 10px;
color: #ff0000; color: #ff0000;
} }
...@@ -78,3 +85,15 @@ ...@@ -78,3 +85,15 @@
.mentoring .choices-list .choice-selector { .mentoring .choices-list .choice-selector {
margin-right: 5px; margin-right: 5px;
} }
.mentoring .mrq-attempts {
display: inline-block;
vertical-align: baseline;
color: #777;
font-style: italic;
webkit-font-smoothing: antialiased;
}
.mentoring .mrq-attempts div {
display: inline-block;
}
...@@ -74,7 +74,7 @@ function MentoringBlock(runtime, element) { ...@@ -74,7 +74,7 @@ function MentoringBlock(runtime, element) {
} }
function initXBlock() { function initXBlock() {
var submit_dom = $(element).find('.submit'); var submit_dom = $(element).find('.submit .input-main');
submit_dom.bind('click', function() { submit_dom.bind('click', function() {
var data = {}; var data = {};
...@@ -89,6 +89,12 @@ function MentoringBlock(runtime, element) { ...@@ -89,6 +89,12 @@ function MentoringBlock(runtime, element) {
$.post(handlerUrl, JSON.stringify(data)).success(handleSubmitResults); $.post(handlerUrl, JSON.stringify(data)).success(handleSubmitResults);
}); });
// init children (especially mrq blocks)
var children = getChildren(element);
_.each(children, function(child) {
callIfExists(child, 'init');
});
if (submit_dom.length) { if (submit_dom.length) {
renderProgress(); renderProgress();
} }
......
// TODO: Split in two files // TODO: Split in two files
var mrqAttemptsTemplate = _.template($('#xblock-mrq-attempts').html());
function MCQBlock(runtime, element) { function MCQBlock(runtime, element) {
return { return {
...@@ -25,7 +26,29 @@ function MCQBlock(runtime, element) { ...@@ -25,7 +26,29 @@ function MCQBlock(runtime, element) {
function MRQBlock(runtime, element) { function MRQBlock(runtime, element) {
return { return {
renderAttempts: function() {
var data = $('.mrq-attempts', element).data();
$('.mrq-attempts', element).html(mrqAttemptsTemplate(data));
// bind show answer button
var showAnswerButton = $('button', element);
if (showAnswerButton.length != 0) {
if (_.isUndefined(this.answers))
showAnswerButton.hide();
else
showAnswerButton.on('click', _.bind(this.toggleAnswers, this));
}
},
init: function() {
this.renderAttempts();
},
submit: function() { submit: function() {
// hide answers
var choiceInputDOM = $('.choice input', element),
choiceResultDOM = $('.choice-answer', choiceInputDOM.closest('.choice'));
choiceResultDOM.removeClass('incorrect icon-exclamation correct icon-ok');
var checkedCheckboxes = $('input[type=checkbox]:checked', element), var checkedCheckboxes = $('input[type=checkbox]:checked', element),
checkedValues = []; checkedValues = [];
...@@ -58,17 +81,29 @@ function MRQBlock(runtime, element) { ...@@ -58,17 +81,29 @@ function MRQBlock(runtime, element) {
showPopup(messageDOM); showPopup(messageDOM);
} }
var answers = []; // used in displayAnswers
$.each(result.choices, function(index, choice) { $.each(result.choices, function(index, choice) {
var choiceInputDOM = $('.choice input[value='+choice.value+']', element), var choiceInputDOM = $('.choice input[value='+choice.value+']', element),
choiceDOM = choiceInputDOM.closest('.choice'), choiceDOM = choiceInputDOM.closest('.choice'),
choiceResultDOM = $('.choice-result', choiceDOM), choiceResultDOM = $('.choice-result', choiceDOM),
choiceAnswerDOM = $('.choice-answer', choiceDOM),
choiceTipsDOM = $('.choice-tips', choiceDOM), choiceTipsDOM = $('.choice-tips', choiceDOM),
choiceTipsCloseDOM; choiceTipsCloseDOM;
if (choice.completed) { /* update our answers dict */
choiceResultDOM.removeClass('incorrect icon-exclamation').addClass('correct icon-ok'); answers.push({
} else { input: choiceInputDOM,
choiceResultDOM.removeClass('correct icon-ok').addClass('incorrect icon-exclamation'); answer: choice.completed ? choiceInputDOM.attr('checked') : !choiceInputDOM.attr('checked')
});
choiceResultDOM.removeClass('incorrect icon-exclamation correct icon-ok');
/* show hint if checked or max_attempts is disabled */
if (result.completed || choiceInputDOM.prop('checked') || result.max_attempts <= 0) {
if (choice.completed) {
choiceResultDOM.addClass('correct icon-ok');
} else if (!choice.completed) {
choiceResultDOM.addClass('incorrect icon-exclamation');
}
} }
choiceTipsDOM.html(choice.tips); choiceTipsDOM.html(choice.tips);
...@@ -77,7 +112,36 @@ function MRQBlock(runtime, element) { ...@@ -77,7 +112,36 @@ function MRQBlock(runtime, element) {
choiceResultDOM.off('click').on('click', function() { choiceResultDOM.off('click').on('click', function() {
showPopup(choiceTipsDOM); showPopup(choiceTipsDOM);
}); });
choiceAnswerDOM.off('click').on('click', function() {
showPopup(choiceTipsDOM);
});
}); });
} this.answers = answers;
$('.mrq-attempts', element).data('num_attempts', result.num_attempts);
this.renderAttempts();
},
toggleAnswers: function() {
var showAnswerButton = $('button span', element);
var answers_displayed = this.answers_displayed = !this.answers_displayed;
_.each(this.answers, function(answer) {
var choiceResultDOM = $('.choice-answer', answer.input.closest('.choice'));
choiceResultDOM.removeClass('correct icon-ok');
if (answers_displayed) {
if (answer.answer)
choiceResultDOM.addClass('correct icon-ok');
showAnswerButton.text('Hide Answer(s)');
}
else {
showAnswerButton.text('Show Answer(s)');
}
});
}
}; };
} }
...@@ -8,7 +8,7 @@ ...@@ -8,7 +8,7 @@
{% endfor %} {% endfor %}
{% if self.display_submit %} {% if self.display_submit %}
<div class="submit"> <div class="submit">
<input type="button" value="Submit"></input> <input type="button" class="input-main" value="Submit"></input>
<span class="progress" data-completed="{{ self.completed }}" data-attempted="{{ self.attempted }}"> <span class="progress" data-completed="{{ self.completed }}" data-attempted="{{ self.attempted }}">
<span class='indicator'></span> <span class='indicator'></span>
</span> </span>
......
<script type="text/template" id="xblock-mrq-attempts">
<% if (_.isNumber(max_attempts) && max_attempts > 0) {{ %>
<% if (num_attempts >= max_attempts) {{ %>
<button class="show">
<span class="show-label">Show Answer(s)</span>
</button>
<% }} %>
<div> You have used <%= _.min([num_attempts, max_attempts]) %> of <%= max_attempts %> attempts for this question.</div>
<% }} %>
</script>
...@@ -5,12 +5,20 @@ ...@@ -5,12 +5,20 @@
{% for choice in custom_choices %} {% for choice in custom_choices %}
<div class="choice"> <div class="choice">
<span class="choice-result icon-2x"></span> <span class="choice-result icon-2x"></span>
<label class="choice-label"> <div class="choice-checkbox">
<input class="choice-selector" type="checkbox" name="{{ self.name }}" value="{{ choice.value }}"{% if choice.value in self.student_choices %} checked{% endif %}> {{ choice.content }} <label class="choice-label">
</label> <input class="choice-selector" type="checkbox" name="{{ self.name }}"
value="{{ choice.value }}"
{% if choice.value in self.student_choices %} checked{% endif %}>
{{ choice.content }}
</input>
</label>
</div>
<span class="choice-answer icon-2x"></span>
<div class="choice-tips"></div> <div class="choice-tips"></div>
</div> </div>
{% endfor %} {% endfor %}
<div class="choice-message"></div> <div class="choice-message"></div>
</div> </div>
</fieldset> </fieldset>
<div class="mrq-attempts" data-max_attempts="{{ self.max_attempts }}" data-num_attempts="{{ self.num_attempts }}"></div>
...@@ -105,9 +105,9 @@ class XBlockWithChildrenFragmentsMixin(object): ...@@ -105,9 +105,9 @@ class XBlockWithChildrenFragmentsMixin(object):
and a list of fragments, one per children and a list of fragments, one per children
- `view_name` allows to select a specific view method on the children - `view_name` allows to select a specific view method on the children
- `instance_of` allows to only return fragments for children which are instances of - `instance_of` allows to only return fragments for children which are instances of
the provided class the provided class
- `not_instance_of` allows to only return fragments for children which are *NOT* - `not_instance_of` allows to only return fragments for children which are *NOT*
instances of the provided class instances of the provided class
""" """
fragment = Fragment() fragment = Fragment()
......
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