Merge branch 'master' into asset-pipeline
Conflicts: static/css static/css/application.css
This commit is contained in:
@@ -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.
|
||||
@@ -147,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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import os
|
||||
import os.path
|
||||
|
||||
from django.conf import settings
|
||||
|
||||
import capa_module
|
||||
import html_module
|
||||
import schematic_module
|
||||
|
||||
@@ -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="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':
|
||||
@@ -200,7 +202,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 +309,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 +390,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)
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -20,6 +20,7 @@ div.info-wrapper {
|
||||
border-bottom: 1px solid #e3e3e3;
|
||||
margin-bottom: lh(.5);
|
||||
padding-bottom: lh(.5);
|
||||
list-style-type: disk;
|
||||
|
||||
&:first-child {
|
||||
background: $cream;
|
||||
@@ -28,6 +29,11 @@ div.info-wrapper {
|
||||
padding: lh(.5);
|
||||
}
|
||||
|
||||
ol, ul {
|
||||
margin: lh() 0 0 lh();
|
||||
list-style-type: circle;
|
||||
}
|
||||
|
||||
h2 {
|
||||
float: left;
|
||||
margin: 0 flex-gutter() 0 0;
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// Styles for individual answers
|
||||
|
||||
div.answer-controls {
|
||||
@include box-sizing(border-box);
|
||||
display: inline-block;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// original styles
|
||||
// original Askbot styles
|
||||
|
||||
// body {
|
||||
// background: #fff;
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// Style for the user badge list (can be accessed by clicking "View all MIT badges" in the badge section of the Askbot user profile
|
||||
|
||||
div.badges-intro {
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
// Layout
|
||||
// Generic layout styles for the discussion forums
|
||||
|
||||
body.askbot {
|
||||
|
||||
section.main-content {
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// Styles for the WYSIWYG question/answer editor
|
||||
|
||||
.wmd-panel
|
||||
{
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// Styles for different forms in the system
|
||||
|
||||
form.answer-form {
|
||||
@include box-sizing(border-box);
|
||||
border-top: 1px solid #ddd;
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// Style for modal boxes that pop up to notify the user of various events
|
||||
|
||||
.vote-notification {
|
||||
background-color: darken($mit-red, 7%);
|
||||
@include border-radius(4px);
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// Style for the user profile view
|
||||
|
||||
body.user-profile-page {
|
||||
|
||||
section.questions {
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// Styles for the single question view
|
||||
|
||||
div.question-header {
|
||||
|
||||
div.official-stamp {
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// Styles for the default question list view
|
||||
|
||||
div.question-list-header {
|
||||
display: block;
|
||||
margin-bottom: 0px;
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// Styles for the Askbot sidebar
|
||||
|
||||
div.discussion-wrapper aside {
|
||||
@extend .sidebar;
|
||||
border-left: 1px solid #d3d3d3;
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// Styles for the question tags
|
||||
|
||||
ul.tags {
|
||||
list-style: none;
|
||||
display: inline;
|
||||
|
||||
Reference in New Issue
Block a user