From cad13b30a17f8f745b56658d145505e24213be96 Mon Sep 17 00:00:00 2001 From: Piotr Mitros Date: Thu, 17 May 2012 15:37:08 -0400 Subject: [PATCH 1/2] Passing i4x system deeper into capa_problem --- djangoapps/courseware/capa/capa_problem.py | 18 ++++++------ djangoapps/courseware/capa/inputtypes.py | 5 ++++ djangoapps/courseware/capa/responsetypes.py | 26 ++++++++--------- djangoapps/courseware/module_render.py | 3 +- djangoapps/courseware/modules/__init__.py | 2 -- djangoapps/courseware/modules/capa_module.py | 10 +++---- .../courseware/modules/template_module.py | 12 +++++--- djangoapps/courseware/modules/x_module.py | 18 ++++++++++++ djangoapps/courseware/tests.py | 28 +++++++++++++++---- 9 files changed, 84 insertions(+), 38 deletions(-) diff --git a/djangoapps/courseware/capa/capa_problem.py b/djangoapps/courseware/capa/capa_problem.py index f5739fd8b0..0f9b5dacab 100644 --- a/djangoapps/courseware/capa/capa_problem.py +++ b/djangoapps/courseware/capa/capa_problem.py @@ -73,20 +73,22 @@ html_skip = ["numericalresponse", "customresponse", "schematicresponse", "formul # removed in MC ## These should be transformed -#html_special_response = {"textline":textline.render, -# "schematic":schematic.render, -# "textbox":textbox.render, -# "solution":solution.render, +#html_special_response = {"textline":inputtypes.textline.render, +# "schematic":inputtypes.schematic.render, +# "textbox":inputtypes.textbox.render, +# "formulainput":inputtypes.jstextline.render, +# "solution":inputtypes.solution.render, # } class LoncapaProblem(object): - def __init__(self, fileobject, id, state=None, seed=None): + def __init__(self, fileobject, id, state=None, seed=None, system=None): ## Initialize class variables from state self.seed = None self.student_answers = dict() self.correct_map = dict() self.done = False self.problem_id = id + self.system = system if seed != None: self.seed = seed @@ -117,7 +119,7 @@ class LoncapaProblem(object): self.preprocess_problem(self.tree, correct_map=self.correct_map, answer_map = self.student_answers) self.context = self.extract_context(self.tree, seed=self.seed) for response in self.tree.xpath('//'+"|//".join(response_types)): - responder = response_types[response.tag](response, self.context) + responder = response_types[response.tag](response, self.context, self.system) responder.preprocess_response() def get_state(self): @@ -163,7 +165,7 @@ class LoncapaProblem(object): self.correct_map = dict() problems_simple = self.extract_problems(self.tree) for response in problems_simple: - grader = response_types[response.tag](response, self.context) + grader = response_types[response.tag](response, self.context, self.system) results = grader.grade(answers) # call the responsetype instance to do the actual grading self.correct_map.update(results) return self.correct_map @@ -177,7 +179,7 @@ class LoncapaProblem(object): answer_map = dict() problems_simple = self.extract_problems(self.tree) # purified (flat) XML tree of just response queries for response in problems_simple: - responder = response_types[response.tag](response, self.context) # instance of numericalresponse, customresponse,... + responder = response_types[response.tag](response, self.context, self.system) # instance of numericalresponse, customresponse,... results = responder.get_answers() answer_map.update(results) # dict of (id,correct_answer) diff --git a/djangoapps/courseware/capa/inputtypes.py b/djangoapps/courseware/capa/inputtypes.py index 0388b35d0b..6e5436b253 100644 --- a/djangoapps/courseware/capa/inputtypes.py +++ b/djangoapps/courseware/capa/inputtypes.py @@ -101,6 +101,11 @@ def textline(element, value, state, msg=""): #----------------------------------------------------------------------------- def js_textline(element, value, status, msg=''): + ''' + Plan: We will inspect element to figure out type + ''' + # TODO: Make a wrapper for + # TODO: Make an AJAX loop to confirm equation is okay in real-time as user types ## TODO: Code should follow PEP8 (4 spaces per indentation level) ''' textline is used for simple one-line inputs, like formularesponse and symbolicresponse. diff --git a/djangoapps/courseware/capa/responsetypes.py b/djangoapps/courseware/capa/responsetypes.py index d9b18428fe..190f52e7b6 100644 --- a/djangoapps/courseware/capa/responsetypes.py +++ b/djangoapps/courseware/capa/responsetypes.py @@ -71,17 +71,17 @@ class MultipleChoiceResponse(GenericResponse): - `a+b`
- a+b^2
- a+b+c - a+b+d + `a+b`
+ a+b^2
+ a+b+c + a+b+d
TODO: handle direction and randomize ''' - def __init__(self, xml, context): + def __init__(self, xml, context, system=None): self.xml = xml self.correct_choices = xml.xpath('//*[@id=$id]//choice[@correct="true"]', id=xml.get('id')) @@ -155,7 +155,7 @@ class OptionResponse(GenericResponse): TODO: handle direction and randomize ''' - def __init__(self, xml, context): + def __init__(self, xml, context, system=None): self.xml = xml self.answer_fields = xml.findall('optioninput') if settings.DEBUG: @@ -179,7 +179,7 @@ class OptionResponse(GenericResponse): #----------------------------------------------------------------------------- class NumericalResponse(GenericResponse): - def __init__(self, xml, context): + def __init__(self, xml, context, system=None): self.xml = xml self.correct_answer = contextualize_text(xml.get('answer'), context) try: @@ -257,7 +257,7 @@ def sympy_check2(): ''' - def __init__(self, xml, context): + def __init__(self, xml, context, system=None): self.xml = xml ## CRITICAL TODO: Should cover all entrytypes ## NOTE: xpath will look at root of XML tree, not just @@ -412,7 +412,7 @@ class ExternalResponse(GenericResponse): Typically used by coding problems. """ - def __init__(self, xml, context): + def __init__(self, xml, context, system=None): self.xml = xml self.answer_ids = xml.xpath('//*[@id=$id]//textbox/@id|//*[@id=$id]//textline/@id', id=xml.get('id')) @@ -472,7 +472,7 @@ class StudentInputError(Exception): #----------------------------------------------------------------------------- class FormulaResponse(GenericResponse): - def __init__(self, xml, context): + def __init__(self, xml, context, system=None): self.xml = xml self.correct_answer = contextualize_text(xml.get('answer'), context) self.samples = contextualize_text(xml.get('samples'), context) @@ -553,7 +553,7 @@ class FormulaResponse(GenericResponse): #----------------------------------------------------------------------------- class SchematicResponse(GenericResponse): - def __init__(self, xml, context): + def __init__(self, xml, context, system=None): self.xml = xml self.answer_ids = xml.xpath('//*[@id=$id]//schematic/@id', id=xml.get('id')) @@ -562,7 +562,7 @@ class SchematicResponse(GenericResponse): id=xml.get('id'))[0] answer_src = answer.get('src') if answer_src != None: - self.code = open(settings.DATA_DIR+'src/'+answer_src).read() + self.code = self.system.filestore.open('src/'+answer_src).read() # Untested; never used else: self.code = answer.text @@ -599,7 +599,7 @@ class ImageResponse(GenericResponse): """ - def __init__(self, xml, context): + def __init__(self, xml, context, system=None): self.xml = xml self.context = context self.ielements = xml.findall('imageinput') diff --git a/djangoapps/courseware/module_render.py b/djangoapps/courseware/module_render.py index 0e317f7004..210cddb0b2 100644 --- a/djangoapps/courseware/module_render.py +++ b/djangoapps/courseware/module_render.py @@ -15,7 +15,6 @@ from mitxmako.shortcuts import render_to_string from models import StudentModule -import track.views import courseware.modules @@ -56,6 +55,8 @@ def make_track_function(request): tracking function to them. This generates a closure for each request that gives a clean interface on both sides. ''' + import track.views + def f(event_type, event): return track.views.server_track(request, event_type, event, page='x_module') return f diff --git a/djangoapps/courseware/modules/__init__.py b/djangoapps/courseware/modules/__init__.py index b20ce3778c..4fc5fb67cb 100644 --- a/djangoapps/courseware/modules/__init__.py +++ b/djangoapps/courseware/modules/__init__.py @@ -1,8 +1,6 @@ import os import os.path -from django.conf import settings - import capa_module import html_module import schematic_module diff --git a/djangoapps/courseware/modules/capa_module.py b/djangoapps/courseware/modules/capa_module.py index 0b75faa491..efc964913d 100644 --- a/djangoapps/courseware/modules/capa_module.py +++ b/djangoapps/courseware/modules/capa_module.py @@ -188,7 +188,7 @@ class Module(XModule): if state!=None and 'attempts' in state: self.attempts=state['attempts'] - self.filename="problems/"+content_parser.item(dom2.xpath('/problem/@filename'))+".xml" + self.filename=content_parser.item(dom2.xpath('/problem/@filename')) # "problems/"+content_parser.item(dom2.xpath('/problem/@filename'))+".xml" self.name=content_parser.item(dom2.xpath('/problem/@name')) self.weight=content_parser.item(dom2.xpath('/problem/@weight')) if self.rerandomize == 'never': @@ -200,7 +200,7 @@ class Module(XModule): except Exception,err: print '[courseware.capa.capa_module.Module.init] error %s: cannot open file %s' % (err,self.filename) raise Exception,err - self.lcp=LoncapaProblem(fp, self.item_id, state, seed = seed) + self.lcp=LoncapaProblem(fp, self.item_id, state, seed = seed, system=self.system) def handle_ajax(self, dispatch, get): ''' @@ -307,11 +307,11 @@ class Module(XModule): lcp_id = self.lcp.problem_id correct_map = self.lcp.grade_answers(answers) except StudentInputError as inst: - self.lcp = LoncapaProblem(self.filestore.open(self.filename), id=lcp_id, state=old_state) + self.lcp = LoncapaProblem(self.filestore.open(self.filename), id=lcp_id, state=old_state, system=self.system) traceback.print_exc() return json.dumps({'success':inst.message}) except: - self.lcp = LoncapaProblem(self.filestore.open(self.filename), id=lcp_id, state=old_state) + self.lcp = LoncapaProblem(self.filestore.open(self.filename), id=lcp_id, state=old_state, system=self.system) traceback.print_exc() raise Exception,"error in capa_module" return json.dumps({'success':'Unknown Error'}) @@ -388,7 +388,7 @@ class Module(XModule): self.lcp.questions=dict() # Detailed info about questions in problem instance. TODO: Should be by id and not lid. self.lcp.seed=None - self.lcp=LoncapaProblem(self.filestore.open(self.filename), self.item_id, self.lcp.get_state()) + self.lcp=LoncapaProblem(self.filestore.open(self.filename), self.item_id, self.lcp.get_state(), system=self.system) event_info['new_state']=self.lcp.get_state() self.tracker('reset_problem', event_info) diff --git a/djangoapps/courseware/modules/template_module.py b/djangoapps/courseware/modules/template_module.py index 41556eb9d4..a8899f985c 100644 --- a/djangoapps/courseware/modules/template_module.py +++ b/djangoapps/courseware/modules/template_module.py @@ -1,8 +1,6 @@ import json import os -## TODO: Abstract out from Django -from django.conf import settings from mitxmako.shortcuts import render_to_response, render_to_string from x_module import XModule @@ -14,8 +12,14 @@ class Module(XModule): @classmethod def get_xml_tags(c): - ## TODO: Abstract out from filesystem - tags = os.listdir(settings.DATA_DIR+'/custom_tags') + ## TODO: Abstract out from filesystem and Django + ## HACK: For now, this lets us import without abstracting out + try: + from django.conf import settings + tags = os.listdir(settings.DATA_DIR+'/custom_tags') + except: + print "Could not open tags directory." + tags = [] return tags def get_html(self): diff --git a/djangoapps/courseware/modules/x_module.py b/djangoapps/courseware/modules/x_module.py index 9e70a16891..0898ac4f8f 100644 --- a/djangoapps/courseware/modules/x_module.py +++ b/djangoapps/courseware/modules/x_module.py @@ -7,6 +7,8 @@ class XModule(object): ''' Implements a generic learning module. Initialized on access with __init__, first time with state=None, and then with state + + See the HTML module for a simple example ''' id_attribute='id' # An attribute guaranteed to be unique @@ -16,18 +18,31 @@ class XModule(object): return [] def get_completion(self): + ''' This is mostly unimplemented. + It gives a progress indication -- e.g. 30 minutes of 1.5 hours watched. 3 of 5 problems done, etc. ''' return courseware.progress.completion() def get_state(self): + ''' State of the object, as stored in the database + ''' return "" def get_score(self): + ''' Score the student received on the problem. + ''' return None def max_score(self): + ''' Maximum score. Two notes: + * This is generic; in abstract, a problem could be 3/5 points on one randomization, and 5/7 on another + * In practice, this is a Very Bad Idea, and (a) will break some code in place (although that code + should get fixed), and (b) break some analytics we plan to put in place. + ''' return None def get_html(self): + ''' HTML, as shown in the browser. This is the only method that must be implemented + ''' return "Unimplemented" def get_init_js(self): @@ -38,6 +53,9 @@ class XModule(object): return "" def get_destroy_js(self): + ''' JavaScript called to destroy the problem (e.g. when a user switches to a different tab). + We make an attempt, but not a promise, to call this when the user closes the web page. + ''' return "" def handle_ajax(self, dispatch, get): diff --git a/djangoapps/courseware/tests.py b/djangoapps/courseware/tests.py index 682927efb7..fabb79e4e7 100644 --- a/djangoapps/courseware/tests.py +++ b/djangoapps/courseware/tests.py @@ -10,6 +10,24 @@ import courseware.graders as graders from courseware.graders import Score, CourseGrader, WeightedSubsectionsGrader, SingleSectionGrader, AssignmentFormatGrader from courseware.grades import aggregate_scores +class I4xSystem(object): + ''' + This is an abstraction such that x_modules can function independent + of the courseware (e.g. import into other types of courseware, LMS, + or if we want to have a sandbox server for user-contributed content) + ''' + def __init__(self): + self.ajax_url = '/' + self.track_function = lambda x: None + self.render_function = lambda x: {} # Probably incorrect + self.exception404 = Exception + def __repr__(self): + return repr(self.__dict__) + def __str__(self): + return str(self.__dict__) + +i4xs = I4xSystem + class ModelsTest(unittest.TestCase): def setUp(self): pass @@ -69,7 +87,7 @@ class ModelsTest(unittest.TestCase): class MultiChoiceTest(unittest.TestCase): def test_MC_grade(self): multichoice_file = os.path.dirname(__file__)+"/test_files/multichoice.xml" - test_lcp = lcp.LoncapaProblem(open(multichoice_file), '1') + test_lcp = lcp.LoncapaProblem(open(multichoice_file), '1', system=i4xs) correct_answers = {'1_2_1':'choice_foil3'} self.assertEquals(test_lcp.grade_answers(correct_answers)['1_2_1'], 'correct') false_answers = {'1_2_1':'choice_foil2'} @@ -77,7 +95,7 @@ class MultiChoiceTest(unittest.TestCase): def test_MC_bare_grades(self): multichoice_file = os.path.dirname(__file__)+"/test_files/multi_bare.xml" - test_lcp = lcp.LoncapaProblem(open(multichoice_file), '1') + test_lcp = lcp.LoncapaProblem(open(multichoice_file), '1', system=i4xs) correct_answers = {'1_2_1':'choice_2'} self.assertEquals(test_lcp.grade_answers(correct_answers)['1_2_1'], 'correct') false_answers = {'1_2_1':'choice_1'} @@ -85,7 +103,7 @@ class MultiChoiceTest(unittest.TestCase): def test_TF_grade(self): truefalse_file = os.getcwd()+"/djangoapps/courseware/test_files/truefalse.xml" - test_lcp = lcp.LoncapaProblem(open(truefalse_file), '1') + test_lcp = lcp.LoncapaProblem(open(truefalse_file), '1', system=i4xs) correct_answers = {'1_2_1':['choice_foil2', 'choice_foil1']} self.assertEquals(test_lcp.grade_answers(correct_answers)['1_2_1'], 'correct') false_answers = {'1_2_1':['choice_foil1']} @@ -100,7 +118,7 @@ class MultiChoiceTest(unittest.TestCase): class ImageResponseTest(unittest.TestCase): def test_ir_grade(self): imageresponse_file = os.path.dirname(__file__)+"/test_files/imageresponse.xml" - test_lcp = lcp.LoncapaProblem(open(imageresponse_file), '1') + test_lcp = lcp.LoncapaProblem(open(imageresponse_file), '1', system=i4xs) correct_answers = {'1_2_1':'(490,11)-(556,98)', '1_2_2':'(242,202)-(296,276)'} test_answers = {'1_2_1':'[500,20]', @@ -117,7 +135,7 @@ class OptionResponseTest(unittest.TestCase): ''' def test_or_grade(self): optionresponse_file = os.path.dirname(__file__)+"/test_files/optionresponse.xml" - test_lcp = lcp.LoncapaProblem(open(optionresponse_file), '1') + test_lcp = lcp.LoncapaProblem(open(optionresponse_file), '1', system=i4xs) correct_answers = {'1_2_1':'True', '1_2_2':'False'} test_answers = {'1_2_1':'True', From 0e3349706a0bcc8c59a3f3adfb77e421dbbb8110 Mon Sep 17 00:00:00 2001 From: Piotr Mitros Date: Thu, 17 May 2012 15:48:47 -0400 Subject: [PATCH 2/2] Fix minor bugs, commenting out future code --- djangoapps/courseware/capa/inputtypes.py | 2 +- djangoapps/courseware/modules/capa_module.py | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/djangoapps/courseware/capa/inputtypes.py b/djangoapps/courseware/capa/inputtypes.py index 6e5436b253..ae8809c066 100644 --- a/djangoapps/courseware/capa/inputtypes.py +++ b/djangoapps/courseware/capa/inputtypes.py @@ -152,7 +152,7 @@ def schematic(element, value, status, msg=''): 'id':eid, 'value':value, 'initial_value':initial_value, - 'state':state, + 'state':status, 'width':width, 'height':height, 'parts':parts, diff --git a/djangoapps/courseware/modules/capa_module.py b/djangoapps/courseware/modules/capa_module.py index efc964913d..7d42cfb250 100644 --- a/djangoapps/courseware/modules/capa_module.py +++ b/djangoapps/courseware/modules/capa_module.py @@ -142,7 +142,8 @@ class Module(XModule): dom2 = etree.fromstring(xml) - self.explanation=content_parser.item(dom2.xpath('/problem/@explain'), default="closed") + self.explanation="problems/"+content_parser.item(dom2.xpath('/problem/@explain'), default="closed") + # TODO: Should be converted to: self.explanation=content_parser.item(dom2.xpath('/problem/@explain'), default="closed") self.explain_available=content_parser.item(dom2.xpath('/problem/@explain_available')) display_due_date_string=content_parser.item(dom2.xpath('/problem/@due')) @@ -188,7 +189,8 @@ class Module(XModule): if state!=None and 'attempts' in state: self.attempts=state['attempts'] - self.filename=content_parser.item(dom2.xpath('/problem/@filename')) # "problems/"+content_parser.item(dom2.xpath('/problem/@filename'))+".xml" + # TODO: Should be: self.filename=content_parser.item(dom2.xpath('/problem/@filename')) + self.filename= "problems/"+content_parser.item(dom2.xpath('/problem/@filename'))+".xml" self.name=content_parser.item(dom2.xpath('/problem/@name')) self.weight=content_parser.item(dom2.xpath('/problem/@weight')) if self.rerandomize == 'never':