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 = '
\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
+
+ 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):