diff --git a/common/lib/xmodule/capa_module.py b/common/lib/xmodule/capa_module.py index 5047b9483285c81b47ff1d165e98a387ba1905fb..0563017ff2ecea1a24f1bee795f9417bef622377 100644 --- a/common/lib/xmodule/capa_module.py +++ b/common/lib/xmodule/capa_module.py @@ -10,8 +10,8 @@ import StringIO from datetime import timedelta from lxml import etree -from x_module import XModule -from mako_module import MakoModuleDescriptor +from xmodule.x_module import XModule +from xmodule.raw_module import RawDescriptor from progress import Progress from capa.capa_problem import LoncapaProblem from capa.responsetypes import StudentInputError @@ -64,37 +64,25 @@ class ComplexEncoder(json.JSONEncoder): return json.JSONEncoder.default(self, obj) -class CapaModuleDescriptor(MakoModuleDescriptor): - """ - Module implementing problems in the LON-CAPA format, - as implemented by capa.capa_problem - """ - - mako_template = 'widgets/problem-edit.html' - - - -class Module(XModule): +class CapaModule(XModule): ''' Interface between capa_problem and x_module. Originally a hack meant to be refactored out, but it seems to be serving a useful prupose now. We can e.g .destroy and create the capa_problem on a reset. ''' + icon_class = 'problem' def get_instance_state(self): state = self.lcp.get_state() state['attempts'] = self.attempts return json.dumps(state) - 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 ''' @@ -105,14 +93,13 @@ class Module(XModule): return Progress(score, total) return None - def get_html(self): return self.system.render_template('problem_ajax.html', { - 'id': self.item_id, - 'ajax_url': self.ajax_url, + 'element_id': self.location.html_id(), + 'id': self.id, + 'ajax_url': self.system.ajax_url, }) - 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.''' @@ -165,12 +152,12 @@ class Module(XModule): explain = False context = {'problem': content, - 'id': self.item_id, + 'id': self.id, 'check_button': check_button, 'reset_button': reset_button, 'save_button': save_button, 'answer_available': self.answer_available(), - 'ajax_url': self.ajax_url, + 'ajax_url': self.system.ajax_url, 'attempts_used': self.attempts, 'attempts_allowed': self.max_attempts, 'explain': explain, @@ -180,17 +167,17 @@ class Module(XModule): html = self.system.render_template('problem.html', context) if encapsulate: html = '<div id="problem_{id}" class="problem" data-url="{ajax_url}">'.format( - id=self.item_id, ajax_url=self.ajax_url) + html + "</div>" + id=self.location.html_id(), ajax_url=self.system.ajax_url) + html + "</div>" return html - def __init__(self, system, xml, item_id, instance_state=None, shared_state=None): - XModule.__init__(self, system, xml, item_id, instance_state, shared_state) + def __init__(self, system, location, definition, instance_state=None, shared_state=None, **kwargs): + XModule.__init__(self, system, location, definition, instance_state, shared_state, **kwargs) self.attempts = 0 self.max_attempts = None - dom2 = etree.fromstring(xml) + dom2 = etree.fromstring(definition['data']) self.explanation = "problems/" + only_one(dom2.xpath('/problem/@explain'), default="closed") @@ -205,7 +192,7 @@ class Module(XModule): self.display_due_date = None grace_period_string = only_one(dom2.xpath('/problem/@graceperiod')) - if len(grace_period_string) >0 and self.display_due_date: + if len(grace_period_string) > 0 and self.display_due_date: self.grace_period = parse_timedelta(grace_period_string) self.close_date = self.display_due_date + self.grace_period #log.debug("Then parsed " + grace_period_string + " to closing date" + str(self.close_date)) @@ -240,9 +227,9 @@ class Module(XModule): self.attempts = instance_state['attempts'] # TODO: Should be: self.filename=only_one(dom2.xpath('/problem/@filename')) - self.filename= "problems/"+only_one(dom2.xpath('/problem/@filename'))+".xml" - self.name=only_one(dom2.xpath('/problem/@name')) - self.weight=only_one(dom2.xpath('/problem/@weight')) + self.filename = "problems/" + only_one(dom2.xpath('/problem/@filename')) + ".xml" + self.name = only_one(dom2.xpath('/problem/@name')) + self.weight = only_one(dom2.xpath('/problem/@weight')) if self.rerandomize == 'never': seed = 1 elif self.rerandomize == "per_student" and hasattr(system, 'id'): @@ -250,27 +237,27 @@ class Module(XModule): else: seed = None try: - fp = self.filestore.open(self.filename) - except Exception,err: - log.exception('[courseware.capa.capa_module.Module.init] error %s: cannot open file %s' % (err,self.filename)) - if self.DEBUG: + fp = self.system.filestore.open(self.filename) + except Exception: + log.exception('cannot open file %s' % self.filename) + if self.system.DEBUG: # create a dummy problem instead of failing fp = StringIO.StringIO('<problem><text><font color="red" size="+2">Problem file %s is missing</font></text></problem>' % self.filename) fp.name = "StringIO" else: raise try: - self.lcp=LoncapaProblem(fp, self.item_id, instance_state, seed = seed, system=self.system) - except Exception,err: - msg = '[courseware.capa.capa_module.Module.init] error %s: cannot create LoncapaProblem %s' % (err,self.filename) + self.lcp = LoncapaProblem(fp, self.id, instance_state, seed=seed, system=self.system) + except Exception: + msg = 'cannot create LoncapaProblem %s' % self.filename log.exception(msg) - if self.DEBUG: - msg = '<p>%s</p>' % msg.replace('<','<') - msg += '<p><pre>%s</pre></p>' % traceback.format_exc().replace('<','<') + if self.system.DEBUG: + msg = '<p>%s</p>' % msg.replace('<', '<') + msg += '<p><pre>%s</pre></p>' % traceback.format_exc().replace('<', '<') # create a dummy problem with error message instead of failing - fp = StringIO.StringIO('<problem><text><font color="red" size="+2">Problem file %s has an error:</font>%s</text></problem>' % (self.filename,msg)) + fp = StringIO.StringIO('<problem><text><font color="red" size="+2">Problem file %s has an error:</font>%s</text></problem>' % (self.filename, msg)) fp.name = "StringIO" - self.lcp=LoncapaProblem(fp, self.item_id, instance_state, seed = seed, system=self.system) + self.lcp = LoncapaProblem(fp, self.id, instance_state, seed=seed, system=self.system) else: raise @@ -299,8 +286,8 @@ class Module(XModule): d = handlers[dispatch](get) after = self.get_progress() d.update({ - 'progress_changed' : after != before, - 'progress_status' : Progress.to_js_status_str(after), + 'progress_changed': after != before, + 'progress_status': Progress.to_js_status_str(after), }) return json.dumps(d, cls=ComplexEncoder) @@ -313,7 +300,6 @@ class Module(XModule): return False - def answer_available(self): ''' Is the user allowed to see an answer? ''' @@ -334,7 +320,8 @@ class Module(XModule): if self.show_answer == 'always': return True - raise self.system.exception404 #TODO: Not 404 + #TODO: Not 404 + raise self.system.exception404 def get_answer(self, get): ''' @@ -348,8 +335,7 @@ class Module(XModule): raise self.system.exception404 else: answers = self.lcp.get_question_answers() - return {'answers' : answers} - + return {'answers': answers} # Figure out if we should move these to capa_problem? def get_problem(self, get): @@ -358,8 +344,8 @@ class Module(XModule): 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)} + ''' + return {'html': self.get_problem_html(encapsulate=False)} @staticmethod def make_dict_of_responses(get): @@ -409,18 +395,16 @@ class Module(XModule): correct_map = self.lcp.grade_answers(answers) except StudentInputError as inst: # TODO (vshnayder): why is this line here? - self.lcp = LoncapaProblem(self.filestore.open(self.filename), + self.lcp = LoncapaProblem(self.system.filestore.open(self.filename), id=lcp_id, state=old_state, system=self.system) traceback.print_exc() return {'success': inst.message} except: # TODO: why is this line here? - self.lcp = LoncapaProblem(self.filestore.open(self.filename), + self.lcp = LoncapaProblem(self.system.filestore.open(self.filename), id=lcp_id, state=old_state, system=self.system) traceback.print_exc() - raise Exception,"error in capa_module" - # TODO: Dead code... is this a bug, or just old? - return {'success':'Unknown Error'} + raise Exception("error in capa_module") self.attempts = self.attempts + 1 self.lcp.done = True @@ -431,21 +415,18 @@ class Module(XModule): if not correct_map.is_correct(answer_id): success = 'incorrect' - event_info['correct_map'] = correct_map.get_dict() # log this in the tracker + # log this in the tracker + event_info['correct_map'] = correct_map.get_dict() event_info['success'] = success self.tracker('save_problem_check', event_info) - try: - html = self.get_problem_html(encapsulate=False) # render problem into HTML - except Exception,err: - log.error('failed to generate html') - raise + # render problem into HTML + html = self.get_problem_html(encapsulate=False) return {'success': success, 'contents': html, } - def save_problem(self, get): ''' Save the passed in answers. @@ -471,8 +452,8 @@ class Module(XModule): if self.lcp.done and self.rerandomize == "always": event_info['failure'] = 'done' self.tracker('save_problem_fail', event_info) - return {'success' : False, - 'error' : "Problem needs to be reset prior to save."} + return {'success': False, + 'error': "Problem needs to be reset prior to save."} self.lcp.student_answers = answers @@ -485,7 +466,7 @@ class Module(XModule): and causes problem to rerender itself. Returns problem html as { 'html' : html-string }. - ''' + ''' event_info = dict() event_info['old_state'] = self.lcp.get_state() event_info['filename'] = self.filename @@ -503,12 +484,21 @@ class Module(XModule): self.lcp.do_reset() if self.rerandomize == "always": # reset random number generator seed (note the self.lcp.get_state() in next line) - self.lcp.seed=None - - self.lcp = LoncapaProblem(self.filestore.open(self.filename), - self.item_id, self.lcp.get_state(), system=self.system) + self.lcp.seed = None + + self.lcp = LoncapaProblem(self.system.filestore.open(self.filename), + self.id, self.lcp.get_state(), system=self.system) event_info['new_state'] = self.lcp.get_state() self.tracker('reset_problem', event_info) - return {'html' : self.get_problem_html(encapsulate=False)} + return {'html': self.get_problem_html(encapsulate=False)} + + +class CapaDescriptor(RawDescriptor): + """ + Module implementing problems in the LON-CAPA format, + as implemented by capa.capa_problem + """ + + module_class = CapaModule diff --git a/common/lib/xmodule/setup.py b/common/lib/xmodule/setup.py index 3e3e33805f76c3e1f3b3a8bd843e73f23959d2b0..a9f4e1f4dc90e109e6a8bc911d75029197fba412 100644 --- a/common/lib/xmodule/setup.py +++ b/common/lib/xmodule/setup.py @@ -19,9 +19,10 @@ setup( "section = xmodule.translation_module:SemanticSectionDescriptor", "sequential = xmodule.seq_module:SequenceDescriptor", "vertical = xmodule.vertical_module:VerticalDescriptor", + "problem = xmodule.capa_module:CapaDescriptor", "problemset = xmodule.seq_module:SequenceDescriptor", - "videosequence = xmodule.seq_module:SequenceDescriptor", "video = xmodule.video_module:VideoDescriptor", + "videosequence = xmodule.seq_module:SequenceDescriptor", ] } ) diff --git a/common/lib/xmodule/vertical_module.py b/common/lib/xmodule/vertical_module.py index 6153aff324aff353b5e32e4b5126ef7f63f66dad..6008eb422613ceff6878aebf91746031c177ec44 100644 --- a/common/lib/xmodule/vertical_module.py +++ b/common/lib/xmodule/vertical_module.py @@ -10,6 +10,9 @@ class_priority = ['video', 'problem'] class VerticalModule(XModule): ''' Layout module for laying out submodules vertically.''' def get_html(self): + if self.contents is None: + self.contents = [child.get_html() for child in self.get_display_items()] + return self.system.render_template('vert_module.html', { 'items': self.contents }) @@ -31,7 +34,7 @@ class VerticalModule(XModule): def __init__(self, system, location, definition, instance_state=None, shared_state=None, **kwargs): XModule.__init__(self, system, location, definition, instance_state, shared_state, **kwargs) - self.contents = [child.get_html() for child in self.get_display_items()] + self.contents = None class VerticalDescriptor(SequenceDescriptor): diff --git a/lms/djangoapps/courseware/module_render.py b/lms/djangoapps/courseware/module_render.py index d05bdcefab8b72f8e84d84f59869a1ec08c83617..d8ebb82adb64adb61f04a26811f2e0e33b88944b 100644 --- a/lms/djangoapps/courseware/module_render.py +++ b/lms/djangoapps/courseware/module_render.py @@ -60,13 +60,7 @@ class I4xSystem(object): ''' self.ajax_url = ajax_url self.track_function = track_function - if not filestore: - self.filestore = OSFS(settings.DATA_DIR) - else: - self.filestore = filestore - if settings.DEBUG: - log.info("[courseware.module_render.I4xSystem] filestore path = %s", - filestore) + self.filestore = filestore self.get_module = get_module self.render_function = render_function self.render_template = render_template @@ -241,7 +235,7 @@ def get_module(user, request, location, student_module_cache, position=None): shared_state = shared_module.state if shared_module is not None else None # Setup system context for module instance - ajax_url = settings.MITX_ROOT_URL + '/modx/' + descriptor.type + '/' + descriptor.url + '/' + ajax_url = settings.MITX_ROOT_URL + '/modx/' + descriptor.url + '/' def _get_module(location): (module, _, _, _) = get_module(user, request, location, student_module_cache, position) @@ -330,94 +324,33 @@ def render_x_module(user, request, module_xml, student_module_cache, position=No return context -def modx_dispatch(request, module=None, dispatch=None, id=None): +def modx_dispatch(request, dispatch=None, id=None): ''' Generic view for extensions. This is where AJAX calls go. Arguments: - request -- the django request. - - module -- the type of the module, as used in the course configuration xml. - e.g. 'problem', 'video', etc - dispatch -- the command string to pass through to the module's handle_ajax call (e.g. 'problem_reset'). If this string contains '?', only pass through the part before the first '?'. - - id -- the module id. Used to look up the student module. - e.g. filenamexformularesponse + - id -- the module id. Used to look up the XModule instance ''' # ''' (fix emacs broken parsing) - if not request.user.is_authenticated(): - return redirect('/') - - # python concats adjacent strings - error_msg = ("We're sorry, this module is temporarily unavailable. " - "Our staff is working to fix it as soon as possible") # If there are arguments, get rid of them dispatch, _, _ = dispatch.partition('?') - ajax_url = '{root}/modx/{module}/{id}'.format(root=settings.MITX_ROOT_URL, - module=module, id=id) - coursename = multicourse_settings.get_coursename_from_request(request) - if coursename and settings.ENABLE_MULTICOURSE: - xp = multicourse_settings.get_course_xmlpath(coursename) - data_root = settings.DATA_DIR + xp - else: - data_root = settings.DATA_DIR + student_module_cache = StudentModuleCache(request.user, keystore().get_item(id)) + instance, instance_module, shared_module, module_type = get_module(request.user, request, id, student_module_cache) - # Grab the XML corresponding to the request from course.xml - try: - xml = content_parser.module_xml(request.user, module, 'id', id, coursename) - except: - log.exception( - "Unable to load module during ajax call. module=%s, dispatch=%s, id=%s", - module, dispatch, id) - if accepts(request, 'text/html'): - return render_to_response("module-error.html", {}) - else: - response = HttpResponse(json.dumps({'success': error_msg})) - return response - - module_xml = etree.fromstring(xml) - student_module_cache = StudentModuleCache(request.user, module_xml) - (instance, instance_state, shared_state, module_type) = get_module( - request.user, request, module_xml, - student_module_cache, None) - - if instance_state is None: - log.debug("Couldn't find module '%s' for user '%s' and id '%s'", - module, request.user, id) + if instance_module is None: + log.debug("Couldn't find module '%s' for user '%s'", + id, request.user) raise Http404 - oldgrade = instance_state.grade - old_instance_state = instance_state.state - old_shared_state = shared_state.state if shared_state is not None else None - - module_from_xml = make_module_from_xml_fn( - request.user, request, student_module_cache, None) - - # Create the module - system = I4xSystem(track_function=make_track_function(request), - render_function=None, - module_from_xml=module_from_xml, - render_template=render_to_string, - ajax_url=ajax_url, - request=request, - filestore=OSFS(data_root), - ) - - try: - module_class = xmodule.get_module_class(module) - instance = module_class( - system, xml, id, - instance_state=old_instance_state, - shared_state=old_shared_state) - except: - log.exception("Unable to load module instance during ajax call") - if accepts(request, 'text/html'): - return render_to_response("module-error.html", {}) - else: - response = HttpResponse(json.dumps({'success': error_msg})) - return response + oldgrade = instance_module.grade + old_instance_state = instance_module.state + old_shared_state = shared_module.state if shared_module is not None else None # Let the module handle the AJAX try: @@ -427,16 +360,16 @@ def modx_dispatch(request, module=None, dispatch=None, id=None): raise # Save the state back to the database - instance_state.state = instance.get_instance_state() + instance_module.state = instance.get_instance_state() if instance.get_score(): - instance_state.grade = instance.get_score()['score'] - if instance_state.grade != oldgrade or instance_state.state != old_instance_state: - instance_state.save() - - if shared_state is not None: - shared_state.state = instance.get_shared_state() - if shared_state.state != old_shared_state: - shared_state.save() + instance_module.grade = instance.get_score()['score'] + if instance_module.grade != oldgrade or instance_module.state != old_instance_state: + instance_module.save() + + if shared_module is not None: + shared_module.state = instance.get_shared_state() + if shared_module.state != old_shared_state: + shared_module.save() # Return whatever the module wanted to return to the client/caller return HttpResponse(ajax_return) diff --git a/lms/lib/dogfood/views.py b/lms/lib/dogfood/views.py index a91314d228bd59b7b30a3c3f1cd82c66676808b1..ba8601cc20e4fc932d779a62d39435351b64a0fb 100644 --- a/lms/lib/dogfood/views.py +++ b/lms/lib/dogfood/views.py @@ -174,7 +174,7 @@ def quickedit(request, id=None, qetemplate='quickedit.html',coursename=None): module = 'problem' xml = content_parser.module_xml(request.user, module, 'id', id, coursename) - ajax_url = settings.MITX_ROOT_URL + '/modx/'+module+'/'+id+'/' + ajax_url = settings.MITX_ROOT_URL + '/modx/'+id+'/' # Create the module (instance of capa_module.Module) system = I4xSystem(track_function = make_track_function(request), diff --git a/lms/static/coffee/src/courseware.coffee b/lms/static/coffee/src/courseware.coffee index de232e05e407e1c953ba6b35f5194d1efae4ee35..4e57d13194cdc7df7f7f80019d4e229ee61f47bb 100644 --- a/lms/static/coffee/src/courseware.coffee +++ b/lms/static/coffee/src/courseware.coffee @@ -20,8 +20,8 @@ class @Courseware id = $(this).attr('id').replace(/video_/, '') new Video id, $(this).data('streams') $('.course-content .problems-wrapper').each -> - id = $(this).attr('id').replace(/problem_/, '') - new Problem id, $(this).data('url') + id = $(this).attr('problem-id') + new Problem id, $(this).attr('id'), $(this).data('url') $('.course-content .histogram').each -> id = $(this).attr('id').replace(/histogram_/, '') new Histogram id, $(this).data('histogram') diff --git a/lms/static/coffee/src/modules/problem.coffee b/lms/static/coffee/src/modules/problem.coffee index f29c9eb72bbd78bad7eba03222d52a63bc99ffc9..eb2c057beff47116bfc1dda88b79fad1298e3cbd 100644 --- a/lms/static/coffee/src/modules/problem.coffee +++ b/lms/static/coffee/src/modules/problem.coffee @@ -1,6 +1,6 @@ class @Problem - constructor: (@id, url) -> - @element = $("#problem_#{id}") + constructor: (@id, @element_id, url) -> + @element = $("##{element_id}") @render() $: (selector) -> @@ -26,13 +26,13 @@ class @Problem @element.html(content) @bind() else - $.postWithPrefix "/modx/problem/#{@id}/problem_get", (response) => + $.postWithPrefix "/modx/#{@id}/problem_get", (response) => @element.html(response.html) @bind() check: => Logger.log 'problem_check', @answers - $.postWithPrefix "/modx/problem/#{@id}/problem_check", @answers, (response) => + $.postWithPrefix "/modx/#{@id}/problem_check", @answers, (response) => switch response.success when 'incorrect', 'correct' @render(response.contents) @@ -42,14 +42,14 @@ class @Problem reset: => Logger.log 'problem_reset', @answers - $.postWithPrefix "/modx/problem/#{@id}/problem_reset", id: @id, (response) => + $.postWithPrefix "/modx/#{@id}/problem_reset", id: @id, (response) => @render(response.html) @updateProgress response show: => if !@element.hasClass 'showed' Logger.log 'problem_show', problem: @id - $.postWithPrefix "/modx/problem/#{@id}/problem_show", (response) => + $.postWithPrefix "/modx/#{@id}/problem_show", (response) => answers = response.answers $.each answers, (key, value) => if $.isArray(value) @@ -69,7 +69,7 @@ class @Problem save: => Logger.log 'problem_save', @answers - $.postWithPrefix "/modx/problem/#{@id}/problem_save", @answers, (response) => + $.postWithPrefix "/modx/#{@id}/problem_save", @answers, (response) => if response.success alert 'Saved' @updateProgress response @@ -94,4 +94,4 @@ class @Problem element.schematic.update_value() @$(".CodeMirror").each (index, element) -> element.CodeMirror.save() if element.CodeMirror.save - @answers = @$("[id^=input_#{@id}_]").serialize() + @answers = @$("[id^=input_#{@element_id}_]").serialize() diff --git a/lms/static/coffee/src/modules/sequence.coffee b/lms/static/coffee/src/modules/sequence.coffee index a4a80e34078a933c4c4867557c165231becdd7ba..2c979f08539faebdcc03f65d3ac77fd8650fa323 100644 --- a/lms/static/coffee/src/modules/sequence.coffee +++ b/lms/static/coffee/src/modules/sequence.coffee @@ -88,7 +88,7 @@ class @Sequence if @position != new_position if @position != undefined @mark_visited @position - $.postWithPrefix "/modx/#{@tag}/#{@id}/goto_position", position: new_position + $.postWithPrefix "/modx/#{@id}/goto_position", position: new_position @mark_active new_position @$('#seq_content').html @elements[new_position - 1].content diff --git a/lms/templates/problem_ajax.html b/lms/templates/problem_ajax.html index 78b85df3c1eb2f0811ee1ead0b67bef30ca74a7e..6330edfac05871cd53a71d0effaf0cf23ad840d8 100644 --- a/lms/templates/problem_ajax.html +++ b/lms/templates/problem_ajax.html @@ -1 +1 @@ -<section id="problem_${id}" class="problems-wrapper" data-url="${ajax_url}"></section> +<section id="problem_${element_id}" class="problems-wrapper" problem-id="${id}" data-url="${ajax_url}"></section> diff --git a/lms/urls.py b/lms/urls.py index 313be62c516d43985b66e48e9282a3281950c2f5..e43c949643c62b9b8307cab5c0b365d032d2c543 100644 --- a/lms/urls.py +++ b/lms/urls.py @@ -57,7 +57,7 @@ if settings.COURSEWARE_ENABLED: url(r'^courseware/(?P<course>[^/]*)/$', 'courseware.views.index', name="courseware_course"), url(r'^jumpto/(?P<probname>[^/]+)/$', 'courseware.views.jump_to'), url(r'^section/(?P<section>[^/]*)/$', 'courseware.views.render_section'), - url(r'^modx/(?P<module>[^/]*)/(?P<id>[^/]*)/(?P<dispatch>[^/]*)$', 'courseware.module_render.modx_dispatch'), #reset_problem'), + url(r'^modx/(?P<id>.*?)/(?P<dispatch>[^/]*)$', 'courseware.module_render.modx_dispatch'), #reset_problem'), url(r'^profile$', 'courseware.views.profile'), url(r'^profile/(?P<student_id>[^/]*)/$', 'courseware.views.profile'), url(r'^change_setting$', 'student.views.change_setting'),