From b366ad6d3302f1319b6e87876ca5459e7da31172 Mon Sep 17 00:00:00 2001 From: cahrens Date: Thu, 13 Oct 2016 11:45:45 -0400 Subject: [PATCH 1/4] Convert display.coffee to js. --- common/lib/xmodule/xmodule/js/src/capa/.gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/common/lib/xmodule/xmodule/js/src/capa/.gitignore b/common/lib/xmodule/xmodule/js/src/capa/.gitignore index 13b8deb002..a25aa57778 100644 --- a/common/lib/xmodule/xmodule/js/src/capa/.gitignore +++ b/common/lib/xmodule/xmodule/js/src/capa/.gitignore @@ -1,2 +1,3 @@ !imageinput.js !schematic.js +!display.js From 7a443737f635f801be53b866778d310fa664db2b Mon Sep 17 00:00:00 2001 From: cahrens Date: Thu, 13 Oct 2016 11:47:17 -0400 Subject: [PATCH 2/4] Convert display.coffee to js. First just rename. --- .../xmodule/xmodule/js/src/capa/{display.coffee => display.js} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename common/lib/xmodule/xmodule/js/src/capa/{display.coffee => display.js} (100%) diff --git a/common/lib/xmodule/xmodule/js/src/capa/display.coffee b/common/lib/xmodule/xmodule/js/src/capa/display.js similarity index 100% rename from common/lib/xmodule/xmodule/js/src/capa/display.coffee rename to common/lib/xmodule/xmodule/js/src/capa/display.js From 1b03a0090df929b1b7863de73bedc11d11762524 Mon Sep 17 00:00:00 2001 From: cahrens Date: Fri, 7 Oct 2016 10:43:18 -0400 Subject: [PATCH 3/4] Replace with CoffeeScript generated version of JS. Includes reformatting to 4 spaces. --- common/lib/capa/capa/inputtypes.py | 2 +- common/lib/xmodule/xmodule/capa_module.py | 4 +- .../xmodule/xmodule/js/src/capa/display.js | 2055 +++++++++-------- .../xmodule/js/src/sequence/display.coffee | 2 +- 4 files changed, 1153 insertions(+), 910 deletions(-) diff --git a/common/lib/capa/capa/inputtypes.py b/common/lib/capa/capa/inputtypes.py index 25b5fe30b2..691eb558fb 100644 --- a/common/lib/capa/capa/inputtypes.py +++ b/common/lib/capa/capa/inputtypes.py @@ -549,7 +549,7 @@ class JavascriptInput(InputTypeBase): TODO (arjun?): document this in detail. Initial notes: - display_class is a subclass of XProblemClassDisplay (see - xmodule/xmodule/js/src/capa/display.coffee), + xmodule/xmodule/js/src/capa/display.js), - display_file is the js script to be in /static/js/ where display_class is defined. """ diff --git a/common/lib/xmodule/xmodule/capa_module.py b/common/lib/xmodule/xmodule/capa_module.py index d97feaf30b..118e207e99 100644 --- a/common/lib/xmodule/xmodule/capa_module.py +++ b/common/lib/xmodule/xmodule/capa_module.py @@ -29,11 +29,9 @@ class CapaModule(CapaMixin, XModule): icon_class = 'problem' js = { - 'coffee': [ - resource_string(__name__, 'js/src/capa/display.coffee'), - ], 'js': [ resource_string(__name__, 'js/src/javascript_loader.js'), + resource_string(__name__, 'js/src/capa/display.js'), resource_string(__name__, 'js/src/collapsible.js'), resource_string(__name__, 'js/src/capa/imageinput.js'), resource_string(__name__, 'js/src/capa/schematic.js'), diff --git a/common/lib/xmodule/xmodule/js/src/capa/display.js b/common/lib/xmodule/xmodule/js/src/capa/display.js index f0ef87b24c..a52802afe7 100644 --- a/common/lib/xmodule/xmodule/js/src/capa/display.js +++ b/common/lib/xmodule/xmodule/js/src/capa/display.js @@ -1,905 +1,1150 @@ -class @Problem - - constructor: (element) -> - @el = $(element).find('.problems-wrapper') - @id = @el.data('problem-id') - @element_id = @el.attr('id') - @url = @el.data('url') - @content = @el.data('content') - - # has_timed_out and has_response are used to ensure that are used to - # ensure that we wait a minimum of ~ 1s before transitioning the submit - # button from disabled to enabled - @has_timed_out = false - @has_response = false - - @render(@content) - - $: (selector) -> - $(selector, @el) - - bind: => - if MathJax? - @el.find('.problem > div').each (index, element) => - MathJax.Hub.Queue ["Typeset", MathJax.Hub, element] - - window.update_schematics() - - problem_prefix = @element_id.replace(/problem_/,'') - @inputs = @$("[id^='input_#{problem_prefix}_']") - @$('div.action button').click @refreshAnswers - @reviewButton = @$('.notification-btn.review-btn') - @reviewButton.click @scroll_to_problem_meta - @submitButton = @$('.action .submit') - @submitButtonLabel = @$('.action .submit .submit-label') - @submitButtonSubmitText = @submitButtonLabel.text() - @submitButtonSubmittingText = @submitButton.data('submitting') - @submitButton.click @submit_fd - @hintButton = @$('.action .hint-button') - @hintButton.click @hint_button - @resetButton = @$('.action .reset') - @resetButton.click @reset - @showButton = @$('.action .show') - @showButton.click @show - @saveButton = @$('.action .save') - @saveNotification = @$('.notification-save') - @saveButtonLabel = @$('.action .save .save-label') - @saveButton.click @save - @gentleAlertNotification = @$('.notification-gentle-alert') - @submitNotification = @$('.notification-submit') - - # Accessibility helper for sighted keyboard users to show tooltips on focus: - @$('.clarification').focus (ev) => - icon = $(ev.target).children "i" - window.globalTooltipManager.openTooltip icon - @$('.clarification').blur (ev) => - window.globalTooltipManager.hide() - - @$('.review-btn').focus (ev) => - $(ev.target).removeClass('sr'); - - @$('.review-btn').blur (ev) => - $(ev.target).addClass('sr'); - - @bindResetCorrectness() - if @submitButton.length - @submitAnswersAndSubmitButton true - - # Collapsibles - Collapsible.setCollapsibles(@el) - - # Dynamath - @$('input.math').keyup(@refreshMath) - if MathJax? - @$('input.math').each (index, element) => - MathJax.Hub.Queue [@refreshMath, null, element] - - renderProgressState: => - detail = @el.data('progress_detail') - status = @el.data('progress_status') - graded = @el.data('graded') - - # Render 'x/y point(s)' if student has attempted question - if status != 'none' and detail? and (jQuery.type(detail) == "string") and detail.indexOf('/') > 0 - a = detail.split('/') - earned = parseFloat(a[0]) - possible = parseFloat(a[1]) - - if graded == "True" and possible != 0 - # This comment needs to be on one line to be properly scraped for the translators. Sry for length. - `// Translators: %(earned)s is the number of points earned. %(possible)s is the total number of points (examples: 0/1, 1/1, 2/3, 5/10). The total number of points will always be at least 1. We pluralize based on the total number of points (example: 0/1 point; 1/2 points)` - progress_template = ngettext('%(earned)s/%(possible)s point (graded)', '%(earned)s/%(possible)s points (graded)', possible) - else - # This comment needs to be on one line to be properly scraped for the translators. Sry for length. - `// Translators: %(earned)s is the number of points earned. %(possible)s is the total number of points (examples: 0/1, 1/1, 2/3, 5/10). The total number of points will always be at least 1. We pluralize based on the total number of points (example: 0/1 point; 1/2 points)` - progress_template = ngettext('%(earned)s/%(possible)s point (ungraded)', '%(earned)s/%(possible)s points (ungraded)', possible) - progress = interpolate(progress_template, {'earned': earned, 'possible': possible}, true) - - # Render 'x point(s) possible' if student has not yet attempted question - # Status is set to none when a user has a score of 0, and 0 when the problem has a weight of 0. - if status == 'none' or status == 0 - if detail? and (jQuery.type(detail) == "string") and detail.indexOf('/') > 0 - a = detail.split('/') - possible = parseFloat(a[1]) - else - possible = 0 - - if graded == "True" and possible != 0 - `// Translators: %(num_points)s is the number of points possible (examples: 1, 3, 10).` - progress_template = ngettext("%(num_points)s point possible (graded)", "%(num_points)s points possible (graded)", possible) - else - `// Translators: %(num_points)s is the number of points possible (examples: 1, 3, 10).` - progress_template = ngettext("%(num_points)s point possible (ungraded)", "%(num_points)s points possible (ungraded)", possible) - progress = interpolate(progress_template, {'num_points': possible}, true) - - @$('.problem-progress').text(progress) - - updateProgress: (response) => - if response.progress_changed - @el.data('progress_status', response.progress_status) - @el.data('progress_detail', response.progress_detail) - @el.trigger('progressChanged') - @renderProgressState() - - forceUpdate: (response) => - @el.data('progress_status', response.progress_status) - @el.data('progress_detail', response.progress_detail) - @el.trigger('progressChanged') - @renderProgressState() - - queueing: (focus_callback) => - @queued_items = @$(".xqueue") - @num_queued_items = @queued_items.length - if @num_queued_items > 0 - if window.queuePollerID # Only one poller 'thread' per Problem - window.clearTimeout(window.queuePollerID) - window.queuePollerID = window.setTimeout( - => @poll(1000, focus_callback), - 1000) - - poll: (prev_timeout, focus_callback) => - $.postWithPrefix "#{@url}/problem_get", (response) => - # If queueing status changed, then render - @new_queued_items = $(response.html).find(".xqueue") - if @new_queued_items.length isnt @num_queued_items - edx.HtmlUtils.setHtml(@el, edx.HtmlUtils.HTML(response.html)).promise().done => - focus_callback?() - JavascriptLoader.executeModuleScripts @el, () => - @setupInputTypes() - @bind() - - @num_queued_items = @new_queued_items.length - if @num_queued_items == 0 - @forceUpdate response - delete window.queuePollerID - else - new_timeout = prev_timeout * 2 - # if the timeout is greather than 1 minute - if new_timeout >= 60000 - delete window.queuePollerID - @gentle_alert gettext("The grading process is still running. Refresh the page to see updates.") - else - window.queuePollerID = window.setTimeout( - => @poll(new_timeout, focus_callback), - new_timeout - ) - - - # Use this if you want to make an ajax call on the input type object - # static method so you don't have to instantiate a Problem in order to use it - # Input: - # url: the AJAX url of the problem - # input_id: the input_id of the input you would like to make the call on - # NOTE: the id is the ${id} part of "input_${id}" during rendering - # If this function is passed the entire prefixed id, the backend may have trouble - # finding the correct input - # dispatch: string that indicates how this data should be handled by the inputtype - # callback: the function that will be called once the AJAX call has been completed. - # It will be passed a response object - @inputAjax: (url, input_id, dispatch, data, callback) -> - data['dispatch'] = dispatch - data['input_id'] = input_id - $.postWithPrefix "#{url}/input_ajax", data, callback - - - render: (content, focus_callback) -> - if content - @el.html(content) - JavascriptLoader.executeModuleScripts @el, () => - @setupInputTypes() - @bind() - @queueing(focus_callback) - @renderProgressState() - focus_callback?() - else - $.postWithPrefix "#{@url}/problem_get", (response) => - @el.html(response.html) - JavascriptLoader.executeModuleScripts @el, () => - @setupInputTypes() - @bind() - @queueing() - @forceUpdate response - - # TODO add hooks for problem types here by inspecting response.html and doing - # stuff if a div w a class is found - - setupInputTypes: => - @inputtypeDisplays = {} - @el.find(".capa_inputtype").each (index, inputtype) => - classes = $(inputtype).attr('class').split(' ') - id = $(inputtype).attr('id') - for cls in classes - setupMethod = @inputtypeSetupMethods[cls] - if setupMethod? - @inputtypeDisplays[id] = setupMethod(inputtype) - - # If some function wants to be called before sending the answer to the - # server, give it a chance to do so. - # - # submit_save_waitfor allows the callee to send alerts if the user's input is - # invalid. To do so, the callee must throw an exception named "Waitfor - # Exception". This and any other errors or exceptions that arise from the - # callee are rethrown and abort the submission. - # - # In order to use this feature, add a 'data-waitfor' attribute to the input, - # and specify the function to be called by the submit button before sending - # off @answers - submit_save_waitfor: (callback) => - flag = false - for inp in @inputs - if ($(inp).is("input[waitfor]")) - try - $(inp).data("waitfor")(() => - @refreshAnswers() - callback() - ) - catch e - if e.name == "Waitfor Exception" - alert e.message - else - alert "Could not grade your answer. The submission was aborted." - throw e - flag = true - else - flag = false - return flag - - # Scroll to problem metadata and next focus is problem input - scroll_to_problem_meta: => - questionTitle = @$(".problem-header") - if questionTitle.length > 0 - $('html, body').animate({ - scrollTop: questionTitle.offset().top - }, 500); - questionTitle.focus() - - focus_on_notification: (type) => - notification = @$('.notification-'+type) - if notification.length > 0 - notification.focus() - - focus_on_submit_notification: => - @focus_on_notification('submit') - - focus_on_hint_notification: => - @focus_on_notification('hint') - - focus_on_save_notification: => - @focus_on_notification('save') - - ### - # 'submit_fd' uses FormData to allow file submissions in the 'problem_check' dispatch, - # in addition to simple querystring-based answers - # - # NOTE: The dispatch 'problem_check' is being singled out for the use of FormData; - # maybe preferable to consolidate all dispatches to use FormData - ### - submit_fd: => - # If there are no file inputs in the problem, we can fall back on @submit - if @el.find('input:file').length == 0 - @submit() - return - - @enableSubmitButton false - - if not window.FormData - alert "Submission aborted! Sorry, your browser does not support file uploads. If you can, please use Chrome or Safari which have been verified to support file uploads." - @enableSubmitButton true - return - - timeout_id = @enableSubmitButtonAfterTimeout() - - fd = new FormData() - - # Sanity checks on submission - max_filesize = 4*1000*1000 # 4 MB - file_too_large = false - file_not_selected = false - required_files_not_submitted = false - unallowed_file_submitted = false - - errors = [] - - @inputs.each (index, element) -> - if element.type is 'file' - required_files = $(element).data("required_files") - allowed_files = $(element).data("allowed_files") - for file in element.files - if allowed_files.length != 0 and file.name not in allowed_files - unallowed_file_submitted = true - errors.push "You submitted #{file.name}; only #{allowed_files} are allowed." - if file.name in required_files - required_files.splice(required_files.indexOf(file.name), 1) - if file.size > max_filesize - file_too_large = true - max_size = max_filesize / (1000*1000) - errors.push "Your file #{file.name} is too large (max size: {max_size}MB)" - fd.append(element.id, file) - if element.files.length == 0 - file_not_selected = true - fd.append(element.id, '') # In case we want to allow submissions with no file - if required_files.length != 0 - required_files_not_submitted = true - errors.push "You did not submit the required files: #{required_files}." - else - fd.append(element.id, element.value) - - - if file_not_selected - errors.push 'You did not select any files to submit' - - error_html = '
    \n' - for error in errors - error_html += '
  • ' + error + '
  • \n' - error_html += '
' - @gentle_alert error_html - - abort_submission = file_too_large or file_not_selected or unallowed_file_submitted or required_files_not_submitted - if abort_submission - window.clearTimeout(timeout_id) - @enableSubmitButton true - return - - settings = - type: "POST" - data: fd - processData: false - contentType: false - complete: @enableSubmitButtonAfterResponse - success: (response) => - switch response.success - when 'incorrect', 'correct' - @render(response.contents) - @updateProgress response - else - @gentle_alert response.success - Logger.log 'problem_graded', [@answers, response.contents], @id - - $.ajaxWithPrefix("#{@url}/problem_check", settings) - - submit: => - if not @submit_save_waitfor(@submit_internal) - @disableAllButtonsWhileRunning @submit_internal, true - - submit_internal: => - Logger.log 'problem_check', @answers - $.postWithPrefix "#{@url}/problem_check", @answers, (response) => - switch response.success - when 'incorrect', 'correct' - window.SR.readTexts(@get_sr_status(response.contents)) - @el.trigger('contentChanged', [@id, response.contents]) - @render(response.contents, @focus_on_submit_notification) - @updateProgress response - else - @saveNotification.hide() - @gentle_alert response.success - Logger.log 'problem_graded', [@answers, response.contents], @id - - get_sr_status: (contents) => - # This method builds up an array of strings to send to the page screen-reader span. - # It first gets all elements with class "status", and then looks to see if they are contained - # in sections with aria-labels. If so, labels are prepended to the status element text. - # If not, just the text of the status elements are returned. - status_elements = $(contents).find('.status') - labeled_status = [] - for element in status_elements - parent_section = $(element).closest('section') - added_status = false - if parent_section - aria_label = parent_section.attr('aria-label') - if aria_label - `// Translators: This is only translated to allow for reording of label and associated status.` - template = gettext("{label}: {status}") - labeled_status.push(edx.StringUtils.interpolate(template, {label: aria_label, status: $(element).text()})) - added_status = true - - if not added_status - labeled_status.push($(element).text()) - - return labeled_status - - reset: => - @disableAllButtonsWhileRunning @reset_internal, false - - reset_internal: => - Logger.log 'problem_reset', @answers - $.postWithPrefix "#{@url}/problem_reset", id: @id, (response) => - if response.success - @el.trigger('contentChanged', [@id, response.html]) - @render(response.html, @scroll_to_problem_meta) - @updateProgress response - window.SR.readText(gettext('This problem has been reset.')) - else - @gentle_alert response.msg - - # TODO this needs modification to deal with javascript responses; perhaps we - # need something where responsetypes can define their own behavior when show - # is called. - show: => - Logger.log 'problem_show', problem: @id - $.postWithPrefix "#{@url}/problem_show", (response) => - answers = response.answers - $.each answers, (key, value) => - if $.isArray(value) - for choice in value - @$("label[for='input_#{key}_#{choice}']").attr correct_answer: 'true' - else - answer = @$("#answer_#{key}, #solution_#{key}") - edx.HtmlUtils.setHtml(answer, edx.HtmlUtils.HTML(value)) - Collapsible.setCollapsibles(answer) - - # Sometimes, `value` is just a string containing a MathJax formula. - # If this is the case, jQuery will throw an error in some corner cases - # because of an incorrect selector. We setup a try..catch so that - # the script doesn't break in such cases. - # - # We will fallback to the second `if statement` below, if an - # error is thrown by jQuery. - try - solution = $(value).find('.detailed-solution') - catch e - solution = {} - - # TODO remove the above once everything is extracted into its own - # inputtype functions. - - @el.find(".capa_inputtype").each (index, inputtype) => - classes = $(inputtype).attr('class').split(' ') - for cls in classes - display = @inputtypeDisplays[$(inputtype).attr('id')] - showMethod = @inputtypeShowAnswerMethods[cls] - showMethod(inputtype, display, answers) if showMethod? - - if MathJax? - @el.find('.problem > div').each (index, element) => - MathJax.Hub.Queue ["Typeset", MathJax.Hub, element] - - @el.find('.show').attr('disabled', 'disabled') - @updateProgress response - window.SR.readText(gettext('Answers to this problem are now shown. Navigate through the problem to review it with answers inline.')) - @scroll_to_problem_meta() - - clear_all_notifications: => - @submitNotification.remove() - @gentleAlertNotification.hide() - @saveNotification.hide() - - gentle_alert: (msg) => - edx.HtmlUtils.setHtml(@el.find('.notification-gentle-alert .notification-message'), edx.HtmlUtils.HTML(msg)) - @clear_all_notifications() - @gentleAlertNotification.show() - @gentleAlertNotification.focus() - - save: => - if not @submit_save_waitfor(@save_internal) - @disableAllButtonsWhileRunning @save_internal, false - - save_internal: => - Logger.log 'problem_save', @answers - $.postWithPrefix "#{@url}/problem_save", @answers, (response) => - saveMessage = response.msg - if response.success - @el.trigger('contentChanged', [@id, response.html]) - edx.HtmlUtils.setHtml(@el.find('.notification-save .notification-message'), edx.HtmlUtils.HTML(saveMessage)) - @clear_all_notifications() - @saveNotification.show() - @focus_on_save_notification() - else - @gentle_alert saveMessage - - refreshMath: (event, element) => - element = event.target unless element - elid = element.id.replace(/^input_/,'') - target = "display_" + elid - - # MathJax preprocessor is loaded by 'setupInputTypes' - preprocessor_tag = "inputtype_" + elid - mathjax_preprocessor = @inputtypeDisplays[preprocessor_tag] - - if MathJax? and jax = MathJax.Hub.getAllJax(target)[0] - eqn = $(element).val() - if mathjax_preprocessor - eqn = mathjax_preprocessor(eqn) - MathJax.Hub.Queue(['Text', jax, eqn], [@updateMathML, jax, element]) - - return # Explicit return for CoffeeScript - - updateMathML: (jax, element) => - try - $("##{element.id}_dynamath").val(jax.root.toMathML '') - catch exception - throw exception unless exception.restart - if MathJax? - MathJax.Callback.After [@refreshMath, jax], exception.restart - - refreshAnswers: => - @$('input.schematic').each (index, element) -> - element.schematic.update_value() - @$(".CodeMirror").each (index, element) -> - element.CodeMirror.save() if element.CodeMirror.save - @answers = @inputs.serialize() - - submitAnswersAndSubmitButton: (bind=false) => - """ - Used to check available answers and if something is checked (or the answer is set in some textbox) - "Submit" button becomes enabled. Otherwise it is disabled by default. - - Arguments: - bind (bool): used on the first check to attach event handlers to input fields - to change "Submit" enable status in case of some manipulations with answers - """ - answered = true - - at_least_one_text_input_found = false - one_text_input_filled = false - @el.find("input:text").each (i, text_field) => - if $(text_field).is(':visible') - at_least_one_text_input_found = true - if $(text_field).val() isnt '' - one_text_input_filled = true - if bind - $(text_field).on 'input', (e) => - @saveNotification.hide() - @submitAnswersAndSubmitButton() - return - return - if at_least_one_text_input_found and not one_text_input_filled - answered = false - - @el.find(".choicegroup").each (i, choicegroup_block) => - checked = false - $(choicegroup_block).find("input[type=checkbox], input[type=radio]").each (j, checkbox_or_radio) => - if $(checkbox_or_radio).is(':checked') - checked = true - if bind - $(checkbox_or_radio).on 'click', (e) => - @saveNotification.hide() - @submitAnswersAndSubmitButton() - return - return - if not checked - answered = false - return - - @el.find("select").each (i, select_field) => - selected_option = $(select_field).find("option:selected").text().trim() - if selected_option is 'Select an option' - answered = false - if bind - $(select_field).on 'change', (e) => - @saveNotification.hide() - @submitAnswersAndSubmitButton() - return - return - - if answered - @enableSubmitButton true - else - @enableSubmitButton false, false - - bindResetCorrectness: -> - # Loop through all input types - # Bind the reset functions at that scope. - $inputtypes = @el.find(".capa_inputtype").add(@el.find(".inputtype")) - $inputtypes.each (index, inputtype) => - classes = $(inputtype).attr('class').split(' ') - for cls in classes - bindMethod = @bindResetCorrectnessByInputtype[cls] - if bindMethod? - bindMethod(inputtype) - - # Find all places where each input type displays its correct-ness - # Replace them with their original state--'unanswered'. - bindResetCorrectnessByInputtype: - # These are run at the scope of the capa inputtype - # They should set handlers on each to reset the whole. - formulaequationinput: (element) -> - $(element).find('input').on 'input', -> - $p = $(element).find('span.status') - `// Translators: the word unanswered here is about answering a problem the student must solve.` - $p.parent().removeClass().addClass "unsubmitted" - - choicegroup: (element) -> - $element = $(element) - id = ($element.attr('id').match /^inputtype_(.*)$/)[1] - $element.find('input').on 'change', -> - $status = $("#status_#{id}") - if $status[0] # We found a status icon. - $status.removeClass().addClass "unanswered" - $status.empty().css 'display', 'inline-block' - else - # Recreate the unanswered dot on left. - $("", {"class": "unanswered", "style": "display: inline-block;", "id": "status_#{id}"}) - - $element.find("label").removeClass() - - 'option-input': (element) -> - $select = $(element).find('select') - id = ($select.attr('id').match /^input_(.*)$/)[1] - $select.on 'change', -> - $status = $("#status_#{id}") - .removeClass().addClass("unanswered") - .find('span').text(gettext('Status: unsubmitted')) - - textline: (element) -> - $(element).find('input').on 'input', -> - $p = $(element).find('span.status') - `// Translators: the word unanswered here is about answering a problem the student must solve.` - $p.parent().removeClass("correct incorrect").addClass "unsubmitted" - - inputtypeSetupMethods: - - 'text-input-dynamath': (element) => - ### - Return: function (eqn) -> eqn that preprocesses the user formula input before - it is fed into MathJax. Return 'false' if no preprocessor specified - ### - data = $(element).find('.text-input-dynamath_data') - - preprocessorClassName = data.data('preprocessor') - preprocessorClass = window[preprocessorClassName] - if not preprocessorClass? - return false - else - preprocessor = new preprocessorClass() - return preprocessor.fn - - javascriptinput: (element) => - - data = $(element).find(".javascriptinput_data") - - params = data.data("params") - submission = data.data("submission") - evaluation = data.data("evaluation") - problemState = data.data("problem_state") - displayClass = window[data.data('display_class')] - - if evaluation == '' - evaluation = null - - container = $(element).find(".javascriptinput_container") - submissionField = $(element).find(".javascriptinput_input") - - display = new displayClass(problemState, submission, evaluation, container, submissionField, params) - display.render() - - return display - - cminput: (container) => - element = $(container).find("textarea") - tabsize = element.data("tabsize") - mode = element.data("mode") - linenumbers = element.data("linenums") - spaces = Array(parseInt(tabsize) + 1).join(" ") - CodeMirrorEditor = CodeMirror.fromTextArea element[0], { - lineNumbers: linenumbers - indentUnit: tabsize - tabSize: tabsize - mode: mode - matchBrackets: true - lineWrapping: true - indentWithTabs: false - smartIndent: false - extraKeys: { - "Esc": (cm) -> - $(".grader-status").focus() - return false - "Tab": (cm) -> - cm.replaceSelection(spaces, "end") - return false - } - } - id = element.attr("id").replace(/^input_/, "") - CodeMirrorTextArea = CodeMirrorEditor.getInputField() - CodeMirrorTextArea.setAttribute("id", "cm-textarea-#{id}") - CodeMirrorTextArea.setAttribute("aria-describedby", "cm-editor-exit-message-#{id} status_#{id}") - return CodeMirrorEditor - - inputtypeShowAnswerMethods: - choicegroup: (element, display, answers) => - element = $(element) - - input_id = element.attr('id').replace(/inputtype_/, '') - answer = answers[input_id] - for choice in answer - element.find("#input_#{input_id}_#{choice}").parent("label").addClass 'choicegroup_correct' - - javascriptinput: (element, display, answers) => - answer_id = $(element).attr('id').split("_")[1...].join("_") - answer = JSON.parse(answers[answer_id]) - display.showAnswer(answer) - - choicetextgroup: (element, display, answers) => - element = $(element) - - input_id = element.attr('id').replace(/inputtype_/, '') - answer = answers[input_id] - for choice in answer - element.find("section#forinput#{choice}").addClass 'choicetextgroup_show_correct' - - imageinput: (element, display, answers) => - # answers is a dict of (answer_id, answer_text) for each answer for this - # question. - # @Examples: - # {'anwser_id': { - # 'rectangle': '(10,10)-(20,30);(12,12)-(40,60)', - # 'regions': '[[10,10], [30,30], [10, 30], [30, 10]]' - # } } - types = - rectangle: (ctx, coords) => - reg = /^\(([0-9]+),([0-9]+)\)-\(([0-9]+),([0-9]+)\)$/ - rects = coords.replace(/\s*/g, '').split(/;/) - - $.each rects, (index, rect) => - abs = Math.abs - points = reg.exec(rect) - if points - width = abs(points[3] - points[1]) - height = abs(points[4] - points[2]) - - ctx.rect(points[1], points[2], width, height) - - ctx.stroke() - ctx.fill() - - regions: (ctx, coords) => - parseCoords = (coords) => - reg = JSON.parse(coords) - - # Regions is list of lists [region1, region2, region3, ...] where regionN - # is disordered list of points: [[1,1], [100,100], [50,50], [20, 70]]. - # If there is only one region in the list, simpler notation can be used: - # regions="[[10,10], [30,30], [10, 30], [30, 10]]" (without explicitly - # setting outer list) - if typeof reg[0][0][0] == "undefined" - # we have [[1,2],[3,4],[5,6]] - single region - # instead of [[[1,2],[3,4],[5,6], [[1,2],[3,4],[5,6]]] - # or [[[1,2],[3,4],[5,6]]] - multiple regions syntax - reg = [reg] - - return reg - - $.each parseCoords(coords), (index, region) => - ctx.beginPath() - $.each region, (index, point) => - if index is 0 - ctx.moveTo(point[0], point[1]) - else - ctx.lineTo(point[0], point[1]); - - ctx.closePath() - ctx.stroke() - ctx.fill() - - element = $(element) - id = element.attr('id').replace(/inputtype_/,'') - container = element.find("#answer_#{id}") - canvas = document.createElement('canvas') - canvas.width = container.data('width') - canvas.height = container.data('height') - - if canvas.getContext - ctx = canvas.getContext('2d') - else - return console.log 'Canvas is not supported.' - - ctx.fillStyle = 'rgba(255,255,255,.3)'; - ctx.strokeStyle = "#FF0000"; - ctx.lineWidth = "2"; - - if answers[id] - $.each answers[id], (key, value) => - types[key](ctx, value) if types[key]? and value - container.html(canvas) - else - console.log "Answer is absent for image input with id=#{id}" - - inputtypeHideAnswerMethods: - choicegroup: (element, display) => - element = $(element) - element.find('label').removeClass('choicegroup_correct') - - javascriptinput: (element, display) => - display.hideAnswer() - - choicetextgroup: (element, display) => - element = $(element) - element.find("section[id^='forinput']").removeClass('choicetextgroup_show_correct') - - disableAllButtonsWhileRunning: (operationCallback, isFromCheckOperation) => - # Used to keep the buttons disabled while operationCallback is running. - # params: - # 'operationCallback' is an operation to be run. - # 'isFromCheckOperation' is a boolean to keep track if 'operationCallback' was - # @submit, if so then text of submit button will be changed as well. - @enableAllButtons false, isFromCheckOperation - operationCallback().always => - @enableAllButtons true, isFromCheckOperation - - # Called by disableAllButtonsWhileRunning to automatically disable all buttons while check,reset, or - # save internal are running. Then enable all the buttons again after it is done. - enableAllButtons: (enable, isFromCheckOperation) => - # Used to enable/disable all buttons in problem. - # params: - # 'enable' is a boolean to determine enabling/disabling of buttons. - # 'isFromCheckOperation' is a boolean to keep track if operation was initiated - # from @submit so that text of submit button will also be changed while disabling/enabling - # the submit button. - if enable - @resetButton - .add(@saveButton) - .add(@hintButton) - .add(@showButton) - .removeAttr 'disabled' - else - @resetButton - .add(@saveButton) - .add(@hintButton) - .add(@showButton) - .attr({'disabled': 'disabled'}) - - @enableSubmitButton enable, isFromCheckOperation - - enableSubmitButton: (enable, changeText = true) => - # Used to disable submit button to reduce chance of accidental double-submissions. - # params: - # 'enable' is a boolean to determine enabling/disabling of submit button. - # 'changeText' is a boolean to determine if there is need to change the - # text of submit button as well. - if enable - submitCanBeEnabled = @submitButton.data('should-enable-submit-button') == 'True' - if submitCanBeEnabled - @submitButton.removeAttr 'disabled' - if changeText - @submitButtonLabel.text(@submitButtonSubmitText) - else - @submitButton.attr({'disabled': 'disabled'}) - if changeText - @submitButtonLabel.text(@submitButtonSubmittingText) - - enableSubmitButtonAfterResponse: => - @has_response = true - if not @has_timed_out - # Server has returned response before our timeout - @enableSubmitButton false - else - @enableSubmitButton true - - enableSubmitButtonAfterTimeout: => - @has_timed_out = false - @has_response = false - enableSubmitButton = () => - @has_timed_out = true - if @has_response - @enableSubmitButton true - window.setTimeout(enableSubmitButton, 750) - - hint_button: => - # Store the index of the currently shown hint as an attribute. - # Use that to compute the next hint number when the button is clicked. - hint_container = @.$('.problem-hint') - hint_index = hint_container.attr('hint_index') - if hint_index == undefined - next_index = 0 - else - next_index = parseInt(hint_index) + 1 - $.postWithPrefix "#{@url}/hint_button", hint_index: next_index, input_id: @id, (response) => - if response.success - hint_msg_container = @.$('.problem-hint .notification-message') - hint_container.attr('hint_index', response.hint_index) - edx.HtmlUtils.setHtml(hint_msg_container, edx.HtmlUtils.HTML(response.msg)) - # Update any Mathjax entries - MathJax.Hub.Queue [ - 'Typeset' - MathJax.Hub - hint_container[0] - ] - # Enable/Disable the next hint button - if response.should_enable_next_hint - @hintButton.removeAttr 'disabled' - else - @hintButton.attr({'disabled': 'disabled'}) - @el.find('.notification-hint').show() - @focus_on_hint_notification() - else - @gentle_alert response.msg +// Generated by CoffeeScript 1.6.1 +(function () { + var _this = this, + __indexOf = [].indexOf || function (item) { + for (var i = 0, l = this.length; i < l; i++) { + if (i in this && this[i] === item) return i; + } + return -1; + }; + + this.Problem = (function () { + var _this = this; + + function Problem(element) { + var _this = this; + this.hint_button = function () { + return Problem.prototype.hint_button.apply(_this, arguments); + }; + this.enableSubmitButtonAfterTimeout = function () { + return Problem.prototype.enableSubmitButtonAfterTimeout.apply(_this, arguments); + }; + this.enableSubmitButtonAfterResponse = function () { + return Problem.prototype.enableSubmitButtonAfterResponse.apply(_this, arguments); + }; + this.enableSubmitButton = function (enable, changeText) { + if (changeText == null) { + changeText = true; + } + return Problem.prototype.enableSubmitButton.apply(_this, arguments); + }; + this.enableAllButtons = function (enable, isFromCheckOperation) { + return Problem.prototype.enableAllButtons.apply(_this, arguments); + }; + this.disableAllButtonsWhileRunning = function (operationCallback, isFromCheckOperation) { + return Problem.prototype.disableAllButtonsWhileRunning.apply(_this, arguments); + }; + this.submitAnswersAndSubmitButton = function (bind) { + if (bind == null) { + bind = false; + } + return Problem.prototype.submitAnswersAndSubmitButton.apply(_this, arguments); + }; + this.refreshAnswers = function () { + return Problem.prototype.refreshAnswers.apply(_this, arguments); + }; + this.updateMathML = function (jax, element) { + return Problem.prototype.updateMathML.apply(_this, arguments); + }; + this.refreshMath = function (event, element) { + return Problem.prototype.refreshMath.apply(_this, arguments); + }; + this.save_internal = function () { + return Problem.prototype.save_internal.apply(_this, arguments); + }; + this.save = function () { + return Problem.prototype.save.apply(_this, arguments); + }; + this.gentle_alert = function (msg) { + return Problem.prototype.gentle_alert.apply(_this, arguments); + }; + this.clear_all_notifications = function () { + return Problem.prototype.clear_all_notifications.apply(_this, arguments); + }; + this.show = function () { + return Problem.prototype.show.apply(_this, arguments); + }; + this.reset_internal = function () { + return Problem.prototype.reset_internal.apply(_this, arguments); + }; + this.reset = function () { + return Problem.prototype.reset.apply(_this, arguments); + }; + this.get_sr_status = function (contents) { + return Problem.prototype.get_sr_status.apply(_this, arguments); + }; + this.submit_internal = function () { + return Problem.prototype.submit_internal.apply(_this, arguments); + }; + this.submit = function () { + return Problem.prototype.submit.apply(_this, arguments); + }; + this.submit_fd = function () { + return Problem.prototype.submit_fd.apply(_this, arguments); + }; + this.focus_on_save_notification = function () { + return Problem.prototype.focus_on_save_notification.apply(_this, arguments); + }; + this.focus_on_hint_notification = function () { + return Problem.prototype.focus_on_hint_notification.apply(_this, arguments); + }; + this.focus_on_submit_notification = function () { + return Problem.prototype.focus_on_submit_notification.apply(_this, arguments); + }; + this.focus_on_notification = function (type) { + return Problem.prototype.focus_on_notification.apply(_this, arguments); + }; + this.scroll_to_problem_meta = function () { + return Problem.prototype.scroll_to_problem_meta.apply(_this, arguments); + }; + this.submit_save_waitfor = function (callback) { + return Problem.prototype.submit_save_waitfor.apply(_this, arguments); + }; + this.setupInputTypes = function () { + return Problem.prototype.setupInputTypes.apply(_this, arguments); + }; + this.poll = function (prev_timeout, focus_callback) { + return Problem.prototype.poll.apply(_this, arguments); + }; + this.queueing = function (focus_callback) { + return Problem.prototype.queueing.apply(_this, arguments); + }; + this.forceUpdate = function (response) { + return Problem.prototype.forceUpdate.apply(_this, arguments); + }; + this.updateProgress = function (response) { + return Problem.prototype.updateProgress.apply(_this, arguments); + }; + this.renderProgressState = function () { + return Problem.prototype.renderProgressState.apply(_this, arguments); + }; + this.bind = function () { + return Problem.prototype.bind.apply(_this, arguments); + }; + this.el = $(element).find('.problems-wrapper'); + this.id = this.el.data('problem-id'); + this.element_id = this.el.attr('id'); + this.url = this.el.data('url'); + this.content = this.el.data('content'); + this.has_timed_out = false; + this.has_response = false; + this.render(this.content); + } + + Problem.prototype.$ = function (selector) { + return $(selector, this.el); + }; + + Problem.prototype.bind = function () { + var problem_prefix, + _this = this; + if (typeof MathJax !== "undefined" && MathJax !== null) { + this.el.find('.problem > div').each(function (index, element) { + return MathJax.Hub.Queue(["Typeset", MathJax.Hub, element]); + }); + } + window.update_schematics(); + problem_prefix = this.element_id.replace(/problem_/, ''); + this.inputs = this.$("[id^='input_" + problem_prefix + "_']"); + this.$('div.action button').click(this.refreshAnswers); + this.reviewButton = this.$('.notification-btn.review-btn'); + this.reviewButton.click(this.scroll_to_problem_meta); + this.submitButton = this.$('.action .submit'); + this.submitButtonLabel = this.$('.action .submit .submit-label'); + this.submitButtonSubmitText = this.submitButtonLabel.text(); + this.submitButtonSubmittingText = this.submitButton.data('submitting'); + this.submitButton.click(this.submit_fd); + this.hintButton = this.$('.action .hint-button'); + this.hintButton.click(this.hint_button); + this.resetButton = this.$('.action .reset'); + this.resetButton.click(this.reset); + this.showButton = this.$('.action .show'); + this.showButton.click(this.show); + this.saveButton = this.$('.action .save'); + this.saveNotification = this.$('.notification-save'); + this.saveButtonLabel = this.$('.action .save .save-label'); + this.saveButton.click(this.save); + this.gentleAlertNotification = this.$('.notification-gentle-alert'); + this.submitNotification = this.$('.notification-submit'); + this.$('.clarification').focus(function (ev) { + var icon; + icon = $(ev.target).children("i"); + return window.globalTooltipManager.openTooltip(icon); + }); + this.$('.clarification').blur(function (ev) { + return window.globalTooltipManager.hide(); + }); + this.$('.review-btn').focus(function (ev) { + return $(ev.target).removeClass('sr'); + }); + this.$('.review-btn').blur(function (ev) { + return $(ev.target).addClass('sr'); + }); + this.bindResetCorrectness(); + if (this.submitButton.length) { + this.submitAnswersAndSubmitButton(true); + } + Collapsible.setCollapsibles(this.el); + this.$('input.math').keyup(this.refreshMath); + if (typeof MathJax !== "undefined" && MathJax !== null) { + return this.$('input.math').each(function (index, element) { + return MathJax.Hub.Queue([_this.refreshMath, null, element]); + }); + } + }; + + Problem.prototype.renderProgressState = function () { + var a, detail, earned, graded, possible, progress, progress_template, status; + detail = this.el.data('progress_detail'); + status = this.el.data('progress_status'); + graded = this.el.data('graded'); + if (status !== 'none' && (detail != null) && (jQuery.type(detail) === "string") && detail.indexOf('/') > 0) { + a = detail.split('/'); + earned = parseFloat(a[0]); + possible = parseFloat(a[1]); + if (graded === "True" && possible !== 0) { + // Translators: %(earned)s is the number of points earned. %(possible)s is the total number of points (examples: 0/1, 1/1, 2/3, 5/10). The total number of points will always be at least 1. We pluralize based on the total number of points (example: 0/1 point; 1/2 points); + progress_template = ngettext('%(earned)s/%(possible)s point (graded)', '%(earned)s/%(possible)s points (graded)', possible); + } else { + // Translators: %(earned)s is the number of points earned. %(possible)s is the total number of points (examples: 0/1, 1/1, 2/3, 5/10). The total number of points will always be at least 1. We pluralize based on the total number of points (example: 0/1 point; 1/2 points); + progress_template = ngettext('%(earned)s/%(possible)s point (ungraded)', '%(earned)s/%(possible)s points (ungraded)', possible); + } + progress = interpolate(progress_template, { + 'earned': earned, + 'possible': possible + }, true); + } + if (status === 'none' || status === 0) { + if ((detail != null) && (jQuery.type(detail) === "string") && detail.indexOf('/') > 0) { + a = detail.split('/'); + possible = parseFloat(a[1]); + } else { + possible = 0; + } + if (graded === "True" && possible !== 0) { + // Translators: %(num_points)s is the number of points possible (examples: 1, 3, 10).; + progress_template = ngettext("%(num_points)s point possible (graded)", "%(num_points)s points possible (graded)", possible); + } else { + // Translators: %(num_points)s is the number of points possible (examples: 1, 3, 10).; + progress_template = ngettext("%(num_points)s point possible (ungraded)", "%(num_points)s points possible (ungraded)", possible); + } + progress = interpolate(progress_template, { + 'num_points': possible + }, true); + } + return this.$('.problem-progress').text(progress); + }; + + Problem.prototype.updateProgress = function (response) { + if (response.progress_changed) { + this.el.data('progress_status', response.progress_status); + this.el.data('progress_detail', response.progress_detail); + this.el.trigger('progressChanged'); + } + return this.renderProgressState(); + }; + + Problem.prototype.forceUpdate = function (response) { + this.el.data('progress_status', response.progress_status); + this.el.data('progress_detail', response.progress_detail); + this.el.trigger('progressChanged'); + return this.renderProgressState(); + }; + + Problem.prototype.queueing = function (focus_callback) { + var _this = this; + this.queued_items = this.$(".xqueue"); + this.num_queued_items = this.queued_items.length; + if (this.num_queued_items > 0) { + if (window.queuePollerID) { + window.clearTimeout(window.queuePollerID); + } + return window.queuePollerID = window.setTimeout(function () { + return _this.poll(1000, focus_callback); + }, 1000); + } + }; + + Problem.prototype.poll = function (prev_timeout, focus_callback) { + var _this = this; + return $.postWithPrefix("" + this.url + "/problem_get", function (response) { + var new_timeout; + _this.new_queued_items = $(response.html).find(".xqueue"); + if (_this.new_queued_items.length !== _this.num_queued_items) { + edx.HtmlUtils.setHtml(_this.el, edx.HtmlUtils.HTML(response.html)).promise().done(function () { + return typeof focus_callback === "function" ? focus_callback() : void 0; + }); + JavascriptLoader.executeModuleScripts(_this.el, function () { + _this.setupInputTypes(); + return _this.bind(); + }); + } + _this.num_queued_items = _this.new_queued_items.length; + if (_this.num_queued_items === 0) { + _this.forceUpdate(response); + return delete window.queuePollerID; + } else { + new_timeout = prev_timeout * 2; + if (new_timeout >= 60000) { + delete window.queuePollerID; + return _this.gentle_alert(gettext("The grading process is still running. Refresh the page to see updates.")); + } else { + return window.queuePollerID = window.setTimeout(function () { + return _this.poll(new_timeout, focus_callback); + }, new_timeout); + } + } + }); + }; + + Problem.inputAjax = function (url, input_id, dispatch, data, callback) { + data['dispatch'] = dispatch; + data['input_id'] = input_id; + return $.postWithPrefix("" + url + "/input_ajax", data, callback); + }; + + Problem.prototype.render = function (content, focus_callback) { + var _this = this; + if (content) { + this.el.html(content); + return JavascriptLoader.executeModuleScripts(this.el, function () { + _this.setupInputTypes(); + _this.bind(); + _this.queueing(focus_callback); + _this.renderProgressState(); + return typeof focus_callback === "function" ? focus_callback() : void 0; + }); + } else { + return $.postWithPrefix("" + this.url + "/problem_get", function (response) { + _this.el.html(response.html); + return JavascriptLoader.executeModuleScripts(_this.el, function () { + _this.setupInputTypes(); + _this.bind(); + _this.queueing(); + return _this.forceUpdate(response); + }); + }); + } + }; + + Problem.prototype.setupInputTypes = function () { + var _this = this; + this.inputtypeDisplays = {}; + return this.el.find(".capa_inputtype").each(function (index, inputtype) { + var classes, cls, id, setupMethod, _i, _len, _results; + classes = $(inputtype).attr('class').split(' '); + id = $(inputtype).attr('id'); + _results = []; + for (_i = 0, _len = classes.length; _i < _len; _i++) { + cls = classes[_i]; + setupMethod = _this.inputtypeSetupMethods[cls]; + if (setupMethod != null) { + _results.push(_this.inputtypeDisplays[id] = setupMethod(inputtype)); + } else { + _results.push(void 0); + } + } + return _results; + }); + }; + + Problem.prototype.submit_save_waitfor = function (callback) { + var flag, inp, _i, _len, _ref, + _this = this; + flag = false; + _ref = this.inputs; + for (_i = 0, _len = _ref.length; _i < _len; _i++) { + inp = _ref[_i]; + if ($(inp).is("input[waitfor]")) { + try { + $(inp).data("waitfor")(function () { + _this.refreshAnswers(); + return callback(); + }); + } catch (e) { + if (e.name === "Waitfor Exception") { + alert(e.message); + } else { + alert("Could not grade your answer. The submission was aborted."); + } + throw e; + } + flag = true; + } else { + flag = false; + } + } + return flag; + }; + + Problem.prototype.scroll_to_problem_meta = function () { + var questionTitle; + questionTitle = this.$(".problem-header"); + if (questionTitle.length > 0) { + $('html, body').animate({ + scrollTop: questionTitle.offset().top + }, 500); + return questionTitle.focus(); + } + }; + + Problem.prototype.focus_on_notification = function (type) { + var notification; + notification = this.$('.notification-' + type); + if (notification.length > 0) { + return notification.focus(); + } + }; + + Problem.prototype.focus_on_submit_notification = function () { + return this.focus_on_notification('submit'); + }; + + Problem.prototype.focus_on_hint_notification = function () { + return this.focus_on_notification('hint'); + }; + + Problem.prototype.focus_on_save_notification = function () { + return this.focus_on_notification('save'); + }; + + /* + # 'submit_fd' uses FormData to allow file submissions in the 'problem_check' dispatch, + # in addition to simple querystring-based answers + # + # NOTE: The dispatch 'problem_check' is being singled out for the use of FormData; + # maybe preferable to consolidate all dispatches to use FormData + */ + + + Problem.prototype.submit_fd = function () { + var abort_submission, error, error_html, errors, fd, file_not_selected, file_too_large, max_filesize, required_files_not_submitted, settings, timeout_id, unallowed_file_submitted, _i, _len, + _this = this; + if (this.el.find('input:file').length === 0) { + this.submit(); + return; + } + this.enableSubmitButton(false); + if (!window.FormData) { + alert("Submission aborted! Sorry, your browser does not support file uploads. If you can, please use Chrome or Safari which have been verified to support file uploads."); + this.enableSubmitButton(true); + return; + } + timeout_id = this.enableSubmitButtonAfterTimeout(); + fd = new FormData(); + max_filesize = 4 * 1000 * 1000; + file_too_large = false; + file_not_selected = false; + required_files_not_submitted = false; + unallowed_file_submitted = false; + errors = []; + this.inputs.each(function (index, element) { + var allowed_files, file, max_size, required_files, _i, _len, _ref, _ref1, _ref2; + if (element.type === 'file') { + required_files = $(element).data("required_files"); + allowed_files = $(element).data("allowed_files"); + _ref = element.files; + for (_i = 0, _len = _ref.length; _i < _len; _i++) { + file = _ref[_i]; + if (allowed_files.length !== 0 && (_ref1 = file.name, __indexOf.call(allowed_files, _ref1) < 0)) { + unallowed_file_submitted = true; + errors.push("You submitted " + file.name + "; only " + allowed_files + " are allowed."); + } + if (_ref2 = file.name, __indexOf.call(required_files, _ref2) >= 0) { + required_files.splice(required_files.indexOf(file.name), 1); + } + if (file.size > max_filesize) { + file_too_large = true; + max_size = max_filesize / (1000 * 1000); + errors.push("Your file " + file.name + " is too large (max size: {max_size}MB)"); + } + fd.append(element.id, file); + } + if (element.files.length === 0) { + file_not_selected = true; + fd.append(element.id, ''); + } + if (required_files.length !== 0) { + required_files_not_submitted = true; + return errors.push("You did not submit the required files: " + required_files + "."); + } + } else { + return fd.append(element.id, element.value); + } + }); + if (file_not_selected) { + errors.push('You did not select any files to submit'); + } + error_html = '
    \n'; + for (_i = 0, _len = errors.length; _i < _len; _i++) { + error = errors[_i]; + error_html += '
  • ' + error + '
  • \n'; + } + error_html += '
'; + this.gentle_alert(error_html); + abort_submission = file_too_large || file_not_selected || unallowed_file_submitted || required_files_not_submitted; + if (abort_submission) { + window.clearTimeout(timeout_id); + this.enableSubmitButton(true); + return; + } + settings = { + type: "POST", + data: fd, + processData: false, + contentType: false, + complete: this.enableSubmitButtonAfterResponse, + success: function (response) { + switch (response.success) { + case 'incorrect': + case 'correct': + _this.render(response.contents); + _this.updateProgress(response); + break; + default: + _this.gentle_alert(response.success); + } + return Logger.log('problem_graded', [_this.answers, response.contents], _this.id); + } + }; + return $.ajaxWithPrefix("" + this.url + "/problem_check", settings); + }; + + Problem.prototype.submit = function () { + if (!this.submit_save_waitfor(this.submit_internal)) { + return this.disableAllButtonsWhileRunning(this.submit_internal, true); + } + }; + + Problem.prototype.submit_internal = function () { + var _this = this; + Logger.log('problem_check', this.answers); + return $.postWithPrefix("" + this.url + "/problem_check", this.answers, function (response) { + switch (response.success) { + case 'incorrect': + case 'correct': + window.SR.readTexts(_this.get_sr_status(response.contents)); + _this.el.trigger('contentChanged', [_this.id, response.contents]); + _this.render(response.contents, _this.focus_on_submit_notification); + _this.updateProgress(response); + break; + default: + _this.saveNotification.hide(); + _this.gentle_alert(response.success); + } + return Logger.log('problem_graded', [_this.answers, response.contents], _this.id); + }); + }; + + Problem.prototype.get_sr_status = function (contents) { + var added_status, aria_label, element, labeled_status, parent_section, status_elements, template, _i, _len; + status_elements = $(contents).find('.status'); + labeled_status = []; + for (_i = 0, _len = status_elements.length; _i < _len; _i++) { + element = status_elements[_i]; + parent_section = $(element).closest('section'); + added_status = false; + if (parent_section) { + aria_label = parent_section.attr('aria-label'); + if (aria_label) { + // Translators: This is only translated to allow for reording of label and associated status.; + template = gettext("{label}: {status}"); + labeled_status.push(edx.StringUtils.interpolate(template, { + label: aria_label, + status: $(element).text() + })); + added_status = true; + } + } + if (!added_status) { + labeled_status.push($(element).text()); + } + } + return labeled_status; + }; + + Problem.prototype.reset = function () { + return this.disableAllButtonsWhileRunning(this.reset_internal, false); + }; + + Problem.prototype.reset_internal = function () { + var _this = this; + Logger.log('problem_reset', this.answers); + return $.postWithPrefix("" + this.url + "/problem_reset", { + id: this.id + }, function (response) { + if (response.success) { + _this.el.trigger('contentChanged', [_this.id, response.html]); + _this.render(response.html, _this.scroll_to_problem_meta); + _this.updateProgress(response); + return window.SR.readText(gettext('This problem has been reset.')); + } else { + return _this.gentle_alert(response.msg); + } + }); + }; + + Problem.prototype.show = function () { + var _this = this; + Logger.log('problem_show', { + problem: this.id + }); + return $.postWithPrefix("" + this.url + "/problem_show", function (response) { + var answers; + answers = response.answers; + $.each(answers, function (key, value) { + var answer, choice, solution, _i, _len, _results; + if ($.isArray(value)) { + _results = []; + for (_i = 0, _len = value.length; _i < _len; _i++) { + choice = value[_i]; + _results.push(_this.$("label[for='input_" + key + "_" + choice + "']").attr({ + correct_answer: 'true' + })); + } + return _results; + } else { + answer = _this.$("#answer_" + key + ", #solution_" + key); + edx.HtmlUtils.setHtml(answer, edx.HtmlUtils.HTML(value)); + Collapsible.setCollapsibles(answer); + try { + return solution = $(value).find('.detailed-solution'); + } catch (e) { + return solution = {}; + } + } + }); + _this.el.find(".capa_inputtype").each(function (index, inputtype) { + var classes, cls, display, showMethod, _i, _len, _results; + classes = $(inputtype).attr('class').split(' '); + _results = []; + for (_i = 0, _len = classes.length; _i < _len; _i++) { + cls = classes[_i]; + display = _this.inputtypeDisplays[$(inputtype).attr('id')]; + showMethod = _this.inputtypeShowAnswerMethods[cls]; + if (showMethod != null) { + _results.push(showMethod(inputtype, display, answers)); + } else { + _results.push(void 0); + } + } + return _results; + }); + if (typeof MathJax !== "undefined" && MathJax !== null) { + _this.el.find('.problem > div').each(function (index, element) { + return MathJax.Hub.Queue(["Typeset", MathJax.Hub, element]); + }); + } + _this.el.find('.show').attr('disabled', 'disabled'); + _this.updateProgress(response); + window.SR.readText(gettext('Answers to this problem are now shown. Navigate through the problem to review it with answers inline.')); + return _this.scroll_to_problem_meta(); + }); + }; + + Problem.prototype.clear_all_notifications = function () { + this.submitNotification.remove(); + this.gentleAlertNotification.hide(); + return this.saveNotification.hide(); + }; + + Problem.prototype.gentle_alert = function (msg) { + edx.HtmlUtils.setHtml(this.el.find('.notification-gentle-alert .notification-message'), edx.HtmlUtils.HTML(msg)); + this.clear_all_notifications(); + this.gentleAlertNotification.show(); + return this.gentleAlertNotification.focus(); + }; + + Problem.prototype.save = function () { + if (!this.submit_save_waitfor(this.save_internal)) { + return this.disableAllButtonsWhileRunning(this.save_internal, false); + } + }; + + Problem.prototype.save_internal = function () { + var _this = this; + Logger.log('problem_save', this.answers); + return $.postWithPrefix("" + this.url + "/problem_save", this.answers, function (response) { + var saveMessage; + saveMessage = response.msg; + if (response.success) { + _this.el.trigger('contentChanged', [_this.id, response.html]); + edx.HtmlUtils.setHtml(_this.el.find('.notification-save .notification-message'), edx.HtmlUtils.HTML(saveMessage)); + _this.clear_all_notifications(); + _this.saveNotification.show(); + return _this.focus_on_save_notification(); + } else { + return _this.gentle_alert(saveMessage); + } + }); + }; + + Problem.prototype.refreshMath = function (event, element) { + var elid, eqn, jax, mathjax_preprocessor, preprocessor_tag, target; + if (!element) { + element = event.target; + } + elid = element.id.replace(/^input_/, ''); + target = "display_" + elid; + preprocessor_tag = "inputtype_" + elid; + mathjax_preprocessor = this.inputtypeDisplays[preprocessor_tag]; + if ((typeof MathJax !== "undefined" && MathJax !== null) && (jax = MathJax.Hub.getAllJax(target)[0])) { + eqn = $(element).val(); + if (mathjax_preprocessor) { + eqn = mathjax_preprocessor(eqn); + } + MathJax.Hub.Queue(['Text', jax, eqn], [this.updateMathML, jax, element]); + } + }; + + Problem.prototype.updateMathML = function (jax, element) { + try { + return $("#" + element.id + "_dynamath").val(jax.root.toMathML('')); + } catch (exception) { + if (!exception.restart) { + throw exception; + } + if (typeof MathJax !== "undefined" && MathJax !== null) { + return MathJax.Callback.After([this.refreshMath, jax], exception.restart); + } + } + }; + + Problem.prototype.refreshAnswers = function () { + this.$('input.schematic').each(function (index, element) { + return element.schematic.update_value(); + }); + this.$(".CodeMirror").each(function (index, element) { + if (element.CodeMirror.save) { + return element.CodeMirror.save(); + } + }); + return this.answers = this.inputs.serialize(); + }; + + Problem.prototype.submitAnswersAndSubmitButton = function (bind) { + var answered, at_least_one_text_input_found, one_text_input_filled, + _this = this; + if (bind == null) { + bind = false; + } + "Used to check available answers and if something is checked (or the answer is set in some textbox)\n\"Submit\" button becomes enabled. Otherwise it is disabled by default.\n\nArguments:\n bind (bool): used on the first check to attach event handlers to input fields\n to change \"Submit\" enable status in case of some manipulations with answers"; + answered = true; + at_least_one_text_input_found = false; + one_text_input_filled = false; + this.el.find("input:text").each(function (i, text_field) { + if ($(text_field).is(':visible')) { + at_least_one_text_input_found = true; + if ($(text_field).val() !== '') { + one_text_input_filled = true; + } + if (bind) { + $(text_field).on('input', function (e) { + _this.saveNotification.hide(); + _this.submitAnswersAndSubmitButton(); + }); + } + } + }); + if (at_least_one_text_input_found && !one_text_input_filled) { + answered = false; + } + this.el.find(".choicegroup").each(function (i, choicegroup_block) { + var checked; + checked = false; + $(choicegroup_block).find("input[type=checkbox], input[type=radio]").each(function (j, checkbox_or_radio) { + if ($(checkbox_or_radio).is(':checked')) { + checked = true; + } + if (bind) { + $(checkbox_or_radio).on('click', function (e) { + _this.saveNotification.hide(); + _this.submitAnswersAndSubmitButton(); + }); + } + }); + if (!checked) { + answered = false; + } + }); + this.el.find("select").each(function (i, select_field) { + var selected_option; + selected_option = $(select_field).find("option:selected").text().trim(); + if (selected_option === 'Select an option') { + answered = false; + } + if (bind) { + $(select_field).on('change', function (e) { + _this.saveNotification.hide(); + _this.submitAnswersAndSubmitButton(); + }); + } + }); + if (answered) { + return this.enableSubmitButton(true); + } else { + return this.enableSubmitButton(false, false); + } + }; + + Problem.prototype.bindResetCorrectness = function () { + var $inputtypes, + _this = this; + $inputtypes = this.el.find(".capa_inputtype").add(this.el.find(".inputtype")); + return $inputtypes.each(function (index, inputtype) { + var bindMethod, classes, cls, _i, _len, _results; + classes = $(inputtype).attr('class').split(' '); + _results = []; + for (_i = 0, _len = classes.length; _i < _len; _i++) { + cls = classes[_i]; + bindMethod = _this.bindResetCorrectnessByInputtype[cls]; + if (bindMethod != null) { + _results.push(bindMethod(inputtype)); + } else { + _results.push(void 0); + } + } + return _results; + }); + }; + + Problem.prototype.bindResetCorrectnessByInputtype = { + formulaequationinput: function (element) { + return $(element).find('input').on('input', function () { + var $p; + $p = $(element).find('span.status'); + // Translators: the word unanswered here is about answering a problem the student must solve.; + return $p.parent().removeClass().addClass("unsubmitted"); + }); + }, + choicegroup: function (element) { + var $element, id; + $element = $(element); + id = ($element.attr('id').match(/^inputtype_(.*)$/))[1]; + return $element.find('input').on('change', function () { + var $status; + $status = $("#status_" + id); + if ($status[0]) { + $status.removeClass().addClass("unanswered"); + $status.empty().css('display', 'inline-block'); + } else { + $("", { + "class": "unanswered", + "style": "display: inline-block;", + "id": "status_" + id + }); + } + return $element.find("label").removeClass(); + }); + }, + 'option-input': function (element) { + var $select, id; + $select = $(element).find('select'); + id = ($select.attr('id').match(/^input_(.*)$/))[1]; + return $select.on('change', function () { + var $status; + return $status = $("#status_" + id).removeClass().addClass("unanswered").find('span').text(gettext('Status: unsubmitted')); + }); + }, + textline: function (element) { + return $(element).find('input').on('input', function () { + var $p; + $p = $(element).find('span.status'); + // Translators: the word unanswered here is about answering a problem the student must solve.; + return $p.parent().removeClass("correct incorrect").addClass("unsubmitted"); + }); + } + }; + + Problem.prototype.inputtypeSetupMethods = { + 'text-input-dynamath': function (element) { + /* + Return: function (eqn) -> eqn that preprocesses the user formula input before + it is fed into MathJax. Return 'false' if no preprocessor specified + */ + + var data, preprocessor, preprocessorClass, preprocessorClassName; + data = $(element).find('.text-input-dynamath_data'); + preprocessorClassName = data.data('preprocessor'); + preprocessorClass = window[preprocessorClassName]; + if (preprocessorClass == null) { + return false; + } else { + preprocessor = new preprocessorClass(); + return preprocessor.fn; + } + }, + javascriptinput: function (element) { + var container, data, display, displayClass, evaluation, params, problemState, submission, submissionField; + data = $(element).find(".javascriptinput_data"); + params = data.data("params"); + submission = data.data("submission"); + evaluation = data.data("evaluation"); + problemState = data.data("problem_state"); + displayClass = window[data.data('display_class')]; + if (evaluation === '') { + evaluation = null; + } + container = $(element).find(".javascriptinput_container"); + submissionField = $(element).find(".javascriptinput_input"); + display = new displayClass(problemState, submission, evaluation, container, submissionField, params); + display.render(); + return display; + }, + cminput: function (container) { + var CodeMirrorEditor, CodeMirrorTextArea, element, id, linenumbers, mode, spaces, tabsize; + element = $(container).find("textarea"); + tabsize = element.data("tabsize"); + mode = element.data("mode"); + linenumbers = element.data("linenums"); + spaces = Array(parseInt(tabsize) + 1).join(" "); + CodeMirrorEditor = CodeMirror.fromTextArea(element[0], { + lineNumbers: linenumbers, + indentUnit: tabsize, + tabSize: tabsize, + mode: mode, + matchBrackets: true, + lineWrapping: true, + indentWithTabs: false, + smartIndent: false, + extraKeys: { + "Esc": function (cm) { + $(".grader-status").focus(); + return false; + }, + "Tab": function (cm) { + cm.replaceSelection(spaces, "end"); + return false; + } + } + }); + id = element.attr("id").replace(/^input_/, ""); + CodeMirrorTextArea = CodeMirrorEditor.getInputField(); + CodeMirrorTextArea.setAttribute("id", "cm-textarea-" + id); + CodeMirrorTextArea.setAttribute("aria-describedby", "cm-editor-exit-message-" + id + " status_" + id); + return CodeMirrorEditor; + } + }; + + Problem.prototype.inputtypeShowAnswerMethods = { + choicegroup: function (element, display, answers) { + var answer, choice, input_id, _i, _len, _results; + element = $(element); + input_id = element.attr('id').replace(/inputtype_/, ''); + answer = answers[input_id]; + _results = []; + for (_i = 0, _len = answer.length; _i < _len; _i++) { + choice = answer[_i]; + _results.push(element.find("#input_" + input_id + "_" + choice).parent("label").addClass('choicegroup_correct')); + } + return _results; + }, + javascriptinput: function (element, display, answers) { + var answer, answer_id; + answer_id = $(element).attr('id').split("_").slice(1).join("_"); + answer = JSON.parse(answers[answer_id]); + return display.showAnswer(answer); + }, + choicetextgroup: function (element, display, answers) { + var answer, choice, input_id, _i, _len, _results; + element = $(element); + input_id = element.attr('id').replace(/inputtype_/, ''); + answer = answers[input_id]; + _results = []; + for (_i = 0, _len = answer.length; _i < _len; _i++) { + choice = answer[_i]; + _results.push(element.find("section#forinput" + choice).addClass('choicetextgroup_show_correct')); + } + return _results; + }, + imageinput: function (element, display, answers) { + var canvas, container, ctx, id, types; + types = { + rectangle: function (ctx, coords) { + var rects, reg; + reg = /^\(([0-9]+),([0-9]+)\)-\(([0-9]+),([0-9]+)\)$/; + rects = coords.replace(/\s*/g, '').split(/;/); + $.each(rects, function (index, rect) { + var abs, height, points, width; + abs = Math.abs; + points = reg.exec(rect); + if (points) { + width = abs(points[3] - points[1]); + height = abs(points[4] - points[2]); + return ctx.rect(points[1], points[2], width, height); + } + }); + ctx.stroke(); + return ctx.fill(); + }, + regions: function (ctx, coords) { + var parseCoords; + parseCoords = function (coords) { + var reg; + reg = JSON.parse(coords); + if (typeof reg[0][0][0] === "undefined") { + reg = [reg]; + } + return reg; + }; + return $.each(parseCoords(coords), function (index, region) { + ctx.beginPath(); + $.each(region, function (index, point) { + if (index === 0) { + return ctx.moveTo(point[0], point[1]); + } else { + return ctx.lineTo(point[0], point[1]); + } + }); + ctx.closePath(); + ctx.stroke(); + return ctx.fill(); + }); + } + }; + element = $(element); + id = element.attr('id').replace(/inputtype_/, ''); + container = element.find("#answer_" + id); + canvas = document.createElement('canvas'); + canvas.width = container.data('width'); + canvas.height = container.data('height'); + if (canvas.getContext) { + ctx = canvas.getContext('2d'); + } else { + return console.log('Canvas is not supported.'); + } + ctx.fillStyle = 'rgba(255,255,255,.3)'; + ctx.strokeStyle = "#FF0000"; + ctx.lineWidth = "2"; + if (answers[id]) { + $.each(answers[id], function (key, value) { + if ((types[key] != null) && value) { + return types[key](ctx, value); + } + }); + return container.html(canvas); + } else { + return console.log("Answer is absent for image input with id=" + id); + } + } + }; + + Problem.prototype.inputtypeHideAnswerMethods = { + choicegroup: function (element, display) { + element = $(element); + return element.find('label').removeClass('choicegroup_correct'); + }, + javascriptinput: function (element, display) { + return display.hideAnswer(); + }, + choicetextgroup: function (element, display) { + element = $(element); + return element.find("section[id^='forinput']").removeClass('choicetextgroup_show_correct'); + } + }; + + Problem.prototype.disableAllButtonsWhileRunning = function (operationCallback, isFromCheckOperation) { + var _this = this; + this.enableAllButtons(false, isFromCheckOperation); + return operationCallback().always(function () { + return _this.enableAllButtons(true, isFromCheckOperation); + }); + }; + + Problem.prototype.enableAllButtons = function (enable, isFromCheckOperation) { + if (enable) { + this.resetButton.add(this.saveButton).add(this.hintButton).add(this.showButton).removeAttr('disabled'); + } else { + this.resetButton.add(this.saveButton).add(this.hintButton).add(this.showButton).attr({ + 'disabled': 'disabled' + }); + } + return this.enableSubmitButton(enable, isFromCheckOperation); + }; + + Problem.prototype.enableSubmitButton = function (enable, changeText) { + var submitCanBeEnabled; + if (changeText == null) { + changeText = true; + } + if (enable) { + submitCanBeEnabled = this.submitButton.data('should-enable-submit-button') === 'True'; + if (submitCanBeEnabled) { + this.submitButton.removeAttr('disabled'); + } + if (changeText) { + return this.submitButtonLabel.text(this.submitButtonSubmitText); + } + } else { + this.submitButton.attr({ + 'disabled': 'disabled' + }); + if (changeText) { + return this.submitButtonLabel.text(this.submitButtonSubmittingText); + } + } + }; + + Problem.prototype.enableSubmitButtonAfterResponse = function () { + this.has_response = true; + if (!this.has_timed_out) { + return this.enableSubmitButton(false); + } else { + return this.enableSubmitButton(true); + } + }; + + Problem.prototype.enableSubmitButtonAfterTimeout = function () { + var enableSubmitButton, + _this = this; + this.has_timed_out = false; + this.has_response = false; + enableSubmitButton = function () { + _this.has_timed_out = true; + if (_this.has_response) { + return _this.enableSubmitButton(true); + } + }; + return window.setTimeout(enableSubmitButton, 750); + }; + + Problem.prototype.hint_button = function () { + var hint_container, hint_index, next_index, + _this = this; + hint_container = this.$('.problem-hint'); + hint_index = hint_container.attr('hint_index'); + if (hint_index === void 0) { + next_index = 0; + } else { + next_index = parseInt(hint_index) + 1; + } + return $.postWithPrefix("" + this.url + "/hint_button", { + hint_index: next_index, + input_id: this.id + }, function (response) { + var hint_msg_container; + if (response.success) { + hint_msg_container = _this.$('.problem-hint .notification-message'); + hint_container.attr('hint_index', response.hint_index); + edx.HtmlUtils.setHtml(hint_msg_container, edx.HtmlUtils.HTML(response.msg)); + MathJax.Hub.Queue(['Typeset', MathJax.Hub, hint_container[0]]); + if (response.should_enable_next_hint) { + _this.hintButton.removeAttr('disabled'); + } else { + _this.hintButton.attr({ + 'disabled': 'disabled' + }); + } + _this.el.find('.notification-hint').show(); + return _this.focus_on_hint_notification(); + } else { + return _this.gentle_alert(response.msg); + } + }); + }; + + return Problem; + + }).call(this); + +}).call(this); diff --git a/common/lib/xmodule/xmodule/js/src/sequence/display.coffee b/common/lib/xmodule/xmodule/js/src/sequence/display.coffee index 00fc539b8e..7b416e8160 100644 --- a/common/lib/xmodule/xmodule/js/src/sequence/display.coffee +++ b/common/lib/xmodule/xmodule/js/src/sequence/display.coffee @@ -216,7 +216,7 @@ class @Sequence widget_placement: widget_placement # On Sequence change, destroy any existing polling thread - # for queued submissions, see ../capa/display.coffee + # for queued submissions, see ../capa/display.js if window.queuePollerID window.clearTimeout(window.queuePollerID) delete window.queuePollerID From a1d3c3168102f6ac705232258f1b8c705cb66781 Mon Sep 17 00:00:00 2001 From: cahrens Date: Fri, 7 Oct 2016 13:52:34 -0400 Subject: [PATCH 4/4] eslint fixes and other cleanup --- .eslintignore | 1 - .../xmodule/xmodule/js/src/capa/display.js | 1346 ++++++++++------- 2 files changed, 763 insertions(+), 584 deletions(-) diff --git a/.eslintignore b/.eslintignore index e0e659a325..cb233970d6 100644 --- a/.eslintignore +++ b/.eslintignore @@ -45,7 +45,6 @@ common/lib/xmodule/xmodule/js/spec/problem/edit_spec_hint.js common/lib/xmodule/xmodule/js/spec/tabs/edit.js common/lib/xmodule/xmodule/js/src/annotatable/display.js -common/lib/xmodule/xmodule/js/src/capa/display.js common/lib/xmodule/xmodule/js/src/conditional/display.js common/lib/xmodule/xmodule/js/src/discussion/display.js common/lib/xmodule/xmodule/js/src/html/display.js diff --git a/common/lib/xmodule/xmodule/js/src/capa/display.js b/common/lib/xmodule/xmodule/js/src/capa/display.js index a52802afe7..edd233b5e0 100644 --- a/common/lib/xmodule/xmodule/js/src/capa/display.js +++ b/common/lib/xmodule/xmodule/js/src/capa/display.js @@ -1,151 +1,166 @@ -// Generated by CoffeeScript 1.6.1 -(function () { - var _this = this, - __indexOf = [].indexOf || function (item) { - for (var i = 0, l = this.length; i < l; i++) { - if (i in this && this[i] === item) return i; +/* global MathJax, Collapsible, interpolate, JavascriptLoader, Logger, CodeMirror */ +// Note: this code was originally converted from CoffeeScript, and thus follows some +// coding conventions that are discouraged by eslint. Some warnings have been suppressed +// to avoid substantial rewriting of the code. Allow the eslint suppressions to exceed +// the max line length of 120. +/* eslint max-len: ["error", 120, { "ignoreComments": true }] */ + +(function() { + 'use strict'; + var indexOfHelper = [].indexOf || + function(item) { + var i, len; + for (i = 0, len = this.length; i < len; i++) { + if (i in this && this[i] === item) { + return i; } - return -1; - }; - - this.Problem = (function () { - var _this = this; + } + return -1; + }; + this.Problem = (function() { function Problem(element) { - var _this = this; - this.hint_button = function () { - return Problem.prototype.hint_button.apply(_this, arguments); + var that = this; + this.hint_button = function() { + return Problem.prototype.hint_button.apply(that, arguments); }; - this.enableSubmitButtonAfterTimeout = function () { - return Problem.prototype.enableSubmitButtonAfterTimeout.apply(_this, arguments); + this.enableSubmitButtonAfterTimeout = function() { + return Problem.prototype.enableSubmitButtonAfterTimeout.apply(that, arguments); }; - this.enableSubmitButtonAfterResponse = function () { - return Problem.prototype.enableSubmitButtonAfterResponse.apply(_this, arguments); + this.enableSubmitButtonAfterResponse = function() { + return Problem.prototype.enableSubmitButtonAfterResponse.apply(that, arguments); }; - this.enableSubmitButton = function (enable, changeText) { - if (changeText == null) { - changeText = true; + this.enableSubmitButton = function(enable, changeText) { + if (changeText === null || changeText === undefined) { + changeText = true; // eslint-disable-line no-param-reassign } - return Problem.prototype.enableSubmitButton.apply(_this, arguments); + return Problem.prototype.enableSubmitButton.apply(that, arguments); }; - this.enableAllButtons = function (enable, isFromCheckOperation) { - return Problem.prototype.enableAllButtons.apply(_this, arguments); + this.enableAllButtons = function(enable, isFromCheckOperation) { // eslint-disable-line no-unused-vars + return Problem.prototype.enableAllButtons.apply(that, arguments); }; - this.disableAllButtonsWhileRunning = function (operationCallback, isFromCheckOperation) { - return Problem.prototype.disableAllButtonsWhileRunning.apply(_this, arguments); + this.disableAllButtonsWhileRunning = function( + operationCallback, isFromCheckOperation // eslint-disable-line no-unused-vars + ) { + return Problem.prototype.disableAllButtonsWhileRunning.apply(that, arguments); }; - this.submitAnswersAndSubmitButton = function (bind) { - if (bind == null) { - bind = false; + this.submitAnswersAndSubmitButton = function(bind) { + if (bind === null || bind === undefined) { + bind = false; // eslint-disable-line no-param-reassign } - return Problem.prototype.submitAnswersAndSubmitButton.apply(_this, arguments); + return Problem.prototype.submitAnswersAndSubmitButton.apply(that, arguments); }; - this.refreshAnswers = function () { - return Problem.prototype.refreshAnswers.apply(_this, arguments); + this.refreshAnswers = function() { + return Problem.prototype.refreshAnswers.apply(that, arguments); }; - this.updateMathML = function (jax, element) { - return Problem.prototype.updateMathML.apply(_this, arguments); + this.updateMathML = function(jax, el) { // eslint-disable-line no-unused-vars + return Problem.prototype.updateMathML.apply(that, arguments); }; - this.refreshMath = function (event, element) { - return Problem.prototype.refreshMath.apply(_this, arguments); + this.refreshMath = function(event, el) { // eslint-disable-line no-unused-vars + return Problem.prototype.refreshMath.apply(that, arguments); }; - this.save_internal = function () { - return Problem.prototype.save_internal.apply(_this, arguments); + this.save_internal = function() { + return Problem.prototype.save_internal.apply(that, arguments); }; - this.save = function () { - return Problem.prototype.save.apply(_this, arguments); + this.save = function() { + return Problem.prototype.save.apply(that, arguments); }; - this.gentle_alert = function (msg) { - return Problem.prototype.gentle_alert.apply(_this, arguments); + this.gentle_alert = function(msg) { // eslint-disable-line no-unused-vars + return Problem.prototype.gentle_alert.apply(that, arguments); }; - this.clear_all_notifications = function () { - return Problem.prototype.clear_all_notifications.apply(_this, arguments); + this.clear_all_notifications = function() { + return Problem.prototype.clear_all_notifications.apply(that, arguments); }; - this.show = function () { - return Problem.prototype.show.apply(_this, arguments); + this.show = function() { + return Problem.prototype.show.apply(that, arguments); }; - this.reset_internal = function () { - return Problem.prototype.reset_internal.apply(_this, arguments); + this.reset_internal = function() { + return Problem.prototype.reset_internal.apply(that, arguments); }; - this.reset = function () { - return Problem.prototype.reset.apply(_this, arguments); + this.reset = function() { + return Problem.prototype.reset.apply(that, arguments); }; - this.get_sr_status = function (contents) { - return Problem.prototype.get_sr_status.apply(_this, arguments); + this.get_sr_status = function(contents) { // eslint-disable-line no-unused-vars + return Problem.prototype.get_sr_status.apply(that, arguments); }; - this.submit_internal = function () { - return Problem.prototype.submit_internal.apply(_this, arguments); + this.submit_internal = function() { + return Problem.prototype.submit_internal.apply(that, arguments); }; - this.submit = function () { - return Problem.prototype.submit.apply(_this, arguments); + this.submit = function() { + return Problem.prototype.submit.apply(that, arguments); }; - this.submit_fd = function () { - return Problem.prototype.submit_fd.apply(_this, arguments); + this.submit_fd = function() { + return Problem.prototype.submit_fd.apply(that, arguments); }; - this.focus_on_save_notification = function () { - return Problem.prototype.focus_on_save_notification.apply(_this, arguments); + this.focus_on_save_notification = function() { + return Problem.prototype.focus_on_save_notification.apply(that, arguments); }; - this.focus_on_hint_notification = function () { - return Problem.prototype.focus_on_hint_notification.apply(_this, arguments); + this.focus_on_hint_notification = function() { + return Problem.prototype.focus_on_hint_notification.apply(that, arguments); }; - this.focus_on_submit_notification = function () { - return Problem.prototype.focus_on_submit_notification.apply(_this, arguments); + this.focus_on_submit_notification = function() { + return Problem.prototype.focus_on_submit_notification.apply(that, arguments); }; - this.focus_on_notification = function (type) { - return Problem.prototype.focus_on_notification.apply(_this, arguments); + this.focus_on_notification = function(type) { // eslint-disable-line no-unused-vars + return Problem.prototype.focus_on_notification.apply(that, arguments); }; - this.scroll_to_problem_meta = function () { - return Problem.prototype.scroll_to_problem_meta.apply(_this, arguments); + this.scroll_to_problem_meta = function() { + return Problem.prototype.scroll_to_problem_meta.apply(that, arguments); }; - this.submit_save_waitfor = function (callback) { - return Problem.prototype.submit_save_waitfor.apply(_this, arguments); + this.submit_save_waitfor = function(callback) { // eslint-disable-line no-unused-vars + return Problem.prototype.submit_save_waitfor.apply(that, arguments); }; - this.setupInputTypes = function () { - return Problem.prototype.setupInputTypes.apply(_this, arguments); + this.setupInputTypes = function() { + return Problem.prototype.setupInputTypes.apply(that, arguments); }; - this.poll = function (prev_timeout, focus_callback) { - return Problem.prototype.poll.apply(_this, arguments); + this.poll = function(prevTimeout, focusCallback // eslint-disable-line no-unused-vars + ) { + return Problem.prototype.poll.apply(that, arguments); }; - this.queueing = function (focus_callback) { - return Problem.prototype.queueing.apply(_this, arguments); + this.queueing = function(focusCallback) { // eslint-disable-line no-unused-vars + return Problem.prototype.queueing.apply(that, arguments); }; - this.forceUpdate = function (response) { - return Problem.prototype.forceUpdate.apply(_this, arguments); + this.forceUpdate = function(response) { // eslint-disable-line no-unused-vars + return Problem.prototype.forceUpdate.apply(that, arguments); }; - this.updateProgress = function (response) { - return Problem.prototype.updateProgress.apply(_this, arguments); + this.updateProgress = function(response) { // eslint-disable-line no-unused-vars + return Problem.prototype.updateProgress.apply(that, arguments); }; - this.renderProgressState = function () { - return Problem.prototype.renderProgressState.apply(_this, arguments); + this.renderProgressState = function() { + return Problem.prototype.renderProgressState.apply(that, arguments); }; - this.bind = function () { - return Problem.prototype.bind.apply(_this, arguments); + this.bind = function() { + return Problem.prototype.bind.apply(that, arguments); }; this.el = $(element).find('.problems-wrapper'); this.id = this.el.data('problem-id'); this.element_id = this.el.attr('id'); this.url = this.el.data('url'); this.content = this.el.data('content'); + + // has_timed_out and has_response are used to ensure that + // we wait a minimum of ~ 1s before transitioning the submit + // button from disabled to enabled this.has_timed_out = false; this.has_response = false; this.render(this.content); } - Problem.prototype.$ = function (selector) { + Problem.prototype.$ = function(selector) { return $(selector, this.el); }; - Problem.prototype.bind = function () { - var problem_prefix, - _this = this; - if (typeof MathJax !== "undefined" && MathJax !== null) { - this.el.find('.problem > div').each(function (index, element) { - return MathJax.Hub.Queue(["Typeset", MathJax.Hub, element]); + Problem.prototype.bind = function() { + var problemPrefix, + that = this; + if (typeof MathJax !== 'undefined' && MathJax !== null) { + this.el.find('.problem > div').each(function(index, element) { + return MathJax.Hub.Queue(['Typeset', MathJax.Hub, element]); }); } window.update_schematics(); - problem_prefix = this.element_id.replace(/problem_/, ''); - this.inputs = this.$("[id^='input_" + problem_prefix + "_']"); + problemPrefix = this.element_id.replace(/problem_/, ''); + this.inputs = this.$('[id^="input_' + problemPrefix + '_"]'); this.$('div.action button').click(this.refreshAnswers); this.reviewButton = this.$('.notification-btn.review-btn'); this.reviewButton.click(this.scroll_to_problem_meta); @@ -162,22 +177,23 @@ this.showButton.click(this.show); this.saveButton = this.$('.action .save'); this.saveNotification = this.$('.notification-save'); - this.saveButtonLabel = this.$('.action .save .save-label'); this.saveButton.click(this.save); this.gentleAlertNotification = this.$('.notification-gentle-alert'); this.submitNotification = this.$('.notification-submit'); - this.$('.clarification').focus(function (ev) { + + // Accessibility helper for sighted keyboard users to show tooltips on focus: + this.$('.clarification').focus(function(ev) { var icon; - icon = $(ev.target).children("i"); + icon = $(ev.target).children('i'); return window.globalTooltipManager.openTooltip(icon); }); - this.$('.clarification').blur(function (ev) { + this.$('.clarification').blur(function() { return window.globalTooltipManager.hide(); }); - this.$('.review-btn').focus(function (ev) { + this.$('.review-btn').focus(function(ev) { return $(ev.target).removeClass('sr'); }); - this.$('.review-btn').blur(function (ev) { + this.$('.review-btn').blur(function(ev) { return $(ev.target).addClass('sr'); }); this.bindResetCorrectness(); @@ -186,56 +202,80 @@ } Collapsible.setCollapsibles(this.el); this.$('input.math').keyup(this.refreshMath); - if (typeof MathJax !== "undefined" && MathJax !== null) { - return this.$('input.math').each(function (index, element) { - return MathJax.Hub.Queue([_this.refreshMath, null, element]); + if (typeof MathJax !== 'undefined' && MathJax !== null) { + this.$('input.math').each(function(index, element) { + return MathJax.Hub.Queue([that.refreshMath, null, element]); }); } }; - Problem.prototype.renderProgressState = function () { - var a, detail, earned, graded, possible, progress, progress_template, status; + Problem.prototype.renderProgressState = function() { + var a, detail, earned, graded, possible, progress, progressTemplate, status; detail = this.el.data('progress_detail'); status = this.el.data('progress_status'); graded = this.el.data('graded'); - if (status !== 'none' && (detail != null) && (jQuery.type(detail) === "string") && detail.indexOf('/') > 0) { + + // Render 'x/y point(s)' if student has attempted question + if (status !== 'none' && (detail !== null && detail !== undefined) && (jQuery.type(detail) === 'string') && + detail.indexOf('/') > 0) { a = detail.split('/'); earned = parseFloat(a[0]); possible = parseFloat(a[1]); - if (graded === "True" && possible !== 0) { - // Translators: %(earned)s is the number of points earned. %(possible)s is the total number of points (examples: 0/1, 1/1, 2/3, 5/10). The total number of points will always be at least 1. We pluralize based on the total number of points (example: 0/1 point; 1/2 points); - progress_template = ngettext('%(earned)s/%(possible)s point (graded)', '%(earned)s/%(possible)s points (graded)', possible); + if (graded === 'True' && possible !== 0) { + progressTemplate = ngettext( + // This comment needs to be on one line to be properly scraped for the translators. + // Translators: %(earned)s is the number of points earned. %(possible)s is the total number of points (examples: 0/1, 1/1, 2/3, 5/10). The total number of points will always be at least 1. We pluralize based on the total number of points (example: 0/1 point; 1/2 points); + '%(earned)s/%(possible)s point (graded)', '%(earned)s/%(possible)s points (graded)', + possible + ); } else { - // Translators: %(earned)s is the number of points earned. %(possible)s is the total number of points (examples: 0/1, 1/1, 2/3, 5/10). The total number of points will always be at least 1. We pluralize based on the total number of points (example: 0/1 point; 1/2 points); - progress_template = ngettext('%(earned)s/%(possible)s point (ungraded)', '%(earned)s/%(possible)s points (ungraded)', possible); + progressTemplate = ngettext( + // This comment needs to be on one line to be properly scraped for the translators. + // Translators: %(earned)s is the number of points earned. %(possible)s is the total number of points (examples: 0/1, 1/1, 2/3, 5/10). The total number of points will always be at least 1. We pluralize based on the total number of points (example: 0/1 point; 1/2 points); + '%(earned)s/%(possible)s point (ungraded)', '%(earned)s/%(possible)s points (ungraded)', + possible + ); } - progress = interpolate(progress_template, { - 'earned': earned, - 'possible': possible - }, true); + progress = interpolate( + progressTemplate, { + earned: earned, + possible: possible + }, true + ); } + + // Render 'x point(s) possible' if student has not yet attempted question + // Status is set to none when a user has a score of 0, and 0 when the problem has a weight of 0. if (status === 'none' || status === 0) { - if ((detail != null) && (jQuery.type(detail) === "string") && detail.indexOf('/') > 0) { + if ((detail !== null && detail !== undefined) && (jQuery.type(detail) === 'string') && + detail.indexOf('/') > 0) { a = detail.split('/'); possible = parseFloat(a[1]); } else { possible = 0; } - if (graded === "True" && possible !== 0) { - // Translators: %(num_points)s is the number of points possible (examples: 1, 3, 10).; - progress_template = ngettext("%(num_points)s point possible (graded)", "%(num_points)s points possible (graded)", possible); + if (graded === 'True' && possible !== 0) { + progressTemplate = ngettext( + // Translators: %(num_points)s is the number of points possible (examples: 1, 3, 10).; + '%(num_points)s point possible (graded)', '%(num_points)s points possible (graded)', + possible + ); } else { - // Translators: %(num_points)s is the number of points possible (examples: 1, 3, 10).; - progress_template = ngettext("%(num_points)s point possible (ungraded)", "%(num_points)s points possible (ungraded)", possible); + progressTemplate = ngettext( + // Translators: %(num_points)s is the number of points possible (examples: 1, 3, 10).; + '%(num_points)s point possible (ungraded)', '%(num_points)s points possible (ungraded)', + possible + ); } - progress = interpolate(progress_template, { - 'num_points': possible - }, true); + progress = interpolate( + progressTemplate, + {num_points: possible}, true + ); } return this.$('.problem-progress').text(progress); }; - Problem.prototype.updateProgress = function (response) { + Problem.prototype.updateProgress = function(response) { if (response.progress_changed) { this.el.data('progress_status', response.progress_status); this.el.data('progress_detail', response.progress_detail); @@ -244,128 +284,161 @@ return this.renderProgressState(); }; - Problem.prototype.forceUpdate = function (response) { + Problem.prototype.forceUpdate = function(response) { this.el.data('progress_status', response.progress_status); this.el.data('progress_detail', response.progress_detail); this.el.trigger('progressChanged'); return this.renderProgressState(); }; - Problem.prototype.queueing = function (focus_callback) { - var _this = this; - this.queued_items = this.$(".xqueue"); + Problem.prototype.queueing = function(focusCallback) { + var that = this; + this.queued_items = this.$('.xqueue'); this.num_queued_items = this.queued_items.length; if (this.num_queued_items > 0) { - if (window.queuePollerID) { + if (window.queuePollerID) { // Only one poller 'thread' per Problem window.clearTimeout(window.queuePollerID); } - return window.queuePollerID = window.setTimeout(function () { - return _this.poll(1000, focus_callback); + window.queuePollerID = window.setTimeout(function() { + return that.poll(1000, focusCallback); }, 1000); } }; - Problem.prototype.poll = function (prev_timeout, focus_callback) { - var _this = this; - return $.postWithPrefix("" + this.url + "/problem_get", function (response) { - var new_timeout; - _this.new_queued_items = $(response.html).find(".xqueue"); - if (_this.new_queued_items.length !== _this.num_queued_items) { - edx.HtmlUtils.setHtml(_this.el, edx.HtmlUtils.HTML(response.html)).promise().done(function () { - return typeof focus_callback === "function" ? focus_callback() : void 0; + Problem.prototype.poll = function(previousTimeout, focusCallback) { + var that = this; + return $.postWithPrefix('' + this.url + '/problem_get', function(response) { + var newTimeout; + // If queueing status changed, then render + that.new_queued_items = $(response.html).find('.xqueue'); + if (that.new_queued_items.length !== that.num_queued_items) { + edx.HtmlUtils.setHtml(that.el, edx.HtmlUtils.HTML(response.html)).promise().done(function() { + return typeof focusCallback === 'function' ? focusCallback() : void 0; }); - JavascriptLoader.executeModuleScripts(_this.el, function () { - _this.setupInputTypes(); - return _this.bind(); + JavascriptLoader.executeModuleScripts(that.el, function() { + that.setupInputTypes(); + that.bind(); }); } - _this.num_queued_items = _this.new_queued_items.length; - if (_this.num_queued_items === 0) { - _this.forceUpdate(response); - return delete window.queuePollerID; + that.num_queued_items = that.new_queued_items.length; + if (that.num_queued_items === 0) { + that.forceUpdate(response); + delete window.queuePollerID; } else { - new_timeout = prev_timeout * 2; - if (new_timeout >= 60000) { + newTimeout = previousTimeout * 2; + // if the timeout is greather than 1 minute + if (newTimeout >= 60000) { delete window.queuePollerID; - return _this.gentle_alert(gettext("The grading process is still running. Refresh the page to see updates.")); + that.gentle_alert( + gettext('The grading process is still running. Refresh the page to see updates.') + ); } else { - return window.queuePollerID = window.setTimeout(function () { - return _this.poll(new_timeout, focus_callback); - }, new_timeout); + window.queuePollerID = window.setTimeout(function() { + return that.poll(newTimeout, focusCallback); + }, newTimeout); } } }); }; - Problem.inputAjax = function (url, input_id, dispatch, data, callback) { - data['dispatch'] = dispatch; - data['input_id'] = input_id; - return $.postWithPrefix("" + url + "/input_ajax", data, callback); + /** + * Use this if you want to make an ajax call on the input type object + * static method so you don't have to instantiate a Problem in order to use it + * + * Input: + * url: the AJAX url of the problem + * inputId: the inputId of the input you would like to make the call on + * NOTE: the id is the ${id} part of "input_${id}" during rendering + * If this function is passed the entire prefixed id, the backend may have trouble + * finding the correct input + * dispatch: string that indicates how this data should be handled by the inputtype + * data: dictionary of data to send to the server + * callback: the function that will be called once the AJAX call has been completed. + * It will be passed a response object + */ + Problem.inputAjax = function(url, inputId, dispatch, data, callback) { + data.dispatch = dispatch; // eslint-disable-line no-param-reassign + data.input_id = inputId; // eslint-disable-line no-param-reassign + return $.postWithPrefix('' + url + '/input_ajax', data, callback); }; - Problem.prototype.render = function (content, focus_callback) { - var _this = this; + Problem.prototype.render = function(content, focusCallback) { + var that = this; if (content) { this.el.html(content); - return JavascriptLoader.executeModuleScripts(this.el, function () { - _this.setupInputTypes(); - _this.bind(); - _this.queueing(focus_callback); - _this.renderProgressState(); - return typeof focus_callback === "function" ? focus_callback() : void 0; + return JavascriptLoader.executeModuleScripts(this.el, function() { + that.setupInputTypes(); + that.bind(); + that.queueing(focusCallback); + that.renderProgressState(); + return typeof focusCallback === 'function' ? focusCallback() : void 0; }); } else { - return $.postWithPrefix("" + this.url + "/problem_get", function (response) { - _this.el.html(response.html); - return JavascriptLoader.executeModuleScripts(_this.el, function () { - _this.setupInputTypes(); - _this.bind(); - _this.queueing(); - return _this.forceUpdate(response); + return $.postWithPrefix('' + this.url + '/problem_get', function(response) { + that.el.html(response.html); + return JavascriptLoader.executeModuleScripts(that.el, function() { + that.setupInputTypes(); + that.bind(); + that.queueing(); + return that.forceUpdate(response); }); }); } }; - Problem.prototype.setupInputTypes = function () { - var _this = this; + Problem.prototype.setupInputTypes = function() { + var that = this; this.inputtypeDisplays = {}; - return this.el.find(".capa_inputtype").each(function (index, inputtype) { - var classes, cls, id, setupMethod, _i, _len, _results; + return this.el.find('.capa_inputtype').each(function(index, inputtype) { + var classes, cls, id, setupMethod, i, len, results; classes = $(inputtype).attr('class').split(' '); id = $(inputtype).attr('id'); - _results = []; - for (_i = 0, _len = classes.length; _i < _len; _i++) { - cls = classes[_i]; - setupMethod = _this.inputtypeSetupMethods[cls]; + results = []; + for (i = 0, len = classes.length; i < len; i++) { + cls = classes[i]; + setupMethod = that.inputtypeSetupMethods[cls]; if (setupMethod != null) { - _results.push(_this.inputtypeDisplays[id] = setupMethod(inputtype)); + results.push(that.inputtypeDisplays[id] = setupMethod(inputtype)); } else { - _results.push(void 0); + results.push(void 0); } } - return _results; + return results; }); }; - Problem.prototype.submit_save_waitfor = function (callback) { - var flag, inp, _i, _len, _ref, - _this = this; + /** + * If some function wants to be called before sending the answer to the + * server, give it a chance to do so. + * + * submit_save_waitfor allows the callee to send alerts if the user's input is + * invalid. To do so, the callee must throw an exception named "WaitforException". + * This and any other errors or exceptions that arise from the callee are rethrown + * and abort the submission. + * + * In order to use this feature, add a 'data-waitfor' attribute to the input, + * and specify the function to be called by the submit button before sending off @answers + */ + Problem.prototype.submit_save_waitfor = function(callback) { + var flag, inp, i, len, ref, + that = this; flag = false; - _ref = this.inputs; - for (_i = 0, _len = _ref.length; _i < _len; _i++) { - inp = _ref[_i]; - if ($(inp).is("input[waitfor]")) { + ref = this.inputs; + for (i = 0, len = ref.length; i < len; i++) { + inp = ref[i]; + if ($(inp).is('input[waitfor]')) { try { - $(inp).data("waitfor")(function () { - _this.refreshAnswers(); + $(inp).data('waitfor')(function() { + that.refreshAnswers(); return callback(); }); } catch (e) { - if (e.name === "Waitfor Exception") { - alert(e.message); + if (e.name === 'Waitfor Exception') { + alert(e.message); // eslint-disable-line no-alert } else { - alert("Could not grade your answer. The submission was aborted."); + alert( // eslint-disable-line no-alert + gettext('Could not grade your answer. The submission was aborted.') + ); } throw e; } @@ -377,406 +450,464 @@ return flag; }; - Problem.prototype.scroll_to_problem_meta = function () { + // Scroll to problem metadata and next focus is problem input + Problem.prototype.scroll_to_problem_meta = function() { var questionTitle; - questionTitle = this.$(".problem-header"); + questionTitle = this.$('.problem-header'); if (questionTitle.length > 0) { $('html, body').animate({ scrollTop: questionTitle.offset().top }, 500); - return questionTitle.focus(); + questionTitle.focus(); } }; - Problem.prototype.focus_on_notification = function (type) { + Problem.prototype.focus_on_notification = function(type) { var notification; notification = this.$('.notification-' + type); if (notification.length > 0) { - return notification.focus(); + notification.focus(); } }; - Problem.prototype.focus_on_submit_notification = function () { - return this.focus_on_notification('submit'); + Problem.prototype.focus_on_submit_notification = function() { + this.focus_on_notification('submit'); }; - Problem.prototype.focus_on_hint_notification = function () { - return this.focus_on_notification('hint'); + Problem.prototype.focus_on_hint_notification = function() { + this.focus_on_notification('hint'); }; - Problem.prototype.focus_on_save_notification = function () { - return this.focus_on_notification('save'); + Problem.prototype.focus_on_save_notification = function() { + this.focus_on_notification('save'); }; - /* - # 'submit_fd' uses FormData to allow file submissions in the 'problem_check' dispatch, - # in addition to simple querystring-based answers - # - # NOTE: The dispatch 'problem_check' is being singled out for the use of FormData; - # maybe preferable to consolidate all dispatches to use FormData + /** + * 'submit_fd' uses FormData to allow file submissions in the 'problem_check' dispatch, + * in addition to simple querystring-based answers + * + * NOTE: The dispatch 'problem_check' is being singled out for the use of FormData; + * maybe preferable to consolidate all dispatches to use FormData */ + Problem.prototype.submit_fd = function() { + var abortSubmission, error, errorHtml, errors, fd, fileNotSelected, fileTooLarge, maxFileSize, + requiredFilesNotSubmitted, settings, timeoutId, unallowedFileSubmitted, i, len, + that = this; - - Problem.prototype.submit_fd = function () { - var abort_submission, error, error_html, errors, fd, file_not_selected, file_too_large, max_filesize, required_files_not_submitted, settings, timeout_id, unallowed_file_submitted, _i, _len, - _this = this; + // If there are no file inputs in the problem, we can fall back on submit. if (this.el.find('input:file').length === 0) { this.submit(); return; } this.enableSubmitButton(false); if (!window.FormData) { - alert("Submission aborted! Sorry, your browser does not support file uploads. If you can, please use Chrome or Safari which have been verified to support file uploads."); + alert(gettext('Submission aborted! Sorry, your browser does not support file uploads. If you can, please use Chrome or Safari which have been verified to support file uploads.')); // eslint-disable-line max-len, no-alert this.enableSubmitButton(true); return; } - timeout_id = this.enableSubmitButtonAfterTimeout(); + timeoutId = this.enableSubmitButtonAfterTimeout(); fd = new FormData(); - max_filesize = 4 * 1000 * 1000; - file_too_large = false; - file_not_selected = false; - required_files_not_submitted = false; - unallowed_file_submitted = false; + + // Sanity checks on submission + maxFileSize = 4 * 1000 * 1000; + fileTooLarge = false; + fileNotSelected = false; + requiredFilesNotSubmitted = false; + unallowedFileSubmitted = false; + errors = []; - this.inputs.each(function (index, element) { - var allowed_files, file, max_size, required_files, _i, _len, _ref, _ref1, _ref2; + this.inputs.each(function(index, element) { + var allowedFiles, file, maxSize, requiredFiles, loopI, loopLen, ref; if (element.type === 'file') { - required_files = $(element).data("required_files"); - allowed_files = $(element).data("allowed_files"); - _ref = element.files; - for (_i = 0, _len = _ref.length; _i < _len; _i++) { - file = _ref[_i]; - if (allowed_files.length !== 0 && (_ref1 = file.name, __indexOf.call(allowed_files, _ref1) < 0)) { - unallowed_file_submitted = true; - errors.push("You submitted " + file.name + "; only " + allowed_files + " are allowed."); + requiredFiles = $(element).data('required_files'); + allowedFiles = $(element).data('allowed_files'); + ref = element.files; + for (loopI = 0, loopLen = ref.length; loopI < loopLen; loopI++) { + file = ref[loopI]; + if (allowedFiles.length !== 0 && indexOfHelper.call(allowedFiles, file.name < 0)) { + unallowedFileSubmitted = true; + errors.push(edx.StringUtils.interpolate( + gettext('You submitted {filename}; only {allowedFiles} are allowed.'), { + filename: file.name, + allowedFiles: allowedFiles + } + )); } - if (_ref2 = file.name, __indexOf.call(required_files, _ref2) >= 0) { - required_files.splice(required_files.indexOf(file.name), 1); + if (indexOfHelper.call(requiredFiles, file.name) >= 0) { + requiredFiles.splice(requiredFiles.indexOf(file.name), 1); } - if (file.size > max_filesize) { - file_too_large = true; - max_size = max_filesize / (1000 * 1000); - errors.push("Your file " + file.name + " is too large (max size: {max_size}MB)"); + if (file.size > maxFileSize) { + fileTooLarge = true; + maxSize = maxFileSize / (1000 * 1000); + errors.push(edx.StringUtils.interpolate( + gettext('Your file {filename} is too large (max size: {maxSize}MB).'), { + filename: file.name, + maxSize: maxSize + } + )); } fd.append(element.id, file); } if (element.files.length === 0) { - file_not_selected = true; - fd.append(element.id, ''); + fileNotSelected = true; + fd.append(element.id, ''); // In case we want to allow submissions with no file } - if (required_files.length !== 0) { - required_files_not_submitted = true; - return errors.push("You did not submit the required files: " + required_files + "."); + if (requiredFiles.length !== 0) { + requiredFilesNotSubmitted = true; + errors.push(edx.StringUtils.interpolate( + gettext('You did not submit the required files: {requiredFiles}.'), { + requiredFiles: requiredFiles + } + )); } } else { - return fd.append(element.id, element.value); + fd.append(element.id, element.value); } }); - if (file_not_selected) { - errors.push('You did not select any files to submit'); + if (fileNotSelected) { + errors.push(gettext('You did not select any files to submit.')); } - error_html = '
    \n'; - for (_i = 0, _len = errors.length; _i < _len; _i++) { - error = errors[_i]; - error_html += '
  • ' + error + '
  • \n'; + errorHtml = '
      \n'; + for (i = 0, len = errors.length; i < len; i++) { + error = errors[i]; + errorHtml += '
    • ' + error + '
    • \n'; } - error_html += '
    '; - this.gentle_alert(error_html); - abort_submission = file_too_large || file_not_selected || unallowed_file_submitted || required_files_not_submitted; - if (abort_submission) { - window.clearTimeout(timeout_id); + errorHtml += '
'; + this.gentle_alert(errorHtml); + abortSubmission = fileTooLarge || fileNotSelected || unallowedFileSubmitted || requiredFilesNotSubmitted; + if (abortSubmission) { + window.clearTimeout(timeoutId); this.enableSubmitButton(true); - return; - } - settings = { - type: "POST", - data: fd, - processData: false, - contentType: false, - complete: this.enableSubmitButtonAfterResponse, - success: function (response) { - switch (response.success) { + } else { + settings = { + type: 'POST', + data: fd, + processData: false, + contentType: false, + complete: this.enableSubmitButtonAfterResponse, + success: function(response) { + switch (response.success) { case 'incorrect': case 'correct': - _this.render(response.contents); - _this.updateProgress(response); + that.render(response.contents); + that.updateProgress(response); break; default: - _this.gentle_alert(response.success); + that.gentle_alert(response.success); + } + return Logger.log('problem_graded', [that.answers, response.contents], that.id); } - return Logger.log('problem_graded', [_this.answers, response.contents], _this.id); - } - }; - return $.ajaxWithPrefix("" + this.url + "/problem_check", settings); - }; - - Problem.prototype.submit = function () { - if (!this.submit_save_waitfor(this.submit_internal)) { - return this.disableAllButtonsWhileRunning(this.submit_internal, true); + }; + $.ajaxWithPrefix('' + this.url + '/problem_check', settings); } }; - Problem.prototype.submit_internal = function () { - var _this = this; + Problem.prototype.submit = function() { + if (!this.submit_save_waitfor(this.submit_internal)) { + this.disableAllButtonsWhileRunning(this.submit_internal, true); + } + }; + + Problem.prototype.submit_internal = function() { + var that = this; Logger.log('problem_check', this.answers); - return $.postWithPrefix("" + this.url + "/problem_check", this.answers, function (response) { + return $.postWithPrefix('' + this.url + '/problem_check', this.answers, function(response) { switch (response.success) { - case 'incorrect': - case 'correct': - window.SR.readTexts(_this.get_sr_status(response.contents)); - _this.el.trigger('contentChanged', [_this.id, response.contents]); - _this.render(response.contents, _this.focus_on_submit_notification); - _this.updateProgress(response); - break; - default: - _this.saveNotification.hide(); - _this.gentle_alert(response.success); + case 'incorrect': + case 'correct': + window.SR.readTexts(that.get_sr_status(response.contents)); + that.el.trigger('contentChanged', [that.id, response.contents]); + that.render(response.contents, that.focus_on_submit_notification); + that.updateProgress(response); + break; + default: + that.saveNotification.hide(); + that.gentle_alert(response.success); } - return Logger.log('problem_graded', [_this.answers, response.contents], _this.id); + return Logger.log('problem_graded', [that.answers, response.contents], that.id); }); }; - Problem.prototype.get_sr_status = function (contents) { - var added_status, aria_label, element, labeled_status, parent_section, status_elements, template, _i, _len; - status_elements = $(contents).find('.status'); - labeled_status = []; - for (_i = 0, _len = status_elements.length; _i < _len; _i++) { - element = status_elements[_i]; - parent_section = $(element).closest('section'); - added_status = false; - if (parent_section) { - aria_label = parent_section.attr('aria-label'); - if (aria_label) { - // Translators: This is only translated to allow for reording of label and associated status.; - template = gettext("{label}: {status}"); - labeled_status.push(edx.StringUtils.interpolate(template, { - label: aria_label, - status: $(element).text() - })); - added_status = true; + /** + * This method builds up an array of strings to send to the page screen-reader span. + * It first gets all elements with class "status", and then looks to see if they are contained + * in sections with aria-labels. If so, labels are prepended to the status element text. + * If not, just the text of the status elements are returned. + */ + Problem.prototype.get_sr_status = function(contents) { + var addedStatus, ariaLabel, element, labeledStatus, parentSection, statusElement, template, i, len; + statusElement = $(contents).find('.status'); + labeledStatus = []; + for (i = 0, len = statusElement.length; i < len; i++) { + element = statusElement[i]; + parentSection = $(element).closest('section'); + addedStatus = false; + if (parentSection) { + ariaLabel = parentSection.attr('aria-label'); + if (ariaLabel) { + // Translators: This is only translated to allow for reordering of label and associated status.; + template = gettext('{label}: {status}'); + labeledStatus.push(edx.StringUtils.interpolate( + template, { + label: ariaLabel, + status: $(element).text() + } + )); + addedStatus = true; } } - if (!added_status) { - labeled_status.push($(element).text()); + if (!addedStatus) { + labeledStatus.push($(element).text()); } } - return labeled_status; + return labeledStatus; }; - Problem.prototype.reset = function () { + Problem.prototype.reset = function() { return this.disableAllButtonsWhileRunning(this.reset_internal, false); }; - Problem.prototype.reset_internal = function () { - var _this = this; + Problem.prototype.reset_internal = function() { + var that = this; Logger.log('problem_reset', this.answers); - return $.postWithPrefix("" + this.url + "/problem_reset", { + return $.postWithPrefix('' + this.url + '/problem_reset', { id: this.id - }, function (response) { + }, function(response) { if (response.success) { - _this.el.trigger('contentChanged', [_this.id, response.html]); - _this.render(response.html, _this.scroll_to_problem_meta); - _this.updateProgress(response); + that.el.trigger('contentChanged', [that.id, response.html]); + that.render(response.html, that.scroll_to_problem_meta); + that.updateProgress(response); return window.SR.readText(gettext('This problem has been reset.')); } else { - return _this.gentle_alert(response.msg); + return that.gentle_alert(response.msg); } }); }; - Problem.prototype.show = function () { - var _this = this; + // TODO this needs modification to deal with javascript responses; perhaps we + // need something where responsetypes can define their own behavior when show + // is called. + Problem.prototype.show = function() { + var that = this; Logger.log('problem_show', { problem: this.id }); - return $.postWithPrefix("" + this.url + "/problem_show", function (response) { + return $.postWithPrefix('' + this.url + '/problem_show', function(response) { var answers; answers = response.answers; - $.each(answers, function (key, value) { - var answer, choice, solution, _i, _len, _results; + $.each(answers, function(key, value) { + var answer, choice, i, len, results; if ($.isArray(value)) { - _results = []; - for (_i = 0, _len = value.length; _i < _len; _i++) { - choice = value[_i]; - _results.push(_this.$("label[for='input_" + key + "_" + choice + "']").attr({ + results = []; + for (i = 0, len = value.length; i < len; i++) { + choice = value[i]; + results.push(that.$('label[for="input_' + key + '_' + choice + '"]').attr({ correct_answer: 'true' })); } - return _results; + return results; } else { - answer = _this.$("#answer_" + key + ", #solution_" + key); + answer = that.$('#answer_' + key + ', #solution_' + key); edx.HtmlUtils.setHtml(answer, edx.HtmlUtils.HTML(value)); Collapsible.setCollapsibles(answer); + + // Sometimes, `value` is just a string containing a MathJax formula. + // If this is the case, jQuery will throw an error in some corner cases + // because of an incorrect selector. We setup a try..catch so that + // the script doesn't break in such cases. + // + // We will fallback to the second `if statement` below, if an + // error is thrown by jQuery. try { - return solution = $(value).find('.detailed-solution'); + return $(value).find('.detailed-solution'); } catch (e) { - return solution = {}; + return {}; } + + // TODO remove the above once everything is extracted into its own + // inputtype functions. } }); - _this.el.find(".capa_inputtype").each(function (index, inputtype) { - var classes, cls, display, showMethod, _i, _len, _results; + that.el.find('.capa_inputtype').each(function(index, inputtype) { + var classes, cls, display, showMethod, i, len, results; classes = $(inputtype).attr('class').split(' '); - _results = []; - for (_i = 0, _len = classes.length; _i < _len; _i++) { - cls = classes[_i]; - display = _this.inputtypeDisplays[$(inputtype).attr('id')]; - showMethod = _this.inputtypeShowAnswerMethods[cls]; + results = []; + for (i = 0, len = classes.length; i < len; i++) { + cls = classes[i]; + display = that.inputtypeDisplays[$(inputtype).attr('id')]; + showMethod = that.inputtypeShowAnswerMethods[cls]; if (showMethod != null) { - _results.push(showMethod(inputtype, display, answers)); + results.push(showMethod(inputtype, display, answers)); } else { - _results.push(void 0); + results.push(void 0); } } - return _results; + return results; }); - if (typeof MathJax !== "undefined" && MathJax !== null) { - _this.el.find('.problem > div').each(function (index, element) { - return MathJax.Hub.Queue(["Typeset", MathJax.Hub, element]); + if (typeof MathJax !== 'undefined' && MathJax !== null) { + that.el.find('.problem > div').each(function(index, element) { + return MathJax.Hub.Queue(['Typeset', MathJax.Hub, element]); }); } - _this.el.find('.show').attr('disabled', 'disabled'); - _this.updateProgress(response); - window.SR.readText(gettext('Answers to this problem are now shown. Navigate through the problem to review it with answers inline.')); - return _this.scroll_to_problem_meta(); + that.el.find('.show').attr('disabled', 'disabled'); + that.updateProgress(response); + window.SR.readText(gettext('Answers to this problem are now shown. Navigate through the problem to review it with answers inline.')); // eslint-disable-line max-len + that.scroll_to_problem_meta(); }); }; - Problem.prototype.clear_all_notifications = function () { + Problem.prototype.clear_all_notifications = function() { this.submitNotification.remove(); this.gentleAlertNotification.hide(); - return this.saveNotification.hide(); + this.saveNotification.hide(); }; - Problem.prototype.gentle_alert = function (msg) { - edx.HtmlUtils.setHtml(this.el.find('.notification-gentle-alert .notification-message'), edx.HtmlUtils.HTML(msg)); + Problem.prototype.gentle_alert = function(msg) { + edx.HtmlUtils.setHtml( + this.el.find('.notification-gentle-alert .notification-message'), + edx.HtmlUtils.HTML(msg) + ); this.clear_all_notifications(); this.gentleAlertNotification.show(); - return this.gentleAlertNotification.focus(); + this.gentleAlertNotification.focus(); }; - Problem.prototype.save = function () { + Problem.prototype.save = function() { if (!this.submit_save_waitfor(this.save_internal)) { - return this.disableAllButtonsWhileRunning(this.save_internal, false); + this.disableAllButtonsWhileRunning(this.save_internal, false); } }; - Problem.prototype.save_internal = function () { - var _this = this; + Problem.prototype.save_internal = function() { + var that = this; Logger.log('problem_save', this.answers); - return $.postWithPrefix("" + this.url + "/problem_save", this.answers, function (response) { + return $.postWithPrefix('' + this.url + '/problem_save', this.answers, function(response) { var saveMessage; saveMessage = response.msg; if (response.success) { - _this.el.trigger('contentChanged', [_this.id, response.html]); - edx.HtmlUtils.setHtml(_this.el.find('.notification-save .notification-message'), edx.HtmlUtils.HTML(saveMessage)); - _this.clear_all_notifications(); - _this.saveNotification.show(); - return _this.focus_on_save_notification(); + that.el.trigger('contentChanged', [that.id, response.html]); + edx.HtmlUtils.setHtml( + that.el.find('.notification-save .notification-message'), + edx.HtmlUtils.HTML(saveMessage) + ); + that.clear_all_notifications(); + that.saveNotification.show(); + that.focus_on_save_notification(); } else { - return _this.gentle_alert(saveMessage); + that.gentle_alert(saveMessage); } }); }; - Problem.prototype.refreshMath = function (event, element) { - var elid, eqn, jax, mathjax_preprocessor, preprocessor_tag, target; + Problem.prototype.refreshMath = function(event, element) { + var elid, eqn, jax, mathjaxPreprocessor, preprocessorTag, target; if (!element) { - element = event.target; + element = event.target; // eslint-disable-line no-param-reassign } elid = element.id.replace(/^input_/, ''); - target = "display_" + elid; - preprocessor_tag = "inputtype_" + elid; - mathjax_preprocessor = this.inputtypeDisplays[preprocessor_tag]; - if ((typeof MathJax !== "undefined" && MathJax !== null) && (jax = MathJax.Hub.getAllJax(target)[0])) { + target = 'display_' + elid; + + // MathJax preprocessor is loaded by 'setupInputTypes' + preprocessorTag = 'inputtype_' + elid; + mathjaxPreprocessor = this.inputtypeDisplays[preprocessorTag]; + if (typeof MathJax !== 'undefined' && MathJax !== null && MathJax.Hub.getAllJax(target)[0]) { + jax = MathJax.Hub.getAllJax(target)[0]; eqn = $(element).val(); - if (mathjax_preprocessor) { - eqn = mathjax_preprocessor(eqn); + if (mathjaxPreprocessor) { + eqn = mathjaxPreprocessor(eqn); } MathJax.Hub.Queue(['Text', jax, eqn], [this.updateMathML, jax, element]); } }; - Problem.prototype.updateMathML = function (jax, element) { + Problem.prototype.updateMathML = function(jax, element) { try { - return $("#" + element.id + "_dynamath").val(jax.root.toMathML('')); + $('#' + element.id + '_dynamath').val(jax.root.toMathML('')); } catch (exception) { if (!exception.restart) { throw exception; } - if (typeof MathJax !== "undefined" && MathJax !== null) { - return MathJax.Callback.After([this.refreshMath, jax], exception.restart); + if (typeof MathJax !== 'undefined' && MathJax !== null) { + MathJax.Callback.After([this.refreshMath, jax], exception.restart); } } }; - Problem.prototype.refreshAnswers = function () { - this.$('input.schematic').each(function (index, element) { + Problem.prototype.refreshAnswers = function() { + this.$('input.schematic').each(function(index, element) { return element.schematic.update_value(); }); - this.$(".CodeMirror").each(function (index, element) { + this.$('.CodeMirror').each(function(index, element) { if (element.CodeMirror.save) { - return element.CodeMirror.save(); + element.CodeMirror.save(); } }); - return this.answers = this.inputs.serialize(); + this.answers = this.inputs.serialize(); }; - Problem.prototype.submitAnswersAndSubmitButton = function (bind) { - var answered, at_least_one_text_input_found, one_text_input_filled, - _this = this; - if (bind == null) { - bind = false; + /** + * Used to check available answers and if something is checked (or the answer is set in some textbox), + * the "Submit" button becomes enabled. Otherwise it is disabled by default. + * + * Arguments: + * bind (boolean): used on the first check to attach event handlers to input fields + * to change "Submit" enable status in case of some manipulations with answers + */ + Problem.prototype.submitAnswersAndSubmitButton = function(bind) { + var answered, atLeastOneTextInputFound, oneTextInputFilled, + that = this; + if (bind === null || bind === undefined) { + bind = false; // eslint-disable-line no-param-reassign } - "Used to check available answers and if something is checked (or the answer is set in some textbox)\n\"Submit\" button becomes enabled. Otherwise it is disabled by default.\n\nArguments:\n bind (bool): used on the first check to attach event handlers to input fields\n to change \"Submit\" enable status in case of some manipulations with answers"; answered = true; - at_least_one_text_input_found = false; - one_text_input_filled = false; - this.el.find("input:text").each(function (i, text_field) { - if ($(text_field).is(':visible')) { - at_least_one_text_input_found = true; - if ($(text_field).val() !== '') { - one_text_input_filled = true; + atLeastOneTextInputFound = false; + oneTextInputFilled = false; + this.el.find('input:text').each(function(i, textField) { + if ($(textField).is(':visible')) { + atLeastOneTextInputFound = true; + if ($(textField).val() !== '') { + oneTextInputFilled = true; } if (bind) { - $(text_field).on('input', function (e) { - _this.saveNotification.hide(); - _this.submitAnswersAndSubmitButton(); + $(textField).on('input', function() { + that.saveNotification.hide(); + that.submitAnswersAndSubmitButton(); }); } } }); - if (at_least_one_text_input_found && !one_text_input_filled) { + if (atLeastOneTextInputFound && !oneTextInputFilled) { answered = false; } - this.el.find(".choicegroup").each(function (i, choicegroup_block) { + this.el.find('.choicegroup').each(function(i, choicegroupBlock) { var checked; checked = false; - $(choicegroup_block).find("input[type=checkbox], input[type=radio]").each(function (j, checkbox_or_radio) { - if ($(checkbox_or_radio).is(':checked')) { - checked = true; - } - if (bind) { - $(checkbox_or_radio).on('click', function (e) { - _this.saveNotification.hide(); - _this.submitAnswersAndSubmitButton(); - }); - } - }); + $(choicegroupBlock).find('input[type=checkbox], input[type=radio]'). + each(function(j, checkboxOrRadio) { + if ($(checkboxOrRadio).is(':checked')) { + checked = true; + } + if (bind) { + $(checkboxOrRadio).on('click', function() { + that.saveNotification.hide(); + that.submitAnswersAndSubmitButton(); + }); + } + }); if (!checked) { answered = false; } }); - this.el.find("select").each(function (i, select_field) { - var selected_option; - selected_option = $(select_field).find("option:selected").text().trim(); - if (selected_option === 'Select an option') { + this.el.find('select').each(function(i, selectField) { + var selectedOption = $(selectField).find('option:selected').text() + .trim(); + if (selectedOption === 'Select an option') { answered = false; } if (bind) { - $(select_field).on('change', function (e) { - _this.saveNotification.hide(); - _this.submitAnswersAndSubmitButton(); + $(selectField).on('change', function() { + that.saveNotification.hide(); + that.submitAnswersAndSubmitButton(); }); } }); @@ -787,82 +918,86 @@ } }; - Problem.prototype.bindResetCorrectness = function () { + Problem.prototype.bindResetCorrectness = function() { + // Loop through all input types. + // Bind the reset functions at that scope. var $inputtypes, - _this = this; - $inputtypes = this.el.find(".capa_inputtype").add(this.el.find(".inputtype")); - return $inputtypes.each(function (index, inputtype) { - var bindMethod, classes, cls, _i, _len, _results; + that = this; + $inputtypes = this.el.find('.capa_inputtype').add(this.el.find('.inputtype')); + return $inputtypes.each(function(index, inputtype) { + var bindMethod, classes, cls, i, len, results; classes = $(inputtype).attr('class').split(' '); - _results = []; - for (_i = 0, _len = classes.length; _i < _len; _i++) { - cls = classes[_i]; - bindMethod = _this.bindResetCorrectnessByInputtype[cls]; + results = []; + for (i = 0, len = classes.length; i < len; i++) { + cls = classes[i]; + bindMethod = that.bindResetCorrectnessByInputtype[cls]; if (bindMethod != null) { - _results.push(bindMethod(inputtype)); + results.push(bindMethod(inputtype)); } else { - _results.push(void 0); + results.push(void 0); } } - return _results; + return results; }); }; + // Find all places where each input type displays its correct-ness + // Replace them with their original state--'unanswered'. Problem.prototype.bindResetCorrectnessByInputtype = { - formulaequationinput: function (element) { - return $(element).find('input').on('input', function () { + // These are run at the scope of the capa inputtype + // They should set handlers on each to reset the whole. + formulaequationinput: function(element) { + return $(element).find('input').on('input', function() { var $p; $p = $(element).find('span.status'); - // Translators: the word unanswered here is about answering a problem the student must solve.; - return $p.parent().removeClass().addClass("unsubmitted"); + return $p.parent().removeClass().addClass('unsubmitted'); }); }, - choicegroup: function (element) { + choicegroup: function(element) { var $element, id; $element = $(element); id = ($element.attr('id').match(/^inputtype_(.*)$/))[1]; - return $element.find('input').on('change', function () { + return $element.find('input').on('change', function() { var $status; - $status = $("#status_" + id); + $status = $('#status_' + id); if ($status[0]) { - $status.removeClass().addClass("unanswered"); + $status.removeClass().addClass('unanswered'); $status.empty().css('display', 'inline-block'); } else { - $("", { - "class": "unanswered", - "style": "display: inline-block;", - "id": "status_" + id + $('', { + class: 'unanswered', + style: 'display: inline-block;', + id: 'status_' + id }); } - return $element.find("label").removeClass(); + return $element.find('label').removeClass(); }); }, - 'option-input': function (element) { + 'option-input': function(element) { var $select, id; $select = $(element).find('select'); id = ($select.attr('id').match(/^input_(.*)$/))[1]; - return $select.on('change', function () { - var $status; - return $status = $("#status_" + id).removeClass().addClass("unanswered").find('span').text(gettext('Status: unsubmitted')); + return $select.on('change', function() { + return $('#status_' + id).removeClass().addClass('unanswered') + .find('span') + .text(gettext('Status: unsubmitted')); }); }, - textline: function (element) { - return $(element).find('input').on('input', function () { + textline: function(element) { + return $(element).find('input').on('input', function() { var $p; $p = $(element).find('span.status'); - // Translators: the word unanswered here is about answering a problem the student must solve.; - return $p.parent().removeClass("correct incorrect").addClass("unsubmitted"); + return $p.parent().removeClass('correct incorrect').addClass('unsubmitted'); }); } }; Problem.prototype.inputtypeSetupMethods = { - 'text-input-dynamath': function (element) { + 'text-input-dynamath': function(element) { /* Return: function (eqn) -> eqn that preprocesses the user formula input before it is fed into MathJax. Return 'false' if no preprocessor specified */ - var data, preprocessor, preprocessorClass, preprocessorClassName; data = $(element).find('.text-input-dynamath_data'); preprocessorClassName = data.data('preprocessor'); @@ -874,30 +1009,31 @@ return preprocessor.fn; } }, - javascriptinput: function (element) { - var container, data, display, displayClass, evaluation, params, problemState, submission, submissionField; - data = $(element).find(".javascriptinput_data"); - params = data.data("params"); - submission = data.data("submission"); - evaluation = data.data("evaluation"); - problemState = data.data("problem_state"); + javascriptinput: function(element) { + var container, data, display, displayClass, evaluation, params, problemState, submission, + submissionField; + data = $(element).find('.javascriptinput_data'); + params = data.data('params'); + submission = data.data('submission'); + evaluation = data.data('evaluation'); + problemState = data.data('problem_state'); displayClass = window[data.data('display_class')]; if (evaluation === '') { evaluation = null; } - container = $(element).find(".javascriptinput_container"); - submissionField = $(element).find(".javascriptinput_input"); + container = $(element).find('.javascriptinput_container'); + submissionField = $(element).find('.javascriptinput_input'); display = new displayClass(problemState, submission, evaluation, container, submissionField, params); display.render(); return display; }, - cminput: function (container) { + cminput: function(container) { var CodeMirrorEditor, CodeMirrorTextArea, element, id, linenumbers, mode, spaces, tabsize; - element = $(container).find("textarea"); - tabsize = element.data("tabsize"); - mode = element.data("mode"); - linenumbers = element.data("linenums"); - spaces = Array(parseInt(tabsize) + 1).join(" "); + element = $(container).find('textarea'); + tabsize = element.data('tabsize'); + mode = element.data('mode'); + linenumbers = element.data('linenums'); + spaces = Array(parseInt(tabsize, 10) + 1).join(' '); CodeMirrorEditor = CodeMirror.fromTextArea(element[0], { lineNumbers: linenumbers, indentUnit: tabsize, @@ -908,89 +1044,108 @@ indentWithTabs: false, smartIndent: false, extraKeys: { - "Esc": function (cm) { - $(".grader-status").focus(); + Esc: function() { + $('.grader-status').focus(); return false; }, - "Tab": function (cm) { - cm.replaceSelection(spaces, "end"); + Tab: function(cm) { + cm.replaceSelection(spaces, 'end'); return false; } } }); - id = element.attr("id").replace(/^input_/, ""); + id = element.attr('id').replace(/^input_/, ''); CodeMirrorTextArea = CodeMirrorEditor.getInputField(); - CodeMirrorTextArea.setAttribute("id", "cm-textarea-" + id); - CodeMirrorTextArea.setAttribute("aria-describedby", "cm-editor-exit-message-" + id + " status_" + id); + CodeMirrorTextArea.setAttribute('id', 'cm-textarea-' + id); + CodeMirrorTextArea.setAttribute('aria-describedby', 'cm-editor-exit-message-' + id + ' status_' + id); return CodeMirrorEditor; } }; Problem.prototype.inputtypeShowAnswerMethods = { - choicegroup: function (element, display, answers) { - var answer, choice, input_id, _i, _len, _results; - element = $(element); - input_id = element.attr('id').replace(/inputtype_/, ''); - answer = answers[input_id]; - _results = []; - for (_i = 0, _len = answer.length; _i < _len; _i++) { - choice = answer[_i]; - _results.push(element.find("#input_" + input_id + "_" + choice).parent("label").addClass('choicegroup_correct')); + choicegroup: function(element, display, answers) { + var answer, choice, inputId, i, len, results, $element; + $element = $(element); + inputId = $element.attr('id').replace(/inputtype_/, ''); + answer = answers[inputId]; + results = []; + for (i = 0, len = answer.length; i < len; i++) { + choice = answer[i]; + results.push($element.find('#input_' + inputId + '_' + choice).parent('label'). + addClass('choicegroup_correct')); } - return _results; + return results; }, - javascriptinput: function (element, display, answers) { - var answer, answer_id; - answer_id = $(element).attr('id').split("_").slice(1).join("_"); - answer = JSON.parse(answers[answer_id]); + javascriptinput: function(element, display, answers) { + var answer, answerId; + answerId = $(element).attr('id').split('_') + .slice(1) + .join('_'); + answer = JSON.parse(answers[answerId]); return display.showAnswer(answer); }, - choicetextgroup: function (element, display, answers) { - var answer, choice, input_id, _i, _len, _results; - element = $(element); - input_id = element.attr('id').replace(/inputtype_/, ''); - answer = answers[input_id]; - _results = []; - for (_i = 0, _len = answer.length; _i < _len; _i++) { - choice = answer[_i]; - _results.push(element.find("section#forinput" + choice).addClass('choicetextgroup_show_correct')); + choicetextgroup: function(element, display, answers) { + var answer, choice, inputId, i, len, results, $element; + $element = $(element); + inputId = $element.attr('id').replace(/inputtype_/, ''); + answer = answers[inputId]; + results = []; + for (i = 0, len = answer.length; i < len; i++) { + choice = answer[i]; + results.push($element.find('section#forinput' + choice).addClass('choicetextgroup_show_correct')); } - return _results; + return results; }, - imageinput: function (element, display, answers) { - var canvas, container, ctx, id, types; + imageinput: function(element, display, answers) { + // answers is a dict of (answer_id, answer_text) for each answer for this question. + // + // @Examples: + // {'anwser_id': { + // 'rectangle': '(10,10)-(20,30);(12,12)-(40,60)', + // 'regions': '[[10,10], [30,30], [10, 30], [30, 10]]' + // } } + var canvas, container, id, types, context, $element; types = { - rectangle: function (ctx, coords) { + rectangle: function(ctx, coords) { var rects, reg; reg = /^\(([0-9]+),([0-9]+)\)-\(([0-9]+),([0-9]+)\)$/; rects = coords.replace(/\s*/g, '').split(/;/); - $.each(rects, function (index, rect) { + $.each(rects, function(index, rect) { var abs, height, points, width; abs = Math.abs; points = reg.exec(rect); if (points) { width = abs(points[3] - points[1]); height = abs(points[4] - points[2]); - return ctx.rect(points[1], points[2], width, height); + ctx.rect(points[1], points[2], width, height); } }); ctx.stroke(); return ctx.fill(); }, - regions: function (ctx, coords) { + regions: function(ctx, coords) { var parseCoords; - parseCoords = function (coords) { + parseCoords = function(coordinates) { var reg; - reg = JSON.parse(coords); - if (typeof reg[0][0][0] === "undefined") { + reg = JSON.parse(coordinates); + + // Regions is list of lists [region1, region2, region3, ...] where regionN + // is disordered list of points: [[1,1], [100,100], [50,50], [20, 70]]. + // If there is only one region in the list, simpler notation can be used: + // regions="[[10,10], [30,30], [10, 30], [30, 10]]" (without explicitly + // setting outer list) + if (typeof reg[0][0][0] === 'undefined') { + // we have [[1,2],[3,4],[5,6]] - single region + // instead of [[[1,2],[3,4],[5,6], [[1,2],[3,4],[5,6]]] + // or [[[1,2],[3,4],[5,6]]] - multiple regions syntax reg = [reg]; } return reg; }; - return $.each(parseCoords(coords), function (index, region) { + return $.each(parseCoords(coords), function(index, region) { ctx.beginPath(); - $.each(region, function (index, point) { - if (index === 0) { + $.each(region, function(idx, point) { + if (idx === 0) { return ctx.moveTo(point[0], point[1]); } else { return ctx.lineTo(point[0], point[1]); @@ -1002,70 +1157,98 @@ }); } }; - element = $(element); - id = element.attr('id').replace(/inputtype_/, ''); - container = element.find("#answer_" + id); + $element = $(element); + id = $element.attr('id').replace(/inputtype_/, ''); + container = $element.find('#answer_' + id); canvas = document.createElement('canvas'); canvas.width = container.data('width'); canvas.height = container.data('height'); if (canvas.getContext) { - ctx = canvas.getContext('2d'); + context = canvas.getContext('2d'); } else { - return console.log('Canvas is not supported.'); + console.log('Canvas is not supported.'); // eslint-disable-line no-console } - ctx.fillStyle = 'rgba(255,255,255,.3)'; - ctx.strokeStyle = "#FF0000"; - ctx.lineWidth = "2"; + context.fillStyle = 'rgba(255,255,255,.3)'; + context.strokeStyle = '#FF0000'; + context.lineWidth = '2'; if (answers[id]) { - $.each(answers[id], function (key, value) { - if ((types[key] != null) && value) { - return types[key](ctx, value); + $.each(answers[id], function(key, value) { + if ((types[key] !== null && types[key] !== undefined) && value) { + types[key](context, value); } }); - return container.html(canvas); + container.html(canvas); } else { - return console.log("Answer is absent for image input with id=" + id); + console.log('Answer is absent for image input with id=' + id); // eslint-disable-line no-console } } }; Problem.prototype.inputtypeHideAnswerMethods = { - choicegroup: function (element, display) { - element = $(element); - return element.find('label').removeClass('choicegroup_correct'); + choicegroup: function(element) { + var $element = $(element); + return $element.find('label').removeClass('choicegroup_correct'); }, - javascriptinput: function (element, display) { + javascriptinput: function(element, display) { return display.hideAnswer(); }, - choicetextgroup: function (element, display) { - element = $(element); - return element.find("section[id^='forinput']").removeClass('choicetextgroup_show_correct'); + choicetextgroup: function(element) { + var $element = $(element); + return $element.find('section[id^="forinput"]').removeClass('choicetextgroup_show_correct'); } }; - Problem.prototype.disableAllButtonsWhileRunning = function (operationCallback, isFromCheckOperation) { - var _this = this; + /** + * Used to keep the buttons disabled while operationCallback is running. + * + * params: + * 'operationCallback' is an operation to be run. + * isFromCheckOperation' is a boolean to keep track if 'operationCallback' was + * from submit, if so then text of submit button will be changed as well. + * + */ + Problem.prototype.disableAllButtonsWhileRunning = function(operationCallback, isFromCheckOperation) { + var that = this; this.enableAllButtons(false, isFromCheckOperation); - return operationCallback().always(function () { - return _this.enableAllButtons(true, isFromCheckOperation); + return operationCallback().always(function() { + return that.enableAllButtons(true, isFromCheckOperation); }); }; - Problem.prototype.enableAllButtons = function (enable, isFromCheckOperation) { + /** + * Used to enable/disable all buttons in problem. + * + * params: + * 'enable' is a boolean to determine enabling/disabling of buttons. + * 'isFromCheckOperation' is a boolean to keep track if operation was initiated + * from submit so that text of submit button will also be changed while disabling/enabling + * the submit button. + */ + Problem.prototype.enableAllButtons = function(enable, isFromCheckOperation) { + // Called by disableAllButtonsWhileRunning to automatically disable all buttons while check,reset, or + // save internal are running. Then enable all the buttons again after it is done. if (enable) { - this.resetButton.add(this.saveButton).add(this.hintButton).add(this.showButton).removeAttr('disabled'); + this.resetButton.add(this.saveButton).add(this.hintButton).add(this.showButton). + removeAttr('disabled'); } else { - this.resetButton.add(this.saveButton).add(this.hintButton).add(this.showButton).attr({ - 'disabled': 'disabled' - }); + this.resetButton.add(this.saveButton).add(this.hintButton).add(this.showButton). + attr({disabled: 'disabled'}); } return this.enableSubmitButton(enable, isFromCheckOperation); }; - Problem.prototype.enableSubmitButton = function (enable, changeText) { + /** + * Used to disable submit button to reduce chance of accidental double-submissions. + * + * params: + * 'enable' is a boolean to determine enabling/disabling of submit button. + * 'changeText' is a boolean to determine if there is need to change the + * text of submit button as well. + */ + Problem.prototype.enableSubmitButton = function(enable, changeText) { var submitCanBeEnabled; - if (changeText == null) { - changeText = true; + if (changeText === null || changeText === undefined) { + changeText = true; // eslint-disable-line no-param-reassign } if (enable) { submitCanBeEnabled = this.submitButton.data('should-enable-submit-button') === 'True'; @@ -1073,78 +1256,75 @@ this.submitButton.removeAttr('disabled'); } if (changeText) { - return this.submitButtonLabel.text(this.submitButtonSubmitText); + this.submitButtonLabel.text(this.submitButtonSubmitText); } } else { - this.submitButton.attr({ - 'disabled': 'disabled' - }); + this.submitButton.attr({disabled: 'disabled'}); if (changeText) { - return this.submitButtonLabel.text(this.submitButtonSubmittingText); + this.submitButtonLabel.text(this.submitButtonSubmittingText); } } }; - Problem.prototype.enableSubmitButtonAfterResponse = function () { + Problem.prototype.enableSubmitButtonAfterResponse = function() { this.has_response = true; if (!this.has_timed_out) { + // Server has returned response before our timeout. return this.enableSubmitButton(false); } else { return this.enableSubmitButton(true); } }; - Problem.prototype.enableSubmitButtonAfterTimeout = function () { + Problem.prototype.enableSubmitButtonAfterTimeout = function() { var enableSubmitButton, - _this = this; + that = this; this.has_timed_out = false; this.has_response = false; - enableSubmitButton = function () { - _this.has_timed_out = true; - if (_this.has_response) { - return _this.enableSubmitButton(true); + enableSubmitButton = function() { + that.has_timed_out = true; + if (that.has_response) { + that.enableSubmitButton(true); } }; return window.setTimeout(enableSubmitButton, 750); }; - Problem.prototype.hint_button = function () { - var hint_container, hint_index, next_index, - _this = this; - hint_container = this.$('.problem-hint'); - hint_index = hint_container.attr('hint_index'); - if (hint_index === void 0) { - next_index = 0; + Problem.prototype.hint_button = function() { + // Store the index of the currently shown hint as an attribute. + // Use that to compute the next hint number when the button is clicked. + var hintContainer, hintIndex, nextIndex, + that = this; + hintContainer = this.$('.problem-hint'); + hintIndex = hintContainer.attr('hint_index'); + if (hintIndex === void 0) { + nextIndex = 0; } else { - next_index = parseInt(hint_index) + 1; + nextIndex = parseInt(hintIndex, 10) + 1; } - return $.postWithPrefix("" + this.url + "/hint_button", { - hint_index: next_index, + return $.postWithPrefix('' + this.url + '/hint_button', { + hint_index: nextIndex, input_id: this.id - }, function (response) { - var hint_msg_container; + }, function(response) { + var hintMsgContainer; if (response.success) { - hint_msg_container = _this.$('.problem-hint .notification-message'); - hint_container.attr('hint_index', response.hint_index); - edx.HtmlUtils.setHtml(hint_msg_container, edx.HtmlUtils.HTML(response.msg)); - MathJax.Hub.Queue(['Typeset', MathJax.Hub, hint_container[0]]); + hintMsgContainer = that.$('.problem-hint .notification-message'); + hintContainer.attr('hint_index', response.hint_index); + edx.HtmlUtils.setHtml(hintMsgContainer, edx.HtmlUtils.HTML(response.msg)); + MathJax.Hub.Queue(['Typeset', MathJax.Hub, hintContainer[0]]); if (response.should_enable_next_hint) { - _this.hintButton.removeAttr('disabled'); + that.hintButton.removeAttr('disabled'); } else { - _this.hintButton.attr({ - 'disabled': 'disabled' - }); + that.hintButton.attr({disabled: 'disabled'}); } - _this.el.find('.notification-hint').show(); - return _this.focus_on_hint_notification(); + that.el.find('.notification-hint').show(); + that.focus_on_hint_notification(); } else { - return _this.gentle_alert(response.msg); + that.gentle_alert(response.msg); } }); }; return Problem; - }).call(this); - }).call(this);