From 9545a82ab9402400908ee69de9334e132f0e998e Mon Sep 17 00:00:00 2001 From: Diana Huang Date: Mon, 4 Mar 2013 15:48:20 -0500 Subject: [PATCH 1/8] Add the ability to make ajax calls on the input type of capa problems. --- common/lib/capa/capa/capa_problem.py | 27 ++++++++++++++++--- common/lib/capa/capa/inputtypes.py | 12 +++++++++ common/lib/xmodule/xmodule/capa_module.py | 2 ++ .../xmodule/js/src/capa/display.coffee | 8 ++++++ 4 files changed, 46 insertions(+), 3 deletions(-) diff --git a/common/lib/capa/capa/capa_problem.py b/common/lib/capa/capa/capa_problem.py index fb0b63b83c..5e3b5627ab 100644 --- a/common/lib/capa/capa/capa_problem.py +++ b/common/lib/capa/capa/capa_problem.py @@ -146,6 +146,9 @@ class LoncapaProblem(object): if not self.student_answers: # True when student_answers is an empty dict self.set_initial_display() + self.extracted_tree = self._extract_html(self.tree) + + def do_reset(self): ''' Reset internal state to unfinished, with no answers @@ -324,7 +327,21 @@ class LoncapaProblem(object): ''' Main method called externally to get the HTML to be rendered for this capa Problem. ''' - return contextualize_text(etree.tostring(self._extract_html(self.tree)), self.context) + html = contextualize_text(etree.tostring(self.extracted_tree), self.context) + return html + + + def handle_input_ajax(self, get): + ''' + This passes any specialized input ajax onto the input class + + It also parses out the dispatch from the get so that it can be passed onto the input type nicely + ''' + if self.input: + dispatch = get['dispatch'] + return self.input.handle_ajax(dispatch, get) + return {} + # ======= Private Methods Below ======== @@ -458,6 +475,8 @@ class LoncapaProblem(object): finally: sys.path = original_path + + def _extract_html(self, problemtree): # private ''' Main (private) function which converts Problem XML tree to HTML. @@ -468,6 +487,7 @@ class LoncapaProblem(object): Used by get_html. ''' + if (problemtree.tag == 'script' and problemtree.get('type') and 'javascript' in problemtree.get('type')): # leave javascript intact. @@ -505,8 +525,9 @@ class LoncapaProblem(object): 'hintmode': hintmode, }} input_type_cls = inputtypes.registry.get_class_for_tag(problemtree.tag) - the_input = input_type_cls(self.system, problemtree, state) - return the_input.get_html() + # save the input type so that we can make ajax calls on it if we need to + self.input = input_type_cls(self.system, problemtree, state) + return self.input.get_html() # let each Response render itself if problemtree in self.responders: diff --git a/common/lib/capa/capa/inputtypes.py b/common/lib/capa/capa/inputtypes.py index 951104501a..1a141338b7 100644 --- a/common/lib/capa/capa/inputtypes.py +++ b/common/lib/capa/capa/inputtypes.py @@ -215,6 +215,18 @@ class InputTypeBase(object): """ pass + def handle_ajax(self, dispatch, get): + """ + InputTypes that need to handle specialized AJAX should override this. + + Input: + dispatch: a string that can be used to determine how to handle the data passed in + get: a dictionary containing the data that was sent with the ajax call + + Output: + a dictionary object that will then get sent back to the Javascript + """ + pass def _get_render_context(self): """ diff --git a/common/lib/xmodule/xmodule/capa_module.py b/common/lib/xmodule/xmodule/capa_module.py index 9ae6583c50..7ab7b60239 100644 --- a/common/lib/xmodule/xmodule/capa_module.py +++ b/common/lib/xmodule/xmodule/capa_module.py @@ -412,6 +412,7 @@ class CapaModule(XModule): 'weight': self.descriptor.weight, } + context = {'problem': content, 'id': self.id, 'check_button': check_button, @@ -449,6 +450,7 @@ class CapaModule(XModule): 'problem_save': self.save_problem, 'problem_show': self.get_answer, 'score_update': self.update_score, + 'input_ajax': self.lcp.handle_input_ajax } if dispatch not in handlers: diff --git a/common/lib/xmodule/xmodule/js/src/capa/display.coffee b/common/lib/xmodule/xmodule/js/src/capa/display.coffee index 41c9b50891..07a96c8b02 100644 --- a/common/lib/xmodule/xmodule/js/src/capa/display.coffee +++ b/common/lib/xmodule/xmodule/js/src/capa/display.coffee @@ -76,6 +76,14 @@ class @Problem # TODO: Some logic to dynamically adjust polling rate based on queuelen window.queuePollerID = window.setTimeout(@poll, 1000) + + # Use this if you want to make an ajax call on the input type object + # static method so you don't have to instantiate a Problem in order to use it + @inputAjax: (url, dispatch, data, callback) -> + data['dispatch'] = dispatch + $.postWithPrefix "#{url}/input_ajax", data, callback + + render: (content) -> if content @el.html(content) From 6f535d9e0ba002126d9e5bb2270cf19be4a7da81 Mon Sep 17 00:00:00 2001 From: Diana Huang Date: Tue, 5 Mar 2013 09:20:40 -0500 Subject: [PATCH 2/8] Fix test rendering so we can parse the problem during tests without it breaking. --- common/lib/xmodule/xmodule/tests/__init__.py | 1 + common/lib/xmodule/xmodule/tests/test_conditional.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/common/lib/xmodule/xmodule/tests/__init__.py b/common/lib/xmodule/xmodule/tests/__init__.py index 43c2bbe24d..20f31315c4 100644 --- a/common/lib/xmodule/xmodule/tests/__init__.py +++ b/common/lib/xmodule/xmodule/tests/__init__.py @@ -18,6 +18,7 @@ import capa.calc as calc import xmodule from xmodule.x_module import ModuleSystem from mock import Mock +import xml.sax.saxutils as saxutils open_ended_grading_interface = { 'url': 'http://sandbox-grader-001.m.edx.org/peer_grading', diff --git a/common/lib/xmodule/xmodule/tests/test_conditional.py b/common/lib/xmodule/xmodule/tests/test_conditional.py index 16bd222b9e..c3468905ad 100644 --- a/common/lib/xmodule/xmodule/tests/test_conditional.py +++ b/common/lib/xmodule/xmodule/tests/test_conditional.py @@ -106,7 +106,7 @@ class ConditionalModuleTest(unittest.TestCase): html = module.get_html() print "html type: ", type(html) print "html: ", html - html_expect = "{'ajax_url': 'courses/course_id/modx/a_location', 'element_id': 'i4x-edX-cond_test-conditional-condone', 'id': 'i4x://edX/cond_test/conditional/condone'}" + html_expect = "
{'ajax_url': 'courses/course_id/modx/a_location', 'element_id': 'i4x-edX-cond_test-conditional-condone', 'id': 'i4x://edX/cond_test/conditional/condone'}
" self.assertEqual(html, html_expect) gdi = module.get_display_items() From 385a62d7d16e75b4125d717cf5360308f0b3d143 Mon Sep 17 00:00:00 2001 From: Diana Huang Date: Tue, 5 Mar 2013 11:11:55 -0500 Subject: [PATCH 3/8] Handle multiple inputs properly for ajax handling. --- common/lib/capa/capa/capa_problem.py | 18 +++++++++++++----- .../xmodule/xmodule/js/src/capa/display.coffee | 3 ++- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/common/lib/capa/capa/capa_problem.py b/common/lib/capa/capa/capa_problem.py index 5e3b5627ab..a7a362d51e 100644 --- a/common/lib/capa/capa/capa_problem.py +++ b/common/lib/capa/capa/capa_problem.py @@ -146,6 +146,8 @@ class LoncapaProblem(object): if not self.student_answers: # True when student_answers is an empty dict self.set_initial_display() + self.inputs = {} + self.extracted_tree = self._extract_html(self.tree) @@ -337,10 +339,16 @@ class LoncapaProblem(object): It also parses out the dispatch from the get so that it can be passed onto the input type nicely ''' - if self.input: + + # pull out the id + problem_id = get['problem_id'] + if self.inputs[problem_id]: dispatch = get['dispatch'] - return self.input.handle_ajax(dispatch, get) - return {} + return self.inputs[problem_id].handle_ajax(dispatch, get) + else: + log.warning("Could not find matching input for id: %s" % problem_id) + return {} + # ======= Private Methods Below ======== @@ -526,8 +534,8 @@ class LoncapaProblem(object): input_type_cls = inputtypes.registry.get_class_for_tag(problemtree.tag) # save the input type so that we can make ajax calls on it if we need to - self.input = input_type_cls(self.system, problemtree, state) - return self.input.get_html() + self.inputs[problemid] = input_type_cls(self.system, problemtree, state) + return self.inputs[problemid].get_html() # let each Response render itself if problemtree in self.responders: diff --git a/common/lib/xmodule/xmodule/js/src/capa/display.coffee b/common/lib/xmodule/xmodule/js/src/capa/display.coffee index 07a96c8b02..15211ca1bb 100644 --- a/common/lib/xmodule/xmodule/js/src/capa/display.coffee +++ b/common/lib/xmodule/xmodule/js/src/capa/display.coffee @@ -79,8 +79,9 @@ class @Problem # Use this if you want to make an ajax call on the input type object # static method so you don't have to instantiate a Problem in order to use it - @inputAjax: (url, dispatch, data, callback) -> + @inputAjax: (url, problem_id, dispatch, data, callback) -> data['dispatch'] = dispatch + data['problem_id'] = problem_id $.postWithPrefix "#{url}/input_ajax", data, callback From 9b640aa06b6b28cddfeebece6e116cec81e4dfb1 Mon Sep 17 00:00:00 2001 From: Diana Huang Date: Tue, 5 Mar 2013 13:24:38 -0500 Subject: [PATCH 4/8] Add more documentation and fix naming. --- common/lib/capa/capa/capa_problem.py | 20 +++++++++---------- common/lib/capa/capa/inputtypes.py | 2 +- .../xmodule/js/src/capa/display.coffee | 13 ++++++++++-- 3 files changed, 22 insertions(+), 13 deletions(-) diff --git a/common/lib/capa/capa/capa_problem.py b/common/lib/capa/capa/capa_problem.py index a7a362d51e..d6356c1585 100644 --- a/common/lib/capa/capa/capa_problem.py +++ b/common/lib/capa/capa/capa_problem.py @@ -335,16 +335,16 @@ class LoncapaProblem(object): def handle_input_ajax(self, get): ''' - This passes any specialized input ajax onto the input class + InputTypes can support specialized AJAX calls. Find the correct input and pass along the correct data - It also parses out the dispatch from the get so that it can be passed onto the input type nicely + Also, parse out the dispatch from the get so that it can be passed onto the input type nicely ''' # pull out the id - problem_id = get['problem_id'] - if self.inputs[problem_id]: + input_id = get['input_id'] + if self.inputs[input_id]: dispatch = get['dispatch'] - return self.inputs[problem_id].handle_ajax(dispatch, get) + return self.inputs[input_id].handle_ajax(dispatch, get) else: log.warning("Could not find matching input for id: %s" % problem_id) return {} @@ -512,8 +512,9 @@ class LoncapaProblem(object): msg = '' hint = '' hintmode = None + input_id = problemtree.get('id') if problemid in self.correct_map: - pid = problemtree.get('id') + pid = input_id status = self.correct_map.get_correctness(pid) msg = self.correct_map.get_msg(pid) hint = self.correct_map.get_hint(pid) @@ -524,18 +525,17 @@ class LoncapaProblem(object): value = self.student_answers[problemid] # do the rendering - state = {'value': value, 'status': status, - 'id': problemtree.get('id'), + 'id': input_id, 'feedback': {'message': msg, 'hint': hint, 'hintmode': hintmode, }} input_type_cls = inputtypes.registry.get_class_for_tag(problemtree.tag) # save the input type so that we can make ajax calls on it if we need to - self.inputs[problemid] = input_type_cls(self.system, problemtree, state) - return self.inputs[problemid].get_html() + self.inputs[input_id] = input_type_cls(self.system, problemtree, state) + return self.inputs[input_id].get_html() # let each Response render itself if problemtree in self.responders: diff --git a/common/lib/capa/capa/inputtypes.py b/common/lib/capa/capa/inputtypes.py index 1a141338b7..1d6c340f37 100644 --- a/common/lib/capa/capa/inputtypes.py +++ b/common/lib/capa/capa/inputtypes.py @@ -224,7 +224,7 @@ class InputTypeBase(object): get: a dictionary containing the data that was sent with the ajax call Output: - a dictionary object that will then get sent back to the Javascript + a dictionary object that can be serialized into JSON. This will be sent back to the Javascript. """ pass diff --git a/common/lib/xmodule/xmodule/js/src/capa/display.coffee b/common/lib/xmodule/xmodule/js/src/capa/display.coffee index 15211ca1bb..158c2b98d0 100644 --- a/common/lib/xmodule/xmodule/js/src/capa/display.coffee +++ b/common/lib/xmodule/xmodule/js/src/capa/display.coffee @@ -79,9 +79,18 @@ class @Problem # Use this if you want to make an ajax call on the input type object # static method so you don't have to instantiate a Problem in order to use it - @inputAjax: (url, problem_id, dispatch, data, callback) -> + # Input: + # url: the AJAX url of the problem + # input_id: the input_id of the input you would like to make the call on + # NOTE: the id is the ${id} part of "input_${id}" during rendering + # If this function is passed the entire prefixed id, the backend may have trouble + # finding the correct input + # dispatch: string that indicates how this data should be handled by the inputtype + # callback: the function that will be called once the AJAX call has been completed. + # It will be passed a response object + @inputAjax: (url, input_id, dispatch, data, callback) -> data['dispatch'] = dispatch - data['problem_id'] = problem_id + data['input_id'] = input_id $.postWithPrefix "#{url}/input_ajax", data, callback From 17680a332a155374e4212cb8285203cc43064a4c Mon Sep 17 00:00:00 2001 From: Diana Huang Date: Tue, 5 Mar 2013 13:26:53 -0500 Subject: [PATCH 5/8] More comments. --- common/lib/capa/capa/capa_problem.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/common/lib/capa/capa/capa_problem.py b/common/lib/capa/capa/capa_problem.py index d6356c1585..62751cd833 100644 --- a/common/lib/capa/capa/capa_problem.py +++ b/common/lib/capa/capa/capa_problem.py @@ -146,6 +146,8 @@ class LoncapaProblem(object): if not self.student_answers: # True when student_answers is an empty dict self.set_initial_display() + # dictionary of InputType objects associated with this problem + # input_id string -> InputType object self.inputs = {} self.extracted_tree = self._extract_html(self.tree) From 78424e167080918f993c449c7ac8009234f13447 Mon Sep 17 00:00:00 2001 From: Diana Huang Date: Wed, 6 Mar 2013 09:19:22 -0500 Subject: [PATCH 6/8] Fix some of the tests so that they work with the new changes to capa_problem --- common/lib/xmodule/xmodule/tests/test_capa_module.py | 5 ++++- common/lib/xmodule/xmodule/tests/test_conditional.py | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/common/lib/xmodule/xmodule/tests/test_capa_module.py b/common/lib/xmodule/xmodule/tests/test_capa_module.py index 0e64e740fd..a1e3d31d76 100644 --- a/common/lib/xmodule/xmodule/tests/test_capa_module.py +++ b/common/lib/xmodule/xmodule/tests/test_capa_module.py @@ -108,7 +108,9 @@ class CapaFactory(object): else: instance_state = None - module = CapaModule(test_system(), location, + system = test_system() + system.render_template = Mock(return_value="
Test Template HTML
") + module = CapaModule(system, location, definition, descriptor, instance_state, None, metadata=metadata) @@ -185,6 +187,7 @@ class CapaModuleTest(unittest.TestCase): max_attempts="1", attempts="0", due=self.yesterday_str) + self.assertTrue(after_due_date.answer_available()) diff --git a/common/lib/xmodule/xmodule/tests/test_conditional.py b/common/lib/xmodule/xmodule/tests/test_conditional.py index c3468905ad..16bd222b9e 100644 --- a/common/lib/xmodule/xmodule/tests/test_conditional.py +++ b/common/lib/xmodule/xmodule/tests/test_conditional.py @@ -106,7 +106,7 @@ class ConditionalModuleTest(unittest.TestCase): html = module.get_html() print "html type: ", type(html) print "html: ", html - html_expect = "
{'ajax_url': 'courses/course_id/modx/a_location', 'element_id': 'i4x-edX-cond_test-conditional-condone', 'id': 'i4x://edX/cond_test/conditional/condone'}
" + html_expect = "{'ajax_url': 'courses/course_id/modx/a_location', 'element_id': 'i4x-edX-cond_test-conditional-condone', 'id': 'i4x://edX/cond_test/conditional/condone'}" self.assertEqual(html, html_expect) gdi = module.get_display_items() From 455dea870f06657daba21787e412dca185a2e3a8 Mon Sep 17 00:00:00 2001 From: Diana Huang Date: Wed, 6 Mar 2013 09:21:37 -0500 Subject: [PATCH 7/8] Remove unnecessary import. --- common/lib/xmodule/xmodule/tests/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/common/lib/xmodule/xmodule/tests/__init__.py b/common/lib/xmodule/xmodule/tests/__init__.py index 20f31315c4..43c2bbe24d 100644 --- a/common/lib/xmodule/xmodule/tests/__init__.py +++ b/common/lib/xmodule/xmodule/tests/__init__.py @@ -18,7 +18,6 @@ import capa.calc as calc import xmodule from xmodule.x_module import ModuleSystem from mock import Mock -import xml.sax.saxutils as saxutils open_ended_grading_interface = { 'url': 'http://sandbox-grader-001.m.edx.org/peer_grading', From 4b7d1deb253f55d348e849498a5500bdea3ceade Mon Sep 17 00:00:00 2001 From: Diana Huang Date: Wed, 6 Mar 2013 10:34:57 -0500 Subject: [PATCH 8/8] Fix a bug in extracting HTML Update tests to reflect new behavior. --- common/lib/capa/capa/capa_problem.py | 2 +- common/lib/capa/capa/tests/test_html_render.py | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/common/lib/capa/capa/capa_problem.py b/common/lib/capa/capa/capa_problem.py index 62751cd833..14c590a660 100644 --- a/common/lib/capa/capa/capa_problem.py +++ b/common/lib/capa/capa/capa_problem.py @@ -331,7 +331,7 @@ class LoncapaProblem(object): ''' Main method called externally to get the HTML to be rendered for this capa Problem. ''' - html = contextualize_text(etree.tostring(self.extracted_tree), self.context) + html = contextualize_text(etree.tostring(self._extract_html(self.tree)), self.context) return html diff --git a/common/lib/capa/capa/tests/test_html_render.py b/common/lib/capa/capa/tests/test_html_render.py index 64f031ea59..e4c54edca0 100644 --- a/common/lib/capa/capa/tests/test_html_render.py +++ b/common/lib/capa/capa/tests/test_html_render.py @@ -125,6 +125,8 @@ class CapaHtmlRenderTest(unittest.TestCase): expected_solution_context = {'id': '1_solution_1'} expected_calls = [mock.call('textline.html', expected_textline_context), + mock.call('solutionspan.html', expected_solution_context), + mock.call('textline.html', expected_textline_context), mock.call('solutionspan.html', expected_solution_context)] self.assertEqual(test_system.render_template.call_args_list,