diff --git a/.gitignore b/.gitignore
index 767c1df8..1c37539a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -5,3 +5,4 @@
/workbench.*
/dist
/templates
+*.iml
\ No newline at end of file
diff --git a/doc/img/mrq-3.png b/doc/img/mrq-3.png
index 5f5ece98..588c6129 100644
Binary files a/doc/img/mrq-3.png and b/doc/img/mrq-3.png differ
diff --git a/problem_builder/answer.py b/problem_builder/answer.py
index f09016d5..7054336a 100644
--- a/problem_builder/answer.py
+++ b/problem_builder/answer.py
@@ -179,6 +179,15 @@ def student_view(self, context=None):
""" Normal view of this XBlock, identical to mentoring_view """
return self.mentoring_view(context)
+ def get_results(self, previous_response=None):
+ # Previous result is actually stored in database table-- ignore.
+ return {
+ 'student_input': self.student_input,
+ 'status': self.status,
+ 'weight': self.weight,
+ 'score': 1 if self.status == 'correct' else 0,
+ }
+
def submit(self, submission):
"""
The parent block is handling a student submission, including a new answer for this
@@ -187,12 +196,7 @@ def submit(self, submission):
self.student_input = submission[0]['value'].strip()
self.save()
log.info(u'Answer submitted for`{}`: "{}"'.format(self.name, self.student_input))
- return {
- 'student_input': self.student_input,
- 'status': self.status,
- 'weight': self.weight,
- 'score': 1 if self.status == 'correct' else 0,
- }
+ return self.get_results()
@property
def status(self):
diff --git a/problem_builder/mcq.py b/problem_builder/mcq.py
index 8782744a..5647a72f 100644
--- a/problem_builder/mcq.py
+++ b/problem_builder/mcq.py
@@ -74,15 +74,15 @@ def describe_choice_correctness(self, choice_value):
return self._(u"Wrong")
return self._(u"Not Acceptable")
- def submit(self, submission):
- log.debug(u'Received MCQ submission: "%s"', submission)
-
+ def calculate_results(self, submission):
correct = submission in self.correct_choices
tips_html = []
for tip in self.get_tips():
if submission in tip.values:
tips_html.append(tip.render('mentoring_view').content)
+ formatted_tips = None
+
if tips_html:
formatted_tips = loader.render_template('templates/html/tip_choice_group.html', {
'tips_html': tips_html,
@@ -94,13 +94,21 @@ def submit(self, submission):
# Also send to the submissions API:
sub_api.create_submission(self.student_item_key, submission)
- result = {
+ return {
'submission': submission,
'status': 'correct' if correct else 'incorrect',
- 'tips': formatted_tips if tips_html else None,
+ 'tips': formatted_tips,
'weight': self.weight,
'score': 1 if correct else 0,
}
+
+ def get_results(self, previous_result):
+ return self.calculate_results(previous_result['submission'])
+
+ def submit(self, submission):
+ log.debug(u'Received MCQ submission: "%s"', submission)
+ result = self.calculate_results(submission)
+ self.student_choice = submission
log.debug(u'MCQ submission result: %s', result)
return result
diff --git a/problem_builder/mentoring.py b/problem_builder/mentoring.py
index 8d8177bb..8f93f0dd 100644
--- a/problem_builder/mentoring.py
+++ b/problem_builder/mentoring.py
@@ -21,6 +21,7 @@
# Imports ###########################################################
import logging
+import json
from collections import namedtuple
@@ -61,6 +62,10 @@ def _(text):
Score = namedtuple("Score", ["raw", "percentage", "correct", "incorrect", "partially_correct"])
+CORRECT = 'correct'
+INCORRECT = 'incorrect'
+PARTIAL = 'partial'
+
@XBlock.needs("i18n")
@XBlock.wants('settings')
@@ -160,6 +165,11 @@ class MentoringBlock(XBlock, StepParentMixin, StudioEditableXBlockMixin, StudioC
default=[],
scope=Scope.user_state
)
+ extended_feedback = Boolean(
+ help=_("Show extended feedback details when all attempts are used up."),
+ default=False,
+ Scope=Scope.content
+ )
# Global user state
next_step = String(
@@ -170,7 +180,7 @@ class MentoringBlock(XBlock, StepParentMixin, StudioEditableXBlockMixin, StudioC
editable_fields = (
'display_name', 'mode', 'followed_by', 'max_attempts', 'enforce_dependency',
- 'display_submit', 'weight',
+ 'display_submit', 'weight', 'extended_feedback'
)
icon_class = 'problem'
has_score = True
@@ -201,17 +211,39 @@ def get_theme(self):
return xblock_settings[self.theme_key]
return _default_theme_config
+ def get_question_number(self, question_id):
+ """
+ Get the step number of the question id
+ """
+ for child_id in self.children:
+ question = self.runtime.get_block(child_id)
+ if isinstance(question, StepMixin) and (question.name == question_id):
+ return question.step_number
+ raise ValueError("Question ID in answer set not a step of this Mentoring Block!")
+
+ def answer_mapper(self, answer_status):
+ """
+ Create a JSON-dumpable object with readable key names from a list of student answers.
+ """
+ return [
+ {
+ 'number': self.get_question_number(answer[0]),
+ 'id': answer[0],
+ 'details': answer[1],
+ } for answer in self.student_results if answer[1]['status'] == answer_status
+ ]
+
@property
def score(self):
"""Compute the student score taking into account the weight of each step."""
weights = (float(self.runtime.get_block(step_id).weight) for step_id in self.steps)
total_child_weight = sum(weights)
if total_child_weight == 0:
- return Score(0, 0, 0, 0, 0)
+ return Score(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]['status'] == 'correct')
- incorrect = sum(1 for r in self.student_results if r[1]['status'] == 'incorrect')
- partially_correct = sum(1 for r in self.student_results if r[1]['status'] == 'partial')
+ correct = self.answer_mapper(CORRECT)
+ incorrect = self.answer_mapper(INCORRECT)
+ partially_correct = self.answer_mapper(PARTIAL)
return Score(score, int(round(score * 100)), correct, incorrect, partially_correct)
@@ -259,6 +291,7 @@ def student_view(self, context):
fragment.add_javascript_url(self.runtime.local_resource_url(self, 'public/js/mentoring.js'))
fragment.add_resource(loader.load_unicode('templates/html/mentoring_attempts.html'), "text/html")
fragment.add_resource(loader.load_unicode('templates/html/mentoring_grade.html'), "text/html")
+ fragment.add_resource(loader.load_unicode('templates/html/mentoring_review_questions.html'), "text/html")
self.include_theme_files(fragment)
# Workbench doesn't have font awesome, so add it:
@@ -335,6 +368,87 @@ def publish_event(self, data, suffix=''):
return {'result': 'ok'}
+ def get_message(self, completed):
+ if self.max_attempts_reached:
+ return self.get_message_html('max_attempts_reached')
+ elif completed:
+ return self.get_message_html('completed')
+ else:
+ return self.get_message_html('incomplete')
+
+ @property
+ def assessment_message(self):
+ if not self.max_attempts_reached:
+ return self.get_message_html('on-assessment-review')
+ else:
+ return None
+
+ def show_extended_feedback(self):
+ return self.extended_feedback and self.max_attempts_reached
+
+ def feedback_dispatch(self, target_data, stringify):
+ if self.show_extended_feedback():
+ if stringify:
+ return json.dumps(target_data)
+ else:
+ return target_data
+
+ def correct_json(self, stringify=True):
+ return self.feedback_dispatch(self.score.correct, stringify)
+
+ def incorrect_json(self, stringify=True):
+ return self.feedback_dispatch(self.score.incorrect, stringify)
+
+ def partial_json(self, stringify=True):
+ return self.feedback_dispatch(self.score.partially_correct, stringify)
+
+ @XBlock.json_handler
+ def get_results(self, queries, suffix=''):
+ """
+ Gets detailed results in the case of extended feedback.
+
+ It may be a good idea to eventually have this function get results
+ in the general case instead of loading them in the template in the future,
+ and only using it for extended feedback situations.
+
+ Right now there are two ways to get results-- through the template upon loading up
+ the mentoring block, or after submission of an AJAX request like in
+ submit or get_results here.
+ """
+ results = []
+ if not self.show_extended_feedback():
+ return {
+ 'results': [],
+ 'error': 'Extended feedback results cannot be obtained.'
+ }
+ completed = True
+ choices = dict(self.student_results)
+ step = self.step
+ # Only one child should ever be of concern with this method.
+ for child_id in self.steps:
+ child = self.runtime.get_block(child_id)
+ if child.name and child.name in queries:
+ results = [child.name, child.get_results(choices[child.name])]
+ # Children may have their own definition of 'completed' which can vary from the general case
+ # of the whole mentoring block being completed. This is because in standard mode, all children
+ # must be correct to complete the block. In assessment mode with extended feedback, completion
+ # happens when you're out of attempts, no matter how you did.
+ completed = choices[child.name]['status']
+ break
+
+ # The 'completed' message should always be shown in this case, since no more attempts are available.
+ message = self.get_message(True)
+
+ return {
+ 'results': results,
+ 'completed': completed,
+ 'attempted': self.attempted,
+ 'message': message,
+ 'step': step,
+ 'max_attempts': self.max_attempts,
+ 'num_attempts': self.num_attempts,
+ }
+
@XBlock.json_handler
def submit(self, submissions, suffix=''):
log.info(u'Received submissions: {}'.format(submissions))
@@ -354,12 +468,7 @@ def submit(self, submissions, suffix=''):
child.save()
completed = completed and (child_result['status'] == 'correct')
- if self.max_attempts_reached:
- message = self.get_message_html('max_attempts_reached')
- elif completed:
- message = self.get_message_html('completed')
- else:
- message = self.get_message_html('incomplete')
+ message = self.get_message(completed)
# Once it has been completed once, keep completion even if user changes values
if self.completed:
@@ -402,7 +511,7 @@ def submit(self, submissions, suffix=''):
})
return {
- 'submitResults': submit_results,
+ 'results': submit_results,
'completed': self.completed,
'attempted': self.attempted,
'message': message,
@@ -416,6 +525,7 @@ def handle_assessment_submit(self, submissions, suffix):
children = [self.runtime.get_block(child_id) for child_id in self.children]
children = [child for child in children if not isinstance(child, MentoringMessageBlock)]
steps = [child for child in children if isinstance(child, StepMixin)] # Faster than the self.steps property
+ assessment_message = None
for child in children:
if child.name and child.name in submissions:
@@ -451,6 +561,7 @@ def handle_assessment_submit(self, submissions, suffix):
'score_type': 'proficiency',
})
event_data['final_grade'] = score.raw
+ assessment_message = self.assessment_message
self.num_attempts += 1
self.completed = True
@@ -468,9 +579,14 @@ def handle_assessment_submit(self, submissions, suffix):
'num_attempts': self.num_attempts,
'step': self.step,
'score': score.percentage,
- 'correct_answer': score.correct,
- 'incorrect_answer': score.incorrect,
- 'partially_correct_answer': score.partially_correct,
+ 'correct_answer': len(score.correct),
+ 'incorrect_answer': len(score.incorrect),
+ 'partially_correct_answer': len(score.partially_correct),
+ 'correct': self.correct_json(stringify=False),
+ 'incorrect': self.incorrect_json(stringify=False),
+ 'partial': self.partial_json(stringify=False),
+ 'extended_feedback': self.show_extended_feedback() or '',
+ 'assessment_message': assessment_message,
}
@XBlock.json_handler
diff --git a/problem_builder/message.py b/problem_builder/message.py
index d57d52d2..cebc6d07 100644
--- a/problem_builder/message.py
+++ b/problem_builder/message.py
@@ -56,6 +56,7 @@ class MentoringMessageBlock(XBlock, StudioEditableXBlockMixin):
{"display_name": "Completed", "value": "completed"},
{"display_name": "Incompleted", "value": "incomplete"},
{"display_name": "Reached max. # of attemps", "value": "max_attempts_reached"},
+ {"display_name": "Review with attempts left", "value": "on-assessment-review"}
),
)
editable_fields = ("content", )
@@ -84,6 +85,8 @@ def display_name_with_default(self):
return self._(u"Message shown when complete")
if self.type == 'incomplete':
return self._(u"Message shown when incomplete")
+ if self.type == 'on-assessment-review':
+ return self._(u"Message shown during review when attempts remain")
return u"INVALID MESSAGE"
@classmethod
diff --git a/problem_builder/mrq.py b/problem_builder/mrq.py
index 0fec59ca..1d8fad59 100644
--- a/problem_builder/mrq.py
+++ b/problem_builder/mrq.py
@@ -81,11 +81,25 @@ def describe_choice_correctness(self, choice_value):
return self._(u"Ignored")
return self._(u"Not Acceptable")
+ def get_results(self, previous_result):
+ """
+ Get the results a student has already submitted.
+ """
+ result = self.calculate_results(previous_result['submissions'])
+ result['completed'] = True
+ return result
+
def submit(self, submissions):
log.debug(u'Received MRQ submissions: "%s"', submissions)
- score = 0
+ result = self.calculate_results(submissions)
+ self.student_choices = submissions
+ log.debug(u'MRQ submissions result: %s', result)
+ return result
+
+ def calculate_results(self, submissions):
+ score = 0
results = []
for choice in self.custom_choices:
choice_completed = True
@@ -106,22 +120,20 @@ def submit(self, submissions):
choice_result = {
'value': choice.value,
'selected': choice_selected,
- }
+ }
# Only include tips/results in returned response if we want to display them
if not self.hide_results:
loader = ResourceLoader(__name__)
choice_result['completed'] = choice_completed
choice_result['tips'] = loader.render_template('templates/html/tip_choice_group.html', {
'tips_html': choice_tips_html,
- })
+ })
results.append(choice_result)
- self.student_choices = submissions
-
status = 'incorrect' if score <= 0 else 'correct' if score >= len(results) else 'partial'
- result = {
+ return {
'submissions': submissions,
'status': status,
'choices': results,
@@ -130,9 +142,6 @@ def submit(self, submissions):
'score': (float(score) / len(results)) if results else 0,
}
- log.debug(u'MRQ submissions result: %s', result)
- return result
-
def validate_field_data(self, validation, data):
"""
Validate this block's field data.
diff --git a/problem_builder/public/css/mentoring.css b/problem_builder/public/css/mentoring.css
index c8a63bba..871ad97c 100644
--- a/problem_builder/public/css/mentoring.css
+++ b/problem_builder/public/css/mentoring.css
@@ -2,14 +2,16 @@
margin: 1em 0em;
}
-.mentoring .messages {
+.mentoring .messages,
+.mentoring .assessment-messages {
display: none;
margin-top: 10px;
border-top: 2px solid #eaeaea;
padding: 12px 0px 20px;
}
-.mentoring .messages .title1 {
+.mentoring .messages .title1,
+.mentoring .assessment-messages .title1 {
color: #333333;
text-transform: uppercase;
font-weight: bold;
@@ -134,3 +136,28 @@
.mentoring input[type="radio"] {
margin: 0;
}
+
+.mentoring .review-list {
+ list-style: none;
+ padding-left: 0 !important;
+ margin-left: 0;
+}
+.mentoring .review-list li {
+ display: inline;
+}
+
+.mentoring .review-list li a{
+ font-weight: bold;
+}
+
+.mentoring .results-section {
+ float: left;
+}
+.mentoring .clear {
+ display: block;
+ clear: both;
+}
+
+.mentoring .review-link {
+ float: right;
+}
\ No newline at end of file
diff --git a/problem_builder/public/js/answer.js b/problem_builder/public/js/answer.js
index 8d6c48d9..4a50a255 100644
--- a/problem_builder/public/js/answer.js
+++ b/problem_builder/public/js/answer.js
@@ -17,15 +17,22 @@ function AnswerBlock(runtime, element) {
return $(':input', element).serializeArray();
},
+ handleReview: function(result) {
+ $('textarea', element).prop('disabled', true);
+ },
+
handleSubmit: function(result) {
- if (this.mode === 'assessment')
- return;
var checkmark = $('.answer-checkmark', element);
$(element).find('.message').text((result || {}).error || '');
this.clearResult();
+ if (this.mode === 'assessment') {
+ // Display of checkmark would be redundant.
+ return
+ }
+
if (result.status === "correct") {
checkmark.addClass('checkmark-correct icon-ok fa-check');
}
diff --git a/problem_builder/public/js/mentoring.js b/problem_builder/public/js/mentoring.js
index 760beb32..f3ffe318 100644
--- a/problem_builder/public/js/mentoring.js
+++ b/problem_builder/public/js/mentoring.js
@@ -60,7 +60,7 @@ function MentoringBlock(runtime, element) {
if (typeof obj !== 'undefined' && typeof obj[fn] == 'function') {
return obj[fn].apply(obj, Array.prototype.slice.call(arguments, 2));
} else {
- return undefined;
+ return null;
}
}
diff --git a/problem_builder/public/js/mentoring_assessment_view.js b/problem_builder/public/js/mentoring_assessment_view.js
index dc060a94..b26329f1 100644
--- a/problem_builder/public/js/mentoring_assessment_view.js
+++ b/problem_builder/public/js/mentoring_assessment_view.js
@@ -1,6 +1,7 @@
function MentoringAssessmentView(runtime, element, mentoring) {
var gradeTemplate = _.template($('#xblock-grade-template').html());
- var submitDOM, nextDOM, reviewDOM, tryAgainDOM;
+ var reviewQuestionsTemplate = _.template($('#xblock-review-questions-template').html());
+ var submitDOM, nextDOM, reviewDOM, tryAgainDOM, messagesDOM, reviewLinkDOM;
var submitXHR;
var checkmark;
var active_child;
@@ -21,15 +22,36 @@ function MentoringAssessmentView(runtime, element, mentoring) {
$('.grade').html('');
$('.attempts').html('');
+ messagesDOM.empty().hide();
+ }
+
+ function no_more_attempts() {
+ var attempts_data = $('.attempts', element).data();
+ return attempts_data.num_attempts >= attempts_data.max_attempts;
}
function renderGrade() {
notify('navigation', {state: 'unlock'})
var data = $('.grade', element).data();
+ data.enable_extended = (no_more_attempts() && data.extended_feedback);
+ _.extend(data, {
+ 'runDetails': function(label) {
+ if (! data.enable_extended) {
+ return ''
+ }
+ var self = this;
+ return reviewQuestionsTemplate({'questions': self[label], 'label': label})
+ }
+ });
cleanAll();
$('.grade', element).html(gradeTemplate(data));
+ reviewLinkDOM.hide();
reviewDOM.hide();
submitDOM.hide();
+ if (data.enable_extended) {
+ nextDOM.unbind('click');
+ nextDOM.bind('click', reviewNextChild)
+ }
nextDOM.hide();
tryAgainDOM.show();
@@ -42,6 +64,11 @@ function MentoringAssessmentView(runtime, element, mentoring) {
}
mentoring.renderAttempts();
+ if (data.assessment_message && data.num_attempts < data.max_attempts) {
+ mentoring.setContent(messagesDOM, data.assessment_message);
+ messagesDOM.show();
+ }
+ $('a.question-link', element).click(reviewJump);
}
function handleTryAgain(result) {
@@ -59,7 +86,6 @@ function MentoringAssessmentView(runtime, element, mentoring) {
}
function tryAgain() {
- var success = true;
var handlerUrl = runtime.handlerUrl(element, 'try_again');
if (submitXHR) {
submitXHR.abort();
@@ -73,17 +99,26 @@ function MentoringAssessmentView(runtime, element, mentoring) {
nextDOM = $(element).find('.submit .input-next');
reviewDOM = $(element).find('.submit .input-review');
tryAgainDOM = $(element).find('.submit .input-try-again');
+ reviewLinkDOM = $(element).find('.review-link');
checkmark = $('.assessment-checkmark', element);
+ messagesDOM = $('.assessment-messages', element);
+ reviewLinkDOM.hide();
submitDOM.show();
submitDOM.bind('click', submit);
nextDOM.bind('click', displayNextChild);
nextDOM.show();
- reviewDOM.bind('click', renderGrade);
tryAgainDOM.bind('click', tryAgain);
active_child = mentoring.step;
+ function renderGradeEvent(event) {
+ event.preventDefault();
+ renderGrade();
+ }
+ reviewLinkDOM.bind('click', renderGradeEvent);
+ reviewDOM.bind('click', renderGradeEvent);
+
var options = {
onChange: onChange
};
@@ -113,24 +148,85 @@ function MentoringAssessmentView(runtime, element, mentoring) {
}
}
- function displayNextChild() {
- cleanAll();
+ function reviewJump(event) {
+ // Used only during extended feedback. Assumes completion and attempts exhausted.
+ event.preventDefault();
- // find the next real child block to display. HTMLBlock are always displayed
- active_child++;
+ var target = parseInt($(event.target).data('step')) - 1;
+ reviewDisplayChild(target);
+ }
+
+ function reviewDisplayChild(child_index) {
+ active_child = child_index;
+ cleanAll();
var child = mentoring.steps[active_child];
$(child.element).show();
$(child.element).find("input, textarea").first().focus();
mentoring.publish_event({
- event_type: 'xblock.problem_builder.assessment.shown',
- exercise_id: child.name
+ event_type: 'xblock.mentoring.assessment.review',
+ exercise_id: $(mentoring.steps[active_child]).attr('name')
});
+ post_display(true);
+ get_results();
+ }
+
+ function reviewNextChild() {
+ nextDOM.attr('disabled', 'disabled');
+ nextDOM.hide();
+ findNextChild();
+ reviewDisplayChild(active_child)
+ }
- if (isDone())
+ function displayNextChild() {
+ cleanAll();
+ findNextChild(true);
+ // find the next real child block to display. HTMLBlock are always displayed
+ if (isDone()) {
renderGrade();
+ } else {
+ post_display();
+ }
+ }
+
+ function findNextChild(fire_event) {
+ // find the next real child block to display. HTMLBlock are always displayed
+ ++active_child;
+ var child = mentoring.steps[active_child];
+ $(child.element).show();
+ $(child.element).find("input, textarea").first().focus();
+ if (fire_event) {
+ mentoring.publish_event({
+ event_type: 'xblock.problem_builder.assessment.shown',
+ exercise_id: child.name
+ });
+ }
+ }
+
+ function post_display(show_link) {
nextDOM.attr('disabled', 'disabled');
- reviewDOM.attr('disabled', 'disabled');
- validateXBlock();
+ if (no_more_attempts()) {
+ if (show_link) {
+ reviewLinkDOM.show();
+ } else {
+ reviewDOM.show();
+ reviewDOM.removeAttr('disabled')
+ }
+ } else {
+ reviewDOM.attr('disabled', 'disabled');
+ }
+ validateXBlock(show_link);
+ if (show_link && ! isLastChild()) {
+ // User should also be able to browse forward if we're showing the review link.
+ nextDOM.show();
+ nextDOM.removeAttr('disabled');
+ }
+ if (show_link) {
+ // The user has no more tries, so the try again button is noise. A disabled submit button
+ // emphasizes that the user cannot change their answer.
+ tryAgainDOM.hide();
+ submitDOM.show();
+ submitDOM.attr('disabled', 'disabled')
+ }
}
function onChange() {
@@ -142,19 +238,20 @@ function MentoringAssessmentView(runtime, element, mentoring) {
}
}
- 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('partially_correct_answer', result.partially_correct_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 === 'partial') {
+ function handleResults(response) {
+ $('.grade', element).data('score', response.score);
+ $('.grade', element).data('correct_answer', response.correct_answer);
+ $('.grade', element).data('incorrect_answer', response.incorrect_answer);
+ $('.grade', element).data('partially_correct_answer', response.partially_correct_answer);
+ $('.grade', element).data('max_attempts', response.max_attempts);
+ $('.grade', element).data('num_attempts', response.num_attempts);
+ $('.grade', element).data('assessment_message', response.assessment_message);
+ $('.attempts', element).data('max_attempts', response.max_attempts);
+ $('.attempts', element).data('num_attempts', response.num_attempts);
+
+ if (response.completed === 'partial') {
checkmark.addClass('checkmark-partially-correct icon-ok fa-check');
- } else if (result.completed === 'correct') {
+ } else if (response.completed === 'correct') {
checkmark.addClass('checkmark-correct icon-ok fa-check');
} else {
checkmark.addClass('checkmark-incorrect icon-exclamation fa-exclamation');
@@ -162,40 +259,58 @@ function MentoringAssessmentView(runtime, element, mentoring) {
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");
- if (nextDOM.is(':visible')) { nextDOM.focus(); }
- reviewDOM.removeAttr("disabled");
- if (reviewDOM.is(':visible')) { reviewDOM.focus(); }
+ /* We're not dealing with the current step */
+ if (response.step != active_child+1) {
+ return
}
+ nextDOM.removeAttr("disabled");
+ reviewDOM.removeAttr("disabled");
+ if (nextDOM.is(':visible')) { nextDOM.focus(); }
+ if (reviewDOM.is(':visible')) { reviewDOM.focus(); }
}
- function submit() {
- var success = true;
+ function handleReviewResults(response) {
+ handleResults(response);
+ var options = {
+ max_attempts: response.max_attempts,
+ num_attempts: response.num_attempts
+ };
+ var result = response.results[1];
+ var child = mentoring.steps[active_child];
+ callIfExists(child, 'handleSubmit', result, options);
+ callIfExists(child, 'handleReview', result, options);
+ }
+
+ function handleSubmitResults(response){
+ handleResults(response);
+ // Update grade information
+ $('.grade').data(response);
+ }
+
+
+ function calculate_results(handler_name, callback) {
var data = {};
var child = mentoring.steps[active_child];
if (child && child.name !== undefined) {
- data[child.name] = callIfExists(child, 'submit');
+ data[child.name] = callIfExists(child, handler_name);
}
- var handlerUrl = runtime.handlerUrl(element, 'submit');
+ var handlerUrl = runtime.handlerUrl(element, handler_name);
if (submitXHR) {
submitXHR.abort();
}
- submitXHR = $.post(handlerUrl, JSON.stringify(data)).success(handleSubmitResults);
+ submitXHR = $.post(handlerUrl, JSON.stringify(data)).success(callback);
}
- function validateXBlock() {
- var is_valid = true;
- var data = $('.attempts', element).data();
- var steps = mentoring.steps;
+ function submit() {
+ calculate_results('submit', handleSubmitResults)
+ }
- // if ((data.max_attempts > 0) && (data.num_attempts >= data.max_attempts)) {
- // is_valid = false;
- // }
+ function get_results() {
+ calculate_results('get_results', handleReviewResults)
+ }
+
+ function validateXBlock(hide_nav) {
+ var is_valid = true;
var child = mentoring.steps[active_child];
if (child && child.name !== undefined) {
var child_validation = callIfExists(child, 'validate');
@@ -212,7 +327,7 @@ function MentoringAssessmentView(runtime, element, mentoring) {
submitDOM.removeAttr("disabled");
}
- if (isLastChild()) {
+ if (isLastChild() && ! hide_nav) {
nextDOM.hide();
reviewDOM.show();
}
diff --git a/problem_builder/public/js/mentoring_standard_view.js b/problem_builder/public/js/mentoring_standard_view.js
index ac45f0c2..a70d9ec7 100644
--- a/problem_builder/public/js/mentoring_standard_view.js
+++ b/problem_builder/public/js/mentoring_standard_view.js
@@ -4,26 +4,26 @@ function MentoringStandardView(runtime, element, mentoring) {
var callIfExists = mentoring.callIfExists;
- function handleSubmitResults(results) {
+ function handleSubmitResults(response) {
messagesDOM.empty().hide();
- $.each(results.submitResults || [], function(index, submitResult) {
- var input = submitResult[0];
- var result = submitResult[1];
+ $.each(response.results || [], function(index, result_spec) {
+ var input = result_spec[0];
+ var result = result_spec[1];
var child = mentoring.getChildByName(input);
var options = {
- max_attempts: results.max_attempts,
- num_attempts: results.num_attempts
+ max_attempts: response.max_attempts,
+ num_attempts: response.num_attempts
};
callIfExists(child, 'handleSubmit', result, options);
});
- $('.attempts', element).data('max_attempts', results.max_attempts);
- $('.attempts', element).data('num_attempts', results.num_attempts);
+ $('.attempts', element).data('max_attempts', response.max_attempts);
+ $('.attempts', element).data('num_attempts', response.num_attempts);
mentoring.renderAttempts();
// Messages should only be displayed upon hitting 'submit', not on page reload
- mentoring.setContent(messagesDOM, results.message);
+ mentoring.setContent(messagesDOM, response.message);
if (messagesDOM.html().trim()) {
messagesDOM.prepend('
' + gettext('Feedback') + '
');
messagesDOM.show();
@@ -32,23 +32,30 @@ function MentoringStandardView(runtime, element, mentoring) {
submitDOM.attr('disabled', 'disabled');
}
- function submit() {
- var success = true;
+ function calculate_results(handler_name) {
var data = {};
var children = mentoring.children;
for (var i = 0; i < children.length; i++) {
var child = children[i];
- if (child && child.name !== undefined && typeof(child.submit) !== "undefined") {
- data[child.name] = child.submit();
+ if (child && child.name !== undefined && typeof(child[handler_name]) !== "undefined") {
+ data[child.name] = child[handler_name]();
}
}
- var handlerUrl = runtime.handlerUrl(element, 'submit');
+ var handlerUrl = runtime.handlerUrl(element, handler_name);
if (submitXHR) {
submitXHR.abort();
}
submitXHR = $.post(handlerUrl, JSON.stringify(data)).success(handleSubmitResults);
}
+ function get_results() {
+ calculate_results('get_results');
+ }
+
+ function submit() {
+ calculate_results('submit')
+ }
+
function clearResults() {
messagesDOM.empty().hide();
@@ -68,6 +75,8 @@ function MentoringStandardView(runtime, element, mentoring) {
submitDOM = $(element).find('.submit .input-main');
submitDOM.bind('click', submit);
submitDOM.show();
+ // Not used in standard mode.
+ $(element).find('.review-link').hide();
var options = {
onChange: onChange
diff --git a/problem_builder/public/js/questionnaire.js b/problem_builder/public/js/questionnaire.js
index a967c5b6..f6470959 100644
--- a/problem_builder/public/js/questionnaire.js
+++ b/problem_builder/public/js/questionnaire.js
@@ -97,23 +97,24 @@ function MCQBlock(runtime, element) {
}
},
- handleSubmit: function(result) {
- if (this.mode === 'assessment')
- return;
+ handleReview: function(result){
+ $('.choice input[value="' + result.submission + '"]', element).prop('checked', true);
+ $('.choice input', element).prop('disabled', true);
+ },
+ handleSubmit: function(result) {
mentoring = this.mentoring;
var messageView = MessageView(element, mentoring);
messageView.clearResult();
- var choiceInputs = $('.choice input', element);
+ var choiceInputs = $('.choice-selector input', element);
$.each(choiceInputs, function(index, choiceInput) {
var choiceInputDOM = $(choiceInput);
var choiceDOM = choiceInputDOM.closest('.choice');
var choiceResultDOM = $('.choice-result', choiceDOM);
var choiceTipsDOM = $('.choice-tips', choiceDOM);
- var choiceTipsCloseDOM;
if (result.status === "correct" && choiceInputDOM.val() === result.submission) {
choiceDOM.addClass('correct');
@@ -129,7 +130,6 @@ function MCQBlock(runtime, element) {
messageView.showMessage(choiceTipsDOM);
}
- choiceTipsCloseDOM = $('.close', choiceTipsDOM);
choiceResultDOM.off('click').on('click', function() {
if (choiceTipsDOM.html() !== '') {
messageView.showMessage(choiceTipsDOM);
@@ -178,9 +178,14 @@ function MRQBlock(runtime, element) {
return checkedValues;
},
+ handleReview: function(result) {
+ $.each(result.submissions, function (index, value) {
+ $('input[type="checkbox"][value="' + value + '"]').prop('checked', true)
+ });
+ $('input', element).prop('disabled', true);
+ },
+
handleSubmit: function(result, options) {
- if (this.mode === 'assessment')
- return;
mentoring = this.mentoring;
@@ -193,14 +198,13 @@ function MRQBlock(runtime, element) {
var questionnaireDOM = $('fieldset.questionnaire', element);
var data = questionnaireDOM.data();
- var hide_results = (data.hide_results === 'True') ? true : false;
+ var hide_results = (data.hide_results === 'True');
$.each(result.choices, function(index, choice) {
var choiceInputDOM = $('.choice input[value='+choice.value+']', element);
var choiceDOM = choiceInputDOM.closest('.choice');
var choiceResultDOM = $('.choice-result', choiceDOM);
var choiceTipsDOM = $('.choice-tips', choiceDOM);
- var choiceTipsCloseDOM;
/* show hint if checked or max_attempts is disabled */
if (!hide_results &&
@@ -215,7 +219,6 @@ function MRQBlock(runtime, element) {
mentoring.setContent(choiceTipsDOM, choice.tips);
- choiceTipsCloseDOM = $('.close', choiceTipsDOM);
choiceResultDOM.off('click').on('click', function() {
messageView.showMessage(choiceTipsDOM);
});
diff --git a/problem_builder/questionnaire.py b/problem_builder/questionnaire.py
index b2a352fd..d57e3e41 100644
--- a/problem_builder/questionnaire.py
+++ b/problem_builder/questionnaire.py
@@ -21,10 +21,10 @@
# Imports ###########################################################
from django.utils.safestring import mark_safe
-import logging
-from lxml import etree
+from lazy import lazy
+import uuid
from xblock.core import XBlock
-from xblock.fields import Scope, String, Float, List, UNIQUE_ID
+from xblock.fields import Scope, String, Float, UNIQUE_ID
from xblock.fragment import Fragment
from xblock.validation import ValidationMessage
from xblockutils.helpers import child_isinstance
@@ -90,14 +90,14 @@ def _(self, text):
""" translate text """
return self.runtime.service(self, "i18n").ugettext(text)
- @property
+ @lazy
def html_id(self):
"""
A short, simple ID string used to uniquely identify this question.
This is only used by templates for matching and