add self assessment
This commit is contained in:
303
common/lib/xmodule/xmodule/js/src/selfassessment/display.coffee
Normal file
303
common/lib/xmodule/xmodule/js/src/selfassessment/display.coffee
Normal file
@@ -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 = '<ul>\n'
|
||||
for error in errors
|
||||
error_html += '<li>' + error + '</li>\n'
|
||||
error_html += '</ul>'
|
||||
@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 = "<div class='capa_alert'>" + msg + "</div>"
|
||||
@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()
|
||||
@@ -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',
|
||||
<other request-specific values here > }
|
||||
'''
|
||||
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):
|
||||
|
||||
Reference in New Issue
Block a user