Commit 2cb38a2c by Vik Paruchuri

working on new xmodule

parent 0f5af40b
class @SequenceDescriptor extends XModule.Descriptor
constructor: (@element) ->
@$tabs = $(@element).find("#sequence-list")
@$tabs.sortable(
update: (event, ui) => @update()
)
save: ->
children: $('#sequence-list li a', @element).map((idx, el) -> $(el).data('id')).toArray()
class @JSONEditingDescriptor extends XModule.Descriptor
constructor: (@element) ->
@edit_box = CodeMirror.fromTextArea($(".edit-box", @element)[0], {
mode: { name: "javascript", json: true }
lineNumbers: true
lineWrapping: true
})
save: ->
data: JSON.parse @edit_box.getValue()
class @XMLEditingDescriptor extends XModule.Descriptor
constructor: (@element) ->
@edit_box = CodeMirror.fromTextArea($(".edit-box", @element)[0], {
mode: "xml"
lineNumbers: true
lineWrapping: true
})
save: ->
data: @edit_box.getValue()
class @HTMLEditingDescriptor
constructor: (@element) ->
@edit_box = CodeMirror.fromTextArea($(".edit-box", @element)[0], {
mode: "text/html"
lineNumbers: true
lineWrapping: true
})
save: ->
data: @edit_box.getValue()
class @VerticalDescriptor extends XModule.Descriptor
constructor: (@element) ->
@$items = $(@element).find(".vert-mod")
@$items.sortable(
update: (event, ui) => @update()
)
save: ->
children: $('.vert-mod li', @element).map((idx, el) -> $(el).data('id')).toArray()
.CodeMirror {
background: #fff;
font-size: 13px;
color: #3c3c3c;
}
\ No newline at end of file
.xmodule_edit.xmodule_DiscussionDescriptor { @import "0-3d2f9902e9ecf2dac554cd9b9e661a89.scss"; }.xmodule_edit.xmodule_CapaDescriptor { @import "0-3d2f9902e9ecf2dac554cd9b9e661a89.scss"; }.xmodule_edit.xmodule_ABTestDescriptor { @import "0-3d2f9902e9ecf2dac554cd9b9e661a89.scss"; }.xmodule_edit.xmodule_RawDescriptor { @import "0-3d2f9902e9ecf2dac554cd9b9e661a89.scss"; }.xmodule_edit.xmodule_ErrorDescriptor { @import "0-3d2f9902e9ecf2dac554cd9b9e661a89.scss"; }.xmodule_edit.xmodule_VideoDescriptor { @import "0-3d2f9902e9ecf2dac554cd9b9e661a89.scss"; }.xmodule_edit.xmodule_CustomTagDescriptor { @import "0-3d2f9902e9ecf2dac554cd9b9e661a89.scss"; }
\ No newline at end of file
......@@ -15,8 +15,7 @@ class @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.check').click @check
@$('section.action input.show').click @show
@$('section.action input.save').click @save
......@@ -47,88 +46,6 @@ class @Problem
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) =>
......
......@@ -26,6 +26,7 @@ class SelfAssessmentModule(XModule):
resource_string(__name__, 'js/src/selfassessment/display.coffee')
]
}
js_module_name = "SelfAssessmentModule"
def get_html(self):
......@@ -36,7 +37,82 @@ class SelfAssessmentModule(XModule):
instance_state=None, shared_state=None, **kwargs):
XModule.__init__(self, system, location, definition, descriptor,
instance_state, shared_state, **kwargs)
self.html = self.definition['data']
self.attempts = 0
self.max_attempts = None
dom2 = etree.fromstring(definition['data'])
self.max_attempts = self.metadata.get('attempts', None)
if self.max_attempts is not None:
self.max_attempts = int(self.max_attempts)
self.show_answer = self.metadata.get('showanswer', 'closed')
self.force_save_button = self.metadata.get('force_save_button', 'false')
if self.show_answer == "":
self.show_answer = "closed"
if instance_state is not None:
instance_state = json.loads(instance_state)
if instance_state is not None and 'attempts' in instance_state:
self.attempts = instance_state['attempts']
self.name = only_one(dom2.xpath('/problem/@name'))
self.seed = 1
try:
# TODO (vshnayder): move as much as possible of this work and error
# checking to descriptor load time
self.lcp = LoncapaProblem(self.definition['data'], self.location.html_id(),
instance_state, seed=self.seed, system=self.system)
except Exception as err:
msg = 'cannot create LoncapaProblem {loc}: {err}'.format(
loc=self.location.url(), err=err)
# TODO (vshnayder): do modules need error handlers too?
# We shouldn't be switching on DEBUG.
if self.system.DEBUG:
log.warning(msg)
# TODO (vshnayder): This logic should be general, not here--and may
# want to preserve the data instead of replacing it.
# e.g. in the CMS
msg = '<p>%s</p>' % msg.replace('<', '&lt;')
msg += '<p><pre>%s</pre></p>' % traceback.format_exc().replace('<', '&lt;')
# create a dummy problem with error message instead of failing
problem_text = ('<problem><text><span class="inline-error">'
'Problem %s has an error:</span>%s</text></problem>' %
(self.location.url(), msg))
self.lcp = LoncapaProblem(
problem_text, self.location.html_id(),
instance_state, seed=self.seed, system=self.system)
else:
# add extra info and raise
raise Exception(msg), None, sys.exc_info()[2]
@staticmethod
def make_dict_of_responses(get):
'''Make dictionary of student responses (aka "answers")
get is POST dictionary.
'''
answers = dict()
for key in get:
# e.g. input_resistor_1 ==> resistor_1
_, _, name = key.partition('_')
# This allows for answers which require more than one value for
# the same form input (e.g. checkbox inputs). The convention is that
# if the name ends with '[]' (which looks like an array), then the
# answer will be an array.
if not name.endswith('[]'):
answers[name] = get[key]
else:
name = name[:-2]
answers[name] = get.getlist(key)
return answers
def handle_ajax(self, dispatch, get):
'''
......@@ -50,8 +126,8 @@ class SelfAssessmentModule(XModule):
'''
handlers = {
'problem_get': self.get_problem,
'problem_check': self.check_problem,
'problem_save': self.save_problem,
'problem_check': self.check_problem,
}
if dispatch not in handlers:
......@@ -66,136 +142,314 @@ class SelfAssessmentModule(XModule):
})
return json.dumps(d, cls=ComplexEncoder)
def save_problem(self, get):
'''
Save the passed in answers.
Returns a dict { 'success' : bool, ['error' : error-msg]},
with the error key only present if success is False.
'''
event_info = dict()
event_info['state'] = self.lcp.get_state()
event_info['problem_id'] = self.location.url()
class SelfAssessmentDescriptor(XmlDescriptor, EditingDescriptor):
"""
Module for putting self assessment questions into a course
"""
mako_template = "widgets/html-edit.html"
module_class = SelfAssessmentModule
filename_extension = "xml"
template_dir_name = "selfassessment"
answers = self.make_dict_of_responses(get)
event_info['answers'] = answers
js = {'coffee': [resource_string(__name__, 'js/src/html/edit.coffee')]}
js_module_name = "HTMLEditingDescriptor"
self.lcp.student_answers = answers
# VS[compat] TODO (cpennington): Delete this method once all fall 2012 course
# are being edited in the cms
@classmethod
def backcompat_paths(cls, path):
if path.endswith('.html.xml'):
path = path[:-9] + '.html' # backcompat--look for html instead of xml
if path.endswith('.html.html'):
path = path[:-5] # some people like to include .html in filenames..
candidates = []
while os.sep in path:
candidates.append(path)
_, _, path = path.partition(os.sep)
# also look for .html versions instead of .xml
nc = []
for candidate in candidates:
if candidate.endswith('.xml'):
nc.append(candidate[:-4] + '.html')
return candidates + nc
# NOTE: html descriptors are special. We do not want to parse and
# export them ourselves, because that can break things (e.g. lxml
# adds body tags when it exports, but they should just be html
# snippets that will be included in the middle of pages.
# TODO: should this be save_problem_fail? Looks like success to me...
self.system.track_function('save_problem_fail', event_info)
return {'success': True}
@classmethod
def load_definition(cls, xml_object, system, location):
'''Load a descriptor from the specified xml_object:
def check_problem(self, get):
''' Checks whether answers to a problem are correct, and
returns a map of correct/incorrect answers:
{'success' : bool,
'contents' : html}
'''
event_info = dict()
event_info['state'] = self.lcp.get_state()
event_info['problem_id'] = self.location.url()
answers = self.make_dict_of_responses(get)
event_info['answers'] = convert_files_to_filenames(answers)
try:
old_state = self.lcp.get_state()
lcp_id = self.lcp.problem_id
correct_map = self.lcp.grade_answers(answers)
except StudentInputError as inst:
# TODO (vshnayder): why is this line here?
#self.lcp = LoncapaProblem(self.definition['data'],
# id=lcp_id, state=old_state, system=self.system)
log.exception("StudentInputError in capa_module:problem_check")
return {'success': inst.message}
except Exception, err:
# TODO: why is this line here?
#self.lcp = LoncapaProblem(self.definition['data'],
# id=lcp_id, state=old_state, system=self.system)
if self.system.DEBUG:
msg = "Error checking problem: " + str(err)
msg += '\nTraceback:\n' + traceback.format_exc()
return {'success': msg}
log.exception("Error in capa_module problem checking")
raise Exception("error in capa_module")
self.attempts = self.attempts + 1
self.lcp.done = True
# success = correct if ALL questions in this problem are correct
success = 'correct'
for answer_id in correct_map:
if not correct_map.is_correct(answer_id):
success = 'incorrect'
# NOTE: We are logging both full grading and queued-grading submissions. In the latter,
# 'success' will always be incorrect
event_info['correct_map'] = correct_map.get_dict()
event_info['success'] = success
event_info['attempts'] = self.attempts
self.system.track_function('save_problem_check', event_info)
if hasattr(self.system,'psychometrics_handler'): # update PsychometricsData using callback
self.system.psychometrics_handler(self.get_instance_state())
# render problem into HTML
html = self.get_problem_html(encapsulate=False)
return {'success': success,
'contents': html,
}
# Figure out if we should move these to capa_problem?
def get_problem(self, get):
''' Return results of get_problem_html, as a simple dict for json-ing.
{ 'html': <the-html> }
If there is a filename attribute, load it as a string, and
log a warning if it is not parseable by etree.HTMLParser.
Used if we want to reconfirm we have the right thing e.g. after
several AJAX calls.
'''
return {'html': self.get_problem_html(encapsulate=False)}
If there is not a filename attribute, the definition is the body
of the xml_object, without the root tag (do not want <html> in the
middle of a page)
@staticmethod
def make_dict_of_responses(get):
'''Make dictionary of student responses (aka "answers")
get is POST dictionary.
'''
filename = xml_object.get('filename')
if filename is None:
definition_xml = copy.deepcopy(xml_object)
cls.clean_metadata_from_xml(definition_xml)
return {'data': stringify_children(definition_xml)}
answers = dict()
for key in get:
# e.g. input_resistor_1 ==> resistor_1
_, _, name = key.partition('_')
# This allows for answers which require more than one value for
# the same form input (e.g. checkbox inputs). The convention is that
# if the name ends with '[]' (which looks like an array), then the
# answer will be an array.
if not name.endswith('[]'):
answers[name] = get[key]
else:
# html is special. cls.filename_extension is 'xml', but
# if 'filename' is in the definition, that means to load
# from .html
# 'filename' in html pointers is a relative path
# (not same as 'html/blah.html' when the pointer is in a directory itself)
pointer_path = "{category}/{url_path}".format(category='html',
url_path=name_to_pathname(location.name))
base = path(pointer_path).dirname()
#log.debug("base = {0}, base.dirname={1}, filename={2}".format(base, base.dirname(), filename))
filepath = "{base}/{name}.html".format(base=base, name=filename)
#log.debug("looking for html file for {0} at {1}".format(location, filepath))
name = name[:-2]
answers[name] = get.getlist(key)
return answers
def get_instance_state(self):
state = self.lcp.get_state()
state['attempts'] = self.attempts
return json.dumps(state)
# VS[compat]
# TODO (cpennington): If the file doesn't exist at the right path,
# give the class a chance to fix it up. The file will be written out
# again in the correct format. This should go away once the CMS is
# online and has imported all current (fall 2012) courses from xml
if not system.resources_fs.exists(filepath):
candidates = cls.backcompat_paths(filepath)
#log.debug("candidates = {0}".format(candidates))
for candidate in candidates:
if system.resources_fs.exists(candidate):
filepath = candidate
break
def get_score(self):
return self.lcp.get_score()
def max_score(self):
return self.lcp.get_max_score()
def get_progress(self):
''' For now, just return score / max_score
'''
d = self.get_score()
score = d['score']
total = d['total']
if total > 0:
try:
with system.resources_fs.open(filepath) as file:
html = file.read()
# Log a warning if we can't parse the file, but don't error
if not check_html(html):
msg = "Couldn't parse html in {0}.".format(filepath)
log.warning(msg)
system.error_tracker("Warning: " + msg)
return Progress(score, total)
except Exception as err:
log.exception("Got bad progress")
return None
return None
definition = {'data': html}
def get_html(self):
return self.system.render_template('problem_ajax.html', {
'element_id': self.location.html_id(),
'id': self.id,
'ajax_url': self.system.ajax_url,
})
# TODO (ichuang): remove this after migration
# for Fall 2012 LMS migration: keep filename (and unmangled filename)
definition['filename'] = [ filepath, filename ]
def get_problem_html(self, encapsulate=True):
'''Return html for the problem. Adds check, reset, save buttons
as necessary based on the problem config and state.'''
return definition
try:
html = self.lcp.get_html()
except Exception, err:
log.exception(err)
# TODO (vshnayder): another switch on DEBUG.
if self.system.DEBUG:
msg = (
'[courseware.capa.capa_module] <font size="+1" color="red">'
'Failed to generate HTML for problem %s</font>' %
(self.location.url()))
msg += '<p>Error:</p><p><pre>%s</pre></p>' % str(err).replace('<', '&lt;')
msg += '<p><pre>%s</pre></p>' % traceback.format_exc().replace('<', '&lt;')
html = msg
else:
# We're in non-debug mode, and possibly even in production. We want
# to avoid bricking of problem as much as possible
# Presumably, student submission has corrupted LoncapaProblem HTML.
# First, pull down all student answers
student_answers = self.lcp.student_answers
answer_ids = student_answers.keys()
# Some inputtypes, such as dynamath, have additional "hidden" state that
# is not exposed to the student. Keep those hidden
# TODO: Use regex, e.g. 'dynamath' is suffix at end of answer_id
hidden_state_keywords = ['dynamath']
for answer_id in answer_ids:
for hidden_state_keyword in hidden_state_keywords:
if answer_id.find(hidden_state_keyword) >= 0:
student_answers.pop(answer_id)
# Next, generate a fresh LoncapaProblem
self.lcp = LoncapaProblem(self.definition['data'], self.location.html_id(),
state=None, # Tabula rasa
seed=self.seed, system=self.system)
# Prepend a scary warning to the student
warning = '<div class="capa_reset">'\
'<h2>Warning: The problem has been reset to its initial state!</h2>'\
'The problem\'s state was corrupted by an invalid submission. '\
'The submission consisted of:'\
'<ul>'
for student_answer in student_answers.values():
if student_answer != '':
warning += '<li>' + cgi.escape(student_answer) + '</li>'
warning += '</ul>'\
'If this error persists, please contact the course staff.'\
'</div>'
html = warning
try:
html += self.lcp.get_html()
except Exception, err: # Couldn't do it. Give up
log.exception(err)
raise
content = {'name': self.display_name,
'html': html,
'weight': self.descriptor.weight,
}
except (ResourceNotFoundError) as err:
msg = 'Unable to load file contents at path {0}: {1} '.format(
filepath, err)
# add more info and re-raise
raise Exception(msg), None, sys.exc_info()[2]
# We using strings as truthy values, because the terminology of the
# check button is context-specific.
# TODO (vshnayder): make export put things in the right places.
# Put a "Check" button if unlimited attempts or still some left
if self.max_attempts is None or self.attempts < self.max_attempts-1:
check_button = "Check"
else:
# Will be final check so let user know that
check_button = "Final Check"
reset_button = True
save_button = True
# If we're after deadline, or user has exhausted attempts,
# question is read-only.
if self.closed():
check_button = False
reset_button = False
save_button = False
# User submitted a problem, and hasn't reset. We don't want
# more submissions.
if self.lcp.done and self.rerandomize == "always":
check_button = False
save_button = False
# Only show the reset button if pressing it will show different values
if self.rerandomize not in ["always", "onreset"]:
reset_button = False
# User hasn't submitted an answer yet -- we don't want resets
if not self.lcp.done:
reset_button = False
# We may not need a "save" button if infinite number of attempts and
# non-randomized. The problem author can force it. It's a bit weird for
# randomization to control this; should perhaps be cleaned up.
if (self.force_save_button == "false") and (self.max_attempts is None and self.rerandomize != "always"):
save_button = False
context = {'problem': content,
'id': self.id,
'check_button': check_button,
'reset_button': reset_button,
'save_button': save_button,
'answer_available': self.answer_available(),
'ajax_url': self.system.ajax_url,
'attempts_used': self.attempts,
'attempts_allowed': self.max_attempts,
'progress': self.get_progress(),
}
def definition_to_xml(self, resource_fs):
'''If the contents are valid xml, write them to filename.xml. Otherwise,
write just <html filename="" [meta-attrs="..."]> to filename.xml, and the html
string to filename.html.
'''
try:
return etree.fromstring(self.definition['data'])
except etree.XMLSyntaxError:
pass
# Not proper format. Write html to file, return an empty tag
pathname = name_to_pathname(self.url_name)
pathdir = path(pathname).dirname()
filepath = u'{category}/{pathname}.html'.format(category=self.category,
pathname=pathname)
resource_fs.makedir(os.path.dirname(filepath), allow_recreate=True)
with resource_fs.open(filepath, 'w') as file:
file.write(self.definition['data'])
# write out the relative name
relname = path(pathname).basename()
elt = etree.Element('html')
elt.set("filename", relname)
return elt
html = self.system.render_template('problem.html', context)
if encapsulate:
html = '<div id="problem_{id}" class="problem" data-url="{ajax_url}">'.format(
id=self.location.html_id(), ajax_url=self.system.ajax_url) + html + "</div>"
# cdodge: OK, we have to do two rounds of url reference subsitutions
# one which uses the 'asset library' that is served by the contentstore and the
# more global /static/ filesystem based static content.
# NOTE: rewrite_content_links is defined in XModule
# This is a bit unfortunate and I'm sure we'll try to considate this into
# a one step process.
html = rewrite_links(html, self.rewrite_content_links)
# now do the substitutions which are filesystem based, e.g. '/static/' prefixes
return self.system.replace_urls(html, self.metadata['data_dir'])
class SelfAssessmentDescriptor(XmlDescriptor, EditingDescriptor):
"""
Module for putting self assessment questions into a course
"""
stores_state = True
has_score = True
template_dir_name = 'selfassessment'
# Capa modules have some additional metadata:
# TODO (vshnayder): do problems have any other metadata? Do they
# actually use type and points?
metadata_attributes = RawDescriptor.metadata_attributes + ('type', 'points')
# VS[compat]
# TODO (cpennington): Delete this method once all fall 2012 course are being
# edited in the cms
@classmethod
def backcompat_paths(cls, path):
return [
'problems/' + path[8:],
path[8:],
]
def __init__(self, *args, **kwargs):
super(CapaDescriptor, self).__init__(*args, **kwargs)
weight_string = self.metadata.get('weight', None)
if weight_string:
self.weight = float(weight_string)
else:
self.weight = None
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment