diff --git a/problem_builder/mentoring.py b/problem_builder/mentoring.py index 5f4420bf..d437cc29 100644 --- a/problem_builder/mentoring.py +++ b/problem_builder/mentoring.py @@ -1027,6 +1027,7 @@ 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/vendor/underscore-min.js')) + fragment.add_javascript_url(self.runtime.local_resource_url(self, 'public/js/step_util.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 aa323b7c..e95fe7dd 100644 --- a/problem_builder/public/js/mentoring_with_steps.js +++ b/problem_builder/public/js/mentoring_with_steps.js @@ -7,6 +7,7 @@ function MentoringWithStepsBlock(runtime, element) { } var children = runtime.children(element); + var steps = []; for (var i = 0; i < children.length; i++) { @@ -17,20 +18,57 @@ function MentoringWithStepsBlock(runtime, element) { } } - var activeStep = $('.mentoring', element).data('active-step'); + var activeStepIndex = $('.mentoring', element).data('active-step'); var attemptsTemplate = _.template($('#xblock-attempts-template').html()); var message = $('.sb-step-message', element); var checkmark, submitDOM, nextDOM, reviewButtonDOM, tryAgainDOM, gradeDOM, attemptsDOM, reviewLinkDOM, submitXHR; var reviewStepDOM = $("div.xblock[data-block-type=sb-review-step], div.xblock-v1[data-block-type=sb-review-step]", element); + var reviewStepAnchor = $("").addClass("review-anchor").insertBefore(reviewStepDOM); var hasAReviewStep = reviewStepDOM.length == 1; + /** + * Returns the active step + * @returns MentoringStepBlock + */ + function getActiveStep() { + return steps[activeStepIndex]; + } + + /** + * Calls a function for each registered step. The object passed to this function is a MentoringStepBlock. + * + * @param func single arg function. + */ + function forEachStep(func) { + for (var idx=0; idx < steps.length; idx++) { + func(steps[idx]); + } + } + + /** + * Displays the active step + */ + function showActiveStep() { + var step = getActiveStep(); + step.showStep(); + } + + /** + * Hides all steps + */ + function hideAllSteps() { + forEachStep(function(step) { + step.hideStep(); + }); + } + function isLastStep() { - return (activeStep === steps.length-1); + return (activeStepIndex === steps.length-1); } function atReviewStep() { - return (activeStep === -1); + return (activeStepIndex === -1); } function someAttemptsLeft() { @@ -49,7 +87,7 @@ function MentoringWithStepsBlock(runtime, element) { } else { checkmark.addClass('checkmark-incorrect icon-exclamation fa-exclamation'); } - var step = steps[activeStep]; + var step = getActiveStep(); if (typeof step.showFeedback == 'function') { step.showFeedback(response); } @@ -78,14 +116,14 @@ function MentoringWithStepsBlock(runtime, element) { function submit() { submitDOM.attr('disabled', 'disabled'); // Disable the button until the results load. var submitUrl = runtime.handlerUrl(element, 'submit'); - - var hasQuestion = steps[activeStep].hasQuestion(); - var data = steps[activeStep].getSubmitData(); - data["active_step"] = activeStep; + var activeStep = getActiveStep(); + var hasQuestion = activeStep.hasQuestion(); + var data = activeStep.getSubmitData(); + data["active_step"] = activeStepIndex; $.post(submitUrl, JSON.stringify(data)).success(function(response) { showFeedback(response); - activeStep = response.active_step; - if (activeStep === -1) { + activeStepIndex = response.active_step; + if (activeStepIndex === -1) { // We are now showing the review step / end // Update the number of attempts. attemptsDOM.data('num_attempts', response.num_attempts); @@ -102,15 +140,14 @@ function MentoringWithStepsBlock(runtime, element) { } function getResults() { - var step = steps[activeStep]; - step.getResults(handleReviewResults); + getActiveStep().getResults(handleReviewResults); } function handleReviewResults(response) { // Show step-level feedback showFeedback(response); // Forward to active step to show answer level feedback - var step = steps[activeStep]; + var step = getActiveStep(); var results = response.results; var options = { checkmark: checkmark @@ -118,14 +155,11 @@ function MentoringWithStepsBlock(runtime, element) { step.handleReview(results, options); } - function hideAllSteps() { - for (var i=0; i < steps.length; i++) { - $(steps[i].element).hide(); - } - } function clearSelections() { - $('input[type=radio], input[type=checkbox]', element).prop('checked', false); + forEachStep(function (step) { + $('input[type=radio], input[type=checkbox]', step.element).prop('checked', false); + }); } function cleanAll() { @@ -139,7 +173,7 @@ function MentoringWithStepsBlock(runtime, element) { } function updateNextLabel() { - var step = steps[activeStep]; + var step = getActiveStep(); nextDOM.attr('value', step.getStepLabel()); } @@ -164,7 +198,7 @@ function MentoringWithStepsBlock(runtime, element) { nextDOM.on('click', updateDisplay); reviewButtonDOM.on('click', showGrade); - var step = steps[activeStep]; + var step = getActiveStep(); if (step.hasQuestion()) { // Step includes one or more questions nextDOM.attr('disabled', 'disabled'); submitDOM.show(); @@ -217,11 +251,22 @@ function MentoringWithStepsBlock(runtime, element) { reviewButtonDOM.hide(); tryAgainDOM.show(); + // reviewStepDOM is detached in hideReviewStep + reviewStepDOM.insertBefore(reviewStepAnchor); reviewStepDOM.show(); } + /** + * We detach review step from DOM, this is required to handle HTML + * blocks with embedded videos, that can be added to that step. + * + * NOTE: Review steps are handled differently than "normal" steps: + * the HTML contents of a review step are replaced with fresh + * contents in submit function. + */ function hideReviewStep() { reviewStepDOM.hide(); + reviewStepDOM.detach(); } function getStepToReview(event) { @@ -231,7 +276,7 @@ function MentoringWithStepsBlock(runtime, element) { } function jumpToReview(stepIndex) { - activeStep = stepIndex; + activeStepIndex = stepIndex; cleanAll(); showActiveStep(); updateNextLabel(); @@ -245,7 +290,7 @@ function MentoringWithStepsBlock(runtime, element) { nextDOM.show(); nextDOM.removeAttr('disabled'); } - var step = steps[activeStep]; + var step = getActiveStep(); tryAgainDOM.hide(); if (step.hasQuestion()) { @@ -269,12 +314,6 @@ function MentoringWithStepsBlock(runtime, element) { } // Don't show attempts if unlimited attempts available (max_attempts === 0) } - function showActiveStep() { - var step = steps[activeStep]; - $(step.element).show(); - step.updateChildren(); - } - function onChange() { // We do not allow users to modify answers belonging to a step after submitting them: // Once an answer has been submitted ("Next Step" button is enabled), @@ -286,7 +325,7 @@ function MentoringWithStepsBlock(runtime, element) { function validateXBlock() { var isValid = true; - var step = steps[activeStep]; + var step = getActiveStep(); if (step) { isValid = step.validate(); } @@ -298,16 +337,14 @@ function MentoringWithStepsBlock(runtime, element) { } function initSteps(options) { - for (var i=0; i < steps.length; i++) { - var step = steps[i]; - var mentoring = { + forEachStep(function (step) { + options.mentoring = { setContent: setContent, publish_event: publishEvent, is_step_builder: true }; - options.mentoring = mentoring; step.initChildren(options); - } + }); } function setContent(dom, content) { @@ -347,7 +384,7 @@ function MentoringWithStepsBlock(runtime, element) { } function reviewNextStep() { - jumpToReview(activeStep+1); + jumpToReview(activeStepIndex+1); } function handleTryAgain(result) { @@ -356,7 +393,7 @@ function MentoringWithStepsBlock(runtime, element) { // and interrupting their experience with the current unit notify('navigation', {state: 'lock'}); - activeStep = result.active_step; + activeStepIndex = result.active_step; clearSelections(); updateDisplay(); tryAgainDOM.hide(); @@ -377,7 +414,7 @@ function MentoringWithStepsBlock(runtime, element) { submitXHR = $.post(handlerUrl, JSON.stringify({})).success(handleTryAgain); } - function notify(name, data){ + function notify(name, data) { // Notification interface does not exist in the workbench. if (runtime.notify) { runtime.notify(name, data); @@ -403,7 +440,7 @@ function MentoringWithStepsBlock(runtime, element) { var itemFeedbackParentSelector = '.choice'; var itemFeedbackSelector = ".choice .choice-tips"; - function clickedInside(selector, parent_selector){ + function clickedInside(selector, parent_selector) { return target.is(selector) || target.parents(parent_selector).length>0; } diff --git a/problem_builder/public/js/step.js b/problem_builder/public/js/step.js index 38993dc2..a53ff032 100644 --- a/problem_builder/public/js/step.js +++ b/problem_builder/public/js/step.js @@ -4,6 +4,8 @@ function MentoringStepBlock(runtime, element) { var submitXHR, resultsXHR, message = $(element).find('.sb-step-message'); + + var childManager = new ProblemBuilderStepUtil.ChildManager(element, runtime); function callIfExists(obj, fn) { if (typeof obj !== 'undefined' && typeof obj[fn] == 'function') { @@ -13,13 +15,6 @@ function MentoringStepBlock(runtime, element) { } } - function updateVideo(video) { - video.resizer.align(); - } - - function updatePlot(plot) { - plot.update(); - } return { @@ -56,7 +51,7 @@ function MentoringStepBlock(runtime, element) { }, showFeedback: function(response) { - // Called when user has just submitted an answer or is reviewing their answer durign extended feedback. + // Called when user has just submitted an answer or is reviewing their answer during extended feedback. if (message.length) { message.fadeIn(); $(document).click(function() { @@ -110,20 +105,21 @@ function MentoringStepBlock(runtime, element) { return $('.sb-step', element).data('has-question'); }, - updateChildren: function() { - children.forEach(function(child) { - var type = $(child.element).data('block-type'); - switch (type) { - case 'video': - updateVideo(child); - break; - case 'sb-plot': - updatePlot(child); - break; - } - }); - } + /** + * Shows a step, updating all children. + */ + showStep: function () { + $(element).show(); + childManager.show(); + }, + /** + * Hides a step, updating all children. + */ + hideStep: function () { + $(element).hide(); + childManager.hide(); + } }; } diff --git a/problem_builder/public/js/step_util.js b/problem_builder/public/js/step_util.js new file mode 100644 index 00000000..50c5f58e --- /dev/null +++ b/problem_builder/public/js/step_util.js @@ -0,0 +1,120 @@ +(function () { + + /** + * Manager for HTML XBlocks. These blocks are hidden by detaching and shown + * by re-attaching them to the DOM. This is only way to generically + * handle things like video players (they should stop playing when removed from DOM). + * + * @param html an html xblock + */ + function HtmlManager(html) { + var $element = $(html.element); + var $anchor = $("").addClass("sb-video-anchor").insertBefore($element); + this.show = function () { + $element.insertAfter($anchor); + }; + this.hide = function () { + $element.detach() + }; + } + + /** + * + * Manager for HTML Video child. Videos are re-sized when showing them. + * @param video an video xblock + * + */ + function VideoManager(video) { + this.show = function () { + if (typeof video.resizer === 'undefined') { + // This one is tricky: but it looks like resizer is undefined only if the video is on the + // step that is initially visible (and then no resizing is necessary) + return; + } + video.resizer.align(); + }; + /** + * Videos should be paused when user leaves a step containing a video. There is was a proposed implementation + * but since it didn't work on every system we decided to drop it (it was out of scope for current task + * nevertheless). See OC-1441 for details. + */ + this.hide = function () {}; + } + + /** + * Manager for Plot Xblocks. Handles updating a plot before displaying it. + * @param plot + */ + function PlotManager(plot) { + this.show = function () { + plot.update(); + }; + this.hide = function () {}; + } + + + function ChildManager(xblock_element, runtime) { + + var Managers = { + 'video': VideoManager, + 'sb-plot': PlotManager + }; + + var children = runtime.children(xblock_element); + + /** + * A list of managers for children that need special care when showing or hiding. + * + * @type {show, hide}[] + */ + var managedChildren = []; + + /*** + * This is a workaround for issue where jquery.xblock.Runtime doesn't return HTML blocks when querying + * for children. + * + * This can be removed when: + * + * * We allow inclusion of Ooyala blocks inside StepBuilder and our clients migrate to Ooyala, in this case + * we may drop special handling of HTML blocks. See discussions in OC-1441. + * * We include HTML blocks in runtime.children for runtime of jquery.xblock, then just add + * `html: HtmlManager` to `Managers`, and remove this block. + */ + $("div.xblock.xblock-student_view.xmodule_HtmlModule", xblock_element).each(function(idx, element) { + managedChildren.push(new HtmlManager({ element: element })); + }); + + for (var idx = 0; idx < children.length; idx++) { + var child = children[idx]; + // NOTE: While the following assertion is true for e.g Video blocks: + // child.type == $(child.element).data('block-type') it is invalid for all sb-* blocks + var type = $(child.element).data('block-type'); + var constructor = Managers[type]; + if (typeof constructor === 'undefined') { + // This block does not requires special care, moving on + continue; + } + managedChildren.push(new constructor(child)); + } + + this.show = function () { + for (var idx = 0; idx < managedChildren.length; idx++) { + managedChildren[idx].show(); + } + }; + + this.hide = function () { + for (var idx = 0; idx < managedChildren.length; idx++) { + managedChildren[idx].hide(); + } + }; + + } + + window.ProblemBuilderStepUtil = { + + ChildManager: ChildManager + + }; +})(); + diff --git a/problem_builder/tests/integration/test_step_builder.py b/problem_builder/tests/integration/test_step_builder.py index 7846ed77..7657b552 100644 --- a/problem_builder/tests/integration/test_step_builder.py +++ b/problem_builder/tests/integration/test_step_builder.py @@ -624,9 +624,7 @@ def test_review_tips(self): @data(True, False) def test_conditional_messages(self, include_messages): - """ - Test that conditional messages in the review step are visible or not, as appropriate. - """ + # Test that conditional messages in the review step are visible or not, as appropriate. max_attempts = 3 extended_feedback = False params = {