From 2d9110ddbeccaf0713e4bdff8eb05202043d552f Mon Sep 17 00:00:00 2001 From: Tim Krones Date: Tue, 22 Sep 2015 11:48:05 +0200 Subject: [PATCH 01/43] Rename: pb-mentoring -> step-builder; pb-mentoring-step -> sb-step. --- .../public/css/problem-builder-edit.css | 22 +++++++++---------- .../public/js/mentoring_with_steps.js | 2 +- problem_builder/step.py | 2 +- problem_builder/templates/html/step.html | 2 +- setup.py | 4 ++-- 5 files changed, 16 insertions(+), 16 deletions(-) diff --git a/problem_builder/public/css/problem-builder-edit.css b/problem_builder/public/css/problem-builder-edit.css index 175d03e4..e96433c9 100644 --- a/problem_builder/public/css/problem-builder-edit.css +++ b/problem_builder/public/css/problem-builder-edit.css @@ -1,13 +1,13 @@ /* Display of url_name below content */ -.xblock[data-block-type=pb-mentoring-step] .url-name-footer, -.xblock[data-block-type=pb-mentoring] .url-name-footer, +.xblock[data-block-type=sb-step] .url-name-footer, +.xblock[data-block-type=step-builder] .url-name-footer, .xblock[data-block-type=problem-builder] .url-name-footer, .xblock[data-block-type=mentoring] .url-name-footer { font-style: italic; } -.xblock[data-block-type=pb-mentoring-step] .url-name-footer .url-name, -.xblock[data-block-type=pb-mentoring] .url-name-footer .url-name, +.xblock[data-block-type=sb-step] .url-name-footer .url-name, +.xblock[data-block-type=step-builder] .url-name-footer .url-name, .xblock[data-block-type=problem-builder] .url-name-footer .url-name, .xblock[data-block-type=mentoring] .url-name-footer .url-name { margin: 0 10px; @@ -15,8 +15,8 @@ } /* Custom appearance for our "Add" buttons */ -.xblock[data-block-type=pb-mentoring-step] .add-xblock-component .new-component .new-component-type .add-xblock-component-button, -.xblock[data-block-type=pb-mentoring] .add-xblock-component .new-component .new-component-type .add-xblock-component-button, +.xblock[data-block-type=sb-step] .add-xblock-component .new-component .new-component-type .add-xblock-component-button, +.xblock[data-block-type=step-builder] .add-xblock-component .new-component .new-component-type .add-xblock-component-button, .xblock[data-block-type=problem-builder] .add-xblock-component .new-component .new-component-type .add-xblock-component-button, .xblock[data-block-type=mentoring] .add-xblock-component .new-component .new-component-type .add-xblock-component-button { width: 200px; @@ -24,10 +24,10 @@ line-height: 30px; } -.xblock[data-block-type=pb-mentoring-step] .add-xblock-component .new-component .new-component-type .add-xblock-component-button.disabled, -.xblock[data-block-type=pb-mentoring-step] .add-xblock-component .new-component .new-component-type .add-xblock-component-button.disabled:hover, -.xblock[data-block-type=pb-mentoring] .add-xblock-component .new-component .new-component-type .add-xblock-component-button.disabled, -.xblock[data-block-type=pb-mentoring] .add-xblock-component .new-component .new-component-type .add-xblock-component-button.disabled:hover, +.xblock[data-block-type=sb-step] .add-xblock-component .new-component .new-component-type .add-xblock-component-button.disabled, +.xblock[data-block-type=sb-step] .add-xblock-component .new-component .new-component-type .add-xblock-component-button.disabled:hover, +.xblock[data-block-type=step-builder] .add-xblock-component .new-component .new-component-type .add-xblock-component-button.disabled, +.xblock[data-block-type=step-builder] .add-xblock-component .new-component .new-component-type .add-xblock-component-button.disabled:hover, .xblock[data-block-type=problem-builder] .add-xblock-component .new-component .new-component-type .add-xblock-component-button.disabled, .xblock[data-block-type=problem-builder] .add-xblock-component .new-component .new-component-type .add-xblock-component-button.disabled:hover, .xblock[data-block-type=mentoring] .add-xblock-component .new-component .new-component-type .add-xblock-component-button.disabled, @@ -37,7 +37,7 @@ cursor: default; } -.xblock[data-block-type=pb-mentoring] .submission-message-help p, +.xblock[data-block-type=step-builder] .submission-message-help p, .xblock[data-block-type=problem-builder] .submission-message-help p { border-top: 1px solid #ddd; font-size: 0.85em; diff --git a/problem_builder/public/js/mentoring_with_steps.js b/problem_builder/public/js/mentoring_with_steps.js index 8a3fab57..c507b9fb 100644 --- a/problem_builder/public/js/mentoring_with_steps.js +++ b/problem_builder/public/js/mentoring_with_steps.js @@ -1,7 +1,7 @@ function MentoringWithStepsBlock(runtime, element) { var steps = runtime.children(element).filter( - function(c) { return c.element.className.indexOf('pb-mentoring-step') > -1; } + function(c) { return c.element.className.indexOf('sb-step') > -1; } ); var activeStep = $('.mentoring', element).data('active-step'); var checkmark, submitDOM, nextDOM, tryAgainDOM, submitXHR; diff --git a/problem_builder/step.py b/problem_builder/step.py index ac31dec3..d79d46ab 100644 --- a/problem_builder/step.py +++ b/problem_builder/step.py @@ -79,7 +79,7 @@ class MentoringStepBlock( """ CAPTION = _(u"Step") STUDIO_LABEL = _(u"Mentoring Step") - CATEGORY = 'pb-mentoring-step' + CATEGORY = 'sb-step' # Settings display_name = String( diff --git a/problem_builder/templates/html/step.html b/problem_builder/templates/html/step.html index 0deb7d33..1cb86b08 100644 --- a/problem_builder/templates/html/step.html +++ b/problem_builder/templates/html/step.html @@ -1,4 +1,4 @@ -
+
{% if show_title %}

diff --git a/setup.py b/setup.py index 37b06f27..a4d2f050 100644 --- a/setup.py +++ b/setup.py @@ -41,8 +41,8 @@ def package_data(pkg, root_list): BLOCKS = [ 'problem-builder = problem_builder:MentoringBlock', - 'pb-mentoring = problem_builder:MentoringWithExplicitStepsBlock', - 'pb-mentoring-step = problem_builder:MentoringStepBlock', + 'step-builder = problem_builder:MentoringWithExplicitStepsBlock', + 'sb-step = problem_builder:MentoringStepBlock', 'pb-table = problem_builder:MentoringTableBlock', 'pb-column = problem_builder:MentoringTableColumn', From 7a5e230af6441718b510e93e204d8742da7745a0 Mon Sep 17 00:00:00 2001 From: Tim Krones Date: Tue, 22 Sep 2015 12:24:10 +0200 Subject: [PATCH 02/43] Introduce ReviewStep block and allow adding it to new mentoring block (step builder). --- problem_builder/__init__.py | 2 +- problem_builder/mentoring.py | 3 ++- problem_builder/step.py | 25 +++++++++++++++++++ .../templates/html/review_step.html | 5 ++++ setup.py | 1 + 5 files changed, 34 insertions(+), 2 deletions(-) create mode 100644 problem_builder/templates/html/review_step.html diff --git a/problem_builder/__init__.py b/problem_builder/__init__.py index 0354e597..28e2a153 100644 --- a/problem_builder/__init__.py +++ b/problem_builder/__init__.py @@ -1,5 +1,5 @@ from .mentoring import MentoringBlock, MentoringWithExplicitStepsBlock -from .step import MentoringStepBlock +from .step import MentoringStepBlock, ReviewStepBlock from .answer import AnswerBlock, AnswerRecapBlock from .choice import ChoiceBlock from .dashboard import DashboardBlock diff --git a/problem_builder/mentoring.py b/problem_builder/mentoring.py index 76f4b013..dab24bdf 100644 --- a/problem_builder/mentoring.py +++ b/problem_builder/mentoring.py @@ -905,9 +905,10 @@ def allowed_nested_blocks(self): NestedXBlockSpec allows explicitly setting disabled/enabled state, disabled reason (if any) and single/multiple instances """ - from .step import MentoringStepBlock # Import here to avoid circular dependency + from .step import MentoringStepBlock, ReviewStepBlock # Import here to avoid circular dependency return [ MentoringStepBlock, + ReviewStepBlock, NestedXBlockSpec(CompletedMentoringMessageShim, boilerplate='completed'), NestedXBlockSpec(IncompleteMentoringMessageShim, boilerplate='incomplete'), NestedXBlockSpec(MaxAttemptsReachedMentoringMessageShim, boilerplate='max_attempts_reached'), diff --git a/problem_builder/step.py b/problem_builder/step.py index d79d46ab..ba8715be 100644 --- a/problem_builder/step.py +++ b/problem_builder/step.py @@ -207,3 +207,28 @@ def _render_view(self, context, view): fragment.initialize_js('MentoringStepBlock') return fragment + + +class ReviewStepBlock(XBlockWithPreviewMixin, XBlock): + """ A dedicated step for reviewing results for a mentoring block """ + CATEGORY = 'sb-review-step' + STUDIO_LABEL = _("Review Step") + + display_name = String( + default="Review Step" + ) + + def mentoring_view(self, context=None): + """ Mentoring View """ + return self._render_view(context) + + def student_view(self, context=None): + """ Student View """ + return self._render_view(context) + + def _render_view(self, context): + fragment = Fragment() + fragment.add_content(loader.render_template('templates/html/review_step.html', { + 'self': self, + })) + return fragment diff --git a/problem_builder/templates/html/review_step.html b/problem_builder/templates/html/review_step.html new file mode 100644 index 00000000..8c4f0962 --- /dev/null +++ b/problem_builder/templates/html/review_step.html @@ -0,0 +1,5 @@ +
+ +

...

+ +
diff --git a/setup.py b/setup.py index a4d2f050..f0d5efd6 100644 --- a/setup.py +++ b/setup.py @@ -43,6 +43,7 @@ def package_data(pkg, root_list): 'problem-builder = problem_builder:MentoringBlock', 'step-builder = problem_builder:MentoringWithExplicitStepsBlock', 'sb-step = problem_builder:MentoringStepBlock', + 'sb-review-step = problem_builder:ReviewStepBlock', 'pb-table = problem_builder:MentoringTableBlock', 'pb-column = problem_builder:MentoringTableColumn', From 8b4df834a8888fbaf88113285d87c9a2b21a08ca Mon Sep 17 00:00:00 2001 From: Tim Krones Date: Tue, 22 Sep 2015 14:32:09 +0200 Subject: [PATCH 03/43] Hide review step by default. --- problem_builder/public/js/mentoring_with_steps.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/problem_builder/public/js/mentoring_with_steps.js b/problem_builder/public/js/mentoring_with_steps.js index c507b9fb..5afed272 100644 --- a/problem_builder/public/js/mentoring_with_steps.js +++ b/problem_builder/public/js/mentoring_with_steps.js @@ -4,7 +4,7 @@ function MentoringWithStepsBlock(runtime, element) { function(c) { return c.element.className.indexOf('sb-step') > -1; } ); var activeStep = $('.mentoring', element).data('active-step'); - var checkmark, submitDOM, nextDOM, tryAgainDOM, submitXHR; + var reviewStep, checkmark, submitDOM, nextDOM, tryAgainDOM, submitXHR; function isLastStep() { return (activeStep === steps.length-1); @@ -124,6 +124,9 @@ function MentoringWithStepsBlock(runtime, element) { } function initXBlockView() { + reviewStep = $('.sb-review-step', element); + reviewStep.hide(); + checkmark = $('.assessment-checkmark', element); submitDOM = $(element).find('.submit .input-main'); From 987e5837cb530722fbc014be598046cebdac4e2e Mon Sep 17 00:00:00 2001 From: Tim Krones Date: Tue, 22 Sep 2015 14:44:21 +0200 Subject: [PATCH 04/43] Add "Review grade" button and hide/show/enable/disable it when appropriate. --- problem_builder/public/js/mentoring_with_steps.js | 11 +++++++++-- .../templates/html/mentoring_with_steps.html | 1 + 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/problem_builder/public/js/mentoring_with_steps.js b/problem_builder/public/js/mentoring_with_steps.js index 5afed272..aa5940ed 100644 --- a/problem_builder/public/js/mentoring_with_steps.js +++ b/problem_builder/public/js/mentoring_with_steps.js @@ -4,7 +4,7 @@ function MentoringWithStepsBlock(runtime, element) { function(c) { return c.element.className.indexOf('sb-step') > -1; } ); var activeStep = $('.mentoring', element).data('active-step'); - var reviewStep, checkmark, submitDOM, nextDOM, tryAgainDOM, submitXHR; + var reviewStep, checkmark, submitDOM, nextDOM, reviewDOM, tryAgainDOM, submitXHR; function isLastStep() { return (activeStep === steps.length-1); @@ -37,6 +37,7 @@ function MentoringWithStepsBlock(runtime, element) { if (nextDOM.is(':visible')) { nextDOM.focus(); } if (isLastStep()) { + reviewDOM.removeAttr('disabled'); tryAgainDOM.removeAttr('disabled'); tryAgainDOM.show(); } @@ -64,8 +65,11 @@ function MentoringWithStepsBlock(runtime, element) { function updateDisplay() { cleanAll(); showActiveStep(); - nextDOM.attr('disabled', 'disabled'); validateXBlock(); + nextDOM.attr('disabled', 'disabled'); + if (isLastStep()) { + reviewDOM.show(); + } } function showActiveStep() { @@ -112,6 +116,7 @@ function MentoringWithStepsBlock(runtime, element) { submitDOM.show(); if (! isLastStep()) { nextDOM.show(); + reviewDOM.hide(); } } @@ -137,6 +142,8 @@ function MentoringWithStepsBlock(runtime, element) { nextDOM.on('click', updateDisplay); nextDOM.show(); + reviewDOM = $(element).find('.submit .input-review'); + tryAgainDOM = $(element).find('.submit .input-try-again'); tryAgainDOM.on('click', tryAgain); diff --git a/problem_builder/templates/html/mentoring_with_steps.html b/problem_builder/templates/html/mentoring_with_steps.html index a521c0d0..795a99d6 100644 --- a/problem_builder/templates/html/mentoring_with_steps.html +++ b/problem_builder/templates/html/mentoring_with_steps.html @@ -17,6 +17,7 @@

{{ title }}

+

From defaff350a3c974f0cf7807836fbe3a7b90ef254 Mon Sep 17 00:00:00 2001 From: Tim Krones Date: Tue, 22 Sep 2015 15:17:26 +0200 Subject: [PATCH 05/43] Show review step when clicking on "Review grade" button. --- problem_builder/public/js/mentoring_with_steps.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/problem_builder/public/js/mentoring_with_steps.js b/problem_builder/public/js/mentoring_with_steps.js index aa5940ed..04945994 100644 --- a/problem_builder/public/js/mentoring_with_steps.js +++ b/problem_builder/public/js/mentoring_with_steps.js @@ -109,9 +109,15 @@ function MentoringWithStepsBlock(runtime, element) { } } + function showGrade() { + cleanAll(); + reviewStep.show(); + } + function handleTryAgain(result) { activeStep = result.active_step; updateDisplay(); + reviewStep.hide(); tryAgainDOM.hide(); submitDOM.show(); if (! isLastStep()) { @@ -143,6 +149,7 @@ function MentoringWithStepsBlock(runtime, element) { nextDOM.show(); reviewDOM = $(element).find('.submit .input-review'); + reviewDOM.on('click', showGrade); tryAgainDOM = $(element).find('.submit .input-try-again'); tryAgainDOM.on('click', tryAgain); From 433ce153da1e2262d5eb0337421fb9d6abf9c191 Mon Sep 17 00:00:00 2001 From: Tim Krones Date: Tue, 22 Sep 2015 15:57:28 +0200 Subject: [PATCH 06/43] Allow only one review step per mentoring block. --- .../public/js/mentoring_with_steps_edit.js | 36 +++++++++++++------ 1 file changed, 26 insertions(+), 10 deletions(-) diff --git a/problem_builder/public/js/mentoring_with_steps_edit.js b/problem_builder/public/js/mentoring_with_steps_edit.js index 6bc6197f..31e48e69 100644 --- a/problem_builder/public/js/mentoring_with_steps_edit.js +++ b/problem_builder/public/js/mentoring_with_steps_edit.js @@ -1,22 +1,38 @@ function MentoringWithStepsEdit(runtime, element) { "use strict"; - // Disable "add" buttons when a message of that type already exists: - var $buttons = $('.add-xblock-component-button[data-category=pb-message]', element); - var updateButtons = function() { - $buttons.each(function() { - var msg_type = $(this).data('boilerplate'); - $(this).toggleClass('disabled', $('.xblock .submission-message.'+msg_type).length > 0); - }); + + var blockIsPresent = function(klass) { + return $('.xblock ' + klass).length > 0; }; - updateButtons(); - $buttons.click(function(ev) { + + var updateButton = function(button, condition) { + button.toggleClass('disabled', condition); + }; + + var disableButton = function(ev) { if ($(this).is('.disabled')) { ev.preventDefault(); ev.stopPropagation(); } else { $(this).addClass('disabled'); } - }); + }; + + var initButtons = function(dataCategory) { + var $buttons = $('.add-xblock-component-button[data-category='+dataCategory+']', element); + $buttons.each(function() { + if (dataCategory === 'pb-message') { + var msg_type = $(this).data('boilerplate'); + updateButton($(this), blockIsPresent('.submission-message.'+msg_type)); + } else { + updateButton($(this), blockIsPresent('.xblock-header-sb-review-step')); + } + }); + $buttons.on('click', disableButton); + }; + + initButtons('pb-message'); + initButtons('sb-review-step'); ProblemBuilderUtil.transformClarifications(element); StudioEditableXBlockMixin(runtime, element); From f07502fe50f09aac5bbba7c2b63623b5ce7b9929 Mon Sep 17 00:00:00 2001 From: Tim Krones Date: Tue, 22 Sep 2015 16:47:18 +0200 Subject: [PATCH 07/43] Redirect to review step when reloading page after submitting last step. --- problem_builder/mentoring.py | 8 ++++++ .../public/js/mentoring_with_steps.js | 26 +++++++++++++++---- 2 files changed, 29 insertions(+), 5 deletions(-) diff --git a/problem_builder/mentoring.py b/problem_builder/mentoring.py index dab24bdf..5ad0d97e 100644 --- a/problem_builder/mentoring.py +++ b/problem_builder/mentoring.py @@ -865,6 +865,11 @@ def steps(self): child_isinstance(self, child_id, MentoringStepBlock) ] + @property + def has_review_step(self): + from .step import ReviewStepBlock + return any(child_isinstance(self, child_id, ReviewStepBlock) for child_id in self.children) + def student_view(self, context): fragment = Fragment() children_contents = [] @@ -919,6 +924,9 @@ def allowed_nested_blocks(self): def update_active_step(self, new_value, suffix=''): if new_value < len(self.steps): self.active_step = new_value + elif new_value == len(self.steps): + if self.has_review_step: + self.active_step = -1 return { 'active_step': self.active_step } diff --git a/problem_builder/public/js/mentoring_with_steps.js b/problem_builder/public/js/mentoring_with_steps.js index 04945994..89abfc3c 100644 --- a/problem_builder/public/js/mentoring_with_steps.js +++ b/problem_builder/public/js/mentoring_with_steps.js @@ -10,6 +10,10 @@ function MentoringWithStepsBlock(runtime, element) { return (activeStep === steps.length-1); } + function atReviewStep() { + return (activeStep === -1); + } + function updateActiveStep(newValue) { var handlerUrl = runtime.handlerUrl(element, 'update_active_step'); $.post(handlerUrl, JSON.stringify(newValue)) @@ -64,14 +68,26 @@ function MentoringWithStepsBlock(runtime, element) { function updateDisplay() { cleanAll(); - showActiveStep(); - validateXBlock(); - nextDOM.attr('disabled', 'disabled'); - if (isLastStep()) { - reviewDOM.show(); + if (atReviewStep()) { + showReviewStep(); + } else { + showActiveStep(); + validateXBlock(); + nextDOM.attr('disabled', 'disabled'); + if (isLastStep()) { + reviewDOM.show(); + } } } + function showReviewStep() { + reviewStep.show(); + submitDOM.hide(); + nextDOM.hide(); + tryAgainDOM.removeAttr('disabled'); + tryAgainDOM.show(); + } + function showActiveStep() { var step = steps[activeStep]; $(step.element).show(); From 1806c75400d7ebd82160f941fec5648e2212cb4c Mon Sep 17 00:00:00 2001 From: Tim Krones Date: Tue, 22 Sep 2015 16:51:55 +0200 Subject: [PATCH 08/43] Hide "Submit", "Next Step", "Review grade" buttons when displaying review step. --- problem_builder/public/js/mentoring_with_steps.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/problem_builder/public/js/mentoring_with_steps.js b/problem_builder/public/js/mentoring_with_steps.js index 89abfc3c..f343aaa1 100644 --- a/problem_builder/public/js/mentoring_with_steps.js +++ b/problem_builder/public/js/mentoring_with_steps.js @@ -84,6 +84,7 @@ function MentoringWithStepsBlock(runtime, element) { reviewStep.show(); submitDOM.hide(); nextDOM.hide(); + reviewDOM.hide(); tryAgainDOM.removeAttr('disabled'); tryAgainDOM.show(); } @@ -127,7 +128,7 @@ function MentoringWithStepsBlock(runtime, element) { function showGrade() { cleanAll(); - reviewStep.show(); + showReviewStep(); } function handleTryAgain(result) { From d91007aea2cf9336e7582b8cf31c43d7310ce184 Mon Sep 17 00:00:00 2001 From: Tim Krones Date: Tue, 22 Sep 2015 16:55:25 +0200 Subject: [PATCH 09/43] Don't show "Try again" button at last step. --- problem_builder/public/js/mentoring_with_steps.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/problem_builder/public/js/mentoring_with_steps.js b/problem_builder/public/js/mentoring_with_steps.js index f343aaa1..4f6a7ed4 100644 --- a/problem_builder/public/js/mentoring_with_steps.js +++ b/problem_builder/public/js/mentoring_with_steps.js @@ -42,8 +42,6 @@ function MentoringWithStepsBlock(runtime, element) { if (isLastStep()) { reviewDOM.removeAttr('disabled'); - tryAgainDOM.removeAttr('disabled'); - tryAgainDOM.show(); } } From 431bc4808b8851ed89805535ecca5cd413233e66 Mon Sep 17 00:00:00 2001 From: Tim Krones Date: Tue, 22 Sep 2015 17:11:47 +0200 Subject: [PATCH 10/43] Handle cases where review step is not present (because it has not been added to current mentoring block). --- problem_builder/public/js/mentoring_with_steps.js | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/problem_builder/public/js/mentoring_with_steps.js b/problem_builder/public/js/mentoring_with_steps.js index 4f6a7ed4..7009d7c9 100644 --- a/problem_builder/public/js/mentoring_with_steps.js +++ b/problem_builder/public/js/mentoring_with_steps.js @@ -14,6 +14,10 @@ function MentoringWithStepsBlock(runtime, element) { return (activeStep === -1); } + function reviewStepPresent() { + return reviewStep.length > 0; + } + function updateActiveStep(newValue) { var handlerUrl = runtime.handlerUrl(element, 'update_active_step'); $.post(handlerUrl, JSON.stringify(newValue)) @@ -41,7 +45,12 @@ function MentoringWithStepsBlock(runtime, element) { if (nextDOM.is(':visible')) { nextDOM.focus(); } if (isLastStep()) { - reviewDOM.removeAttr('disabled'); + if (reviewStepPresent()) { + reviewDOM.removeAttr('disabled'); + } else { + tryAgainDOM.removeAttr('disabled'); + tryAgainDOM.show(); + } } } @@ -72,7 +81,7 @@ function MentoringWithStepsBlock(runtime, element) { showActiveStep(); validateXBlock(); nextDOM.attr('disabled', 'disabled'); - if (isLastStep()) { + if (isLastStep() && reviewStepPresent()) { reviewDOM.show(); } } From a06370c01cbc06a1f3b1adba1336f19cead207ac Mon Sep 17 00:00:00 2001 From: Tim Krones Date: Wed, 23 Sep 2015 11:21:10 +0200 Subject: [PATCH 11/43] Show appropriate assessment message when reaching review step. --- problem_builder/mentoring.py | 29 +++++++++++++++++++ .../public/js/mentoring_with_steps.js | 12 +++++++- .../templates/html/mentoring_with_steps.html | 6 ++++ 3 files changed, 46 insertions(+), 1 deletion(-) diff --git a/problem_builder/mentoring.py b/problem_builder/mentoring.py index 5ad0d97e..a0e7b4e3 100644 --- a/problem_builder/mentoring.py +++ b/problem_builder/mentoring.py @@ -870,6 +870,35 @@ def has_review_step(self): from .step import ReviewStepBlock return any(child_isinstance(self, child_id, ReviewStepBlock) for child_id in self.children) + @property + def max_attempts_reached(self): + return False + + @property + def assessment_message(self): + """ + Get the message to display to a student following a submission in assessment mode. + """ + if not self.max_attempts_reached: + return self.get_message_content('on-assessment-review', or_default=True) + else: + assessment_message = _("Note: you have used all attempts. Continue to the next unit") + return '

{}

'.format(assessment_message) + + def get_message_content(self, message_type, or_default=False): + for child_id in self.children: + if child_isinstance(self, child_id, MentoringMessageBlock): + child = self.runtime.get_block(child_id) + if child.type == message_type: + content = child.content + if hasattr(self.runtime, 'replace_jump_to_id_urls'): + content = self.runtime.replace_jump_to_id_urls(content) + return content + if or_default: + # Return the default value since no custom message is set. + # Note the WYSIWYG editor usually wraps the .content HTML in a

tag so we do the same here. + return '

{}

'.format(MentoringMessageBlock.MESSAGE_TYPES[message_type]['default']) + def student_view(self, context): fragment = Fragment() children_contents = [] diff --git a/problem_builder/public/js/mentoring_with_steps.js b/problem_builder/public/js/mentoring_with_steps.js index 7009d7c9..a9e4189f 100644 --- a/problem_builder/public/js/mentoring_with_steps.js +++ b/problem_builder/public/js/mentoring_with_steps.js @@ -4,7 +4,7 @@ function MentoringWithStepsBlock(runtime, element) { function(c) { return c.element.className.indexOf('sb-step') > -1; } ); var activeStep = $('.mentoring', element).data('active-step'); - var reviewStep, checkmark, submitDOM, nextDOM, reviewDOM, tryAgainDOM, submitXHR; + var reviewStep, checkmark, submitDOM, nextDOM, reviewDOM, tryAgainDOM, assessmentMessageDOM, submitXHR; function isLastStep() { return (activeStep === steps.length-1); @@ -71,11 +71,13 @@ function MentoringWithStepsBlock(runtime, element) { checkmark.removeClass('checkmark-partially-correct icon-ok fa-check'); checkmark.removeClass('checkmark-incorrect icon-exclamation fa-exclamation'); hideAllSteps(); + assessmentMessageDOM.html(''); } function updateDisplay() { cleanAll(); if (atReviewStep()) { + showAssessmentMessage(); showReviewStep(); } else { showActiveStep(); @@ -87,6 +89,11 @@ function MentoringWithStepsBlock(runtime, element) { } } + function showAssessmentMessage() { + var data = $('.grade', element).data(); + assessmentMessageDOM.html(data.assessment_message); + } + function showReviewStep() { reviewStep.show(); submitDOM.hide(); @@ -135,6 +142,7 @@ function MentoringWithStepsBlock(runtime, element) { function showGrade() { cleanAll(); + showAssessmentMessage(); showReviewStep(); } @@ -178,6 +186,8 @@ function MentoringWithStepsBlock(runtime, element) { tryAgainDOM = $(element).find('.submit .input-try-again'); tryAgainDOM.on('click', tryAgain); + assessmentMessageDOM = $('.assessment-message', element); + var options = { onChange: onChange }; diff --git a/problem_builder/templates/html/mentoring_with_steps.html b/problem_builder/templates/html/mentoring_with_steps.html index 795a99d6..b1f9891d 100644 --- a/problem_builder/templates/html/mentoring_with_steps.html +++ b/problem_builder/templates/html/mentoring_with_steps.html @@ -9,10 +9,16 @@

{{ title }}

+
+ {% for child_content in children_contents %} {{ child_content|safe }} {% endfor %} +
+
From 839840a3627016337c202976a8bbdeed0cf4ce59 Mon Sep 17 00:00:00 2001 From: Tim Krones Date: Wed, 23 Sep 2015 11:29:23 +0200 Subject: [PATCH 12/43] Add max_attempts and num_attempts fields to new mentoring block and make max_attempts editable in Studio. --- problem_builder/mentoring.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/problem_builder/mentoring.py b/problem_builder/mentoring.py index a0e7b4e3..580e33d7 100644 --- a/problem_builder/mentoring.py +++ b/problem_builder/mentoring.py @@ -831,6 +831,15 @@ class MentoringWithExplicitStepsBlock(BaseMentoringBlock, StudioContainerWithNes """ An XBlock providing mentoring capabilities with explicit steps """ + # Content + max_attempts = Integer( + display_name=_("Max. Attempts Allowed"), + help=_("Maximum number of attempts allowed for this mentoring block"), + default=0, + scope=Scope.content, + enforce_type=True + ) + # Settings display_name = String( display_name=_("Title (Display name)"), @@ -840,6 +849,12 @@ class MentoringWithExplicitStepsBlock(BaseMentoringBlock, StudioContainerWithNes ) # User state + num_attempts = Integer( + # Number of attempts user has attempted this mentoring block + default=0, + scope=Scope.user_state, + enforce_type=True + ) active_step = Integer( # Keep track of the student progress. default=0, @@ -847,7 +862,7 @@ class MentoringWithExplicitStepsBlock(BaseMentoringBlock, StudioContainerWithNes enforce_type=True ) - editable_fields = ('display_name',) + editable_fields = ('display_name', 'max_attempts') @lazy def questions(self): From 677ffef8e06a09b067ff606a5c1d411f877495d4 Mon Sep 17 00:00:00 2001 From: Tim Krones Date: Wed, 23 Sep 2015 13:56:29 +0200 Subject: [PATCH 13/43] Update num_attempts field of new mentoring block after submitting last step. --- problem_builder/mentoring.py | 5 +++++ problem_builder/public/js/mentoring_with_steps.js | 8 ++++++++ problem_builder/step.py | 6 ++++++ 3 files changed, 19 insertions(+) diff --git a/problem_builder/mentoring.py b/problem_builder/mentoring.py index 580e33d7..ee1868bf 100644 --- a/problem_builder/mentoring.py +++ b/problem_builder/mentoring.py @@ -975,6 +975,11 @@ def update_active_step(self, new_value, suffix=''): 'active_step': self.active_step } + @XBlock.json_handler + def update_num_attempts(self, data, suffix=''): + self.num_attempts += 1 + return {} + @XBlock.json_handler def try_again(self, data, suffix=''): self.active_step = 0 diff --git a/problem_builder/public/js/mentoring_with_steps.js b/problem_builder/public/js/mentoring_with_steps.js index a9e4189f..b7e66e03 100644 --- a/problem_builder/public/js/mentoring_with_steps.js +++ b/problem_builder/public/js/mentoring_with_steps.js @@ -26,9 +26,17 @@ function MentoringWithStepsBlock(runtime, element) { }); } + function updateNumAttempts() { + var handlerUrl = runtime.handlerUrl(element, 'update_num_attempts'); + $.post(handlerUrl, JSON.stringify({})); + } + function handleResults(response) { // Update active step so next step is shown on page reload (even if user does not click "Next Step") updateActiveStep(activeStep+1); + if (response.update_attempts) { + updateNumAttempts(); + } // Update UI if (response.completed === 'correct') { diff --git a/problem_builder/step.py b/problem_builder/step.py index ba8715be..db3cb49f 100644 --- a/problem_builder/step.py +++ b/problem_builder/step.py @@ -102,6 +102,11 @@ class MentoringStepBlock( def siblings(self): return self.get_parent().steps + @property + def is_last_step(self): + parent = self.get_parent() + return self.step_number == len(parent.steps) + @property def allowed_nested_blocks(self): """ @@ -149,6 +154,7 @@ def submit(self, submissions, suffix=''): 'message': 'Success!', 'completed': completed, 'results': submit_results, + 'update_attempts': self.is_last_step } def reset(self): From 177e01e29088a7ff6591ebb89357d59e74587efa Mon Sep 17 00:00:00 2001 From: Tim Krones Date: Wed, 23 Sep 2015 13:57:47 +0200 Subject: [PATCH 14/43] Add implementation for max_attempts_reached property (was just returning False until now). --- problem_builder/mentoring.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/problem_builder/mentoring.py b/problem_builder/mentoring.py index ee1868bf..85fc9966 100644 --- a/problem_builder/mentoring.py +++ b/problem_builder/mentoring.py @@ -887,7 +887,7 @@ def has_review_step(self): @property def max_attempts_reached(self): - return False + return self.max_attempts > 0 and self.num_attempts >= self.max_attempts @property def assessment_message(self): From c251a61445286b4cac6a60956c3821a2edab232f Mon Sep 17 00:00:00 2001 From: Tim Krones Date: Wed, 23 Sep 2015 14:46:28 +0200 Subject: [PATCH 15/43] Show info about number of attempts used if limited number of attempts available. --- problem_builder/mentoring.py | 2 ++ problem_builder/public/js/mentoring_with_steps.js | 14 +++++++++++++- .../templates/html/mentoring_with_steps.html | 5 +++++ 3 files changed, 20 insertions(+), 1 deletion(-) diff --git a/problem_builder/mentoring.py b/problem_builder/mentoring.py index 85fc9966..7a5345f5 100644 --- a/problem_builder/mentoring.py +++ b/problem_builder/mentoring.py @@ -937,6 +937,8 @@ def student_view(self, context): fragment.add_css_url(self.runtime.local_resource_url(self, 'public/css/problem-builder.css')) fragment.add_javascript_url(self.runtime.local_resource_url(self, 'public/js/mentoring_with_steps.js')) + fragment.add_resource(loader.load_unicode('templates/html/mentoring_attempts.html'), "text/html") + self.include_theme_files(fragment) fragment.initialize_js('MentoringWithStepsBlock') diff --git a/problem_builder/public/js/mentoring_with_steps.js b/problem_builder/public/js/mentoring_with_steps.js index b7e66e03..1fe1a81a 100644 --- a/problem_builder/public/js/mentoring_with_steps.js +++ b/problem_builder/public/js/mentoring_with_steps.js @@ -4,7 +4,8 @@ function MentoringWithStepsBlock(runtime, element) { function(c) { return c.element.className.indexOf('sb-step') > -1; } ); var activeStep = $('.mentoring', element).data('active-step'); - var reviewStep, checkmark, submitDOM, nextDOM, reviewDOM, tryAgainDOM, assessmentMessageDOM, submitXHR; + var attemptsTemplate = _.template($('#xblock-attempts-template').html()); + var reviewStep, checkmark, submitDOM, nextDOM, reviewDOM, tryAgainDOM, assessmentMessageDOM, attemptsDOM, submitXHR; function isLastStep() { return (activeStep === steps.length-1); @@ -80,6 +81,7 @@ function MentoringWithStepsBlock(runtime, element) { checkmark.removeClass('checkmark-incorrect icon-exclamation fa-exclamation'); hideAllSteps(); assessmentMessageDOM.html(''); + attemptsDOM.html(''); } function updateDisplay() { @@ -87,6 +89,7 @@ function MentoringWithStepsBlock(runtime, element) { if (atReviewStep()) { showAssessmentMessage(); showReviewStep(); + showAttempts(); } else { showActiveStep(); validateXBlock(); @@ -111,6 +114,13 @@ function MentoringWithStepsBlock(runtime, element) { tryAgainDOM.show(); } + function showAttempts() { + var data = attemptsDOM.data(); + if (data.max_attempts > 0) { + attemptsDOM.html(attemptsTemplate(data)); + } // Don't show attempts if unlimited attempts available (max_attempts === 0) + } + function showActiveStep() { var step = steps[activeStep]; $(step.element).show(); @@ -152,6 +162,7 @@ function MentoringWithStepsBlock(runtime, element) { cleanAll(); showAssessmentMessage(); showReviewStep(); + showAttempts(); } function handleTryAgain(result) { @@ -195,6 +206,7 @@ function MentoringWithStepsBlock(runtime, element) { tryAgainDOM.on('click', tryAgain); assessmentMessageDOM = $('.assessment-message', element); + attemptsDOM = $('.attempts', element); var options = { onChange: onChange diff --git a/problem_builder/templates/html/mentoring_with_steps.html b/problem_builder/templates/html/mentoring_with_steps.html index b1f9891d..23656ed1 100644 --- a/problem_builder/templates/html/mentoring_with_steps.html +++ b/problem_builder/templates/html/mentoring_with_steps.html @@ -25,6 +25,11 @@

{{ title }}

+ +
+
+
From 6a86b6785c32a92531ad1612e6e65529850dcc7d Mon Sep 17 00:00:00 2001 From: Tim Krones Date: Wed, 23 Sep 2015 14:58:50 +0200 Subject: [PATCH 16/43] Disable "Try again" button if no attempts left. --- problem_builder/mentoring.py | 4 +++- problem_builder/public/js/mentoring_with_steps.js | 10 +++++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/problem_builder/mentoring.py b/problem_builder/mentoring.py index 7a5345f5..2d615643 100644 --- a/problem_builder/mentoring.py +++ b/problem_builder/mentoring.py @@ -980,7 +980,9 @@ def update_active_step(self, new_value, suffix=''): @XBlock.json_handler def update_num_attempts(self, data, suffix=''): self.num_attempts += 1 - return {} + return { + 'num_attempts': self.num_attempts + } @XBlock.json_handler def try_again(self, data, suffix=''): diff --git a/problem_builder/public/js/mentoring_with_steps.js b/problem_builder/public/js/mentoring_with_steps.js index 1fe1a81a..c139dd6e 100644 --- a/problem_builder/public/js/mentoring_with_steps.js +++ b/problem_builder/public/js/mentoring_with_steps.js @@ -29,7 +29,10 @@ function MentoringWithStepsBlock(runtime, element) { function updateNumAttempts() { var handlerUrl = runtime.handlerUrl(element, 'update_num_attempts'); - $.post(handlerUrl, JSON.stringify({})); + $.post(handlerUrl, JSON.stringify({})) + .success(function(response) { + attemptsDOM.data('num_attempts', response.num_attempts); + }); } function handleResults(response) { @@ -163,6 +166,11 @@ function MentoringWithStepsBlock(runtime, element) { showAssessmentMessage(); showReviewStep(); showAttempts(); + // Disable "Try again" button if no attempts left + var data = attemptsDOM.data(); + if (data.max_attempts > 0 && data.num_attempts >= data.max_attempts) { + tryAgainDOM.attr("disabled", "disabled"); + } } function handleTryAgain(result) { From 8f86a2e286df4f432af5d576090c56b927266e4a Mon Sep 17 00:00:00 2001 From: Tim Krones Date: Wed, 23 Sep 2015 15:01:21 +0200 Subject: [PATCH 17/43] Make sure "Review grade" button is disabled at last step. --- problem_builder/public/js/mentoring_with_steps.js | 1 + 1 file changed, 1 insertion(+) diff --git a/problem_builder/public/js/mentoring_with_steps.js b/problem_builder/public/js/mentoring_with_steps.js index c139dd6e..11e9df76 100644 --- a/problem_builder/public/js/mentoring_with_steps.js +++ b/problem_builder/public/js/mentoring_with_steps.js @@ -98,6 +98,7 @@ function MentoringWithStepsBlock(runtime, element) { validateXBlock(); nextDOM.attr('disabled', 'disabled'); if (isLastStep() && reviewStepPresent()) { + reviewDOM.attr('disabled', 'disabled'); reviewDOM.show(); } } From 95c94869dc73d241bf52e9570cbd3d5f81e98d97 Mon Sep 17 00:00:00 2001 From: Tim Krones Date: Wed, 23 Sep 2015 15:35:45 +0200 Subject: [PATCH 18/43] Don't show "Try again" button if no review step present; show message about number of submissions used instead. --- .../public/js/mentoring_with_steps.js | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/problem_builder/public/js/mentoring_with_steps.js b/problem_builder/public/js/mentoring_with_steps.js index 11e9df76..4f73777c 100644 --- a/problem_builder/public/js/mentoring_with_steps.js +++ b/problem_builder/public/js/mentoring_with_steps.js @@ -19,6 +19,14 @@ function MentoringWithStepsBlock(runtime, element) { return reviewStep.length > 0; } + function someAttemptsLeft() { + var data = attemptsDOM.data(); + if (data.max_attempts === 0) { // Unlimited number of attempts available + return true; + } + return (data.num_attempts < data.max_attempts); + } + function updateActiveStep(newValue) { var handlerUrl = runtime.handlerUrl(element, 'update_active_step'); $.post(handlerUrl, JSON.stringify(newValue)) @@ -60,8 +68,12 @@ function MentoringWithStepsBlock(runtime, element) { if (reviewStepPresent()) { reviewDOM.removeAttr('disabled'); } else { - tryAgainDOM.removeAttr('disabled'); - tryAgainDOM.show(); + if (someAttemptsLeft()) { + tryAgainDOM.removeAttr('disabled'); + tryAgainDOM.show(); + } else { + showAttempts(); + } } } } @@ -168,8 +180,7 @@ function MentoringWithStepsBlock(runtime, element) { showReviewStep(); showAttempts(); // Disable "Try again" button if no attempts left - var data = attemptsDOM.data(); - if (data.max_attempts > 0 && data.num_attempts >= data.max_attempts) { + if (!someAttemptsLeft()) { tryAgainDOM.attr("disabled", "disabled"); } } From ca1e9f95df7999c19fe98f65b0c774b1410c7abc Mon Sep 17 00:00:00 2001 From: Tim Krones Date: Wed, 23 Sep 2015 15:37:28 +0200 Subject: [PATCH 19/43] Don't allow num_attempts to be larger than max_attempts. --- problem_builder/mentoring.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/problem_builder/mentoring.py b/problem_builder/mentoring.py index 2d615643..08096a23 100644 --- a/problem_builder/mentoring.py +++ b/problem_builder/mentoring.py @@ -979,7 +979,8 @@ def update_active_step(self, new_value, suffix=''): @XBlock.json_handler def update_num_attempts(self, data, suffix=''): - self.num_attempts += 1 + if self.num_attempts < self.max_attempts: + self.num_attempts += 1 return { 'num_attempts': self.num_attempts } From bc87907751cfa2d85284a309ddfa7e7e78df1d94 Mon Sep 17 00:00:00 2001 From: Tim Krones Date: Wed, 23 Sep 2015 19:03:30 +0200 Subject: [PATCH 20/43] Display score (percentage) and count of correct/partial/incorrect answers. --- problem_builder/mentoring.py | 52 +++++++++++++++++++ .../public/js/mentoring_with_steps.js | 26 ++++++++-- problem_builder/step.py | 2 +- .../templates/html/mentoring_with_steps.html | 4 ++ 4 files changed, 80 insertions(+), 4 deletions(-) diff --git a/problem_builder/mentoring.py b/problem_builder/mentoring.py index 08096a23..fbbafa49 100644 --- a/problem_builder/mentoring.py +++ b/problem_builder/mentoring.py @@ -869,6 +869,12 @@ def questions(self): """ Get the usage_ids of all of this XBlock's children that are "Questions" """ return list(chain.from_iterable(self.runtime.get_block(step_id).steps for step_id in self.steps)) + def get_questions(self): + """ Get all questions associated with this block, cached if possible. """ + if getattr(self, "_questions_cache", None) is None: + self._questions_cache = [self.runtime.get_block(question_id) for question_id in self.questions] + return self._questions_cache + @property def steps(self): """ @@ -880,6 +886,21 @@ def steps(self): child_isinstance(self, child_id, MentoringStepBlock) ] + def get_steps(self): + """ Get the step children of this block, cached if possible. """ + if getattr(self, "_steps_cache", None) is None: + self._steps_cache = [self.runtime.get_block(child_id) for child_id in self.steps] + return self._steps_cache + + def answer_mapper(self, answer_status): + steps = self.get_steps() + answer_map = [] + for step in steps: + for answer in step.student_results: + if answer[1]['status'] == answer_status: + answer_map.append({'id': answer[0], 'details': answer[1]}) + return answer_map + @property def has_review_step(self): from .step import ReviewStepBlock @@ -900,6 +921,27 @@ def assessment_message(self): assessment_message = _("Note: you have used all attempts. Continue to the next unit") return '

{}

'.format(assessment_message) + @property + def score(self): + questions = self.get_questions() + total_child_weight = sum(float(question.weight) for question in questions) + if total_child_weight == 0: + return Score(0, 0, [], [], []) + steps = self.get_steps() + questions_map = {question.name: question for question in questions} + points_earned = 0 + for step in steps: + for question_name, question_results in step.student_results: + question = questions_map.get(question_name) + if question: # Under what conditions would this evaluate to False? + points_earned += question_results['score'] * question.weight + score = points_earned / total_child_weight + 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) + def get_message_content(self, message_type, or_default=False): for child_id in self.children: if child_isinstance(self, child_id, MentoringMessageBlock): @@ -938,6 +980,7 @@ def student_view(self, context): fragment.add_javascript_url(self.runtime.local_resource_url(self, 'public/js/mentoring_with_steps.js')) fragment.add_resource(loader.load_unicode('templates/html/mentoring_attempts.html'), "text/html") + fragment.add_resource(loader.load_unicode('templates/html/mentoring_assessment_templates.html'), "text/html") self.include_theme_files(fragment) @@ -985,6 +1028,15 @@ def update_num_attempts(self, data, suffix=''): 'num_attempts': self.num_attempts } + @XBlock.json_handler + def get_score(self, data, suffix): + return { + 'score': self.score.percentage, + 'correct_answers': len(self.score.correct), + 'incorrect_answers': len(self.score.incorrect), + 'partially_correct_answers': len(self.score.partially_correct), + } + @XBlock.json_handler def try_again(self, data, suffix=''): self.active_step = 0 diff --git a/problem_builder/public/js/mentoring_with_steps.js b/problem_builder/public/js/mentoring_with_steps.js index 4f73777c..0a8052b8 100644 --- a/problem_builder/public/js/mentoring_with_steps.js +++ b/problem_builder/public/js/mentoring_with_steps.js @@ -4,8 +4,10 @@ function MentoringWithStepsBlock(runtime, element) { function(c) { return c.element.className.indexOf('sb-step') > -1; } ); var activeStep = $('.mentoring', element).data('active-step'); + var gradeTemplate = _.template($('#xblock-grade-template').html()); var attemptsTemplate = _.template($('#xblock-attempts-template').html()); - var reviewStep, checkmark, submitDOM, nextDOM, reviewDOM, tryAgainDOM, assessmentMessageDOM, attemptsDOM, submitXHR; + var reviewStep, checkmark, submitDOM, nextDOM, reviewDOM, tryAgainDOM, + assessmentMessageDOM, gradeDOM, attemptsDOM, submitXHR; function isLastStep() { return (activeStep === steps.length-1); @@ -43,11 +45,25 @@ function MentoringWithStepsBlock(runtime, element) { }); } + function updateGrade() { + var handlerUrl = runtime.handlerUrl(element, 'get_score'); + $.post(handlerUrl, JSON.stringify({})) + .success(function(response) { + gradeDOM.data('score', response.score); + gradeDOM.data('correct_answer', response.correct_answers); + gradeDOM.data('incorrect_answer', response.incorrect_answers); + gradeDOM.data('partially_correct_answer', response.partially_correct_answers); + }); + } + function handleResults(response) { // Update active step so next step is shown on page reload (even if user does not click "Next Step") updateActiveStep(activeStep+1); - if (response.update_attempts) { + + // If step submitted was last step of this mentoring block, update grade and number of attempts used + if (response.attempt_complete) { updateNumAttempts(); + updateGrade(); } // Update UI @@ -96,6 +112,7 @@ function MentoringWithStepsBlock(runtime, element) { checkmark.removeClass('checkmark-incorrect icon-exclamation fa-exclamation'); hideAllSteps(); assessmentMessageDOM.html(''); + gradeDOM.html(''); attemptsDOM.html(''); } @@ -117,12 +134,14 @@ function MentoringWithStepsBlock(runtime, element) { } function showAssessmentMessage() { - var data = $('.grade', element).data(); + var data = gradeDOM.data(); assessmentMessageDOM.html(data.assessment_message); } function showReviewStep() { reviewStep.show(); + var data = gradeDOM.data(); + gradeDOM.html(gradeTemplate(data)); submitDOM.hide(); nextDOM.hide(); reviewDOM.hide(); @@ -226,6 +245,7 @@ function MentoringWithStepsBlock(runtime, element) { tryAgainDOM.on('click', tryAgain); assessmentMessageDOM = $('.assessment-message', element); + gradeDOM = $('.grade', element); attemptsDOM = $('.attempts', element); var options = { diff --git a/problem_builder/step.py b/problem_builder/step.py index db3cb49f..42e66368 100644 --- a/problem_builder/step.py +++ b/problem_builder/step.py @@ -154,7 +154,7 @@ def submit(self, submissions, suffix=''): 'message': 'Success!', 'completed': completed, 'results': submit_results, - 'update_attempts': self.is_last_step + 'attempt_complete': self.is_last_step } def reset(self): diff --git a/problem_builder/templates/html/mentoring_with_steps.html b/problem_builder/templates/html/mentoring_with_steps.html index 23656ed1..d3aae3ae 100644 --- a/problem_builder/templates/html/mentoring_with_steps.html +++ b/problem_builder/templates/html/mentoring_with_steps.html @@ -17,6 +17,10 @@

{{ title }}

From fce6efc17c3658ea63903254ee8734d2dc575a88 Mon Sep 17 00:00:00 2001 From: Tim Krones Date: Wed, 23 Sep 2015 20:57:44 +0200 Subject: [PATCH 21/43] Introduce extended_feedback field and make it editable in Studio. --- problem_builder/mentoring.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/problem_builder/mentoring.py b/problem_builder/mentoring.py index fbbafa49..1d97d385 100644 --- a/problem_builder/mentoring.py +++ b/problem_builder/mentoring.py @@ -833,16 +833,22 @@ class MentoringWithExplicitStepsBlock(BaseMentoringBlock, StudioContainerWithNes """ # Content max_attempts = Integer( - display_name=_("Max. Attempts Allowed"), - help=_("Maximum number of attempts allowed for this mentoring block"), + display_name=_("Max. attempts allowed"), + help=_("Maximum number of times students are allowed to attempt this mentoring block"), default=0, scope=Scope.content, enforce_type=True ) + extended_feedback = Boolean( + display_name=_("Extended feedback"), + help=_("Show extended feedback when all attempts are used up?"), + default=False, + Scope=Scope.content + ) # Settings display_name = String( - display_name=_("Title (Display name)"), + display_name=_("Title (display name)"), help=_("Title to display"), default=_("Mentoring Questions (with explicit steps)"), scope=Scope.settings @@ -862,7 +868,7 @@ class MentoringWithExplicitStepsBlock(BaseMentoringBlock, StudioContainerWithNes enforce_type=True ) - editable_fields = ('display_name', 'max_attempts') + editable_fields = ('display_name', 'max_attempts', 'extended_feedback') @lazy def questions(self): From a69d48616493c97a32fb8dcdd9f74d3e8661e699 Mon Sep 17 00:00:00 2001 From: Tim Krones Date: Wed, 23 Sep 2015 22:18:20 +0200 Subject: [PATCH 22/43] Change default display names of existing mentoring block ("Problem Builder") and new mentoring block ("Step Builder"). --- problem_builder/mentoring.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/problem_builder/mentoring.py b/problem_builder/mentoring.py index 1d97d385..761b4c54 100644 --- a/problem_builder/mentoring.py +++ b/problem_builder/mentoring.py @@ -225,7 +225,7 @@ class MentoringBlock(BaseMentoringBlock, StudioContainerXBlockMixin, StepParentM display_name = String( display_name=_("Title (Display name)"), help=_("Title to display"), - default=_("Mentoring Questions"), + default=_("Problem Builder"), scope=Scope.settings ) feedback_label = String( @@ -850,7 +850,7 @@ class MentoringWithExplicitStepsBlock(BaseMentoringBlock, StudioContainerWithNes display_name = String( display_name=_("Title (display name)"), help=_("Title to display"), - default=_("Mentoring Questions (with explicit steps)"), + default=_("Step Builder"), scope=Scope.settings ) From ef0812a600d74b9dbb5d8e71fb494631768f53ac Mon Sep 17 00:00:00 2001 From: Tim Krones Date: Thu, 24 Sep 2015 06:43:45 +0200 Subject: [PATCH 23/43] Show review tips if there are some attempts left. --- problem_builder/mentoring.py | 29 +++++++++++++++++++ .../public/js/mentoring_with_steps.js | 25 +++++++++++++++- .../templates/html/mentoring_with_steps.html | 6 +++- 3 files changed, 58 insertions(+), 2 deletions(-) diff --git a/problem_builder/mentoring.py b/problem_builder/mentoring.py index 761b4c54..da86844a 100644 --- a/problem_builder/mentoring.py +++ b/problem_builder/mentoring.py @@ -948,6 +948,35 @@ def score(self): return Score(score, int(round(score * 100)), correct, incorrect, partially_correct) + @property + def review_tips(self): + """ Get review tips, shown for wrong answers. """ + review_tips = [] + status_cache = dict() + steps = self.get_steps() + for step in steps: + status_cache.update(dict(step.student_results)) + for question in self.get_questions(): + result = status_cache.get(question.name) + if result and result.get('status') != 'correct': + # The student got this wrong. Check if there is a review tip to show. + tip_html = question.get_review_tip() + if tip_html: + if hasattr(self.runtime, 'replace_jump_to_id_urls'): + tip_html = self.runtime.replace_jump_to_id_urls(tip_html) + review_tips.append(tip_html) + return review_tips + + @property + def review_tips_json(self): + return json.dumps(self.review_tips) + + @XBlock.json_handler + def get_review_tips(self, data, suffix): + return { + 'review_tips': self.review_tips + } + def get_message_content(self, message_type, or_default=False): for child_id in self.children: if child_isinstance(self, child_id, MentoringMessageBlock): diff --git a/problem_builder/public/js/mentoring_with_steps.js b/problem_builder/public/js/mentoring_with_steps.js index 0a8052b8..855b9a95 100644 --- a/problem_builder/public/js/mentoring_with_steps.js +++ b/problem_builder/public/js/mentoring_with_steps.js @@ -5,9 +5,10 @@ function MentoringWithStepsBlock(runtime, element) { ); var activeStep = $('.mentoring', element).data('active-step'); var gradeTemplate = _.template($('#xblock-grade-template').html()); + var reviewTipsTemplate = _.template($('#xblock-review-tips-template').html()); // Tips about specific questions the user got wrong var attemptsTemplate = _.template($('#xblock-attempts-template').html()); var reviewStep, checkmark, submitDOM, nextDOM, reviewDOM, tryAgainDOM, - assessmentMessageDOM, gradeDOM, attemptsDOM, submitXHR; + assessmentMessageDOM, gradeDOM, attemptsDOM, reviewTipsDOM, submitXHR; function isLastStep() { return (activeStep === steps.length-1); @@ -56,6 +57,14 @@ function MentoringWithStepsBlock(runtime, element) { }); } + function updateReviewTips() { + var handlerUrl = runtime.handlerUrl(element, 'get_review_tips'); + $.post(handlerUrl, JSON.stringify({})) + .success(function(response) { + gradeDOM.data('assessment_review_tips', response.review_tips); + }); + } + function handleResults(response) { // Update active step so next step is shown on page reload (even if user does not click "Next Step") updateActiveStep(activeStep+1); @@ -64,6 +73,7 @@ function MentoringWithStepsBlock(runtime, element) { if (response.attempt_complete) { updateNumAttempts(); updateGrade(); + updateReviewTips(); } // Update UI @@ -114,6 +124,7 @@ function MentoringWithStepsBlock(runtime, element) { assessmentMessageDOM.html(''); gradeDOM.html(''); attemptsDOM.html(''); + reviewTipsDOM.empty().hide(); } function updateDisplay() { @@ -142,6 +153,17 @@ function MentoringWithStepsBlock(runtime, element) { reviewStep.show(); var data = gradeDOM.data(); gradeDOM.html(gradeTemplate(data)); + // Review tips + if (someAttemptsLeft()) { + if (data.assessment_review_tips.length > 0) { + // on-assessment-review-question messages specific to questions the student got wrong: + reviewTipsDOM.html(reviewTipsTemplate({ + tips: data.assessment_review_tips + })); + reviewTipsDOM.show(); + } + } + submitDOM.hide(); nextDOM.hide(); reviewDOM.hide(); @@ -247,6 +269,7 @@ function MentoringWithStepsBlock(runtime, element) { assessmentMessageDOM = $('.assessment-message', element); gradeDOM = $('.grade', element); attemptsDOM = $('.attempts', element); + reviewTipsDOM = $('.assessment-review-tips', element); var options = { onChange: onChange diff --git a/problem_builder/templates/html/mentoring_with_steps.html b/problem_builder/templates/html/mentoring_with_steps.html index d3aae3ae..6845e54b 100644 --- a/problem_builder/templates/html/mentoring_with_steps.html +++ b/problem_builder/templates/html/mentoring_with_steps.html @@ -20,7 +20,8 @@

{{ title }}

data-score="{{ self.score.percentage }}" data-correct_answer="{{ self.score.correct|length }}" data-incorrect_answer="{{ self.score.incorrect|length }}" - data-partially_correct_answer="{{ self.score.partially_correct|length }}"> + data-partially_correct_answer="{{ self.score.partially_correct|length }}" + data-assessment_review_tips="{{ self.review_tips_json }}">
@@ -35,6 +36,9 @@

{{ title }}

+ +
+
From 9e86458330a87a1de9a3d50d483a1213eed6ec3e Mon Sep 17 00:00:00 2001 From: Tim Krones Date: Thu, 24 Sep 2015 19:46:46 +0200 Subject: [PATCH 24/43] Get rid of "runDetails not defined error". --- problem_builder/mentoring.py | 2 +- .../public/js/mentoring_with_steps.js | 2 +- .../html/mentoring_review_templates.html | 69 +++++++++++++++++++ 3 files changed, 71 insertions(+), 2 deletions(-) create mode 100644 problem_builder/templates/html/mentoring_review_templates.html diff --git a/problem_builder/mentoring.py b/problem_builder/mentoring.py index da86844a..5d9fd834 100644 --- a/problem_builder/mentoring.py +++ b/problem_builder/mentoring.py @@ -1015,7 +1015,7 @@ def student_view(self, context): fragment.add_javascript_url(self.runtime.local_resource_url(self, 'public/js/mentoring_with_steps.js')) fragment.add_resource(loader.load_unicode('templates/html/mentoring_attempts.html'), "text/html") - fragment.add_resource(loader.load_unicode('templates/html/mentoring_assessment_templates.html'), "text/html") + fragment.add_resource(loader.load_unicode('templates/html/mentoring_review_templates.html'), "text/html") self.include_theme_files(fragment) diff --git a/problem_builder/public/js/mentoring_with_steps.js b/problem_builder/public/js/mentoring_with_steps.js index 855b9a95..bf12b9e6 100644 --- a/problem_builder/public/js/mentoring_with_steps.js +++ b/problem_builder/public/js/mentoring_with_steps.js @@ -4,7 +4,7 @@ function MentoringWithStepsBlock(runtime, element) { function(c) { return c.element.className.indexOf('sb-step') > -1; } ); var activeStep = $('.mentoring', element).data('active-step'); - var gradeTemplate = _.template($('#xblock-grade-template').html()); + var gradeTemplate = _.template($('#xblock-review-template').html()); var reviewTipsTemplate = _.template($('#xblock-review-tips-template').html()); // Tips about specific questions the user got wrong var attemptsTemplate = _.template($('#xblock-attempts-template').html()); var reviewStep, checkmark, submitDOM, nextDOM, reviewDOM, tryAgainDOM, diff --git a/problem_builder/templates/html/mentoring_review_templates.html b/problem_builder/templates/html/mentoring_review_templates.html new file mode 100644 index 00000000..6c453d7e --- /dev/null +++ b/problem_builder/templates/html/mentoring_review_templates.html @@ -0,0 +1,69 @@ + + + + + + + From 67a9c25f88e46e84c18d9890a5d2d40a972211a7 Mon Sep 17 00:00:00 2001 From: Tim Krones Date: Thu, 24 Sep 2015 23:46:08 +0200 Subject: [PATCH 25/43] Add functionality for reviewing steps. --- problem_builder/mentoring.py | 27 +++++++- .../public/js/mentoring_with_steps.js | 67 ++++++++++++++++++- .../html/mentoring_review_templates.html | 9 ++- .../templates/html/mentoring_with_steps.html | 8 ++- 4 files changed, 104 insertions(+), 7 deletions(-) diff --git a/problem_builder/mentoring.py b/problem_builder/mentoring.py index 5d9fd834..0fcb8409 100644 --- a/problem_builder/mentoring.py +++ b/problem_builder/mentoring.py @@ -898,13 +898,22 @@ def get_steps(self): self._steps_cache = [self.runtime.get_block(child_id) for child_id in self.steps] return self._steps_cache + def get_question_number(self, question_name): + question_names = [q.name for q in self.get_questions()] + return question_names.index(question_name) + 1 + def answer_mapper(self, answer_status): steps = self.get_steps() answer_map = [] for step in steps: for answer in step.student_results: if answer[1]['status'] == answer_status: - answer_map.append({'id': answer[0], 'details': answer[1]}) + answer_map.append({ + 'id': answer[0], + 'details': answer[1], + 'step': step.step_number, + 'number': self.get_question_number(answer[0]), + }) return answer_map @property @@ -977,6 +986,22 @@ def get_review_tips(self, data, suffix): 'review_tips': self.review_tips } + def show_extended_feedback(self): + return self.extended_feedback and self.max_attempts_reached + + def feedback_dispatch(self, target_data): + if self.show_extended_feedback(): + return json.dumps(target_data) + + def correct_json(self): + return self.feedback_dispatch(self.score.correct) + + def incorrect_json(self): + return self.feedback_dispatch(self.score.incorrect) + + def partial_json(self): + return self.feedback_dispatch(self.score.partially_correct) + def get_message_content(self, message_type, or_default=False): for child_id in self.children: if child_isinstance(self, child_id, MentoringMessageBlock): diff --git a/problem_builder/public/js/mentoring_with_steps.js b/problem_builder/public/js/mentoring_with_steps.js index bf12b9e6..9cc844c4 100644 --- a/problem_builder/public/js/mentoring_with_steps.js +++ b/problem_builder/public/js/mentoring_with_steps.js @@ -5,10 +5,11 @@ function MentoringWithStepsBlock(runtime, element) { ); var activeStep = $('.mentoring', element).data('active-step'); var gradeTemplate = _.template($('#xblock-review-template').html()); + var reviewStepsTemplate = _.template($('#xblock-review-steps-template').html()); var reviewTipsTemplate = _.template($('#xblock-review-tips-template').html()); // Tips about specific questions the user got wrong var attemptsTemplate = _.template($('#xblock-attempts-template').html()); var reviewStep, checkmark, submitDOM, nextDOM, reviewDOM, tryAgainDOM, - assessmentMessageDOM, gradeDOM, attemptsDOM, reviewTipsDOM, submitXHR; + assessmentMessageDOM, gradeDOM, attemptsDOM, reviewTipsDOM, reviewLinkDOM, submitXHR; function isLastStep() { return (activeStep === steps.length-1); @@ -151,8 +152,24 @@ function MentoringWithStepsBlock(runtime, element) { function showReviewStep() { reviewStep.show(); + var data = gradeDOM.data(); + + // Links for reviewing individual questions (WIP) + var enableExtendedFeedback = (!someAttemptsLeft() && data.extended_feedback); + + _.extend(data, { + 'runDetails': function(correctness) { + if (!enableExtendedFeedback) { + return ''; + } + var self = this; + return reviewStepsTemplate({'questions': self[correctness], 'correctness': correctness}); + } + }); gradeDOM.html(gradeTemplate(data)); + $('a.step-link', element).on('click', getStepToReview); + // Review tips if (someAttemptsLeft()) { if (data.assessment_review_tips.length > 0) { @@ -171,6 +188,36 @@ function MentoringWithStepsBlock(runtime, element) { tryAgainDOM.show(); } + function getStepToReview(event) { + event.preventDefault(); + var stepIndex = parseInt($(event.target).data('step')) - 1; + jumpToReview(stepIndex); + } + + function jumpToReview(stepIndex) { + activeStep = stepIndex; + cleanAll(); + reviewStep.hide(); + showActiveStep(); + + if (isLastStep()) { + reviewDOM.show(); + reviewDOM.removeAttr('disabled'); + nextDOM.hide(); + nextDOM.attr('disabled', 'disabled'); + } else { + nextDOM.show(); + nextDOM.removeAttr('disabled'); + } + + // ... + tryAgainDOM.hide(); + submitDOM.show(); + submitDOM.attr('disabled', 'disabled'); + reviewLinkDOM.show(); + // ... + } + function showAttempts() { var data = attemptsDOM.data(); if (data.max_attempts > 0) { @@ -224,6 +271,13 @@ function MentoringWithStepsBlock(runtime, element) { if (!someAttemptsLeft()) { tryAgainDOM.attr("disabled", "disabled"); } + nextDOM.off(); + nextDOM.on('click', reviewNextStep); + reviewLinkDOM.hide(); + } + + function reviewNextStep() { + jumpToReview(activeStep+1); } function handleTryAgain(result) { @@ -233,6 +287,8 @@ function MentoringWithStepsBlock(runtime, element) { tryAgainDOM.hide(); submitDOM.show(); if (! isLastStep()) { + nextDOM.off(); + nextDOM.on('click', updateDisplay); nextDOM.show(); reviewDOM.hide(); } @@ -257,7 +313,11 @@ function MentoringWithStepsBlock(runtime, element) { submitDOM.show(); nextDOM = $(element).find('.submit .input-next'); - nextDOM.on('click', updateDisplay); + if (atReviewStep()) { + nextDOM.on('click', reviewNextStep); + } else { + nextDOM.on('click', updateDisplay); + } nextDOM.show(); reviewDOM = $(element).find('.submit .input-review'); @@ -271,6 +331,9 @@ function MentoringWithStepsBlock(runtime, element) { attemptsDOM = $('.attempts', element); reviewTipsDOM = $('.assessment-review-tips', element); + reviewLinkDOM = $(element).find('.review-link'); + reviewLinkDOM.on('click', showGrade); + var options = { onChange: onChange }; diff --git a/problem_builder/templates/html/mentoring_review_templates.html b/problem_builder/templates/html/mentoring_review_templates.html index 6c453d7e..0085e412 100644 --- a/problem_builder/templates/html/mentoring_review_templates.html +++ b/problem_builder/templates/html/mentoring_review_templates.html @@ -16,6 +16,7 @@

), {number_correct: correct_answer}, {interpolate: /\{(.+?)\}/g}) %>

+ <%= runDetails('correct') %>
@@ -29,6 +30,7 @@

), {number_partially_correct: partially_correct_answer}, {interpolate: /\{(.+?)\}/g}) %>

+ <%= runDetails('partial') %>
@@ -42,6 +44,7 @@

), {number_incorrect: incorrect_answer}, {interpolate: /\{(.+?)\}/g}) %>

+ <%= runDetails('incorrect') %>

@@ -49,11 +52,11 @@

- diff --git a/problem_builder/templates/html/mentoring_with_steps.html b/problem_builder/templates/html/mentoring_with_steps.html index 6845e54b..c564971a 100644 --- a/problem_builder/templates/html/mentoring_with_steps.html +++ b/problem_builder/templates/html/mentoring_with_steps.html @@ -21,7 +21,11 @@

{{ title }}

data-correct_answer="{{ self.score.correct|length }}" data-incorrect_answer="{{ self.score.incorrect|length }}" data-partially_correct_answer="{{ self.score.partially_correct|length }}" - data-assessment_review_tips="{{ self.review_tips_json }}"> + data-assessment_review_tips="{{ self.review_tips_json }}" + data-extended_feedback="{{ self.extended_feedback }}" + data-correct="{{ self.correct_json }}" + data-incorrect="{{ self.incorrect_json }}" + data-partial="{{ self.partial_json }}">
@@ -41,4 +45,6 @@

{{ title }}

+ + From 0d62734639cc500fd20ee36ff44cc6d7a984069a Mon Sep 17 00:00:00 2001 From: Tim Krones Date: Fri, 25 Sep 2015 16:14:24 +0200 Subject: [PATCH 26/43] Display step- and answer-level feedback when reviewing step. --- .../public/js/mentoring_with_steps.js | 38 +++++++++++++++++++ problem_builder/public/js/step.js | 32 +++++++++++++++- problem_builder/step.py | 23 +++++++++++ 3 files changed, 92 insertions(+), 1 deletion(-) diff --git a/problem_builder/public/js/mentoring_with_steps.js b/problem_builder/public/js/mentoring_with_steps.js index 9cc844c4..f895e9d8 100644 --- a/problem_builder/public/js/mentoring_with_steps.js +++ b/problem_builder/public/js/mentoring_with_steps.js @@ -111,6 +111,29 @@ function MentoringWithStepsBlock(runtime, element) { step.submit(handleResults); } + function getResults() { + var step = steps[activeStep]; + step.getResults(handleReviewResults); + } + + function handleReviewResults(response) { + // Show step-level feedback + if (response.completed === 'correct') { + checkmark.addClass('checkmark-correct icon-ok fa-check'); + } else if (response.completed === 'partial') { + checkmark.addClass('checkmark-partially-correct icon-ok fa-check'); + } else { + checkmark.addClass('checkmark-incorrect icon-exclamation fa-exclamation'); + } + // Forward to active step to show answer level feedback + var step = steps[activeStep]; + var results = response.results; + var options = { + checkmark: checkmark + }; + step.handleReview(results, options); + } + function hideAllSteps() { for (var i=0; i < steps.length; i++) { $(steps[i].element).hide(); @@ -216,6 +239,8 @@ function MentoringWithStepsBlock(runtime, element) { submitDOM.attr('disabled', 'disabled'); reviewLinkDOM.show(); // ... + + getResults(); } function showAttempts() { @@ -258,10 +283,23 @@ function MentoringWithStepsBlock(runtime, element) { function initSteps(options) { for (var i=0; i < steps.length; i++) { var step = steps[i]; + var mentoring = { + setContent: setContent + }; + options.mentoring = mentoring; step.initChildren(options); } } + function setContent(dom, content) { + dom.html(''); + dom.append(content); + var template = $('#light-child-template', dom).html(); + if (template) { + dom.append(template); + } + } + function showGrade() { cleanAll(); showAssessmentMessage(); diff --git a/problem_builder/public/js/step.js b/problem_builder/public/js/step.js index 618fdda6..f89566bd 100644 --- a/problem_builder/public/js/step.js +++ b/problem_builder/public/js/step.js @@ -1,7 +1,7 @@ function MentoringStepBlock(runtime, element) { var children = runtime.children(element); - var submitXHR; + var submitXHR, resultsXHR; function callIfExists(obj, fn) { if (typeof obj !== 'undefined' && typeof obj[fn] == 'function') { @@ -51,6 +51,36 @@ function MentoringStepBlock(runtime, element) { .success(function(response) { result_handler(response); }); + }, + + getResults: function(result_handler) { + var handler_name = 'get_results'; + var data = []; + for (var i = 0; i < children.length; i++) { + var child = children[i]; + if (child && child.name !== undefined) { // Check if we are dealing with a question + data[i] = child.name; + } + } + var handlerUrl = runtime.handlerUrl(element, handler_name); + if (resultsXHR) { + resultsXHR.abort(); + } + resultsXHR = $.post(handlerUrl, JSON.stringify(data)) + .success(function(response) { + result_handler(response); + }); + }, + + handleReview: function(results, options) { + for (var i = 0; i < children.length; i++) { + var child = children[i]; + if (child && child.name !== undefined) { // Check if we are dealing with a question + var result = results[child.name]; + callIfExists(child, 'handleSubmit', result, options); + callIfExists(child, 'handleReview', result); + } + } } }; diff --git a/problem_builder/step.py b/problem_builder/step.py index 42e66368..9214c96c 100644 --- a/problem_builder/step.py +++ b/problem_builder/step.py @@ -157,6 +157,29 @@ def submit(self, submissions, suffix=''): 'attempt_complete': self.is_last_step } + @XBlock.json_handler + def get_results(self, queries, suffix=''): + results = {} + answers = dict(self.student_results) + for question in self.get_steps(): + previous_results = answers[question.name] + result = question.get_results(previous_results) + results[question.name] = result + + # Compute "answer status" for this step + if all(result[1]['status'] == 'correct' for result in self.student_results): + completed = Correctness.CORRECT + elif all(result[1]['status'] == 'incorrect' for result in self.student_results): + completed = Correctness.INCORRECT + else: + completed = Correctness.PARTIAL + + # Add 'message' to results? Looks like it's not used on the client ... + return { + 'results': results, + 'completed': completed, + } + def reset(self): while self.student_results: self.student_results.pop() From a0dfa541ea41b3c46f4886bc5db0239c81d590f0 Mon Sep 17 00:00:00 2001 From: Tim Krones Date: Fri, 25 Sep 2015 17:38:56 +0200 Subject: [PATCH 27/43] Conditionally enable "Try again" button when showing review step. --- problem_builder/public/js/mentoring_with_steps.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/problem_builder/public/js/mentoring_with_steps.js b/problem_builder/public/js/mentoring_with_steps.js index f895e9d8..e363a966 100644 --- a/problem_builder/public/js/mentoring_with_steps.js +++ b/problem_builder/public/js/mentoring_with_steps.js @@ -193,8 +193,11 @@ function MentoringWithStepsBlock(runtime, element) { gradeDOM.html(gradeTemplate(data)); $('a.step-link', element).on('click', getStepToReview); - // Review tips if (someAttemptsLeft()) { + + tryAgainDOM.removeAttr('disabled'); + + // Review tips if (data.assessment_review_tips.length > 0) { // on-assessment-review-question messages specific to questions the student got wrong: reviewTipsDOM.html(reviewTipsTemplate({ @@ -207,7 +210,6 @@ function MentoringWithStepsBlock(runtime, element) { submitDOM.hide(); nextDOM.hide(); reviewDOM.hide(); - tryAgainDOM.removeAttr('disabled'); tryAgainDOM.show(); } From c3a7d6de5b79bcbc80361614040e7d54b98c9a7d Mon Sep 17 00:00:00 2001 From: Tim Krones Date: Fri, 25 Sep 2015 18:46:42 +0200 Subject: [PATCH 28/43] Make sure relevant info is updated and retrieved sequentially after submitting step. --- .../public/js/mentoring_with_steps.js | 46 +++++++++---------- 1 file changed, 22 insertions(+), 24 deletions(-) diff --git a/problem_builder/public/js/mentoring_with_steps.js b/problem_builder/public/js/mentoring_with_steps.js index e363a966..179f43ce 100644 --- a/problem_builder/public/js/mentoring_with_steps.js +++ b/problem_builder/public/js/mentoring_with_steps.js @@ -31,11 +31,25 @@ function MentoringWithStepsBlock(runtime, element) { return (data.num_attempts < data.max_attempts); } - function updateActiveStep(newValue) { + function updateActiveStep(response) { + // Update UI + if (response.completed === 'correct') { + checkmark.addClass('checkmark-correct icon-ok fa-check'); + } else if (response.completed === 'partial') { + checkmark.addClass('checkmark-partially-correct icon-ok fa-check'); + } else { + checkmark.addClass('checkmark-incorrect icon-exclamation fa-exclamation'); + } + var handlerUrl = runtime.handlerUrl(element, 'update_active_step'); - $.post(handlerUrl, JSON.stringify(newValue)) + $.post(handlerUrl, JSON.stringify(activeStep+1)) .success(function(response) { activeStep = response.active_step; + if (activeStep === -1) { + updateNumAttempts(); + } else { + handleResults(); + } }); } @@ -44,6 +58,7 @@ function MentoringWithStepsBlock(runtime, element) { $.post(handlerUrl, JSON.stringify({})) .success(function(response) { attemptsDOM.data('num_attempts', response.num_attempts); + updateGrade(); }); } @@ -55,6 +70,7 @@ function MentoringWithStepsBlock(runtime, element) { gradeDOM.data('correct_answer', response.correct_answers); gradeDOM.data('incorrect_answer', response.incorrect_answers); gradeDOM.data('partially_correct_answer', response.partially_correct_answers); + updateReviewTips(); }); } @@ -63,35 +79,17 @@ function MentoringWithStepsBlock(runtime, element) { $.post(handlerUrl, JSON.stringify({})) .success(function(response) { gradeDOM.data('assessment_review_tips', response.review_tips); + handleResults(); }); } - function handleResults(response) { - // Update active step so next step is shown on page reload (even if user does not click "Next Step") - updateActiveStep(activeStep+1); - - // If step submitted was last step of this mentoring block, update grade and number of attempts used - if (response.attempt_complete) { - updateNumAttempts(); - updateGrade(); - updateReviewTips(); - } - - // Update UI - if (response.completed === 'correct') { - checkmark.addClass('checkmark-correct icon-ok fa-check'); - } else if (response.completed === 'partial') { - checkmark.addClass('checkmark-partially-correct icon-ok fa-check'); - } else { - checkmark.addClass('checkmark-incorrect icon-exclamation fa-exclamation'); - } - + function handleResults() { submitDOM.attr('disabled', 'disabled'); nextDOM.removeAttr("disabled"); if (nextDOM.is(':visible')) { nextDOM.focus(); } - if (isLastStep()) { + if (atReviewStep()) { if (reviewStepPresent()) { reviewDOM.removeAttr('disabled'); } else { @@ -108,7 +106,7 @@ function MentoringWithStepsBlock(runtime, element) { function submit() { // We do not handle submissions at this level, so just forward to "submit" method of active step var step = steps[activeStep]; - step.submit(handleResults); + step.submit(updateActiveStep); } function getResults() { From 9c847f23c37ed09707185f2d4b3386b44ef0cd03 Mon Sep 17 00:00:00 2001 From: Tim Krones Date: Fri, 25 Sep 2015 19:01:31 +0200 Subject: [PATCH 29/43] Fix: Make sure links for reviewing questions are not displayed multiple times at review step. --- problem_builder/mentoring.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/problem_builder/mentoring.py b/problem_builder/mentoring.py index 0fcb8409..d148c70e 100644 --- a/problem_builder/mentoring.py +++ b/problem_builder/mentoring.py @@ -987,7 +987,7 @@ def get_review_tips(self, data, suffix): } def show_extended_feedback(self): - return self.extended_feedback and self.max_attempts_reached + return self.extended_feedback def feedback_dispatch(self, target_data): if self.show_extended_feedback(): From 026446e421bb8dae4990bc227d1285d0c6f295e9 Mon Sep 17 00:00:00 2001 From: Tim Krones Date: Fri, 25 Sep 2015 20:20:04 +0200 Subject: [PATCH 30/43] Clean up. - Improve naming of functions that handle submission results - Add comments - Remove empty leftover comments - DRY out code that handles showing step-level feedback - Don't name things using snake_case in JS code - Use "callIfExists" in more places --- .../public/js/mentoring_with_steps.js | 31 ++++++++++--------- problem_builder/public/js/step.js | 12 +++---- 2 files changed, 22 insertions(+), 21 deletions(-) diff --git a/problem_builder/public/js/mentoring_with_steps.js b/problem_builder/public/js/mentoring_with_steps.js index 179f43ce..598cf8a5 100644 --- a/problem_builder/public/js/mentoring_with_steps.js +++ b/problem_builder/public/js/mentoring_with_steps.js @@ -31,8 +31,7 @@ function MentoringWithStepsBlock(runtime, element) { return (data.num_attempts < data.max_attempts); } - function updateActiveStep(response) { - // Update UI + function showFeedback(response) { if (response.completed === 'correct') { checkmark.addClass('checkmark-correct icon-ok fa-check'); } else if (response.completed === 'partial') { @@ -40,7 +39,14 @@ function MentoringWithStepsBlock(runtime, element) { } else { checkmark.addClass('checkmark-incorrect icon-exclamation fa-exclamation'); } + } + + function handleResults(response) { + showFeedback(response); + // Update active step: + // If we end up at the review step, proceed with updating the number of attempts used. + // Otherwise, get UI ready for showing next step. var handlerUrl = runtime.handlerUrl(element, 'update_active_step'); $.post(handlerUrl, JSON.stringify(activeStep+1)) .success(function(response) { @@ -48,7 +54,7 @@ function MentoringWithStepsBlock(runtime, element) { if (activeStep === -1) { updateNumAttempts(); } else { - handleResults(); + updateControls(); } }); } @@ -58,6 +64,7 @@ function MentoringWithStepsBlock(runtime, element) { $.post(handlerUrl, JSON.stringify({})) .success(function(response) { attemptsDOM.data('num_attempts', response.num_attempts); + // Now that relevant info is up-to-date, get the latest grade updateGrade(); }); } @@ -79,11 +86,11 @@ function MentoringWithStepsBlock(runtime, element) { $.post(handlerUrl, JSON.stringify({})) .success(function(response) { gradeDOM.data('assessment_review_tips', response.review_tips); - handleResults(); + updateControls(); }); } - function handleResults() { + function updateControls() { submitDOM.attr('disabled', 'disabled'); nextDOM.removeAttr("disabled"); @@ -106,7 +113,7 @@ function MentoringWithStepsBlock(runtime, element) { function submit() { // We do not handle submissions at this level, so just forward to "submit" method of active step var step = steps[activeStep]; - step.submit(updateActiveStep); + step.submit(handleResults); } function getResults() { @@ -116,13 +123,7 @@ function MentoringWithStepsBlock(runtime, element) { function handleReviewResults(response) { // Show step-level feedback - if (response.completed === 'correct') { - checkmark.addClass('checkmark-correct icon-ok fa-check'); - } else if (response.completed === 'partial') { - checkmark.addClass('checkmark-partially-correct icon-ok fa-check'); - } else { - checkmark.addClass('checkmark-incorrect icon-exclamation fa-exclamation'); - } + showFeedback(response); // Forward to active step to show answer level feedback var step = steps[activeStep]; var results = response.results; @@ -233,12 +234,10 @@ function MentoringWithStepsBlock(runtime, element) { nextDOM.removeAttr('disabled'); } - // ... tryAgainDOM.hide(); submitDOM.show(); submitDOM.attr('disabled', 'disabled'); reviewLinkDOM.show(); - // ... getResults(); } @@ -305,10 +304,12 @@ function MentoringWithStepsBlock(runtime, element) { showAssessmentMessage(); showReviewStep(); showAttempts(); + // Disable "Try again" button if no attempts left if (!someAttemptsLeft()) { tryAgainDOM.attr("disabled", "disabled"); } + nextDOM.off(); nextDOM.on('click', reviewNextStep); reviewLinkDOM.hide(); diff --git a/problem_builder/public/js/step.js b/problem_builder/public/js/step.js index f89566bd..0c9eb9b6 100644 --- a/problem_builder/public/js/step.js +++ b/problem_builder/public/js/step.js @@ -34,13 +34,13 @@ function MentoringStepBlock(runtime, element) { return is_valid; }, - submit: function(result_handler) { + submit: function(resultHandler) { var handler_name = 'submit'; var data = {}; for (var i = 0; i < children.length; i++) { var child = children[i]; - if (child && child.name !== undefined && typeof(child[handler_name]) !== "undefined") { - data[child.name.toString()] = child[handler_name](); + if (child && child.name !== undefined) { + data[child.name.toString()] = callIfExists(child, handler_name); } } var handlerUrl = runtime.handlerUrl(element, handler_name); @@ -49,11 +49,11 @@ function MentoringStepBlock(runtime, element) { } submitXHR = $.post(handlerUrl, JSON.stringify(data)) .success(function(response) { - result_handler(response); + resultHandler(response); }); }, - getResults: function(result_handler) { + getResults: function(resultHandler) { var handler_name = 'get_results'; var data = []; for (var i = 0; i < children.length; i++) { @@ -68,7 +68,7 @@ function MentoringStepBlock(runtime, element) { } resultsXHR = $.post(handlerUrl, JSON.stringify(data)) .success(function(response) { - result_handler(response); + resultHandler(response); }); }, From 8133e6331f0d1e65be2c4690462ed5dc06d6686a Mon Sep 17 00:00:00 2001 From: Tim Krones Date: Fri, 25 Sep 2015 22:56:42 +0200 Subject: [PATCH 31/43] Move logic for displaying grade and review links to review step. --- .../public/js/mentoring_with_steps.js | 47 +++++--------- problem_builder/public/js/review_step.js | 28 ++++++++ problem_builder/step.py | 2 + .../html/mentoring_review_templates.html | 63 ------------------ .../templates/html/review_step.html | 65 ++++++++++++++++++- 5 files changed, 112 insertions(+), 93 deletions(-) create mode 100644 problem_builder/public/js/review_step.js diff --git a/problem_builder/public/js/mentoring_with_steps.js b/problem_builder/public/js/mentoring_with_steps.js index 598cf8a5..1cd8ca88 100644 --- a/problem_builder/public/js/mentoring_with_steps.js +++ b/problem_builder/public/js/mentoring_with_steps.js @@ -1,14 +1,22 @@ function MentoringWithStepsBlock(runtime, element) { - var steps = runtime.children(element).filter( + var children = runtime.children(element); + var steps = children.filter( function(c) { return c.element.className.indexOf('sb-step') > -1; } ); + var reviewStep; + for (var i = 0; i < children.length; i++) { + var child = children[i]; + if (child.type === 'sb-review-step') { + reviewStep = child; + break; + } + } + var activeStep = $('.mentoring', element).data('active-step'); - var gradeTemplate = _.template($('#xblock-review-template').html()); - var reviewStepsTemplate = _.template($('#xblock-review-steps-template').html()); var reviewTipsTemplate = _.template($('#xblock-review-tips-template').html()); // Tips about specific questions the user got wrong var attemptsTemplate = _.template($('#xblock-attempts-template').html()); - var reviewStep, checkmark, submitDOM, nextDOM, reviewDOM, tryAgainDOM, + var checkmark, submitDOM, nextDOM, reviewDOM, tryAgainDOM, assessmentMessageDOM, gradeDOM, attemptsDOM, reviewTipsDOM, reviewLinkDOM, submitXHR; function isLastStep() { @@ -19,10 +27,6 @@ function MentoringWithStepsBlock(runtime, element) { return (activeStep === -1); } - function reviewStepPresent() { - return reviewStep.length > 0; - } - function someAttemptsLeft() { var data = attemptsDOM.data(); if (data.max_attempts === 0) { // Unlimited number of attempts available @@ -97,7 +101,7 @@ function MentoringWithStepsBlock(runtime, element) { if (nextDOM.is(':visible')) { nextDOM.focus(); } if (atReviewStep()) { - if (reviewStepPresent()) { + if (reviewStep) { reviewDOM.removeAttr('disabled'); } else { if (someAttemptsLeft()) { @@ -160,7 +164,7 @@ function MentoringWithStepsBlock(runtime, element) { showActiveStep(); validateXBlock(); nextDOM.attr('disabled', 'disabled'); - if (isLastStep() && reviewStepPresent()) { + if (isLastStep() && reviewStep) { reviewDOM.attr('disabled', 'disabled'); reviewDOM.show(); } @@ -173,23 +177,13 @@ function MentoringWithStepsBlock(runtime, element) { } function showReviewStep() { - reviewStep.show(); - var data = gradeDOM.data(); - // Links for reviewing individual questions (WIP) - var enableExtendedFeedback = (!someAttemptsLeft() && data.extended_feedback); + // Forward to review step to render grade data + var showExtendedFeedback = (!someAttemptsLeft() && data.extended_feedback); + reviewStep.renderGrade(gradeDOM, showExtendedFeedback); - _.extend(data, { - 'runDetails': function(correctness) { - if (!enableExtendedFeedback) { - return ''; - } - var self = this; - return reviewStepsTemplate({'questions': self[correctness], 'correctness': correctness}); - } - }); - gradeDOM.html(gradeTemplate(data)); + // Add click handler that takes care of showing associated step to step links $('a.step-link', element).on('click', getStepToReview); if (someAttemptsLeft()) { @@ -221,7 +215,6 @@ function MentoringWithStepsBlock(runtime, element) { function jumpToReview(stepIndex) { activeStep = stepIndex; cleanAll(); - reviewStep.hide(); showActiveStep(); if (isLastStep()) { @@ -322,7 +315,6 @@ function MentoringWithStepsBlock(runtime, element) { function handleTryAgain(result) { activeStep = result.active_step; updateDisplay(); - reviewStep.hide(); tryAgainDOM.hide(); submitDOM.show(); if (! isLastStep()) { @@ -342,9 +334,6 @@ function MentoringWithStepsBlock(runtime, element) { } function initXBlockView() { - reviewStep = $('.sb-review-step', element); - reviewStep.hide(); - checkmark = $('.assessment-checkmark', element); submitDOM = $(element).find('.submit .input-main'); diff --git a/problem_builder/public/js/review_step.js b/problem_builder/public/js/review_step.js new file mode 100644 index 00000000..19636ee5 --- /dev/null +++ b/problem_builder/public/js/review_step.js @@ -0,0 +1,28 @@ +function ReviewStepBlock(runtime, element) { + + var gradeTemplate = _.template($('#xblock-feedback-template').html()); + var reviewStepsTemplate = _.template($('#xblock-step-links-template').html()); + + return { + + 'renderGrade': function(gradeDOM, showExtendedFeedback) { + + var data = gradeDOM.data(); + + _.extend(data, { + 'runDetails': function(correctness) { + if (!showExtendedFeedback) { + return ''; + } + var self = this; + return reviewStepsTemplate({'questions': self[correctness], 'correctness': correctness}); + } + }); + + gradeDOM.html(gradeTemplate(data)); + + } + + }; + +} diff --git a/problem_builder/step.py b/problem_builder/step.py index 9214c96c..af24ad5b 100644 --- a/problem_builder/step.py +++ b/problem_builder/step.py @@ -260,4 +260,6 @@ def _render_view(self, context): fragment.add_content(loader.render_template('templates/html/review_step.html', { 'self': self, })) + fragment.add_javascript_url(self.runtime.local_resource_url(self, 'public/js/review_step.js')) + fragment.initialize_js('ReviewStepBlock') return fragment diff --git a/problem_builder/templates/html/mentoring_review_templates.html b/problem_builder/templates/html/mentoring_review_templates.html index 0085e412..84dee723 100644 --- a/problem_builder/templates/html/mentoring_review_templates.html +++ b/problem_builder/templates/html/mentoring_review_templates.html @@ -1,66 +1,3 @@ - - - - - + + + + + From e5a755761f53d4157404239e0125b6a4a2393dab Mon Sep 17 00:00:00 2001 From: Tim Krones Date: Fri, 25 Sep 2015 22:58:49 +0200 Subject: [PATCH 32/43] Make sure data relevant for displaying review links is up-to-date when displaying review step. --- problem_builder/mentoring.py | 22 ++++++++++++------- .../public/js/mentoring_with_steps.js | 3 +++ 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/problem_builder/mentoring.py b/problem_builder/mentoring.py index d148c70e..e9292b83 100644 --- a/problem_builder/mentoring.py +++ b/problem_builder/mentoring.py @@ -989,18 +989,21 @@ def get_review_tips(self, data, suffix): def show_extended_feedback(self): return self.extended_feedback - def feedback_dispatch(self, target_data): + def feedback_dispatch(self, target_data, stringify): if self.show_extended_feedback(): - return json.dumps(target_data) + if stringify: + return json.dumps(target_data) + else: + return target_data - def correct_json(self): - return self.feedback_dispatch(self.score.correct) + def correct_json(self, stringify=True): + return self.feedback_dispatch(self.score.correct, stringify) - def incorrect_json(self): - return self.feedback_dispatch(self.score.incorrect) + def incorrect_json(self, stringify=True): + return self.feedback_dispatch(self.score.incorrect, stringify) - def partial_json(self): - return self.feedback_dispatch(self.score.partially_correct) + def partial_json(self, stringify=True): + return self.feedback_dispatch(self.score.partially_correct, stringify) def get_message_content(self, message_type, or_default=False): for child_id in self.children: @@ -1095,6 +1098,9 @@ def get_score(self, data, suffix): 'correct_answers': len(self.score.correct), 'incorrect_answers': len(self.score.incorrect), 'partially_correct_answers': len(self.score.partially_correct), + 'correct': self.correct_json(stringify=False), + 'incorrect': self.incorrect_json(False), + 'partial': self.partial_json(False), } @XBlock.json_handler diff --git a/problem_builder/public/js/mentoring_with_steps.js b/problem_builder/public/js/mentoring_with_steps.js index 1cd8ca88..489b4684 100644 --- a/problem_builder/public/js/mentoring_with_steps.js +++ b/problem_builder/public/js/mentoring_with_steps.js @@ -81,6 +81,9 @@ function MentoringWithStepsBlock(runtime, element) { gradeDOM.data('correct_answer', response.correct_answers); gradeDOM.data('incorrect_answer', response.incorrect_answers); gradeDOM.data('partially_correct_answer', response.partially_correct_answers); + gradeDOM.data('correct', response.correct); + gradeDOM.data('incorrect', response.incorrect); + gradeDOM.data('partially', response.partial); updateReviewTips(); }); } From 1645784523ecfe9b253bc9cf4efd1ceb93b923f4 Mon Sep 17 00:00:00 2001 From: Tim Krones Date: Fri, 25 Sep 2015 23:25:40 +0200 Subject: [PATCH 33/43] Clean up: Compute score only once in get_score. --- problem_builder/mentoring.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/problem_builder/mentoring.py b/problem_builder/mentoring.py index e9292b83..ced21726 100644 --- a/problem_builder/mentoring.py +++ b/problem_builder/mentoring.py @@ -1093,11 +1093,12 @@ def update_num_attempts(self, data, suffix=''): @XBlock.json_handler def get_score(self, data, suffix): + score = self.score return { - 'score': self.score.percentage, - 'correct_answers': len(self.score.correct), - 'incorrect_answers': len(self.score.incorrect), - 'partially_correct_answers': len(self.score.partially_correct), + 'score': score.percentage, + 'correct_answers': len(score.correct), + 'incorrect_answers': len(score.incorrect), + 'partially_correct_answers': len(score.partially_correct), 'correct': self.correct_json(stringify=False), 'incorrect': self.incorrect_json(False), 'partial': self.partial_json(False), From 99b72b12c26acf8774445323cbf7c1258d6465ee Mon Sep 17 00:00:00 2001 From: Tim Krones Date: Sat, 26 Sep 2015 10:22:22 +0200 Subject: [PATCH 34/43] Fix test failures. --- problem_builder/tests/integration/test_titles.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/problem_builder/tests/integration/test_titles.py b/problem_builder/tests/integration/test_titles.py index e54983df..0d3fe297 100644 --- a/problem_builder/tests/integration/test_titles.py +++ b/problem_builder/tests/integration/test_titles.py @@ -38,8 +38,8 @@ class TitleTest(SeleniumXBlockTest): @ddt.data( ('', None), - ('', "Mentoring Questions"), - ('', "Mentoring Questions"), + ('', "Problem Builder"), + ('', "Problem Builder"), ('', "A Question"), ('', None), ) From 4f6d3f86200d9720bd2f1280de7ec29e25ab871d Mon Sep 17 00:00:00 2001 From: Tim Krones Date: Sat, 26 Sep 2015 10:49:09 +0200 Subject: [PATCH 35/43] Move code shared by existing mentoring block (problem builder) and new mentoring block (step builder) to common superclass. --- problem_builder/mentoring.py | 155 ++++++++++++----------------------- 1 file changed, 53 insertions(+), 102 deletions(-) diff --git a/problem_builder/mentoring.py b/problem_builder/mentoring.py index ced21726..334ec17f 100644 --- a/problem_builder/mentoring.py +++ b/problem_builder/mentoring.py @@ -92,6 +92,21 @@ class BaseMentoringBlock( default=True, scope=Scope.content ) + max_attempts = Integer( + display_name=_("Max. attempts allowed"), + help=_("Maximum number of times students are allowed to attempt the questions belonging to this block"), + default=0, + scope=Scope.content, + enforce_type=True + ) + + # User state + num_attempts = Integer( + # Number of attempts a user has answered for this questions + default=0, + scope=Scope.user_state, + enforce_type=True + ) has_children = True @@ -110,6 +125,28 @@ def url_name(self): except AttributeError: return unicode(self.scope_ids.usage_id) + @property + def review_tips_json(self): + return json.dumps(self.review_tips) + + @property + def max_attempts_reached(self): + return self.max_attempts > 0 and self.num_attempts >= self.max_attempts + + def get_message_content(self, message_type, or_default=False): + for child_id in self.children: + if child_isinstance(self, child_id, MentoringMessageBlock): + child = self.runtime.get_block(child_id) + if child.type == message_type: + content = child.content + if hasattr(self.runtime, 'replace_jump_to_id_urls'): + content = self.runtime.replace_jump_to_id_urls(content) + return content + if or_default: + # Return the default value since no custom message is set. + # Note the WYSIWYG editor usually wraps the .content HTML in a

tag so we do the same here. + return '

{}

'.format(MentoringMessageBlock.MESSAGE_TYPES[message_type]['default']) + def get_theme(self): """ Gets theme settings from settings service. Falls back to default (LMS) theme @@ -129,6 +166,22 @@ def include_theme_files(self, fragment): for theme_file in theme_files: fragment.add_css(ResourceLoader(theme_package).load_unicode(theme_file)) + 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 view(self, data, suffix=''): """ @@ -185,13 +238,6 @@ class MentoringBlock(BaseMentoringBlock, StudioContainerXBlockMixin, StepParentM default=None, scope=Scope.content ) - max_attempts = Integer( - display_name=_("Max. Attempts Allowed"), - help=_("Number of max attempts allowed for this questions"), - default=0, - scope=Scope.content, - enforce_type=True - ) enforce_dependency = Boolean( display_name=_("Enforce Dependency"), help=_("Should the next step be the current block to complete?"), @@ -247,12 +293,6 @@ class MentoringBlock(BaseMentoringBlock, StudioContainerXBlockMixin, StepParentM default=False, scope=Scope.user_state ) - num_attempts = Integer( - # Number of attempts a user has answered for this questions - default=0, - scope=Scope.user_state, - enforce_type=True - ) step = Integer( # Keep track of the student assessment progress. default=0, @@ -475,29 +515,9 @@ def review_tips(self): review_tips.append(tip_html) return review_tips - @property - def review_tips_json(self): - return json.dumps(self.review_tips) - 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=''): """ @@ -739,24 +759,6 @@ def try_again(self, data, suffix=''): 'result': 'success' } - @property - def max_attempts_reached(self): - return self.max_attempts > 0 and self.num_attempts >= self.max_attempts - - def get_message_content(self, message_type, or_default=False): - for child_id in self.children: - if child_isinstance(self, child_id, MentoringMessageBlock): - child = self.runtime.get_block(child_id) - if child.type == message_type: - content = child.content - if hasattr(self.runtime, 'replace_jump_to_id_urls'): - content = self.runtime.replace_jump_to_id_urls(content) - return content - if or_default: - # Return the default value since no custom message is set. - # Note the WYSIWYG editor usually wraps the .content HTML in a

tag so we do the same here. - return '

{}

'.format(MentoringMessageBlock.MESSAGE_TYPES[message_type]['default']) - def validate(self): """ Validates the state of this XBlock except for individual field values. @@ -832,13 +834,6 @@ class MentoringWithExplicitStepsBlock(BaseMentoringBlock, StudioContainerWithNes An XBlock providing mentoring capabilities with explicit steps """ # Content - max_attempts = Integer( - display_name=_("Max. attempts allowed"), - help=_("Maximum number of times students are allowed to attempt this mentoring block"), - default=0, - scope=Scope.content, - enforce_type=True - ) extended_feedback = Boolean( display_name=_("Extended feedback"), help=_("Show extended feedback when all attempts are used up?"), @@ -855,12 +850,6 @@ class MentoringWithExplicitStepsBlock(BaseMentoringBlock, StudioContainerWithNes ) # User state - num_attempts = Integer( - # Number of attempts user has attempted this mentoring block - default=0, - scope=Scope.user_state, - enforce_type=True - ) active_step = Integer( # Keep track of the student progress. default=0, @@ -921,10 +910,6 @@ def has_review_step(self): from .step import ReviewStepBlock return any(child_isinstance(self, child_id, ReviewStepBlock) for child_id in self.children) - @property - def max_attempts_reached(self): - return self.max_attempts > 0 and self.num_attempts >= self.max_attempts - @property def assessment_message(self): """ @@ -976,10 +961,6 @@ def review_tips(self): review_tips.append(tip_html) return review_tips - @property - def review_tips_json(self): - return json.dumps(self.review_tips) - @XBlock.json_handler def get_review_tips(self, data, suffix): return { @@ -989,36 +970,6 @@ def get_review_tips(self, data, suffix): def show_extended_feedback(self): return self.extended_feedback - 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) - - def get_message_content(self, message_type, or_default=False): - for child_id in self.children: - if child_isinstance(self, child_id, MentoringMessageBlock): - child = self.runtime.get_block(child_id) - if child.type == message_type: - content = child.content - if hasattr(self.runtime, 'replace_jump_to_id_urls'): - content = self.runtime.replace_jump_to_id_urls(content) - return content - if or_default: - # Return the default value since no custom message is set. - # Note the WYSIWYG editor usually wraps the .content HTML in a

tag so we do the same here. - return '

{}

'.format(MentoringMessageBlock.MESSAGE_TYPES[message_type]['default']) - def student_view(self, context): fragment = Fragment() children_contents = [] From 1a3602d87f16e957659ba1a3a37cfd2bc0a9c415 Mon Sep 17 00:00:00 2001 From: Tim Krones Date: Sat, 26 Sep 2015 11:16:23 +0200 Subject: [PATCH 36/43] Fix: Make sure assessment message is up-to-date when submitting last possible attempt. Also, reduce number of trips to the server by merging updateReviewTips into updateGrade. --- problem_builder/mentoring.py | 10 +++------- problem_builder/public/js/mentoring_with_steps.js | 15 ++++----------- 2 files changed, 7 insertions(+), 18 deletions(-) diff --git a/problem_builder/mentoring.py b/problem_builder/mentoring.py index 334ec17f..e9ac2ad0 100644 --- a/problem_builder/mentoring.py +++ b/problem_builder/mentoring.py @@ -961,12 +961,6 @@ def review_tips(self): review_tips.append(tip_html) return review_tips - @XBlock.json_handler - def get_review_tips(self, data, suffix): - return { - 'review_tips': self.review_tips - } - def show_extended_feedback(self): return self.extended_feedback @@ -1043,7 +1037,7 @@ def update_num_attempts(self, data, suffix=''): } @XBlock.json_handler - def get_score(self, data, suffix): + def get_grade(self, data, suffix): score = self.score return { 'score': score.percentage, @@ -1053,6 +1047,8 @@ def get_score(self, data, suffix): 'correct': self.correct_json(stringify=False), 'incorrect': self.incorrect_json(False), 'partial': self.partial_json(False), + 'assessment_message': self.assessment_message, + 'assessment_review_tips': self.review_tips, } @XBlock.json_handler diff --git a/problem_builder/public/js/mentoring_with_steps.js b/problem_builder/public/js/mentoring_with_steps.js index 489b4684..c4c6e636 100644 --- a/problem_builder/public/js/mentoring_with_steps.js +++ b/problem_builder/public/js/mentoring_with_steps.js @@ -74,7 +74,7 @@ function MentoringWithStepsBlock(runtime, element) { } function updateGrade() { - var handlerUrl = runtime.handlerUrl(element, 'get_score'); + var handlerUrl = runtime.handlerUrl(element, 'get_grade'); $.post(handlerUrl, JSON.stringify({})) .success(function(response) { gradeDOM.data('score', response.score); @@ -83,16 +83,9 @@ function MentoringWithStepsBlock(runtime, element) { gradeDOM.data('partially_correct_answer', response.partially_correct_answers); gradeDOM.data('correct', response.correct); gradeDOM.data('incorrect', response.incorrect); - gradeDOM.data('partially', response.partial); - updateReviewTips(); - }); - } - - function updateReviewTips() { - var handlerUrl = runtime.handlerUrl(element, 'get_review_tips'); - $.post(handlerUrl, JSON.stringify({})) - .success(function(response) { - gradeDOM.data('assessment_review_tips', response.review_tips); + gradeDOM.data('partial', response.partial); + gradeDOM.data('assessment_message', response.assessment_message); + gradeDOM.data('assessment_review_tips', response.assessment_review_tips); updateControls(); }); } From bef01e811d983e629e05f97a111e79abd4723647 Mon Sep 17 00:00:00 2001 From: Tim Krones Date: Sat, 26 Sep 2015 11:34:49 +0200 Subject: [PATCH 37/43] DRY out code that computes current status (correct/partial/incorrect) of step. --- .../public/js/mentoring_with_steps.js | 4 +-- problem_builder/step.py | 31 +++++++------------ 2 files changed, 14 insertions(+), 21 deletions(-) diff --git a/problem_builder/public/js/mentoring_with_steps.js b/problem_builder/public/js/mentoring_with_steps.js index c4c6e636..a967acfd 100644 --- a/problem_builder/public/js/mentoring_with_steps.js +++ b/problem_builder/public/js/mentoring_with_steps.js @@ -36,9 +36,9 @@ function MentoringWithStepsBlock(runtime, element) { } function showFeedback(response) { - if (response.completed === 'correct') { + if (response.step_status === 'correct') { checkmark.addClass('checkmark-correct icon-ok fa-check'); - } else if (response.completed === 'partial') { + } else if (response.step_status === 'partial') { checkmark.addClass('checkmark-partially-correct icon-ok fa-check'); } else { checkmark.addClass('checkmark-incorrect icon-exclamation fa-exclamation'); diff --git a/problem_builder/step.py b/problem_builder/step.py index af24ad5b..bfef0ff5 100644 --- a/problem_builder/step.py +++ b/problem_builder/step.py @@ -142,19 +142,10 @@ def submit(self, submissions, suffix=''): for result in submit_results: self.student_results.append(result) - # Compute "answer status" for this step - if all(result[1]['status'] == 'correct' for result in submit_results): - completed = Correctness.CORRECT - elif all(result[1]['status'] == 'incorrect' for result in submit_results): - completed = Correctness.INCORRECT - else: - completed = Correctness.PARTIAL - return { 'message': 'Success!', - 'completed': completed, + 'step_status': self.answer_status, 'results': submit_results, - 'attempt_complete': self.is_last_step } @XBlock.json_handler @@ -166,24 +157,26 @@ def get_results(self, queries, suffix=''): result = question.get_results(previous_results) results[question.name] = result - # Compute "answer status" for this step - if all(result[1]['status'] == 'correct' for result in self.student_results): - completed = Correctness.CORRECT - elif all(result[1]['status'] == 'incorrect' for result in self.student_results): - completed = Correctness.INCORRECT - else: - completed = Correctness.PARTIAL - # Add 'message' to results? Looks like it's not used on the client ... return { 'results': results, - 'completed': completed, + 'step_status': self.answer_status, } def reset(self): while self.student_results: self.student_results.pop() + @property + def answer_status(self): + if all(result[1]['status'] == 'correct' for result in self.student_results): + answer_status = Correctness.CORRECT + elif all(result[1]['status'] == 'incorrect' for result in self.student_results): + answer_status = Correctness.INCORRECT + else: + answer_status = Correctness.PARTIAL + return answer_status + def author_edit_view(self, context): """ Add some HTML to the author view that allows authors to add child blocks. From 9e94e81ff144a907c5e5f037f024bed12f665772 Mon Sep 17 00:00:00 2001 From: Tim Krones Date: Sun, 27 Sep 2015 15:42:58 +0200 Subject: [PATCH 38/43] Implement integration tests. --- problem_builder/mentoring.py | 3 +- .../public/js/mentoring_with_steps.js | 57 ++- .../tests/integration/base_test.py | 92 +++++ .../tests/integration/test_assessment.py | 99 +---- .../tests/integration/test_step_builder.py | 346 ++++++++++++++++++ .../xml_templates/step_builder.xml | 63 ++++ 6 files changed, 554 insertions(+), 106 deletions(-) create mode 100644 problem_builder/tests/integration/test_step_builder.py create mode 100644 problem_builder/tests/integration/xml_templates/step_builder.xml diff --git a/problem_builder/mentoring.py b/problem_builder/mentoring.py index e9ac2ad0..168022e7 100644 --- a/problem_builder/mentoring.py +++ b/problem_builder/mentoring.py @@ -918,7 +918,7 @@ def assessment_message(self): if not self.max_attempts_reached: return self.get_message_content('on-assessment-review', or_default=True) else: - assessment_message = _("Note: you have used all attempts. Continue to the next unit") + assessment_message = _("Note: you have used all attempts. Continue to the next unit.") return '

{}

'.format(assessment_message) @property @@ -985,6 +985,7 @@ def student_view(self, context): 'children_contents': children_contents, })) fragment.add_css_url(self.runtime.local_resource_url(self, 'public/css/problem-builder.css')) + fragment.add_javascript_url(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_with_steps.js')) fragment.add_resource(loader.load_unicode('templates/html/mentoring_attempts.html'), "text/html") diff --git a/problem_builder/public/js/mentoring_with_steps.js b/problem_builder/public/js/mentoring_with_steps.js index a967acfd..3235b960 100644 --- a/problem_builder/public/js/mentoring_with_steps.js +++ b/problem_builder/public/js/mentoring_with_steps.js @@ -1,15 +1,21 @@ function MentoringWithStepsBlock(runtime, element) { + // Set up gettext in case it isn't available in the client runtime: + if (typeof gettext == "undefined") { + window.gettext = function gettext_stub(string) { return string; }; + window.ngettext = function ngettext_stub(strA, strB, n) { return n == 1 ? strA : strB; }; + } + var children = runtime.children(element); - var steps = children.filter( - function(c) { return c.element.className.indexOf('sb-step') > -1; } - ); + var steps = []; var reviewStep; for (var i = 0; i < children.length; i++) { var child = children[i]; - if (child.type === 'sb-review-step') { + var blockType = $(child.element).data('block-type'); + if (blockType === 'sb-step') { + steps.push(child); + } else if (blockType === 'sb-review-step') { reviewStep = child; - break; } } @@ -35,6 +41,11 @@ function MentoringWithStepsBlock(runtime, element) { return (data.num_attempts < data.max_attempts); } + function extendedFeedbackEnabled() { + var data = gradeDOM.data(); + return data.extended_feedback === "True"; + } + function showFeedback(response) { if (response.step_status === 'correct') { checkmark.addClass('checkmark-correct icon-ok fa-check'); @@ -139,6 +150,10 @@ function MentoringWithStepsBlock(runtime, element) { } } + function clearSelections() { + $('input[type=radio], input[type=checkbox]', element).prop('checked', false); + } + function cleanAll() { checkmark.removeClass('checkmark-correct icon-ok fa-check'); checkmark.removeClass('checkmark-partially-correct icon-ok fa-check'); @@ -176,7 +191,7 @@ function MentoringWithStepsBlock(runtime, element) { var data = gradeDOM.data(); // Forward to review step to render grade data - var showExtendedFeedback = (!someAttemptsLeft() && data.extended_feedback); + var showExtendedFeedback = (!someAttemptsLeft() && extendedFeedbackEnabled()); reviewStep.renderGrade(gradeDOM, showExtendedFeedback); // Add click handler that takes care of showing associated step to step links @@ -272,7 +287,8 @@ function MentoringWithStepsBlock(runtime, element) { for (var i=0; i < steps.length; i++) { var step = steps[i]; var mentoring = { - setContent: setContent + setContent: setContent, + publish_event: publishEvent }; options.mentoring = mentoring; step.initChildren(options); @@ -288,6 +304,14 @@ function MentoringWithStepsBlock(runtime, element) { } } + function publishEvent(data) { + $.ajax({ + type: "POST", + url: runtime.handlerUrl(element, 'publish_event'), + data: JSON.stringify(data) + }); + } + function showGrade() { cleanAll(); showAssessmentMessage(); @@ -310,6 +334,7 @@ function MentoringWithStepsBlock(runtime, element) { function handleTryAgain(result) { activeStep = result.active_step; + clearSelections(); updateDisplay(); tryAgainDOM.hide(); submitDOM.show(); @@ -329,6 +354,23 @@ function MentoringWithStepsBlock(runtime, element) { submitXHR = $.post(handlerUrl, JSON.stringify({})).success(handleTryAgain); } + function initClickHandlers() { + $(document).on("click", function(event, ui) { + var target = $(event.target); + var itemFeedbackParentSelector = '.choice'; + var itemFeedbackSelector = ".choice .choice-tips"; + + function clickedInside(selector, parent_selector){ + return target.is(selector) || target.parents(parent_selector).length>0; + } + + if (!clickedInside(itemFeedbackSelector, itemFeedbackParentSelector)) { + $(itemFeedbackSelector).not(':hidden').hide(); + $('.choice-tips-container').removeClass('with-tips'); + } + }); + } + function initXBlockView() { checkmark = $('.assessment-checkmark', element); @@ -366,6 +408,7 @@ function MentoringWithStepsBlock(runtime, element) { updateDisplay(); } + initClickHandlers(); initXBlockView(); } diff --git a/problem_builder/tests/integration/base_test.py b/problem_builder/tests/integration/base_test.py index d7441f07..3113644f 100644 --- a/problem_builder/tests/integration/base_test.py +++ b/problem_builder/tests/integration/base_test.py @@ -30,6 +30,8 @@ loader = ResourceLoader(__name__) +CORRECT, INCORRECT, PARTIAL = "correct", "incorrect", "partially-correct" + class PopupCheckMixin(object): """ @@ -133,6 +135,88 @@ class Namespace(object): return mentoring, controls + def assert_hidden(self, elem): + self.assertFalse(elem.is_displayed()) + + def assert_disabled(self, elem): + self.assertTrue(elem.is_displayed()) + self.assertFalse(elem.is_enabled()) + + def assert_clickable(self, elem): + self.assertTrue(elem.is_displayed()) + self.assertTrue(elem.is_enabled()) + + def ending_controls(self, controls, last): + if last: + self.assert_hidden(controls.next_question) + self.assert_disabled(controls.review) + else: + self.assert_disabled(controls.next_question) + self.assert_hidden(controls.review) + + def selected_controls(self, controls, last): + self.assert_clickable(controls.submit) + self.ending_controls(controls, last) + + def assert_message_text(self, mentoring, text): + message_wrapper = mentoring.find_element_by_css_selector('.assessment-message') + self.assertEqual(message_wrapper.text, text) + self.assertTrue(message_wrapper.is_displayed()) + + def assert_no_message_text(self, mentoring): + message_wrapper = mentoring.find_element_by_css_selector('.assessment-message') + self.assertEqual(message_wrapper.text, '') + + def check_question_feedback(self, step_builder, question): + question_checkmark = step_builder.find_element_by_css_selector('.assessment-checkmark') + question_feedback = question.find_element_by_css_selector(".feedback") + self.assertTrue(question_feedback.is_displayed()) + self.assertEqual(question_feedback.text, "Question Feedback Message") + + question.click() + self.assertFalse(question_feedback.is_displayed()) + + question_checkmark.click() + self.assertTrue(question_feedback.is_displayed()) + + def do_submit_wait(self, controls, last): + if last: + self.wait_until_clickable(controls.review) + else: + self.wait_until_clickable(controls.next_question) + + def do_post(self, controls, last): + if last: + controls.review.click() + else: + controls.next_question.click() + + def multiple_response_question(self, number, mentoring, controls, choice_names, result, last=False): + question = self.peek_at_multiple_response_question(number, mentoring, controls, last=last) + + choices = GetChoices(question) + expected_choices = { + "Its elegance": False, + "Its beauty": False, + "Its gracefulness": False, + "Its bugs": False, + } + self.assertEquals(choices.state, expected_choices) + + for name in choice_names: + choices.select(name) + expected_choices[name] = True + + self.assertEquals(choices.state, expected_choices) + + self.selected_controls(controls, last) + + controls.submit.click() + + self.do_submit_wait(controls, last) + self._assert_checkmark(mentoring, result) + controls.review.click() + def expect_question_visible(self, number, mentoring, question_text=None): if not question_text: question_text = self.question_text(number) @@ -163,6 +247,14 @@ def answer_mcq(self, number, name, value, mentoring, controls, is_last=False): self.wait_until_clickable(controls.next_question) controls.next_question.click() + def _assert_checkmark(self, mentoring, result): + """Assert that only the desired checkmark is present.""" + states = {CORRECT: 0, INCORRECT: 0, PARTIAL: 0} + states[result] += 1 + + for name, count in states.items(): + self.assertEqual(len(mentoring.find_elements_by_css_selector(".checkmark-{}".format(name))), count) + class GetChoices(object): """ Helper class for interacting with MCQ options """ diff --git a/problem_builder/tests/integration/test_assessment.py b/problem_builder/tests/integration/test_assessment.py index 8161d885..cde4af0f 100644 --- a/problem_builder/tests/integration/test_assessment.py +++ b/problem_builder/tests/integration/test_assessment.py @@ -18,9 +18,7 @@ # "AGPLv3". If not, see . # from ddt import ddt, unpack, data -from .base_test import MentoringAssessmentBaseTest, GetChoices - -CORRECT, INCORRECT, PARTIAL = "correct", "incorrect", "partially-correct" +from .base_test import CORRECT, INCORRECT, PARTIAL, MentoringAssessmentBaseTest, GetChoices @ddt @@ -47,29 +45,10 @@ def _selenium_bug_workaround_scroll_to(self, mentoring, question): controls.click() title.click() - def assert_hidden(self, elem): - self.assertFalse(elem.is_displayed()) - - def assert_disabled(self, elem): - self.assertTrue(elem.is_displayed()) - self.assertFalse(elem.is_enabled()) - - def assert_clickable(self, elem): - self.assertTrue(elem.is_displayed()) - self.assertTrue(elem.is_enabled()) - def assert_persistent_elements_present(self, mentoring): self.assertIn("A Simple Assessment", mentoring.text) self.assertIn("This paragraph is shared between all questions.", mentoring.text) - def _assert_checkmark(self, mentoring, result): - """Assert that only the desired checkmark is present.""" - states = {CORRECT: 0, INCORRECT: 0, PARTIAL: 0} - states[result] += 1 - - for name, count in states.items(): - self.assertEqual(len(mentoring.find_elements_by_css_selector(".checkmark-{}".format(name))), count) - def go_to_workbench_main_page(self): self.browser.get(self.live_server_url) @@ -104,35 +83,6 @@ def freeform_answer(self, number, mentoring, controls, text_input, result, saved self._assert_checkmark(mentoring, result) self.do_post(controls, last) - def ending_controls(self, controls, last): - if last: - self.assert_hidden(controls.next_question) - self.assert_disabled(controls.review) - else: - self.assert_disabled(controls.next_question) - self.assert_hidden(controls.review) - - def selected_controls(self, controls, last): - self.assert_clickable(controls.submit) - if last: - self.assert_hidden(controls.next_question) - self.assert_disabled(controls.review) - else: - self.assert_disabled(controls.next_question) - self.assert_hidden(controls.review) - - def do_submit_wait(self, controls, last): - if last: - self.wait_until_clickable(controls.review) - else: - self.wait_until_clickable(controls.next_question) - - def do_post(self, controls, last): - if last: - controls.review.click() - else: - controls.next_question.click() - def single_choice_question(self, number, mentoring, controls, choice_name, result, last=False): question = self.expect_question_visible(number, mentoring) @@ -213,44 +163,6 @@ def peek_at_multiple_response_question( return question - def check_question_feedback(self, mentoring, question): - question_checkmark = mentoring.find_element_by_css_selector('.assessment-checkmark') - question_feedback = question.find_element_by_css_selector(".feedback") - self.assertTrue(question_feedback.is_displayed()) - self.assertEqual(question_feedback.text, "Question Feedback Message") - - question.click() - self.assertFalse(question_feedback.is_displayed()) - - question_checkmark.click() - self.assertTrue(question_feedback.is_displayed()) - - def multiple_response_question(self, number, mentoring, controls, choice_names, result, last=False): - question = self.peek_at_multiple_response_question(number, mentoring, controls, last=last) - - choices = GetChoices(question) - expected_choices = { - "Its elegance": False, - "Its beauty": False, - "Its gracefulness": False, - "Its bugs": False, - } - self.assertEquals(choices.state, expected_choices) - - for name in choice_names: - choices.select(name) - expected_choices[name] = True - - self.assertEquals(choices.state, expected_choices) - - self.selected_controls(controls, last) - - controls.submit.click() - - self.do_submit_wait(controls, last) - self._assert_checkmark(mentoring, result) - controls.review.click() - def peek_at_review(self, mentoring, controls, expected, extended_feedback=False): self.wait_until_text_in("You scored {percentage}% on this assessment.".format(**expected), mentoring) self.assert_persistent_elements_present(mentoring) @@ -288,15 +200,6 @@ def peek_at_review(self, mentoring, controls, expected, extended_feedback=False) self.assert_hidden(controls.review) self.assert_hidden(controls.review_link) - def assert_message_text(self, mentoring, text): - message_wrapper = mentoring.find_element_by_css_selector('.assessment-message') - self.assertEqual(message_wrapper.text, text) - self.assertTrue(message_wrapper.is_displayed()) - - def assert_no_message_text(self, mentoring): - message_wrapper = mentoring.find_element_by_css_selector('.assessment-message') - self.assertEqual(message_wrapper.text, '') - def extended_feedback_checks(self, mentoring, controls, expected_results): # Multiple choice is third correctly answered question self.assert_hidden(controls.review_link) diff --git a/problem_builder/tests/integration/test_step_builder.py b/problem_builder/tests/integration/test_step_builder.py new file mode 100644 index 00000000..ffa06835 --- /dev/null +++ b/problem_builder/tests/integration/test_step_builder.py @@ -0,0 +1,346 @@ +from .base_test import CORRECT, INCORRECT, PARTIAL, MentoringAssessmentBaseTest, GetChoices + +from ddt import ddt, data + + +@ddt +class StepBuilderTest(MentoringAssessmentBaseTest): + + def freeform_answer(self, number, step_builder, controls, text_input, result, saved_value="", last=False): + self.expect_question_visible(number, step_builder) + + answer = step_builder.find_element_by_css_selector("textarea.answer.editable") + + self.assertIn(self.question_text(number), step_builder.text) + self.assertIn("What is your goal?", step_builder.text) + + self.assertEquals(saved_value, answer.get_attribute("value")) + if not saved_value: + self.assert_disabled(controls.submit) + self.assert_disabled(controls.next_question) + + answer.clear() + answer.send_keys(text_input) + self.assertEquals(text_input, answer.get_attribute("value")) + + self.assert_clickable(controls.submit) + self.ending_controls(controls, last) + self.assert_hidden(controls.review) + self.assert_hidden(controls.try_again) + + controls.submit.click() + + self.do_submit_wait(controls, last) + self._assert_checkmark(step_builder, result) + self.do_post(controls, last) + + def single_choice_question(self, number, step_builder, controls, choice_name, result, last=False): + question = self.expect_question_visible(number, step_builder) + + self.assertIn("Do you like this MCQ?", question.text) + + self.assert_disabled(controls.submit) + self.ending_controls(controls, last) + self.assert_hidden(controls.try_again) + + choices = GetChoices(question) + expected_state = {"Yes": False, "Maybe not": False, "I don't understand": False} + self.assertEquals(choices.state, expected_state) + + choices.select(choice_name) + expected_state[choice_name] = True + self.assertEquals(choices.state, expected_state) + + self.selected_controls(controls, last) + + controls.submit.click() + + self.do_submit_wait(controls, last) + self._assert_checkmark(step_builder, result) + + self.do_post(controls, last) + + def rating_question(self, number, step_builder, controls, choice_name, result, last=False): + self.expect_question_visible(number, step_builder) + + self.assertIn("How much do you rate this MCQ?", step_builder.text) + + self.assert_disabled(controls.submit) + self.ending_controls(controls, last) + self.assert_hidden(controls.try_again) + + choices = GetChoices(step_builder, ".rating") + expected_choices = { + "1 - Not good at all": False, + "2": False, "3": False, "4": False, + "5 - Extremely good": False, + "I don't want to rate it": False, + } + self.assertEquals(choices.state, expected_choices) + choices.select(choice_name) + expected_choices[choice_name] = True + self.assertEquals(choices.state, expected_choices) + + self.ending_controls(controls, last) + + controls.submit.click() + + self.do_submit_wait(controls, last) + self._assert_checkmark(step_builder, result) + self.do_post(controls, last) + + def peek_at_multiple_response_question( + self, number, step_builder, controls, last=False, extended_feedback=False, alternative_review=False + ): + question = self.expect_question_visible(number, step_builder) + self.assertIn("What do you like in this MRQ?", step_builder.text) + return question + + if extended_feedback: + self.assert_disabled(controls.submit) + self.check_question_feedback(step_builder, question) + if alternative_review: + self.assert_clickable(controls.review_link) + self.assert_hidden(controls.try_again) + + def peek_at_review(self, step_builder, controls, expected, extended_feedback=False): + self.wait_until_text_in("You scored {percentage}% on this assessment.".format(**expected), step_builder) + + # Check grade breakdown + if expected["correct"] == 1: + self.assertIn("You answered 1 questions correctly.".format(**expected), step_builder.text) + else: + self.assertIn("You answered {correct} questions correctly.".format(**expected), step_builder.text) + + if expected["partial"] == 1: + self.assertIn("You answered 1 question partially correctly.", step_builder.text) + else: + self.assertIn("You answered {partial} questions partially correctly.".format(**expected), step_builder.text) + + if expected["incorrect"] == 1: + self.assertIn("You answered 1 question incorrectly.", step_builder.text) + else: + self.assertIn("You answered {incorrect} questions incorrectly.".format(**expected), step_builder.text) + + # Check presence of review links + # - If unlimited attempts: no review links + # - If limited attempts: + # - If not max attempts reached: no review links + # - If max attempts reached: + # - If extended feedback: review links available + # - If not extended feedback: review links + + review_list = step_builder.find_elements_by_css_selector('.review-list') + + if expected["max_attempts"] == 0: + self.assertFalse(review_list) + else: + if expected["num_attempts"] < expected["max_attempts"]: + self.assertFalse(review_list) + elif expected["num_attempts"] == expected["max_attempts"]: + if extended_feedback: + for correctness in ['correct', 'incorrect', 'partial']: + review_items = step_builder.find_elements_by_css_selector('.%s-list li' % correctness) + self.assertEqual(len(review_items), expected[correctness]) + else: + self.assertFalse(review_list) + + # Check if info about number of attempts used is correct + if expected["max_attempts"] == 1: + self.assertIn("You have used {num_attempts} of 1 submission.".format(**expected), step_builder.text) + elif expected["max_attempts"] == 0: + self.assertNotIn("You have used", step_builder.text) + else: + self.assertIn( + "You have used {num_attempts} of {max_attempts} submissions.".format(**expected), + step_builder.text + ) + + # Check controls + self.assert_hidden(controls.submit) + self.assert_hidden(controls.next_question) + self.assert_hidden(controls.review) + self.assert_hidden(controls.review_link) + + def popup_check(self, step_builder, item_feedbacks, prefix='', do_submit=True): + for index, expected_feedback in enumerate(item_feedbacks): + choice_wrapper = step_builder.find_elements_by_css_selector(prefix + " .choice")[index] + choice_wrapper.click() + + item_feedback_icon = choice_wrapper.find_element_by_css_selector(".choice-result") + item_feedback_icon.click() + + item_feedback_popup = choice_wrapper.find_element_by_css_selector(".choice-tips") + self.assertTrue(item_feedback_popup.is_displayed()) + self.assertEqual(item_feedback_popup.text, expected_feedback) + + item_feedback_popup.click() + self.assertTrue(item_feedback_popup.is_displayed()) + + step_builder.click() + self.assertFalse(item_feedback_popup.is_displayed()) + + def extended_feedback_checks(self, step_builder, controls, expected_results): + # MRQ is third correctly answered question + self.assert_hidden(controls.review_link) + step_builder.find_elements_by_css_selector('.correct-list li a')[2].click() + self.peek_at_multiple_response_question( + None, step_builder, controls, extended_feedback=True, alternative_review=True + ) + + # Step should display 5 checkmarks (4 correct items for MRQ, plus step-level feedback about correctness) + correct_marks = step_builder.find_elements_by_css_selector('.checkmark-correct') + incorrect_marks = step_builder.find_elements_by_css_selector('.checkmark-incorrect') + self.assertEqual(len(correct_marks), 5) + self.assertEqual(len(incorrect_marks), 0) + + item_feedbacks = [ + "This is something everyone has to like about this MRQ", + "This is something everyone has to like about this MRQ", + "This MRQ is indeed very graceful", + "Nah, there aren't any!" + ] + self.popup_check(step_builder, item_feedbacks, prefix='div[data-name="mrq_1_1"]', do_submit=False) + controls.review_link.click() + self.peek_at_review(step_builder, controls, expected_results, extended_feedback=True) + + # Review rating question (directly precedes MRQ) + step_builder.find_elements_by_css_selector('.incorrect-list li a')[0].click() + # It should be possible to visit the MRQ from here + self.wait_until_clickable(controls.next_question) + controls.next_question.click() + self.peek_at_multiple_response_question( + None, step_builder, controls, extended_feedback=True, alternative_review=True + ) + + @data( + {"max_attempts": 0, "extended_feedback": False}, # Unlimited attempts, no extended feedback + {"max_attempts": 1, "extended_feedback": True}, # Limited attempts, extended feedback + {"max_attempts": 1, "extended_feedback": False}, # Limited attempts, no extended feedback + {"max_attempts": 2, "extended_feedback": True}, # Limited attempts, extended feedback + ) + def test_step_builder(self, params): + max_attempts = params['max_attempts'] + extended_feedback = params['extended_feedback'] + step_builder, controls = self.load_assessment_scenario("step_builder.xml", params) + + # Step 1 + # Submit free-form answer, go to next step + self.freeform_answer(None, step_builder, controls, 'This is the answer', CORRECT) + + # Step 2 + # Submit MCQ, go to next step + self.single_choice_question(None, step_builder, controls, 'Maybe not', INCORRECT) + + # Step 3 + # Submit rating, go to next step + self.rating_question(None, step_builder, controls, "5 - Extremely good", CORRECT) + + # Last step + # Submit MRQ, go to review + self.multiple_response_question(None, step_builder, controls, ("Its beauty",), PARTIAL, last=True) + + # Review step + expected_results = { + "correct": 2, "partial": 1, "incorrect": 1, "percentage": 63, + "num_attempts": 1, "max_attempts": max_attempts + } + self.peek_at_review(step_builder, controls, expected_results, extended_feedback=extended_feedback) + + if max_attempts == 1: + self.assert_message_text(step_builder, "Note: you have used all attempts. Continue to the next unit.") + self.assert_disabled(controls.try_again) + return + + self.assert_message_text(step_builder, "Assessment additional feedback message text") + self.assert_clickable(controls.try_again) + + # Try again + controls.try_again.click() + + self.wait_until_hidden(controls.try_again) + self.assert_no_message_text(step_builder) + + self.freeform_answer( + None, step_builder, controls, 'This is a different answer', CORRECT, saved_value='This is the answer' + ) + self.single_choice_question(None, step_builder, controls, 'Yes', CORRECT) + self.rating_question(None, step_builder, controls, "1 - Not good at all", INCORRECT) + + user_selection = ("Its elegance", "Its beauty", "Its gracefulness") + self.multiple_response_question(None, step_builder, controls, user_selection, CORRECT, last=True) + + expected_results = { + "correct": 3, "partial": 0, "incorrect": 1, "percentage": 75, + "num_attempts": 2, "max_attempts": max_attempts + } + self.peek_at_review(step_builder, controls, expected_results, extended_feedback=extended_feedback) + + if max_attempts == 2: + self.assert_disabled(controls.try_again) + else: + self.assert_clickable(controls.try_again) + + if 1 <= max_attempts <= 2: + self.assert_message_text(step_builder, "Note: you have used all attempts. Continue to the next unit.") + else: + self.assert_message_text(step_builder, "Assessment additional feedback message text") + + if extended_feedback: + self.extended_feedback_checks(step_builder, controls, expected_results) + + def test_review_tips(self): + params = { + "max_attempts": 3, + "extended_feedback": False, + "include_review_tips": True + } + step_builder, controls = self.load_assessment_scenario("step_builder.xml", params) + + # Get one question wrong and one partially wrong on attempt 1 of 3: #################### + self.freeform_answer(None, step_builder, controls, 'This is the answer', CORRECT) + self.single_choice_question(None, step_builder, controls, 'Maybe not', INCORRECT) + self.rating_question(None, step_builder, controls, "5 - Extremely good", CORRECT) + self.multiple_response_question(None, step_builder, controls, ("Its beauty",), PARTIAL, last=True) + + # The review tips for MCQ 2 and the MRQ should be shown: + review_tips = step_builder.find_element_by_css_selector('.assessment-review-tips') + self.assertTrue(review_tips.is_displayed()) + self.assertIn('You might consider reviewing the following items', review_tips.text) + self.assertIn('Take another look at', review_tips.text) + self.assertIn('Lesson 1', review_tips.text) + self.assertNotIn('Lesson 2', review_tips.text) # This MCQ was correct + self.assertIn('Lesson 3', review_tips.text) + # The on-assessment-review message is also shown if attempts remain: + self.assert_message_text(step_builder, "Assessment additional feedback message text") + + # Try again + self.assert_clickable(controls.try_again) + controls.try_again.click() + + # Get no questions wrong on attempt 2 of 3: ############################################ + self.freeform_answer( + None, step_builder, controls, 'This is the answer', CORRECT, saved_value='This is the answer' + ) + self.single_choice_question(None, step_builder, controls, 'Yes', CORRECT) + self.rating_question(None, step_builder, controls, "5 - Extremely good", CORRECT) + user_selection = ("Its elegance", "Its beauty", "Its gracefulness") + self.multiple_response_question(None, step_builder, controls, user_selection, CORRECT, last=True) + + self.assert_message_text(step_builder, "Assessment additional feedback message text") + self.assertFalse(review_tips.is_displayed()) + + # Try again + self.assert_clickable(controls.try_again) + controls.try_again.click() + + # Get some questions wrong again on attempt 3 of 3: + self.freeform_answer( + None, step_builder, controls, 'This is the answer', CORRECT, saved_value='This is the answer' + ) + self.single_choice_question(None, step_builder, controls, 'Maybe not', INCORRECT) + self.rating_question(None, step_builder, controls, "1 - Not good at all", INCORRECT) + self.multiple_response_question(None, step_builder, controls, ("Its beauty",), PARTIAL, last=True) + + # The review tips will not be shown because no attempts remain: + self.assertFalse(review_tips.is_displayed()) diff --git a/problem_builder/tests/integration/xml_templates/step_builder.xml b/problem_builder/tests/integration/xml_templates/step_builder.xml new file mode 100644 index 00000000..59299b7e --- /dev/null +++ b/problem_builder/tests/integration/xml_templates/step_builder.xml @@ -0,0 +1,63 @@ + + + + + + + + + Yes + Maybe not + I don't understand + + Great! + Ah, damn. +
Really?
+ {% if include_review_tips %} + + Take another look at Lesson 1 + + {% endif %} +
+
+ + + + I don't want to rate it + I love good grades. + Will do better next time... + Your loss! + {% if include_review_tips %} + + Take another look at Lesson 2 + + {% endif %} + + + + + + Its elegance + Its beauty + Its gracefulness + Its bugs + + This MRQ is indeed very graceful + This is something everyone has to like about this MRQ + Nah, there aren't any! + {% if include_review_tips %} + + Take another look at Lesson 3 + + {% endif %} + + + + + + + Assessment additional feedback message text + + +
From c92b9fecacf139d48324be2da06bac3a669c06a3 Mon Sep 17 00:00:00 2001 From: Tim Krones Date: Mon, 28 Sep 2015 11:58:29 +0200 Subject: [PATCH 39/43] Update documentation. --- README.md | 47 +++-- doc/Usage.md | 181 +++++++++++++----- ...tempts-remaining-extended-feedback-off.png | Bin 0 -> 30642 bytes ...ttempts-remaining-extended-feedback-on.png | Bin 0 -> 35444 bytes .../review-step-some-attempts-remaining.png | Bin 0 -> 44214 bytes ...view-step-unlimited-attempts-available.png | Bin 0 -> 41371 bytes .../reviewing-performance-for-single-step.png | Bin 0 -> 30398 bytes ...p-with-multiple-questions-after-submit.png | Bin 0 -> 24416 bytes ...-with-multiple-questions-before-submit.png | Bin 0 -> 24001 bytes 9 files changed, 157 insertions(+), 71 deletions(-) create mode 100644 doc/img/review-step-no-attempts-remaining-extended-feedback-off.png create mode 100644 doc/img/review-step-no-attempts-remaining-extended-feedback-on.png create mode 100644 doc/img/review-step-some-attempts-remaining.png create mode 100644 doc/img/review-step-unlimited-attempts-available.png create mode 100644 doc/img/reviewing-performance-for-single-step.png create mode 100644 doc/img/step-with-multiple-questions-after-submit.png create mode 100644 doc/img/step-with-multiple-questions-before-submit.png diff --git a/README.md b/README.md index ec26fc3c..7dd87268 100644 --- a/README.md +++ b/README.md @@ -1,21 +1,24 @@ -Problem Builder XBlock ----------------------- +Problem Builder and Step Builder +-------------------------------- [![Build Status](https://travis-ci.org/open-craft/problem-builder.svg?branch=master)](https://travis-ci.org/open-craft/problem-builder) -This XBlock allows creation of questions of various types and simulating the -workflow of real-life mentoring, within an edX course. +This repository provides two XBlocks: Problem Builder and Step Builder. -It supports: +Both blocks allow to create questions of various types. They can be +used to simulate the workflow of real-life mentoring, within an edX +course. + +Supported features include: * **Free-form answers** (textarea) which can be shared accross different XBlock instances (for example, to allow a student to - review and edit an answer he gave before). -* **Self-assessment MCQs** (multiple choice), to display predetermined - feedback to a student based on his choices in the + review and edit an answer they gave before). +* **Self-assessment MCQs** (multiple choice questions), to display + predetermined feedback to a student based on his choices in the self-assessment. Supports rating scales and arbitrary answers. * **MRQs (Multiple Response Questions)**, a type of multiple choice - question that allows the student to choose more than one choice. + question that allows the student to select more than one choice. * **Answer recaps** that display a read-only summary of a user's answer to a free-form question asked earlier in the course. * **Progression tracking**, to require that the student has @@ -26,15 +29,15 @@ It supports: * **Dashboards**, for displaying a summary of the student's answers to multiple choice questions. [Details](doc/Dashboard.md) -The screenshot shows an example of a problem builder block containing a -free-form question, two MCQs and one MRQ. +The following screenshot shows an example of a Problem Builder block +containing a free-form question, two MCQs and one MRQ: ![Problem Builder Example](doc/img/mentoring-example.png) Installation ------------ -Install the requirements into the python virtual environment of your +Install the requirements into the Python virtual environment of your `edx-platform` installation by running the following command from the root folder: @@ -45,14 +48,20 @@ $ pip install -r requirements.txt Enabling in Studio ------------------ -You can enable the Problem Builder XBlock in studio through the advanced -settings. +You can enable the Problem Builder and Step Builder XBlocks in Studio +by modifying the advanced settings for your course: + +1. From the main page of a specific course, navigate to **Settings** -> + **Advanced Settings** from the top menu. +2. Find the **Advanced Module List** setting. +3. To enable Problem Builder for your course, add `"problem-builder"` + to the modules listed there. +4. To enable Step Builder for your course, add `"step-builder"` to the + modules listed there. +5. Click the **Save changes** button. -1. From the main page of a specific course, navigate to `Settings -> - Advanced Settings` from the top menu. -2. Check for the `advanced_modules` policy key, and add `"problem-builder"` - to the policy value list. -3. Click the "Save changes" button. +Note that it is perfectly fine to enable both Problem Builder and Step +Builder for your course -- the blocks do not interfere with each other. Usage ----- diff --git a/doc/Usage.md b/doc/Usage.md index 3bf75868..7f96831b 100644 --- a/doc/Usage.md +++ b/doc/Usage.md @@ -1,23 +1,30 @@ -Mentoring Block Usage +Problem Builder Usage ===================== -When you add the `Problem Builder` component to a course in the studio, the -built-in editing tools guide you through the process of configuring the -block and adding individual questions. +When you add the **Problem Builder** component to a course in the +studio, the built-in editing tools guide you through the process of +configuring the block and adding individual questions. ### Problem Builder modes There are 2 mentoring modes available: -* *standard*: Traditional mentoring. All questions are displayed on the +* **standard**: Traditional mentoring. All questions are displayed on the page and submitted at the same time. The students get some tips and feedback about their answers. This is the default mode. -* *assessment*: Questions are displayed and submitted one by one. The +* **assessment**: Questions are displayed and submitted one by one. The students don't get tips or feedback, but only know if their answer was correct. Assessment mode comes with a default `max_attempts` of `2`. -Below are some LMS screenshots of a problem builder block in assessment mode. +**Note that assessment mode is deprecated**: In the future, Problem +Builder will only provide functionality that is currently part of +standard mode. Assessment mode will remain functional for a while to +ensure backward compatibility with courses that are currently using +it. If you want to use assessment functionality for a new course, +please use the Step Builder XBlock (described below). + +Below are some LMS screenshots of a Problem Builder block in assessment mode. Question before submitting an answer: @@ -35,9 +42,71 @@ Score review and the "Try Again" button: ![Assessment Step 4](img/assessment-4.png) -### Free-form Question -Free-form questions are represented by a "Long Answer" component. +Step Builder Usage +================== + +The Step Builder XBlock replaces assessment mode functionality of the +Problem Builder XBlock, while allowing to group questions into explict +steps: + +Instead of adding questions to Step Builder itself, you'll need to add +one or more **Mentoring Step** blocks to Step Builder. You can then +add one or more questions to each step. This allows you to group +questions into logical units (without being limited to showing only a +single question per step). As students progress through the block, +Step Builder will display one step at a time. All questions belonging +to a step need to be completed before the step can be submitted. + +In addition to regular steps, Step Builder also provides a **Review +Step** block which allows students to review their performance, and to +jump back to individual steps to review their answers (if **Extended +feedback** setting is on and maximum number of attempts has been +reached). Note that only one such block is allowed per instance. + +**Screenshots: Step** + +Step with multiple questions (before submitting it): + +![Step with multiple questions, before submit](img/step-with-multiple-questions-before-submit.png) + +Step with multiple questions (after submitting it): + +![Step with multiple questions, after submit](img/step-with-multiple-questions-after-submit.png) + +As indicated by the orange check mark, this step is *partially* +correct (i.e., some answers are correct and some are incorrect or +partially correct). + +**Screenshots: Review Step** + +Unlimited attempts available: + +![Unlimited attempts available](img/review-step-unlimited-attempts-available.png) +Limited attempts, some attempts remaining: + +![Some attempts remaining](img/review-step-some-attempts-remaining.png) + +Limited attempts, no attempts remaining, extended feedback off: + +![No attempts remaining, extended feedback off](img/review-step-no-attempts-remaining-extended-feedback-off.png) + +Limited attempts, no attempts remaining, extended feedback on: + +![No attempts remaining, extended feedback on](img/review-step-no-attempts-remaining-extended-feedback-on.png) + +**Screenshots: Step-level feedback** + +Reviewing performance for a single step: + +![Reviewing performance for single step](img/reviewing-performance-for-single-step.png) + +Question Types +============== + +### Free-form Questions + +Free-form questions are represented by a **Long Answer** component. Example screenshot before answering the question: @@ -47,39 +116,41 @@ Screenshot after answering the question: ![Answer Complete](img/answer-2.png) -You can add "Long Answer Recap" components to problem builder blocks later on -in the course to provide a read-only view of any answer that the student -entered earlier. +You can add **Long Answer Recap** components to problem builder blocks +later on in the course to provide a read-only view of any answer that +the student entered earlier. The read-only answer is rendered as a quote in the LMS: ![Answer Read-Only](img/answer-3.png) -### Multiple Choice Questions (MCQ) +### Multiple Choice Questions (MCQs) Multiple Choice Questions can be added to a problem builder component and have the following configurable options: -* Question - The question to ask the student -* Message - A feedback message to display to the student after they +* **Question** - The question to ask the student +* **Message** - A feedback message to display to the student after they have made their choice. -* Weight - The weight is used when computing total grade/score of +* **Weight** - The weight is used when computing total grade/score of the problem builder block. The larger the weight, the more influence this question will have on the grade. Value of zero means this question has no influence on the grade (float, defaults to `1`). -* Correct Choice - Specify which choice[s] is considered correct. If +* **Correct Choice[s]** - Specify which choice[s] are considered correct. If a student selects a choice that is not indicated as correct here, the student will get the question wrong. -Using the Studio editor, you can add "Custom Choice" blocks to the MCQ. -Each Custom Choice represents one of the options from which students -will choose their answer. +Using the Studio editor, you can add **Custom Choice** blocks to an +MCQ. Each Custom Choice represents one of the options from which +students will choose their answer. -You can also add "Tip" entries. Each "Tip" must be configured to link -it to one or more of the choices. If the student chooses a choice, the +You can also add **Tip** entries. Each Tip must be configured to link +it to one or more of the choices. If the student selects a choice, the +tip will be displayed. +**Screenshots** -Screenshot: Before attempting to answer the questions: +Before attempting to answer the questions: ![MCQ Initial](img/mcq-1.png) @@ -91,7 +162,7 @@ After successfully completing the questions: ![MCQ Success](img/mcq-3.png) -#### Rating MCQ +#### Rating Questions When constructing questions where the student rates some topic on the scale from `1` to `5` (e.g. a Likert Scale), you can use the Rating @@ -100,11 +171,10 @@ The `Low` and `High` settings specify the text shown next to the lowest and highest valued choice. Rating questions are a specialized type of MCQ, and the same -instructions apply. You can also still add "Custom Choice" components +instructions apply. You can also still add **Custom Choice** components if you want additional choices to be available such as "I don't know". - -### Self-assessment Multiple Response Questions (MRQ) +### Self-assessment Multiple Response Questions (MRQs) Multiple Response Questions are set up similarly to MCQs. The answers are rendered as checkboxes. Unlike MCQs where only a single answer can @@ -113,24 +183,26 @@ time. MRQ questions have these configurable settings: -* Question - The question to ask the student -* Required Choices - For any choices selected here, if the student +* **Question** - The question to ask the student +* **Required Choices** - For any choices selected here, if the student does *not* select that choice, they will lose marks. -* Ignored Choices - For any choices selected here, the student will +* **Ignored Choices** - For any choices selected here, the student will always be considered correct whether they choose this choice or not. * Message - A feedback message to display to the student after they have made their choice. -* Weight - The weight is used when computing total grade/score of +* **Weight** - The weight is used when computing total grade/score of the problem builder block. The larger the weight, the more influence this question will have on the grade. Value of zero means this question has no influence on the grade (float, defaults to `1`). -* Hide Result - If set to True, the feedback icons next to each - choice will not be displayed (This is false by default). +* **Hide Result** - If set to `True`, the feedback icons next to each + choice will not be displayed (This is `False` by default). -The "Custom Choice" and "Tip" components work the same way as they +The **Custom Choice** and **Tip** components work the same way as they do when used with MCQs (see above). -Screenshot - Before attempting to answer the questions: +**Screenshots** + +Before attempting to answer the questions: ![MRQ Initial](img/mrq-1.png) @@ -146,24 +218,33 @@ After successfully completing the questions: ![MRQ Success](img/mrq-4.png) +Other Components +================ + ### Tables -The problem builder table allows you to present answers to multiple -free-form questions in a concise way. Once you create an "Answer -Recap Table" inside a Mentoring component in Studio, you will be -able to add columns to the table. Each column has an optional -"Header" setting that you can use to add a header to that column. -Each column can contain one or more "Answer Recap" element, as -well as HTML components. +Tables allow you to present answers to multiple free-form questions in +a concise way. Once you create an **Answer Recap Table** inside a +Mentoring component in Studio, you will be able to add columns to the +table. Each column has an optional **Header** setting that you can use +to add a header to that column. Each column can contain one or more +**Answer Recap** elements, as well as HTML components. Screenshot: ![Table Screenshot](img/mentoring-table.png) +### "Dashboard" Self-Assessment Summary Block + +[Instructions for using the "Dashboard" Self-Assessment Summary Block](Dashboard.md) + +Configuration Options +==================== + ### Maximum Attempts -You can set the number of maximum attempts for the unit completion by -setting the Max. Attempts option of the Mentoring component. +You can limit the number of times students are allowed to complete a +Mentoring component by setting the **Max. attempts allowed** option. Before submitting an answer for the first time: @@ -173,12 +254,8 @@ After submitting a wrong answer two times: ![Max Attempts Reached](img/max-attempts-reached.png) -### Custom tip popup window size +### Custom Window Size for Tip Popups -You can specify With and Height attributes of any Tip component to -customize the popup window size. The value of those attribute should -be valid CSS (e.g. `50px`). - -### "Dashboard" Self-Assessment Summary Block - -[Instructions for using the "Dashboard" Self-Assessment Summary Block](Dashboard.md) +You can specify **Width** and **Height** attributes of any Tip +component to customize the popup window size. The value of those +attributes should be valid CSS (e.g. `50px`). diff --git a/doc/img/review-step-no-attempts-remaining-extended-feedback-off.png b/doc/img/review-step-no-attempts-remaining-extended-feedback-off.png new file mode 100644 index 0000000000000000000000000000000000000000..e20798fb65cc50e84dc9f8f698d1a5cfdfe19f6d GIT binary patch literal 30642 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 literal 0 HcmV?d00001 diff --git a/doc/img/review-step-no-attempts-remaining-extended-feedback-on.png b/doc/img/review-step-no-attempts-remaining-extended-feedback-on.png new file mode 100644 index 0000000000000000000000000000000000000000..556b330983fd45de37e2d440c4cbc10ae9aad883 GIT binary patch literal 35444 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 Pm&+i-n literal 0 HcmV?d00001 diff --git a/doc/img/review-step-some-attempts-remaining.png b/doc/img/review-step-some-attempts-remaining.png new file mode 100644 index 0000000000000000000000000000000000000000..a70bcd8402522ba7f0a4fd9710d28980128d9069 GIT binary patch literal 44214 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 bG2ht@n6ztK!>;o$0CuP;YeUKvEkgbQ(q*)~ literal 0 HcmV?d00001 diff --git a/doc/img/reviewing-performance-for-single-step.png b/doc/img/reviewing-performance-for-single-step.png new file mode 100644 index 0000000000000000000000000000000000000000..acefb8465b9bffa51917c48f91a826be9283e490 GIT binary patch literal 30398 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 U^giJ=fQUbIGz{-m-?<<2U$l%H`Tzg` literal 0 HcmV?d00001 diff --git a/doc/img/step-with-multiple-questions-after-submit.png b/doc/img/step-with-multiple-questions-after-submit.png new file mode 100644 index 0000000000000000000000000000000000000000..9d0c3dacbe7f7a018919cf8f8385d1f169a4a8e2 GIT binary patch literal 24416 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�VSr~{p}gjJazmWY3?a3m~i}tla?cu zrTdD&$`Yb}-==pYFDFb^i7G0(Rwx*{6h7{*ge@5Gg0SYbo>9ky306wr+-(+4@56Y( zBBQPB15J)r9NO8pYoic~n9Owa!`k(Nl2I;>kkOCiIw-fZ4;Yjo8|@6;kkNcpYWOB3 zQ+3b5mllS&r(JIqm4yj37ad4+G#FW^-YGLq6xkZcX*S7r!1tVd3X}F8hxC<@SaU=9 zc}Nw$;`=OUP^#efvn-NEqSVr7XzsUnW``WYh^NFPGT)BzDb7=~VS#%DhEI=4)b6S;Y_6!Mv$G?mi<*W4nVLl`9)#$;y;cf_2to_fnzmwc z!vl)Mv$YM!`D!1F+$aN2K2#LL+qQg#JLTg$R?1H+S6Y<+2n9sc!dMpt8Wj&8_A2B9 z2%COR-{nD_fr`G7^TRcgZSG+i0>2O36uVb7YHHmkIC{7K7fNfx;3ep%!+HUIxBeDh z+-UgJ?pE(TainWFuoVhheWFq4w{F>?W6?th9e$!bozM1N9{q0pBd9%|!WYf)ZA_^vPxS+pabVLqj$8sZ{U%$6PM!na@sW7&Q_0VvScptYsYw(>DSeU_^afUThkngp?j6e@ zn6vmH|C!7>EU-H^b7OXJ;H@XAn^;~Dit>|~G_t%hoh+1zLtop zXPF&5crZ{qhg7A^n41f1u(7Z>1BY`m8$J;7K7al^n>ZEDc+^Gh94*^<7(#z8*0a zGa}#3c9z>S4`+ngUS;0V)Ah?t!*LoCa#eb&YyLq}|G3WM$B*YlF}LNkwH>syv;rHP z+_zP;dN%0vVcHZHx`K&Q*`BU0_n)-dZ*DcKUP6mFW{;dwV%{ZTolKfkucRY1tuCpp zJ)bI-Ex%!%>rzy|oVG*_TcFTA$)&4k*GqE?cMoq|FjcV)6b1UT8*%0oq5y&ob!dj6 zA3uIP^dZ2NL?$aJD_1{k%i)qGo5cPakV#eBmmszh|Tpt3l4e@ZxT z#w+pe_R%ENc;TBimv4oJ(M1WdyWu9uw>-*~#MG~(^NCb9zYw{^=|7nHnt~bz9jh1! z0ABljHRg^DU-@vEb2Cg1Ff3p+4<9>b`{2QY^-M_&p%O4g>J%0%CnpC4+6W9LEZ&JQ zJ9WL%A!q!QOfB-6#?~+x>0Z(S5way6Ra;iS$v?#@t%5&zQfU1I^KiQRDtfMOD?kcHB)}$n0#8rf6qxuN@55c<Q59>y5;f!j3MtQFlQQfWc62!()9O`y|{0T;TE;GeBk_>OGGZlHNR#E-jvOUrM^*g zX0dsWLw?Wf@J$YTUhE(mcSQktXz20i^l)>zi%o7z#dpJ=;H8g;Zm@Ls5r{2iz5VU0 z?(-)*g}+^O}f(O&M7D=nvIW-Crf4Z6hD9d+YKY5E^qWm;8U*Ly}x`a`6FU` z#q!?0eAxC%DW8}&fEPEDv&~W4tHf#mlgc7?XfI#BEF+n4Gi)2~!~>dRVs3618QIkB zQ9J2}5#@`O0;n}PU0tX3_4SDl0R_PMOm$y}1Ob;iQnzIn-e3=g(Ki0VEaW1Af4cfS_RKQn0*|lDQ+s z&o(+b`m|)jCI8RCzFmC7sk~(GrL1bo_OiAYH7IN`sgEyrllg<*%LGUdtip0?^vd

Gh1 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 literal 0 HcmV?d00001 From a4414a5d0d15ee9805cc6a296a9caf30a9eedc03 Mon Sep 17 00:00:00 2001 From: Tim Krones Date: Mon, 28 Sep 2015 14:40:09 +0200 Subject: [PATCH 40/43] Show friendly message that ReviewStep block is not editable when clicking "Edit" button in Studio. --- problem_builder/step.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/problem_builder/step.py b/problem_builder/step.py index bfef0ff5..2f4ea83f 100644 --- a/problem_builder/step.py +++ b/problem_builder/step.py @@ -248,6 +248,10 @@ def student_view(self, context=None): """ Student View """ return self._render_view(context) + def studio_view(self, context=None): + """ Studio View """ + return Fragment(u'

This is a preconfigured block. It is not editable.

') + def _render_view(self, context): fragment = Fragment() fragment.add_content(loader.render_template('templates/html/review_step.html', { From aa065a122691d10e809865d86cf383c84b725ef7 Mon Sep 17 00:00:00 2001 From: Tim Krones Date: Mon, 28 Sep 2015 14:59:50 +0200 Subject: [PATCH 41/43] Remove option to add "Complete" and "Incomplete" messages to Step Builder. --- problem_builder/mentoring.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/problem_builder/mentoring.py b/problem_builder/mentoring.py index 168022e7..05dbb5aa 100644 --- a/problem_builder/mentoring.py +++ b/problem_builder/mentoring.py @@ -1012,9 +1012,6 @@ def allowed_nested_blocks(self): return [ MentoringStepBlock, ReviewStepBlock, - NestedXBlockSpec(CompletedMentoringMessageShim, boilerplate='completed'), - NestedXBlockSpec(IncompleteMentoringMessageShim, boilerplate='incomplete'), - NestedXBlockSpec(MaxAttemptsReachedMentoringMessageShim, boilerplate='max_attempts_reached'), NestedXBlockSpec(OnAssessmentReviewMentoringMessageShim, boilerplate='on-assessment-review'), ] From 47fe2f6f79ee3888115561ded844155086ed913b Mon Sep 17 00:00:00 2001 From: Tim Krones Date: Mon, 28 Sep 2015 20:13:49 +0200 Subject: [PATCH 42/43] Address review comments. --- problem_builder/mentoring.py | 75 +++++++++++--------- problem_builder/mixins.py | 11 ++- problem_builder/step.py | 8 +-- problem_builder/tests/unit/test_mentoring.py | 5 +- problem_builder/tests/unit/test_step.py | 4 +- 5 files changed, 53 insertions(+), 50 deletions(-) diff --git a/problem_builder/mentoring.py b/problem_builder/mentoring.py index 05dbb5aa..d94b1f54 100644 --- a/problem_builder/mentoring.py +++ b/problem_builder/mentoring.py @@ -359,7 +359,7 @@ def answer_mapper(self, answer_status): @property def score(self): """Compute the student score taking into account the weight of each step.""" - steps = self.get_steps() + steps = self.steps steps_map = {q.name: q for q in steps} total_child_weight = sum(float(step.weight) for step in steps) if total_child_weight == 0: @@ -381,7 +381,7 @@ def student_view(self, context): self.migrate_fields() # Validate self.step: - num_steps = len(self.get_steps()) + num_steps = len(self.steps) if self.step > num_steps: self.step = num_steps @@ -500,11 +500,11 @@ def assessment_message(self): @property def review_tips(self): """ Get review tips, shown for wrong answers in assessment mode. """ - if not self.is_assessment or self.step != len(self.steps): + if not self.is_assessment or self.step != len(self.step_ids): return [] # Review tips are only used in assessment mode, and only on the last step. review_tips = [] status_cache = dict(self.student_results) - for child in self.get_steps(): + for child in self.steps: result = status_cache.get(child.name) if result and result.get('status') != 'correct': # The student got this wrong. Check if there is a review tip to show. @@ -562,7 +562,7 @@ def _get_standard_results(self): show_message = bool(self.student_results) # In standard mode, all children is visible simultaneously, so need collecting responses from all of them - for child in self.get_steps(): + for child in self.steps: child_result = child.get_last_result() results.append([child.name, child_result]) completed = completed and (child_result.get('status', None) == 'correct') @@ -585,7 +585,7 @@ def _get_assessment_results(self, queries): completed = True choices = dict(self.student_results) # Only one child should ever be of concern with this method. - for child in self.get_steps(): + for child in self.steps: 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 @@ -618,7 +618,7 @@ def submit(self, submissions, suffix=''): submit_results = [] previously_completed = self.completed completed = True - for child in self.get_steps(): + for child in self.steps: if child.name and child.name in submissions: submission = submissions[child.name] child_result = child.submit(submission) @@ -673,7 +673,8 @@ def handle_assessment_submit(self, submissions, suffix): current_child = None 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, QuestionMixin)] # Faster than the self.steps property + # The following is faster than the self.step_ids property + steps = [child for child in children if isinstance(child, QuestionMixin)] assessment_message = None review_tips = [] @@ -860,20 +861,23 @@ class MentoringWithExplicitStepsBlock(BaseMentoringBlock, StudioContainerWithNes editable_fields = ('display_name', 'max_attempts', 'extended_feedback') @lazy - def questions(self): - """ Get the usage_ids of all of this XBlock's children that are "Questions" """ - return list(chain.from_iterable(self.runtime.get_block(step_id).steps for step_id in self.steps)) + def question_ids(self): + """ + Get the usage_ids of all of this XBlock's children that are "Questions". + """ + return list(chain.from_iterable(self.runtime.get_block(step_id).step_ids for step_id in self.step_ids)) - def get_questions(self): - """ Get all questions associated with this block, cached if possible. """ - if getattr(self, "_questions_cache", None) is None: - self._questions_cache = [self.runtime.get_block(question_id) for question_id in self.questions] - return self._questions_cache + @lazy + def questions(self): + """ + Get all questions associated with this block. + """ + return [self.runtime.get_block(question_id) for question_id in self.question_ids] - @property - def steps(self): + @lazy + def step_ids(self): """ - Get the usage_ids of all of this XBlock's children that are "Steps" + Get the usage_ids of all of this XBlock's children that are steps. """ from .step import MentoringStepBlock # Import here to avoid circular dependency return [ @@ -881,18 +885,19 @@ def steps(self): child_isinstance(self, child_id, MentoringStepBlock) ] - def get_steps(self): - """ Get the step children of this block, cached if possible. """ - if getattr(self, "_steps_cache", None) is None: - self._steps_cache = [self.runtime.get_block(child_id) for child_id in self.steps] - return self._steps_cache + @lazy + def steps(self): + """ + Get the step children of this block. + """ + return [self.runtime.get_block(step_id) for step_id in self.step_ids] def get_question_number(self, question_name): - question_names = [q.name for q in self.get_questions()] + question_names = [q.name for q in self.questions] return question_names.index(question_name) + 1 def answer_mapper(self, answer_status): - steps = self.get_steps() + steps = self.steps answer_map = [] for step in steps: for answer in step.student_results: @@ -923,11 +928,11 @@ def assessment_message(self): @property def score(self): - questions = self.get_questions() + questions = self.questions total_child_weight = sum(float(question.weight) for question in questions) if total_child_weight == 0: return Score(0, 0, [], [], []) - steps = self.get_steps() + steps = self.steps questions_map = {question.name: question for question in questions} points_earned = 0 for step in steps: @@ -947,10 +952,10 @@ def review_tips(self): """ Get review tips, shown for wrong answers. """ review_tips = [] status_cache = dict() - steps = self.get_steps() + steps = self.steps for step in steps: status_cache.update(dict(step.student_results)) - for question in self.get_questions(): + for question in self.questions: result = status_cache.get(question.name) if result and result.get('status') != 'correct': # The student got this wrong. Check if there is a review tip to show. @@ -1017,9 +1022,9 @@ def allowed_nested_blocks(self): @XBlock.json_handler def update_active_step(self, new_value, suffix=''): - if new_value < len(self.steps): + if new_value < len(self.step_ids): self.active_step = new_value - elif new_value == len(self.steps): + elif new_value == len(self.step_ids): if self.has_review_step: self.active_step = -1 return { @@ -1043,8 +1048,8 @@ def get_grade(self, data, suffix): 'incorrect_answers': len(score.incorrect), 'partially_correct_answers': len(score.partially_correct), 'correct': self.correct_json(stringify=False), - 'incorrect': self.incorrect_json(False), - 'partial': self.partial_json(False), + 'incorrect': self.incorrect_json(stringify=False), + 'partial': self.partial_json(stringify=False), 'assessment_message': self.assessment_message, 'assessment_review_tips': self.review_tips, } @@ -1053,7 +1058,7 @@ def get_grade(self, data, suffix): def try_again(self, data, suffix=''): self.active_step = 0 - step_blocks = [self.runtime.get_block(child_id) for child_id in self.steps] + step_blocks = [self.runtime.get_block(child_id) for child_id in self.step_ids] for step in step_blocks: step.reset() diff --git a/problem_builder/mixins.py b/problem_builder/mixins.py index 392be833..55774ce3 100644 --- a/problem_builder/mixins.py +++ b/problem_builder/mixins.py @@ -79,7 +79,7 @@ class StepParentMixin(object): """ @lazy - def steps(self): + def step_ids(self): """ Get the usage_ids of all of this XBlock's children that are "Steps" """ @@ -87,11 +87,10 @@ def steps(self): _normalize_id(child_id) for child_id in self.children if child_isinstance(self, child_id, QuestionMixin) ] - def get_steps(self): + @lazy + def steps(self): """ Get the step children of this block, cached if possible. """ - if getattr(self, "_steps_cache", None) is None: - self._steps_cache = [self.runtime.get_block(child_id) for child_id in self.steps] - return self._steps_cache + return [self.runtime.get_block(child_id) for child_id in self.step_ids] class QuestionMixin(EnumerableChildMixin): @@ -114,7 +113,7 @@ class QuestionMixin(EnumerableChildMixin): @lazy def siblings(self): - return self.get_parent().steps + return self.get_parent().step_ids def author_view(self, context): context = context.copy() if context else {} diff --git a/problem_builder/step.py b/problem_builder/step.py index 2f4ea83f..2ab470de 100644 --- a/problem_builder/step.py +++ b/problem_builder/step.py @@ -100,12 +100,12 @@ class MentoringStepBlock( @lazy def siblings(self): - return self.get_parent().steps + return self.get_parent().step_ids @property def is_last_step(self): parent = self.get_parent() - return self.step_number == len(parent.steps) + return self.step_number == len(parent.step_ids) @property def allowed_nested_blocks(self): @@ -130,7 +130,7 @@ def submit(self, submissions, suffix=''): # Submit child blocks (questions) and gather results submit_results = [] - for child in self.get_steps(): + for child in self.steps: if child.name and child.name in submissions: submission = submissions[child.name] child_result = child.submit(submission) @@ -152,7 +152,7 @@ def submit(self, submissions, suffix=''): def get_results(self, queries, suffix=''): results = {} answers = dict(self.student_results) - for question in self.get_steps(): + for question in self.steps: previous_results = answers[question.name] result = question.get_results(previous_results) results[question.name] = result diff --git a/problem_builder/tests/unit/test_mentoring.py b/problem_builder/tests/unit/test_mentoring.py index 5f00619c..dcd7ccd3 100644 --- a/problem_builder/tests/unit/test_mentoring.py +++ b/problem_builder/tests/unit/test_mentoring.py @@ -164,8 +164,7 @@ def test_get_tip_content(self): self.mcq_block = MCQBlock(self.runtime_mock, DictFieldData({'name': 'test_mcq'}), Mock()) self.mcq_block.get_review_tip = Mock() self.mcq_block.get_review_tip.return_value = self.message_block.content - self.block.steps = [] - self.block.get_steps = Mock() - self.block.get_steps.return_value = [self.mcq_block] + self.block.step_ids = [] + self.block.steps = [self.mcq_block] self.block.student_results = {'test_mcq': {'status': 'incorrect'}} self.assertEqual(self.block.review_tips, ['replaced-url']) diff --git a/problem_builder/tests/unit/test_step.py b/problem_builder/tests/unit/test_step.py index d03addad..e22df630 100644 --- a/problem_builder/tests/unit/test_step.py +++ b/problem_builder/tests/unit/test_step.py @@ -47,7 +47,7 @@ def test_single_step_is_returned_correctly(self): step = Step() block._children = [step] - steps = [block.runtime.get_block(cid) for cid in block.steps] + steps = [block.runtime.get_block(cid) for cid in block.step_ids] self.assertSequenceEqual(steps, [step]) def test_only_steps_are_returned(self): @@ -56,7 +56,7 @@ def test_only_steps_are_returned(self): step2 = Step() block._set_children_for_test(step1, 1, "2", "Step", NotAStep(), False, step2, NotAStep()) - steps = [block.runtime.get_block(cid) for cid in block.steps] + steps = [block.runtime.get_block(cid) for cid in block.step_ids] self.assertSequenceEqual(steps, [step1, step2]) def test_proper_number_is_returned_for_step(self): From 1ff860a1e544956801ba43b9c544ae2118c47fb7 Mon Sep 17 00:00:00 2001 From: Tim Krones Date: Tue, 29 Sep 2015 11:09:44 +0200 Subject: [PATCH 43/43] Make sure num_attempts info is always up-to-date by fetching it from server when initializing Step Builder XBlock on the client. --- problem_builder/mentoring.py | 6 +++++ .../public/js/mentoring_with_steps.js | 25 ++++++++++++++++--- 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/problem_builder/mentoring.py b/problem_builder/mentoring.py index d94b1f54..5a07bf1a 100644 --- a/problem_builder/mentoring.py +++ b/problem_builder/mentoring.py @@ -1054,6 +1054,12 @@ def get_grade(self, data, suffix): 'assessment_review_tips': self.review_tips, } + @XBlock.json_handler + def get_num_attempts(self, data, suffix): + return { + 'num_attempts': self.num_attempts + } + @XBlock.json_handler def try_again(self, data, suffix=''): self.active_step = 0 diff --git a/problem_builder/public/js/mentoring_with_steps.js b/problem_builder/public/js/mentoring_with_steps.js index 3235b960..56a84b56 100644 --- a/problem_builder/public/js/mentoring_with_steps.js +++ b/problem_builder/public/js/mentoring_with_steps.js @@ -372,11 +372,14 @@ function MentoringWithStepsBlock(runtime, element) { } function initXBlockView() { + // Hide steps until we're ready + hideAllSteps(); + + // Initialize references to relevant DOM elements and set up event handlers checkmark = $('.assessment-checkmark', element); submitDOM = $(element).find('.submit .input-main'); submitDOM.on('click', submit); - submitDOM.show(); nextDOM = $(element).find('.submit .input-next'); if (atReviewStep()) { @@ -384,7 +387,6 @@ function MentoringWithStepsBlock(runtime, element) { } else { nextDOM.on('click', updateDisplay); } - nextDOM.show(); reviewDOM = $(element).find('.submit .input-review'); reviewDOM.on('click', showGrade); @@ -400,12 +402,29 @@ function MentoringWithStepsBlock(runtime, element) { reviewLinkDOM = $(element).find('.review-link'); reviewLinkDOM.on('click', showGrade); + // Initialize individual steps + // (sets up click handlers for questions and makes sure answer data is up-to-date) var options = { onChange: onChange }; initSteps(options); - updateDisplay(); + // Refresh info about number of attempts used: + // In the LMS, the HTML of multiple units can be loaded at once, + // and the user can flip among them. If that happens, information about + // number of attempts student has used up may be out of date. + var handlerUrl = runtime.handlerUrl(element, 'get_num_attempts'); + $.post(handlerUrl, JSON.stringify({})) + .success(function(response) { + attemptsDOM.data('num_attempts', response.num_attempts); + + // Finally, show controls and content + submitDOM.show(); + nextDOM.show(); + + updateDisplay(); + }); + } initClickHandlers();