Commit 11a112a0 by Victor Shnayder

Mid-cleanup.

- remove all the html-module-specific cruft
parent 72265d7f
...@@ -30,15 +30,17 @@ TIMEDELTA_REGEX = re.compile(r'^((?P<days>\d+?) day(?:s?))?(\s)?((?P<hours>\d+?) ...@@ -30,15 +30,17 @@ TIMEDELTA_REGEX = re.compile(r'^((?P<days>\d+?) day(?:s?))?(\s)?((?P<hours>\d+?)
def only_one(lst, default="", process=lambda x: x): def only_one(lst, default="", process=lambda x: x):
""" """
If lst is empty, returns default If lst is empty, returns default
If lst has a single element, applies process to that element and returns it
Otherwise, raises an exeception If lst has a single element, applies process to that element and returns it.
Otherwise, raises an exception.
""" """
if len(lst) == 0: if len(lst) == 0:
return default return default
elif len(lst) == 1: elif len(lst) == 1:
return process(lst[0]) return process(lst[0])
else: else:
raise Exception('Malformed XML') raise Exception('Malformed XML: expected at most one element in list.')
def parse_timedelta(time_str): def parse_timedelta(time_str):
...@@ -292,11 +294,11 @@ class CapaModule(XModule): ...@@ -292,11 +294,11 @@ class CapaModule(XModule):
# check button is context-specific. # check button is context-specific.
# Put a "Check" button if unlimited attempts or still some left # Put a "Check" button if unlimited attempts or still some left
if self.max_attempts is None or self.attempts < self.max_attempts-1: if self.max_attempts is None or self.attempts < self.max_attempts-1:
check_button = "Check" check_button = "Check"
else: else:
# Will be final check so let user know that # Will be final check so let user know that
check_button = "Final Check" check_button = "Final Check"
reset_button = True reset_button = True
save_button = True save_button = True
...@@ -527,11 +529,11 @@ class CapaModule(XModule): ...@@ -527,11 +529,11 @@ class CapaModule(XModule):
# Problem queued. Students must wait a specified waittime before they are allowed to submit # Problem queued. Students must wait a specified waittime before they are allowed to submit
if self.lcp.is_queued(): if self.lcp.is_queued():
current_time = datetime.datetime.now() current_time = datetime.datetime.now()
prev_submit_time = self.lcp.get_recentmost_queuetime() prev_submit_time = self.lcp.get_recentmost_queuetime()
waittime_between_requests = self.system.xqueue['waittime'] waittime_between_requests = self.system.xqueue['waittime']
if (current_time-prev_submit_time).total_seconds() < waittime_between_requests: if (current_time-prev_submit_time).total_seconds() < waittime_between_requests:
msg = 'You must wait at least %d seconds between submissions' % waittime_between_requests msg = 'You must wait at least %d seconds between submissions' % waittime_between_requests
return {'success': msg, 'html': ''} # Prompts a modal dialog in ajax callback return {'success': msg, 'html': ''} # Prompts a modal dialog in ajax callback
try: try:
old_state = self.lcp.get_state() old_state = self.lcp.get_state()
...@@ -678,10 +680,10 @@ class CapaDescriptor(RawDescriptor): ...@@ -678,10 +680,10 @@ class CapaDescriptor(RawDescriptor):
'problems/' + path[8:], 'problems/' + path[8:],
path[8:], path[8:],
] ]
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(CapaDescriptor, self).__init__(*args, **kwargs) super(CapaDescriptor, self).__init__(*args, **kwargs)
weight_string = self.metadata.get('weight', None) weight_string = self.metadata.get('weight', None)
if weight_string: if weight_string:
self.weight = float(weight_string) self.weight = float(weight_string)
......
...@@ -14,12 +14,14 @@ from path import path ...@@ -14,12 +14,14 @@ from path import path
import json import json
from progress import Progress from progress import Progress
from .x_module import XModule
from pkg_resources import resource_string from pkg_resources import resource_string
from .xml_module import XmlDescriptor, name_to_pathname
from .capa_module import only_one, ComplexEncoder
from .editing_module import EditingDescriptor from .editing_module import EditingDescriptor
from .stringify import stringify_children
from .html_checker import check_html from .html_checker import check_html
from .stringify import stringify_children
from .x_module import XModule
from .xml_module import XmlDescriptor, name_to_pathname
from xmodule.modulestore import Location from xmodule.modulestore import Location
from xmodule.contentstore.content import XASSET_SRCREF_PREFIX, StaticContent from xmodule.contentstore.content import XASSET_SRCREF_PREFIX, StaticContent
...@@ -28,35 +30,11 @@ log = logging.getLogger("mitx.courseware") ...@@ -28,35 +30,11 @@ log = logging.getLogger("mitx.courseware")
#Set the default number of max attempts. Should be 1 for production #Set the default number of max attempts. Should be 1 for production
#Set higher for debugging/testing #Set higher for debugging/testing
#maxattempts specified in xml definition overrides this # attempts specified in xml definition overrides this.
max_attempts = 1 MAX_ATTEMPTS = 1
def only_one(lst, default="", process=lambda x: x):
"""
If lst is empty, returns default
If lst has a single element, applies process to that element and returns it
Otherwise, raises an exeception
"""
if len(lst) == 0:
return default
elif len(lst) == 1:
return process(lst[0])
else:
raise Exception('Malformed XML')
class ComplexEncoder(json.JSONEncoder):
def default(self, obj):
if isinstance(obj, complex):
return "{real:.7g}{imag:+.7g}*j".format(real=obj.real, imag=obj.imag)
return json.JSONEncoder.default(self, obj)
class SelfAssessmentModule(XModule): class SelfAssessmentModule(XModule):
js = {'coffee': [resource_string(__name__, 'js/src/javascript_loader.coffee'), js = {'coffee': [resource_string(__name__, 'js/src/selfassessment/display.coffee')]
resource_string(__name__, 'js/src/collapsible.coffee'),
resource_string(__name__, 'js/src/selfassessment/display.coffee')
]
} }
js_module_name = "SelfAssessment" js_module_name = "SelfAssessment"
...@@ -70,96 +48,67 @@ class SelfAssessmentModule(XModule): ...@@ -70,96 +48,67 @@ class SelfAssessmentModule(XModule):
instance_state, shared_state, **kwargs) instance_state, shared_state, **kwargs)
""" """
Definition file should have 4 blocks -- problem, rubric, submitmessage, and maxattempts Definition file should have 3 blocks -- prompt, rubric, submitmessage, and one optional attribute, attempts,
which should be an integer that defaults to 1. If it's >1, the student will be able to re-submit after they see
the rubric. Note: all the submissions are stored.
Sample file: Sample file:
<selfassessment> <selfassessment attempts="1">
<problem> <prompt>
Insert problem text here. Insert prompt text here. (arbitrary html)
</problem> </prompt>
<rubric> <rubric>
Insert grading rubric here. Insert grading rubric here. (arbitrary html)
</rubric> </rubric>
<submitmessage> <submitmessage>
Thanks for submitting! Thanks for submitting! (arbitrary html)
</submitmessage> </submitmessage>
<maxattempts>
1
</maxattempts>
</selfassessment> </selfassessment>
""" """
#Initialize variables # Load instance state
self.answer = [] if instance_state is not None:
self.score = 0 instance_state = json.loads(instance_state)
self.top_score = 1 else:
self.attempts = 0 instance_state = {}
self.correctness = []
self.done = False log.debug('Instance state of self-assessment module {0}: {1}'.format(location.url(), instance_state))
self.max_attempts = self.metadata.get('attempts', None)
self.hint=[] # Pull out state, or initialize variables
self.temp_answer=""
# lists of student answers, correctness responses ('incorrect'/'correct'), and suggested hints
self.student_answers = instance_state.get('student_answers', [])
self.correctness = instance_state.get('correctness', [])
self.hints = instance_state.get('hints', [])
# Used to keep track of a submitted answer for which we don't have a self-assessment and hint yet:
# this means that the answers, correctness, hints always stay in sync, and have the same number of elements.
self.temp_answer = instance_state.get('temp_answer', '')
# Used for progress / grading. Currently get credit just for completion (doesn't matter if you self-assessed
# correct/incorrect).
self.score = instance_state.get('score', 0)
self.top_score = instance_state.get('top_score', 1)
# TODO: do we need this? True once everything is done
self.done = instance_state.get('done', False)
self.attempts = instance_state.get('attempts', 0)
#Try setting maxattempts, use default if not available in metadata #Try setting maxattempts, use default if not available in metadata
if self.max_attempts is not None: self.max_attempts = int(self.metadata.get('attempts', MAX_ATTEMPTS))
self.max_attempts = int(self.max_attempts)
else:
self.max_attempts = max_attempts
#Load instance state #Extract prompt, submission message and rubric from definition file
if instance_state is not None: self.rubric = definition['rubric']
instance_state = json.loads(instance_state) self.prompt = definition['prompt']
log.debug(instance_state) self.submit_message = definition['submitmessage']
#Pull variables from instance state if available #Forms to append to prompt and rubric that capture student responses.
if instance_state is not None and 'attempts' in instance_state:
self.attempts = instance_state['attempts']
if instance_state is not None and 'student_answers' in instance_state:
if(type(instance_state['student_answers']) in [type(u''),type('')]):
self.answer.append(instance_state['student_answers'])
elif(type(instance_state['student_answers'])==type([])):
self.answer = instance_state['student_answers']
if instance_state is not None and 'done' in instance_state:
self.done = instance_state['done']
if instance_state is not None and 'temp_answer' in instance_state:
self.temp_answer = instance_state['temp_answer']
if instance_state is not None and 'hint' in instance_state:
if(type(instance_state['hint']) in [type(u''),type('')]):
self.hint.append(instance_state['hint'])
elif(type(instance_state['hint'])==type([])):
self.hint = instance_state['hint']
if instance_state is not None and 'correct_map' in instance_state:
if 'self_assess' in instance_state['correct_map']:
self.score = instance_state['correct_map']['self_assess']['npoints']
map_correctness=instance_state['correct_map']['self_assess']['correctness']
if(type(map_correctness) in [type(u''),type('')]):
self.correctness.append(map_correctness)
elif(type(map_correctness)==type([])):
self.correctness = map_correctness
#Parse definition file
dom2 = etree.fromstring("<selfassessment>" + self.definition['data'] + "</selfassessment>")
#Try setting max_attempts from definition xml
max_attempt_parsed=dom2.xpath('maxattempts')[0].text
try:
self.max_attempts=int(max_attempt_parsed)
except:
pass
#Extract problem, submission message and rubric from definition file
self.rubric = "<br/>" + ''.join([etree.tostring(child) for child in only_one(dom2.xpath('rubric'))])
self.problem = ''.join([etree.tostring(child) for child in only_one(dom2.xpath('problem'))])
self.submit_message = etree.tostring(dom2.xpath('submitmessage')[0])
#Forms to append to problem and rubric that capture student responses.
#Do not change ids and names, as javascript (selfassessment/display.coffee) depends on them #Do not change ids and names, as javascript (selfassessment/display.coffee) depends on them
problem_form = ('<section class="sa-wrapper"><textarea name="answer" ' # TODO: use templates -- system.render_template will pull them from the right place (lms/templates dir)
prompt_form = ('<section class="sa-wrapper"><textarea name="answer" '
'id="answer" cols="50" rows="5"/><br/>' 'id="answer" cols="50" rows="5"/><br/>'
'<input type="button" value="Check" id ="show" name="show"/>' '<input type="button" value="Check" id ="show" name="show"/>'
'<p id="rubric"></p><input type="hidden" ' '<p id="rubric"></p><input type="hidden" '
...@@ -178,20 +127,24 @@ class SelfAssessmentModule(XModule): ...@@ -178,20 +127,24 @@ class SelfAssessmentModule(XModule):
rubric_header=('<br/><br/><b>Rubric</b>') rubric_header=('<br/><br/><b>Rubric</b>')
#Combine problem, rubric, and the forms # TODO:
if type(self.answer)==type([]): #context = {rubric, ..., answer, etc}
if len(self.answer)>0: # self.html = self.system.render_template('selfassessment.html', context)
answer_html="<br/>Previous answer: {0}<br/>".format(self.answer[len(self.answer)-1])
self.problem = ''.join([self.problem, answer_html, problem_form]) #Combine prompt, rubric, and the forms
if type(self.student_answers)==type([]):
if len(self.student_answers)>0:
answer_html="<br/>Previous answer: {0}<br/>".format(self.student_answers[len(self.student_answers)-1])
self.prompt = ''.join([self.prompt, answer_html, prompt_form])
else: else:
self.problem = ''.join([self.problem, problem_form]) self.prompt = ''.join([self.prompt, prompt_form])
else: else:
self.problem = ''.join([self.problem, problem_form]) self.prompt = ''.join([self.prompt, prompt_form])
self.rubric = ''.join([rubric_header,self.rubric, rubric_form]) self.rubric = ''.join([rubric_header, self.rubric, rubric_form])
#Display the problem to the student to begin with #Display the prompt to the student to begin with
self.html = self.problem self.html = self.prompt
def get_score(self): def get_score(self):
...@@ -244,12 +197,14 @@ class SelfAssessmentModule(XModule): ...@@ -244,12 +197,14 @@ class SelfAssessmentModule(XModule):
def show_rubric(self, get): def show_rubric(self, get):
""" """
After the problem is submitted, show the rubric After the prompt is submitted, show the rubric
""" """
#Check to see if attempts are less than max #Check to see if attempts are less than max
if(self.attempts < self.max_attempts): if(self.attempts < self.max_attempts):
#Dump to temp to keep answer in sync with correctness and hint # Dump to temp to keep answer in sync with correctness and hint
self.temp_answer=get.keys()[0]
# TODO: expecting something like get['answer']
self.temp_answer = get.keys()[0]
log.debug(self.temp_answer) log.debug(self.temp_answer)
return {'success': True, 'rubric': self.rubric} return {'success': True, 'rubric': self.rubric}
else: else:
...@@ -263,25 +218,26 @@ class SelfAssessmentModule(XModule): ...@@ -263,25 +218,26 @@ class SelfAssessmentModule(XModule):
''' '''
#Temp answer check is to keep hints, correctness, and answer in sync #Temp answer check is to keep hints, correctness, and answer in sync
points=0 points = 0
log.debug(self.temp_answer) log.debug(self.temp_answer)
if self.temp_answer is not "": if self.temp_answer is not "":
#Extract correctness and hint from ajax and assign points #Extract correctness and hint from ajax and assign points
self.hint.append(get[get.keys()[1]]) self.hints.append(get[get.keys()[1]])
curr_correctness = get[get.keys()[0]].lower() curr_correctness = get[get.keys()[0]].lower()
if curr_correctness == "correct": if curr_correctness == "correct":
points = 1 points = 1
self.correctness.append(curr_correctness) self.correctness.append(curr_correctness)
self.answer.append(self.temp_answer) self.student_answers.append(self.temp_answer)
#Student is done, and increment attempts #Student is done, and increment attempts
self.done = True self.done = True
self.attempts = self.attempts + 1 self.attempts = self.attempts + 1
# TODO: simplify tracking info to just log the relevant stuff
event_info = dict() event_info = dict()
event_info['state'] = {'seed': 1, event_info['state'] = {'seed': 1,
'student_answers': self.answer, 'student_answers': self.student_answers,
'hint' : self.hint, 'hint' : self.hints,
'correct_map': {'self_assess': {'correctness': self.correctness, 'correct_map': {'self_assess': {'correctness': self.correctness,
'npoints': points, 'npoints': points,
'msg': "", 'msg': "",
...@@ -291,8 +247,9 @@ class SelfAssessmentModule(XModule): ...@@ -291,8 +247,9 @@ class SelfAssessmentModule(XModule):
}}, }},
'done': self.done} 'done': self.done}
event_info['problem_id'] = self.location.url() # TODO: figure out how to identify self assessment. May not want to confuse with problems.
event_info['answers'] = self.answer event_info['selfassessment_id'] = self.location.url()
event_info['answers'] = self.student_answers
self.system.track_function('save_problem_succeed', event_info) self.system.track_function('save_problem_succeed', event_info)
...@@ -306,15 +263,17 @@ class SelfAssessmentModule(XModule): ...@@ -306,15 +263,17 @@ class SelfAssessmentModule(XModule):
points = 1 points = 1
#This is a pointless if structure, but left in place in case points change from #This is a pointless if structure, but left in place in case points change from
#being completion based to correctness based #being completion based to correctness based
# TODO: clean up
if type(self.correctness)==type([]): if type(self.correctness)==type([]):
if(len(self.correctness)>0): if(len(self.correctness)>0):
if self.correctness[len(self.correctness)-1]== "correct": if self.correctness[len(self.correctness)-1]== "correct":
points = 1 points = 1
state = {'seed': 1, state = {'seed': 1,
'student_answers': self.answer, 'student_answers': self.student_answers,
'temp_answer': self.temp_answer, 'temp_answer': self.temp_answer,
'hint' : self.hint, 'hint' : self.hints,
'correct_map': {'self_assess': {'correctness': self.correctness, 'correct_map': {'self_assess': {'correctness': self.correctness,
'npoints': points, 'npoints': points,
'msg': "", 'msg': "",
...@@ -342,123 +301,42 @@ class SelfAssessmentDescriptor(XmlDescriptor, EditingDescriptor): ...@@ -342,123 +301,42 @@ class SelfAssessmentDescriptor(XmlDescriptor, EditingDescriptor):
js = {'coffee': [resource_string(__name__, 'js/src/html/edit.coffee')]} js = {'coffee': [resource_string(__name__, 'js/src/html/edit.coffee')]}
js_module_name = "HTMLEditingDescriptor" js_module_name = "HTMLEditingDescriptor"
# 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.
@classmethod @classmethod
def load_definition(cls, xml_object, system, location): def definition_from_xml(cls, xml_object, system):
'''Load a descriptor from the specified xml_object: """
Pull out the rubric, prompt, and submitmessage into a dictionary.
If there is a filename attribute, load it as a string, and
log a warning if it is not parseable by etree.HTMLParser.
If there is not a filename attribute, the definition is the body Returns:
of the xml_object, without the root tag (do not want <html> in the {
middle of a page) 'rubric' : 'some-html',
''' 'prompt' : 'some-html',
filename = xml_object.get('filename') 'submitmessage' : 'some-html'
if filename is None: }
definition_xml = copy.deepcopy(xml_object) """
cls.clean_metadata_from_xml(definition_xml) expected_children = ['rubric', 'prompt', 'submitmessage']
return {'data': stringify_children(definition_xml)} for child in expected_children:
else: if len(xml_object.xpath(child)) != 1:
# html is special. cls.filename_extension is 'xml', but raise ValueError("Self assessment definition must include exactly one '{0}' tag".format(child))
# 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))
# 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
try: def parse(k):
with system.resources_fs.open(filepath) as file: """Assumes that xml_object has child k"""
html = file.read() return stringify_children(xml_object.xpath(k)[0])
# 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)
definition = {'data': html} return {'rubric' : parse('rubric'),
'prompt' : parse('prompt'),
'submitmessage' : parse('submitmessage'),}
# TODO (ichuang): remove this after migration
# for Fall 2012 LMS migration: keep filename (and unmangled filename)
definition['filename'] = [filepath, filename]
return definition def definition_to_xml(self, resource_fs):
'''Return an xml element representing this definition.'''
elt = etree.Element('selfassessment')
except (ResourceNotFoundError) as err: def add_child(k):
msg = 'Unable to load file contents at path {0}: {1} '.format( child_str = '<{tag}>{body}</{tag}>'.format(tag=k, body=self.definition[k])
filepath, err) child_node = etree.fromstring(child_str)
# add more info and re-raise elt.append(child_node)
raise Exception(msg), None, sys.exc_info()[2]
# TODO (vshnayder): make export put things in the right places. for child in ['rubric', 'prompt', 'submitmessage']:
add_child(child)
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 return elt
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