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