Commit fa4a240f by Piotr Mitros

Passing i4x system deeper into capa_problem

parent 087a7cf9
......@@ -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)
......
......@@ -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 <formulainput>
# 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.
......
......@@ -71,17 +71,17 @@ class MultipleChoiceResponse(GenericResponse):
<multiplechoiceresponse direction="vertical" randomize="yes">
<choicegroup type="MultipleChoice">
<choice location="random" name="1" correct="false"><span>`a+b`<br/></span></choice>
<choice location="random" name="2" correct="true"><span><math>a+b^2</math><br/></span></choice>
<choice location="random" name="3" correct="false"><math>a+b+c</math></choice>
<choice location="bottom" name="4" correct="false"><math>a+b+d</math></choice>
<choice location="random" correct="false"><span>`a+b`<br/></span></choice>
<choice location="random" correct="true"><span><math>a+b^2</math><br/></span></choice>
<choice location="random" correct="false"><math>a+b+c</math></choice>
<choice location="bottom" correct="false"><math>a+b+d</math></choice>
</choicegroup>
</multiplechoiceresponse>
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():
</customresponse>
'''
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):
</imageresponse>
"""
def __init__(self, xml, context):
def __init__(self, xml, context, system=None):
self.xml = xml
self.context = context
self.ielements = xml.findall('imageinput')
......
......@@ -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
......
import os
import os.path
from django.conf import settings
import capa_module
import html_module
import schematic_module
......
......@@ -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)
......
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):
......
......@@ -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):
......
......@@ -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',
......
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