diff --git a/common/lib/xmodule/xmodule/js/src/selfassessment/display.coffee b/common/lib/xmodule/xmodule/js/src/selfassessment/display.coffee new file mode 100644 index 0000000000..5d5e3a8eb8 --- /dev/null +++ b/common/lib/xmodule/xmodule/js/src/selfassessment/display.coffee @@ -0,0 +1,303 @@ +class @Problem + + constructor: (element) -> + @el = $(element).find('.problems-wrapper') + @id = @el.data('problem-id') + @element_id = @el.attr('id') + @url = @el.data('url') + @render() + + $: (selector) -> + $(selector, @el) + + bind: => + problem_prefix = @element_id.replace(/problem_/,'') + @inputs = @$("[id^=input_#{problem_prefix}_]") + + @$('section.action input:button').click @refreshAnswers + @$('section.action input.check').click @check_fd + #@$('section.action input.check').click @check + @$('section.action input.show').click @show + @$('section.action input.save').click @save + + render: (content) -> + if content + @el.html(content) + JavascriptLoader.executeModuleScripts @el, () => + @setupInputTypes() + @bind() + else + $.postWithPrefix "#{@url}/problem_get", (response) => + @el.html(response.html) + JavascriptLoader.executeModuleScripts @el, () => + @setupInputTypes() + @bind() + + + # 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) + + + ### + # 'check_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 + ### + check_fd: => + Logger.log 'problem_check', @answers + + # If there are no file inputs in the problem, we can fall back on @check + if $('input:file').length == 0 + @check() + return + + 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." + return + + 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 + errors.push 'Your file "' + file.name '" is too large (max size: ' + max_filesize/(1000*1000) + ' 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 = '' + @gentle_alert error_html + + abort_submission = file_too_large or file_not_selected or unallowed_file_submitted or required_files_not_submitted + + settings = + type: "POST" + data: fd + processData: false + contentType: false + success: (response) => + switch response.success + when 'incorrect', 'correct' + @render(response.contents) + @updateProgress response + else + @gentle_alert response.success + + if not abort_submission + $.ajaxWithPrefix("#{@url}/problem_check", settings) + + check: => + Logger.log 'problem_check', @answers + $.postWithPrefix "#{@url}/problem_check", @answers, (response) => + switch response.success + when 'incorrect', 'correct' + @render(response.contents) + @updateProgress response + if @el.hasClass 'showed' + @el.removeClass 'showed' + else + @gentle_alert response.success + + reset: => + Logger.log 'problem_reset', @answers + $.postWithPrefix "#{@url}/problem_reset", id: @id, (response) => + @render(response.html) + @updateProgress response + + # 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: => + if !@el.hasClass 'showed' + 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}") + answer.html(value) + Collapsible.setCollapsibles(answer) + + # 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? + + @el.find('.problem > div').each (index, element) => + MathJax.Hub.Queue ["Typeset", MathJax.Hub, element] + + @$('.show').val 'Hide Answer' + @el.addClass 'showed' + @updateProgress response + else + @$('[id^=answer_], [id^=solution_]').text '' + @$('[correct_answer]').attr correct_answer: null + @el.removeClass 'showed' + @$('.show').val 'Show Answer' + + @el.find(".capa_inputtype").each (index, inputtype) => + display = @inputtypeDisplays[$(inputtype).attr('id')] + classes = $(inputtype).attr('class').split(' ') + for cls in classes + hideMethod = @inputtypeHideAnswerMethods[cls] + hideMethod(inputtype, display) if hideMethod? + + gentle_alert: (msg) => + if @el.find('.capa_alert').length + @el.find('.capa_alert').remove() + alert_elem = "
" + msg + "
" + @el.find('.action').after(alert_elem) + @el.find('.capa_alert').css(opacity: 0).animate(opacity: 1, 700) + + save: => + Logger.log 'problem_save', @answers + $.postWithPrefix "#{@url}/problem_save", @answers, (response) => + if response.success + saveMessage = "Your answers have been saved but not graded. Hit 'Check' to grade them." + @gentle_alert saveMessage + @updateProgress response + + 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 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 + 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() + + 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 + + inputtypeShowAnswerMethods: + choicegroup: (element, display, answers) => + element = $(element) + + element.find('input').attr('disabled', 'disabled') + + input_id = element.attr('id').replace(/inputtype_/,'') + answer = answers[input_id] + for choice in answer + element.find("label[for='input_#{input_id}_#{choice}']").addClass 'choicegroup_correct' + + javascriptinput: (element, display, answers) => + answer_id = $(element).attr('id').split("_")[1...].join("_") + answer = JSON.parse(answers[answer_id]) + display.showAnswer(answer) + + inputtypeHideAnswerMethods: + choicegroup: (element, display) => + element = $(element) + element.find('input').attr('disabled', null) + element.find('label').removeClass('choicegroup_correct') + + javascriptinput: (element, display) => + display.hideAnswer() diff --git a/common/lib/xmodule/xmodule/self_assessment_module.py b/common/lib/xmodule/xmodule/self_assessment_module.py index f1b8353c75..17aa3832d8 100644 --- a/common/lib/xmodule/xmodule/self_assessment_module.py +++ b/common/lib/xmodule/xmodule/self_assessment_module.py @@ -23,7 +23,7 @@ log = logging.getLogger("mitx.courseware") class SelfAssessmentModule(XModule): js = {'coffee': [resource_string(__name__, 'js/src/javascript_loader.coffee'), resource_string(__name__, 'js/src/collapsible.coffee'), - resource_string(__name__, 'js/src/html/display.coffee') + resource_string(__name__, 'js/src/selfassessment/display.coffee') ] } js_module_name = "SelfAssessmentModule" @@ -38,6 +38,33 @@ class SelfAssessmentModule(XModule): instance_state, shared_state, **kwargs) self.html = self.definition['data'] + def handle_ajax(self, dispatch, get): + ''' + This is called by courseware.module_render, to handle an AJAX call. + "get" is request.POST. + + Returns a json dictionary: + { 'progress_changed' : True/False, + 'progress' : 'none'/'in_progress'/'done', + } + ''' + handlers = { + 'problem_get': self.get_problem, + 'problem_check': self.check_problem, + 'problem_save': self.save_problem, + } + + if dispatch not in handlers: + return 'Error' + + before = self.get_progress() + d = handlers[dispatch](get) + after = self.get_progress() + d.update({ + 'progress_changed': after != before, + 'progress_status': Progress.to_js_status_str(after), + }) + return json.dumps(d, cls=ComplexEncoder) class SelfAssessmentDescriptor(XmlDescriptor, EditingDescriptor):