Merge branch 'master' into diana/rubric-ui-improvements
Conflicts: common/lib/xmodule/xmodule/combined_open_ended_module.py common/lib/xmodule/xmodule/combined_open_ended_rubric.py
This commit is contained in:
@@ -121,9 +121,9 @@ def evaluator(variables, functions, string, cs=False):
|
||||
# confusing. They may also conflict with variables if we ever allow e.g.
|
||||
# 5R instead of 5*R
|
||||
suffixes = {'%': 0.01, 'k': 1e3, 'M': 1e6, 'G': 1e9,
|
||||
'T': 1e12,# 'P':1e15,'E':1e18,'Z':1e21,'Y':1e24,
|
||||
'T': 1e12, # 'P':1e15,'E':1e18,'Z':1e21,'Y':1e24,
|
||||
'c': 1e-2, 'm': 1e-3, 'u': 1e-6,
|
||||
'n': 1e-9, 'p': 1e-12}# ,'f':1e-15,'a':1e-18,'z':1e-21,'y':1e-24}
|
||||
'n': 1e-9, 'p': 1e-12} # ,'f':1e-15,'a':1e-18,'z':1e-21,'y':1e-24}
|
||||
|
||||
def super_float(text):
|
||||
''' Like float, but with si extensions. 1k goes to 1000'''
|
||||
|
||||
@@ -75,7 +75,7 @@ global_context = {'random': random,
|
||||
'draganddrop': verifiers.draganddrop}
|
||||
|
||||
# These should be removed from HTML output, including all subelements
|
||||
html_problem_semantics = ["codeparam", "responseparam", "answer", "script", "hintgroup", "openendedparam","openendedrubric"]
|
||||
html_problem_semantics = ["codeparam", "responseparam", "answer", "script", "hintgroup", "openendedparam", "openendedrubric"]
|
||||
|
||||
log = logging.getLogger('mitx.' + __name__)
|
||||
|
||||
@@ -453,7 +453,7 @@ class LoncapaProblem(object):
|
||||
exec code in context, context
|
||||
except Exception as err:
|
||||
log.exception("Error while execing script code: " + code)
|
||||
msg = "Error while executing script code: %s" % str(err).replace('<','<')
|
||||
msg = "Error while executing script code: %s" % str(err).replace('<', '<')
|
||||
raise responsetypes.LoncapaProblemError(msg)
|
||||
finally:
|
||||
sys.path = original_path
|
||||
@@ -502,7 +502,7 @@ class LoncapaProblem(object):
|
||||
'id': problemtree.get('id'),
|
||||
'feedback': {'message': msg,
|
||||
'hint': hint,
|
||||
'hintmode': hintmode,}}
|
||||
'hintmode': hintmode, }}
|
||||
|
||||
input_type_cls = inputtypes.registry.get_class_for_tag(problemtree.tag)
|
||||
the_input = input_type_cls(self.system, problemtree, state)
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
|
||||
|
||||
@@ -17,17 +17,17 @@ from nltk.tree import Tree
|
||||
ARROWS = ('<->', '->')
|
||||
|
||||
## Defines a simple pyparsing tokenizer for chemical equations
|
||||
elements = ['Ac','Ag','Al','Am','Ar','As','At','Au','B','Ba','Be',
|
||||
'Bh','Bi','Bk','Br','C','Ca','Cd','Ce','Cf','Cl','Cm',
|
||||
'Cn','Co','Cr','Cs','Cu','Db','Ds','Dy','Er','Es','Eu',
|
||||
'F','Fe','Fl','Fm','Fr','Ga','Gd','Ge','H','He','Hf',
|
||||
'Hg','Ho','Hs','I','In','Ir','K','Kr','La','Li','Lr',
|
||||
'Lu','Lv','Md','Mg','Mn','Mo','Mt','N','Na','Nb','Nd',
|
||||
'Ne','Ni','No','Np','O','Os','P','Pa','Pb','Pd','Pm',
|
||||
'Po','Pr','Pt','Pu','Ra','Rb','Re','Rf','Rg','Rh','Rn',
|
||||
'Ru','S','Sb','Sc','Se','Sg','Si','Sm','Sn','Sr','Ta',
|
||||
'Tb','Tc','Te','Th','Ti','Tl','Tm','U','Uuo','Uup',
|
||||
'Uus','Uut','V','W','Xe','Y','Yb','Zn','Zr']
|
||||
elements = ['Ac', 'Ag', 'Al', 'Am', 'Ar', 'As', 'At', 'Au', 'B', 'Ba', 'Be',
|
||||
'Bh', 'Bi', 'Bk', 'Br', 'C', 'Ca', 'Cd', 'Ce', 'Cf', 'Cl', 'Cm',
|
||||
'Cn', 'Co', 'Cr', 'Cs', 'Cu', 'Db', 'Ds', 'Dy', 'Er', 'Es', 'Eu',
|
||||
'F', 'Fe', 'Fl', 'Fm', 'Fr', 'Ga', 'Gd', 'Ge', 'H', 'He', 'Hf',
|
||||
'Hg', 'Ho', 'Hs', 'I', 'In', 'Ir', 'K', 'Kr', 'La', 'Li', 'Lr',
|
||||
'Lu', 'Lv', 'Md', 'Mg', 'Mn', 'Mo', 'Mt', 'N', 'Na', 'Nb', 'Nd',
|
||||
'Ne', 'Ni', 'No', 'Np', 'O', 'Os', 'P', 'Pa', 'Pb', 'Pd', 'Pm',
|
||||
'Po', 'Pr', 'Pt', 'Pu', 'Ra', 'Rb', 'Re', 'Rf', 'Rg', 'Rh', 'Rn',
|
||||
'Ru', 'S', 'Sb', 'Sc', 'Se', 'Sg', 'Si', 'Sm', 'Sn', 'Sr', 'Ta',
|
||||
'Tb', 'Tc', 'Te', 'Th', 'Ti', 'Tl', 'Tm', 'U', 'Uuo', 'Uup',
|
||||
'Uus', 'Uut', 'V', 'W', 'Xe', 'Y', 'Yb', 'Zn', 'Zr']
|
||||
digits = map(str, range(10))
|
||||
symbols = list("[](){}^+-/")
|
||||
phases = ["(s)", "(l)", "(g)", "(aq)"]
|
||||
@@ -252,7 +252,7 @@ def _get_final_tree(s):
|
||||
'''
|
||||
tokenized = tokenizer.parseString(s)
|
||||
parsed = parser.parse(tokenized)
|
||||
merged = _merge_children(parsed, {'S','group'})
|
||||
merged = _merge_children(parsed, {'S', 'group'})
|
||||
final = _clean_parse_tree(merged)
|
||||
return final
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#
|
||||
# Used by responsetypes and capa_problem
|
||||
|
||||
|
||||
class CorrectMap(object):
|
||||
"""
|
||||
Stores map between answer_id and response evaluation result for each question
|
||||
@@ -152,6 +153,3 @@ class CorrectMap(object):
|
||||
if not isinstance(other_cmap, CorrectMap):
|
||||
raise Exception('CorrectMap.update called with invalid argument %s' % other_cmap)
|
||||
self.cmap.update(other_cmap.get_dict())
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -22,6 +22,8 @@ log = logging.getLogger('mitx.' + __name__)
|
||||
registry = TagRegistry()
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
|
||||
class MathRenderer(object):
|
||||
tags = ['math']
|
||||
|
||||
@@ -77,6 +79,7 @@ registry.register(MathRenderer)
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
|
||||
class SolutionRenderer(object):
|
||||
'''
|
||||
A solution is just a <span>...</span> which is given an ID, that is used for displaying an
|
||||
@@ -97,4 +100,3 @@ class SolutionRenderer(object):
|
||||
return etree.XML(html)
|
||||
|
||||
registry.register(SolutionRenderer)
|
||||
|
||||
|
||||
@@ -54,6 +54,7 @@ log = logging.getLogger('mitx.' + __name__)
|
||||
|
||||
registry = TagRegistry()
|
||||
|
||||
|
||||
class Attribute(object):
|
||||
"""
|
||||
Allows specifying required and optional attributes for input types.
|
||||
@@ -413,7 +414,7 @@ class JavascriptInput(InputTypeBase):
|
||||
return [Attribute('params', None),
|
||||
Attribute('problem_state', None),
|
||||
Attribute('display_class', None),
|
||||
Attribute('display_file', None),]
|
||||
Attribute('display_file', None), ]
|
||||
|
||||
|
||||
def setup(self):
|
||||
@@ -477,12 +478,13 @@ class TextLine(InputTypeBase):
|
||||
|
||||
def _extra_context(self):
|
||||
return {'do_math': self.do_math,
|
||||
'preprocessor': self.preprocessor,}
|
||||
'preprocessor': self.preprocessor, }
|
||||
|
||||
registry.register(TextLine)
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
|
||||
class FileSubmission(InputTypeBase):
|
||||
"""
|
||||
Upload some files (e.g. for programming assignments)
|
||||
@@ -508,7 +510,7 @@ class FileSubmission(InputTypeBase):
|
||||
Convert the list of allowed files to a convenient format.
|
||||
"""
|
||||
return [Attribute('allowed_files', '[]', transform=cls.parse_files),
|
||||
Attribute('required_files', '[]', transform=cls.parse_files),]
|
||||
Attribute('required_files', '[]', transform=cls.parse_files), ]
|
||||
|
||||
def setup(self):
|
||||
"""
|
||||
@@ -524,7 +526,7 @@ class FileSubmission(InputTypeBase):
|
||||
self.msg = FileSubmission.submitted_msg
|
||||
|
||||
def _extra_context(self):
|
||||
return {'queue_len': self.queue_len,}
|
||||
return {'queue_len': self.queue_len, }
|
||||
return context
|
||||
|
||||
registry.register(FileSubmission)
|
||||
@@ -582,7 +584,7 @@ class CodeInput(InputTypeBase):
|
||||
|
||||
def _extra_context(self):
|
||||
"""Defined queue_len, add it """
|
||||
return {'queue_len': self.queue_len,}
|
||||
return {'queue_len': self.queue_len, }
|
||||
|
||||
registry.register(CodeInput)
|
||||
|
||||
@@ -606,7 +608,7 @@ class Schematic(InputTypeBase):
|
||||
Attribute('parts', None),
|
||||
Attribute('analyses', None),
|
||||
Attribute('initial_value', None),
|
||||
Attribute('submit_analyses', None),]
|
||||
Attribute('submit_analyses', None), ]
|
||||
|
||||
return context
|
||||
|
||||
@@ -614,6 +616,7 @@ registry.register(Schematic)
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
|
||||
class ImageInput(InputTypeBase):
|
||||
"""
|
||||
Clickable image as an input field. Element should specify the image source, height,
|
||||
@@ -635,7 +638,7 @@ class ImageInput(InputTypeBase):
|
||||
"""
|
||||
return [Attribute('src'),
|
||||
Attribute('height'),
|
||||
Attribute('width'),]
|
||||
Attribute('width'), ]
|
||||
|
||||
|
||||
def setup(self):
|
||||
@@ -660,6 +663,7 @@ registry.register(ImageInput)
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
|
||||
class Crystallography(InputTypeBase):
|
||||
"""
|
||||
An input for crystallography -- user selects 3 points on the axes, and we get a plane.
|
||||
@@ -728,18 +732,19 @@ class ChemicalEquationInput(InputTypeBase):
|
||||
"""
|
||||
Can set size of text field.
|
||||
"""
|
||||
return [Attribute('size', '20'),]
|
||||
return [Attribute('size', '20'), ]
|
||||
|
||||
def _extra_context(self):
|
||||
"""
|
||||
TODO (vshnayder): Get rid of this once we have a standard way of requiring js to be loaded.
|
||||
"""
|
||||
return {'previewer': '/static/js/capa/chemical_equation_preview.js',}
|
||||
return {'previewer': '/static/js/capa/chemical_equation_preview.js', }
|
||||
|
||||
registry.register(ChemicalEquationInput)
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
|
||||
class DragAndDropInput(InputTypeBase):
|
||||
"""
|
||||
Input for drag and drop problems. Allows student to drag and drop images and
|
||||
|
||||
@@ -186,9 +186,9 @@ class LoncapaResponse(object):
|
||||
tree = etree.Element('span')
|
||||
|
||||
# problem author can make this span display:inline
|
||||
if self.xml.get('inline',''):
|
||||
tree.set('class','inline')
|
||||
|
||||
if self.xml.get('inline', ''):
|
||||
tree.set('class', 'inline')
|
||||
|
||||
for item in self.xml:
|
||||
# call provided procedure to do the rendering
|
||||
item_xhtml = renderer(item)
|
||||
@@ -1294,7 +1294,7 @@ class CodeResponse(LoncapaResponse):
|
||||
|
||||
# State associated with the queueing request
|
||||
queuestate = {'key': queuekey,
|
||||
'time': qtime,}
|
||||
'time': qtime, }
|
||||
|
||||
cmap = CorrectMap()
|
||||
if error:
|
||||
|
||||
@@ -8,6 +8,7 @@ import xml.sax.saxutils as saxutils
|
||||
|
||||
TEST_DIR = os.path.dirname(os.path.realpath(__file__))
|
||||
|
||||
|
||||
def tst_render_template(template, context):
|
||||
"""
|
||||
A test version of render to template. Renders to the repr of the context, completely ignoring
|
||||
@@ -25,7 +26,7 @@ test_system = Mock(
|
||||
user=Mock(),
|
||||
filestore=fs.osfs.OSFS(os.path.join(TEST_DIR, "test_files")),
|
||||
debug=True,
|
||||
xqueue={'interface':None, 'callback_url':'/', 'default_queuename': 'testqueue', 'waittime': 10},
|
||||
xqueue={'interface': None, 'callback_url': '/', 'default_queuename': 'testqueue', 'waittime': 10},
|
||||
node_path=os.environ.get("NODE_PATH", "/usr/local/lib/node_modules"),
|
||||
anonymous_student_id = 'student'
|
||||
anonymous_student_id='student'
|
||||
)
|
||||
|
||||
@@ -8,6 +8,7 @@ from capa import customrender
|
||||
# just a handy shortcut
|
||||
lookup_tag = customrender.registry.get_class_for_tag
|
||||
|
||||
|
||||
def extract_context(xml):
|
||||
"""
|
||||
Given an xml element corresponding to the output of test_system.render_template, get back the
|
||||
@@ -15,9 +16,11 @@ def extract_context(xml):
|
||||
"""
|
||||
return eval(xml.text)
|
||||
|
||||
|
||||
def quote_attr(s):
|
||||
return saxutils.quoteattr(s)[1:-1] # don't want the outer quotes
|
||||
|
||||
|
||||
class HelperTest(unittest.TestCase):
|
||||
'''
|
||||
Make sure that our helper function works!
|
||||
@@ -50,7 +53,7 @@ class SolutionRenderTest(unittest.TestCase):
|
||||
# our test_system "renders" templates to a div with the repr of the context
|
||||
xml = renderer.get_html()
|
||||
context = extract_context(xml)
|
||||
self.assertEqual(context, {'id' : 'solution_12'})
|
||||
self.assertEqual(context, {'id': 'solution_12'})
|
||||
|
||||
|
||||
class MathRenderTest(unittest.TestCase):
|
||||
@@ -65,12 +68,11 @@ class MathRenderTest(unittest.TestCase):
|
||||
renderer = lookup_tag('math')(test_system, element)
|
||||
|
||||
self.assertEqual(renderer.mathstr, mathjax_out)
|
||||
|
||||
|
||||
def test_parsing(self):
|
||||
self.check_parse('$abc$', '[mathjaxinline]abc[/mathjaxinline]')
|
||||
self.check_parse('$abc', '$abc')
|
||||
self.check_parse(r'$\displaystyle 2+2$', '[mathjax] 2+2[/mathjax]')
|
||||
|
||||
|
||||
|
||||
# NOTE: not testing get_html yet because I don't understand why it's doing what it's doing.
|
||||
|
||||
|
||||
@@ -31,6 +31,7 @@ lookup_tag = inputtypes.registry.get_class_for_tag
|
||||
def quote_attr(s):
|
||||
return saxutils.quoteattr(s)[1:-1] # don't want the outer quotes
|
||||
|
||||
|
||||
class OptionInputTest(unittest.TestCase):
|
||||
'''
|
||||
Make sure option inputs work
|
||||
@@ -100,7 +101,7 @@ class ChoiceGroupTest(unittest.TestCase):
|
||||
'input_type': expected_input_type,
|
||||
'choices': [('foil1', '<text>This is foil One.</text>'),
|
||||
('foil2', '<text>This is foil Two.</text>'),
|
||||
('foil3', 'This is foil Three.'),],
|
||||
('foil3', 'This is foil Three.'), ],
|
||||
'name_array_suffix': expected_suffix, # what is this for??
|
||||
}
|
||||
|
||||
@@ -137,7 +138,7 @@ class JavascriptInputTest(unittest.TestCase):
|
||||
|
||||
element = etree.fromstring(xml_str)
|
||||
|
||||
state = {'value': '3',}
|
||||
state = {'value': '3', }
|
||||
the_input = lookup_tag('javascriptinput')(test_system, element, state)
|
||||
|
||||
context = the_input._get_render_context()
|
||||
@@ -149,7 +150,7 @@ class JavascriptInputTest(unittest.TestCase):
|
||||
'params': params,
|
||||
'display_file': display_file,
|
||||
'display_class': display_class,
|
||||
'problem_state': problem_state,}
|
||||
'problem_state': problem_state, }
|
||||
|
||||
self.assertEqual(context, expected)
|
||||
|
||||
@@ -165,7 +166,7 @@ class TextLineTest(unittest.TestCase):
|
||||
|
||||
element = etree.fromstring(xml_str)
|
||||
|
||||
state = {'value': 'BumbleBee',}
|
||||
state = {'value': 'BumbleBee', }
|
||||
the_input = lookup_tag('textline')(test_system, element, state)
|
||||
|
||||
context = the_input._get_render_context()
|
||||
@@ -193,7 +194,7 @@ class TextLineTest(unittest.TestCase):
|
||||
|
||||
element = etree.fromstring(xml_str)
|
||||
|
||||
state = {'value': 'BumbleBee',}
|
||||
state = {'value': 'BumbleBee', }
|
||||
the_input = lookup_tag('textline')(test_system, element, state)
|
||||
|
||||
context = the_input._get_render_context()
|
||||
@@ -231,7 +232,7 @@ class FileSubmissionTest(unittest.TestCase):
|
||||
|
||||
state = {'value': 'BumbleBee.py',
|
||||
'status': 'incomplete',
|
||||
'feedback' : {'message': '3'}, }
|
||||
'feedback': {'message': '3'}, }
|
||||
input_class = lookup_tag('filesubmission')
|
||||
the_input = input_class(test_system, element, state)
|
||||
|
||||
@@ -275,7 +276,7 @@ class CodeInputTest(unittest.TestCase):
|
||||
|
||||
state = {'value': 'print "good evening"',
|
||||
'status': 'incomplete',
|
||||
'feedback' : {'message': '3'}, }
|
||||
'feedback': {'message': '3'}, }
|
||||
|
||||
input_class = lookup_tag('codeinput')
|
||||
the_input = input_class(test_system, element, state)
|
||||
@@ -488,7 +489,7 @@ class ChemicalEquationTest(unittest.TestCase):
|
||||
|
||||
element = etree.fromstring(xml_str)
|
||||
|
||||
state = {'value': 'H2OYeah',}
|
||||
state = {'value': 'H2OYeah', }
|
||||
the_input = lookup_tag('chemicalequationinput')(test_system, element, state)
|
||||
|
||||
context = the_input._get_render_context()
|
||||
|
||||
@@ -16,6 +16,7 @@ from capa.correctmap import CorrectMap
|
||||
from capa.util import convert_files_to_filenames
|
||||
from capa.xqueue_interface import dateformat
|
||||
|
||||
|
||||
class MultiChoiceTest(unittest.TestCase):
|
||||
def test_MC_grade(self):
|
||||
multichoice_file = os.path.dirname(__file__) + "/test_files/multichoice.xml"
|
||||
@@ -295,16 +296,16 @@ class CodeResponseTest(unittest.TestCase):
|
||||
old_cmap = CorrectMap()
|
||||
for i, answer_id in enumerate(answer_ids):
|
||||
queuekey = 1000 + i
|
||||
queuestate = CodeResponseTest.make_queuestate(1000+i, datetime.now())
|
||||
queuestate = CodeResponseTest.make_queuestate(1000 + i, datetime.now())
|
||||
old_cmap.update(CorrectMap(answer_id=answer_ids[i], queuestate=queuestate))
|
||||
|
||||
# Message format common to external graders
|
||||
grader_msg = '<span>MESSAGE</span>' # Must be valid XML
|
||||
correct_score_msg = json.dumps({'correct':True, 'score':1, 'msg': grader_msg})
|
||||
incorrect_score_msg = json.dumps({'correct':False, 'score':0, 'msg': grader_msg})
|
||||
grader_msg = '<span>MESSAGE</span>' # Must be valid XML
|
||||
correct_score_msg = json.dumps({'correct': True, 'score': 1, 'msg': grader_msg})
|
||||
incorrect_score_msg = json.dumps({'correct': False, 'score': 0, 'msg': grader_msg})
|
||||
|
||||
xserver_msgs = {'correct': correct_score_msg,
|
||||
'incorrect': incorrect_score_msg,}
|
||||
'incorrect': incorrect_score_msg, }
|
||||
|
||||
# Incorrect queuekey, state should not be updated
|
||||
for correctness in ['correct', 'incorrect']:
|
||||
@@ -325,7 +326,7 @@ class CodeResponseTest(unittest.TestCase):
|
||||
|
||||
new_cmap = CorrectMap()
|
||||
new_cmap.update(old_cmap)
|
||||
npoints = 1 if correctness=='correct' else 0
|
||||
npoints = 1 if correctness == 'correct' else 0
|
||||
new_cmap.set(answer_id=answer_id, npoints=npoints, correctness=correctness, msg=grader_msg, queuestate=None)
|
||||
|
||||
test_lcp.update_score(xserver_msgs[correctness], queuekey=1000 + i)
|
||||
@@ -361,7 +362,7 @@ class CodeResponseTest(unittest.TestCase):
|
||||
for i, answer_id in enumerate(answer_ids):
|
||||
queuekey = 1000 + i
|
||||
latest_timestamp = datetime.now()
|
||||
queuestate = CodeResponseTest.make_queuestate(1000+i, latest_timestamp)
|
||||
queuestate = CodeResponseTest.make_queuestate(1000 + i, latest_timestamp)
|
||||
cmap.update(CorrectMap(answer_id=answer_id, queuestate=queuestate))
|
||||
test_lcp.correct_map.update(cmap)
|
||||
|
||||
@@ -412,6 +413,7 @@ class ChoiceResponseTest(unittest.TestCase):
|
||||
self.assertEquals(test_lcp.grade_answers(test_answers).get_correctness('1_3_1'), 'incorrect')
|
||||
self.assertEquals(test_lcp.grade_answers(test_answers).get_correctness('1_4_1'), 'correct')
|
||||
|
||||
|
||||
class JavascriptResponseTest(unittest.TestCase):
|
||||
|
||||
def test_jr_grade(self):
|
||||
@@ -424,4 +426,3 @@ class JavascriptResponseTest(unittest.TestCase):
|
||||
|
||||
self.assertEquals(test_lcp.grade_answers(incorrect_answers).get_correctness('1_2_1'), 'incorrect')
|
||||
self.assertEquals(test_lcp.grade_answers(correct_answers).get_correctness('1_2_1'), 'correct')
|
||||
|
||||
|
||||
@@ -51,15 +51,17 @@ def convert_files_to_filenames(answers):
|
||||
new_answers = dict()
|
||||
for answer_id in answers.keys():
|
||||
answer = answers[answer_id]
|
||||
if is_list_of_files(answer): # Files are stored as a list, even if one file
|
||||
if is_list_of_files(answer): # Files are stored as a list, even if one file
|
||||
new_answers[answer_id] = [f.name for f in answer]
|
||||
else:
|
||||
new_answers[answer_id] = answers[answer_id]
|
||||
return new_answers
|
||||
|
||||
|
||||
def is_list_of_files(files):
|
||||
return isinstance(files, list) and all(is_file(f) for f in files)
|
||||
|
||||
|
||||
def is_file(file_to_test):
|
||||
'''
|
||||
Duck typing to check if 'file_to_test' is a File object
|
||||
@@ -79,11 +81,10 @@ def find_with_default(node, path, default):
|
||||
|
||||
Returns:
|
||||
node.find(path).text if the find succeeds, default otherwise.
|
||||
|
||||
|
||||
"""
|
||||
v = node.find(path)
|
||||
if v is not None:
|
||||
return v.text
|
||||
else:
|
||||
return default
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import requests
|
||||
log = logging.getLogger('mitx.' + __name__)
|
||||
dateformat = '%Y%m%d%H%M%S'
|
||||
|
||||
|
||||
def make_hashkey(seed):
|
||||
'''
|
||||
Generate a string key by hashing
|
||||
@@ -29,9 +30,9 @@ def make_xheader(lms_callback_url, lms_key, queue_name):
|
||||
'queue_name': designate a specific queue within xqueue server, e.g. 'MITx-6.00x' (string)
|
||||
}
|
||||
"""
|
||||
return json.dumps({ 'lms_callback_url': lms_callback_url,
|
||||
return json.dumps({'lms_callback_url': lms_callback_url,
|
||||
'lms_key': lms_key,
|
||||
'queue_name': queue_name })
|
||||
'queue_name': queue_name})
|
||||
|
||||
|
||||
def parse_xreply(xreply):
|
||||
@@ -96,18 +97,18 @@ class XQueueInterface(object):
|
||||
|
||||
|
||||
def _login(self):
|
||||
payload = { 'username': self.auth['username'],
|
||||
'password': self.auth['password'] }
|
||||
payload = {'username': self.auth['username'],
|
||||
'password': self.auth['password']}
|
||||
return self._http_post(self.url + '/xqueue/login/', payload)
|
||||
|
||||
|
||||
def _send_to_queue(self, header, body, files_to_upload):
|
||||
payload = {'xqueue_header': header,
|
||||
'xqueue_body' : body}
|
||||
'xqueue_body': body}
|
||||
files = {}
|
||||
if files_to_upload is not None:
|
||||
for f in files_to_upload:
|
||||
files.update({ f.name: f })
|
||||
files.update({f.name: f})
|
||||
|
||||
return self._http_post(self.url + '/xqueue/submit/', payload, files=files)
|
||||
|
||||
|
||||
@@ -3,7 +3,8 @@ A handy util to print a django-debug-screen-like stack trace with
|
||||
values of local variables.
|
||||
"""
|
||||
|
||||
import sys, traceback
|
||||
import sys
|
||||
import traceback
|
||||
from django.utils.encoding import smart_unicode
|
||||
|
||||
|
||||
@@ -48,5 +49,3 @@ def supertrace(max_len=160):
|
||||
print s
|
||||
except:
|
||||
print "<ERROR WHILE PRINTING VALUE>"
|
||||
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ setup(
|
||||
"peergrading = xmodule.peer_grading_module:PeerGradingDescriptor",
|
||||
"problem = xmodule.capa_module:CapaDescriptor",
|
||||
"problemset = xmodule.seq_module:SequenceDescriptor",
|
||||
"randomize = xmodule.randomize_module:RandomizeDescriptor",
|
||||
"randomize = xmodule.randomize_module:RandomizeDescriptor",
|
||||
"section = xmodule.backcompat_module:SemanticSectionDescriptor",
|
||||
"sequential = xmodule.seq_module:SequenceDescriptor",
|
||||
"slides = xmodule.backcompat_module:TranslateCustomTagDescriptor",
|
||||
|
||||
@@ -51,7 +51,7 @@ class ABTestModule(XModule):
|
||||
|
||||
def get_shared_state(self):
|
||||
return json.dumps({'group': self.group})
|
||||
|
||||
|
||||
def get_child_descriptors(self):
|
||||
active_locations = set(self.definition['data']['group_content'][self.group])
|
||||
return [desc for desc in self.descriptor.get_children() if desc.location.url() in active_locations]
|
||||
@@ -171,7 +171,7 @@ class ABTestDescriptor(RawDescriptor, XmlDescriptor):
|
||||
group_elem.append(etree.fromstring(child.export_to_xml(resource_fs)))
|
||||
|
||||
return xml_object
|
||||
|
||||
|
||||
|
||||
|
||||
def has_dynamic_children(self):
|
||||
return True
|
||||
|
||||
@@ -29,6 +29,7 @@ TIMEDELTA_REGEX = re.compile(r'^((?P<days>\d+?) day(?:s?))?(\s)?((?P<hours>\d+?)
|
||||
# Generated this many different variants of problems with rerandomize=per_student
|
||||
NUM_RANDOMIZATION_BINS = 20
|
||||
|
||||
|
||||
def randomization_bin(seed, problem_id):
|
||||
"""
|
||||
Pick a randomization bin for the problem given the user's seed and a problem id.
|
||||
@@ -43,6 +44,7 @@ def randomization_bin(seed, problem_id):
|
||||
# get the first few digits of the hash, convert to an int, then mod.
|
||||
return int(h.hexdigest()[:7], 16) % NUM_RANDOMIZATION_BINS
|
||||
|
||||
|
||||
def only_one(lst, default="", process=lambda x: x):
|
||||
"""
|
||||
If lst is empty, returns default
|
||||
@@ -283,7 +285,7 @@ class CapaModule(XModule):
|
||||
|
||||
# Next, generate a fresh LoncapaProblem
|
||||
self.lcp = LoncapaProblem(self.definition['data'], self.location.html_id(),
|
||||
state=None, # Tabula rasa
|
||||
state=None, # Tabula rasa
|
||||
seed=self.seed, system=self.system)
|
||||
|
||||
# Prepend a scary warning to the student
|
||||
@@ -302,7 +304,7 @@ class CapaModule(XModule):
|
||||
html = warning
|
||||
try:
|
||||
html += self.lcp.get_html()
|
||||
except Exception, err: # Couldn't do it. Give up
|
||||
except Exception, err: # Couldn't do it. Give up
|
||||
log.exception(err)
|
||||
raise
|
||||
|
||||
@@ -315,7 +317,7 @@ class CapaModule(XModule):
|
||||
# check button is context-specific.
|
||||
|
||||
# Put a "Check" button if unlimited attempts or still some left
|
||||
if self.max_attempts is None or self.attempts < self.max_attempts-1:
|
||||
if self.max_attempts is None or self.attempts < self.max_attempts - 1:
|
||||
check_button = "Check"
|
||||
else:
|
||||
# Will be final check so let user know that
|
||||
@@ -561,9 +563,9 @@ class CapaModule(XModule):
|
||||
current_time = datetime.datetime.now()
|
||||
prev_submit_time = self.lcp.get_recentmost_queuetime()
|
||||
waittime_between_requests = self.system.xqueue['waittime']
|
||||
if (current_time-prev_submit_time).total_seconds() < waittime_between_requests:
|
||||
if (current_time - prev_submit_time).total_seconds() < waittime_between_requests:
|
||||
msg = 'You must wait at least %d seconds between submissions' % waittime_between_requests
|
||||
return {'success': msg, 'html': ''} # Prompts a modal dialog in ajax callback
|
||||
return {'success': msg, 'html': ''} # Prompts a modal dialog in ajax callback
|
||||
|
||||
try:
|
||||
old_state = self.lcp.get_state()
|
||||
@@ -596,7 +598,7 @@ class CapaModule(XModule):
|
||||
event_info['attempts'] = self.attempts
|
||||
self.system.track_function('save_problem_check', event_info)
|
||||
|
||||
if hasattr(self.system,'psychometrics_handler'): # update PsychometricsData using callback
|
||||
if hasattr(self.system, 'psychometrics_handler'): # update PsychometricsData using callback
|
||||
self.system.psychometrics_handler(self.get_instance_state())
|
||||
|
||||
# render problem into HTML
|
||||
@@ -707,7 +709,7 @@ class CapaDescriptor(RawDescriptor):
|
||||
@property
|
||||
def editable_metadata_fields(self):
|
||||
"""Remove metadata from the editable fields since it has its own editor"""
|
||||
subset = super(CapaDescriptor,self).editable_metadata_fields
|
||||
subset = super(CapaDescriptor, self).editable_metadata_fields
|
||||
if 'markdown' in subset:
|
||||
subset.remove('markdown')
|
||||
return subset
|
||||
|
||||
@@ -50,8 +50,8 @@ ACCEPT_FILE_UPLOAD = False
|
||||
TRUE_DICT = ["True", True, "TRUE", "true"]
|
||||
|
||||
HUMAN_TASK_TYPE = {
|
||||
'selfassessment' : "Self Assessment",
|
||||
'openended' : "External Grader",
|
||||
'selfassessment': "Self Assessment",
|
||||
'openended': "External Grader",
|
||||
}
|
||||
|
||||
class CombinedOpenEndedModule(XModule):
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import logging
|
||||
from lxml import etree
|
||||
|
||||
log=logging.getLogger(__name__)
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class RubricParsingError(Exception):
|
||||
def __init__(self, msg):
|
||||
self.msg = msg
|
||||
|
||||
|
||||
class CombinedOpenEndedRubric(object):
|
||||
|
||||
def __init__ (self, system, view_only = False):
|
||||
@@ -30,7 +32,7 @@ class CombinedOpenEndedRubric(object):
|
||||
max_scores = map((lambda cat: cat['options'][-1]['points']), rubric_categories)
|
||||
max_score = max(max_scores)
|
||||
html = self.system.render_template('open_ended_rubric.html',
|
||||
{'categories' : rubric_categories,
|
||||
{'categories': rubric_categories,
|
||||
'has_score': self.has_score,
|
||||
'view_only': self.view_only,
|
||||
'max_score': max_score})
|
||||
@@ -71,8 +73,8 @@ class CombinedOpenEndedRubric(object):
|
||||
options: [{text: "Option 1 Name", points: 0}, {text:"Option 2 Name", points: 5}]
|
||||
},
|
||||
{ category: "Category 2 Name",
|
||||
options: [{text: "Option 1 Name", points: 0},
|
||||
{text: "Option 2 Name", points: 1},
|
||||
options: [{text: "Option 1 Name", points: 0},
|
||||
{text: "Option 2 Name", points: 1},
|
||||
{text: "Option 3 Name", points: 2]}]
|
||||
|
||||
'''
|
||||
@@ -88,7 +90,7 @@ class CombinedOpenEndedRubric(object):
|
||||
|
||||
|
||||
def extract_category(self, category):
|
||||
'''
|
||||
'''
|
||||
construct an individual category
|
||||
{category: "Category 1 Name",
|
||||
options: [{text: "Option 1 text", points: 1},
|
||||
@@ -121,7 +123,7 @@ class CombinedOpenEndedRubric(object):
|
||||
autonumbering = True
|
||||
# parse options
|
||||
for option in optionsxml:
|
||||
if option.tag != 'option':
|
||||
if option.tag != 'option':
|
||||
raise RubricParsingError("[extract_category]: expected option tag, got {0} instead".format(option.tag))
|
||||
else:
|
||||
pointstr = option.get("points")
|
||||
@@ -138,7 +140,7 @@ class CombinedOpenEndedRubric(object):
|
||||
cur_points = cur_points + 1
|
||||
else:
|
||||
raise Exception("[extract_category]: missing points attribute. Cannot continue to auto-create points values after a points value is explicitly defined.")
|
||||
|
||||
|
||||
selected = score == points
|
||||
optiontext = option.text
|
||||
options.append({'text': option.text, 'points': points, 'selected': selected})
|
||||
|
||||
@@ -9,12 +9,13 @@ from pkg_resources import resource_string
|
||||
|
||||
log = logging.getLogger('mitx.' + __name__)
|
||||
|
||||
|
||||
class ConditionalModule(XModule):
|
||||
'''
|
||||
Blocks child module from showing unless certain conditions are met.
|
||||
|
||||
Example:
|
||||
|
||||
|
||||
<conditional condition="require_completed" required="tag/url_name1&tag/url_name2">
|
||||
<video url_name="secret_video" />
|
||||
</conditional>
|
||||
@@ -37,13 +38,13 @@ class ConditionalModule(XModule):
|
||||
def __init__(self, system, location, definition, descriptor, instance_state=None, shared_state=None, **kwargs):
|
||||
"""
|
||||
In addition to the normal XModule init, provide:
|
||||
|
||||
|
||||
self.condition = string describing condition required
|
||||
|
||||
"""
|
||||
XModule.__init__(self, system, location, definition, descriptor, instance_state, shared_state, **kwargs)
|
||||
self.contents = None
|
||||
self.condition = self.metadata.get('condition','')
|
||||
self.condition = self.metadata.get('condition', '')
|
||||
#log.debug('conditional module required=%s' % self.required_modules_list)
|
||||
|
||||
def _get_required_modules(self):
|
||||
@@ -56,7 +57,7 @@ class ConditionalModule(XModule):
|
||||
def is_condition_satisfied(self):
|
||||
self._get_required_modules()
|
||||
|
||||
if self.condition=='require_completed':
|
||||
if self.condition == 'require_completed':
|
||||
# all required modules must be completed, as determined by
|
||||
# the modules .is_completed() method
|
||||
for module in self.required_modules:
|
||||
@@ -70,7 +71,7 @@ class ConditionalModule(XModule):
|
||||
else:
|
||||
log.debug('conditional module: %s IS completed' % module)
|
||||
return True
|
||||
elif self.condition=='require_attempted':
|
||||
elif self.condition == 'require_attempted':
|
||||
# all required modules must be attempted, as determined by
|
||||
# the modules .is_attempted() method
|
||||
for module in self.required_modules:
|
||||
@@ -111,9 +112,10 @@ class ConditionalModule(XModule):
|
||||
|
||||
# for now, just deal with one child
|
||||
html = self.contents[0]
|
||||
|
||||
|
||||
return json.dumps({'html': html})
|
||||
|
||||
|
||||
class ConditionalDescriptor(SequenceDescriptor):
|
||||
module_class = ConditionalModule
|
||||
|
||||
@@ -125,7 +127,7 @@ class ConditionalDescriptor(SequenceDescriptor):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(ConditionalDescriptor, self).__init__(*args, **kwargs)
|
||||
|
||||
required_module_list = [tuple(x.split('/',1)) for x in self.metadata.get('required','').split('&')]
|
||||
required_module_list = [tuple(x.split('/', 1)) for x in self.metadata.get('required', '').split('&')]
|
||||
self.required_module_locations = []
|
||||
for (tag, name) in required_module_list:
|
||||
loc = self.location.dict()
|
||||
@@ -133,9 +135,8 @@ class ConditionalDescriptor(SequenceDescriptor):
|
||||
loc['name'] = name
|
||||
self.required_module_locations.append(Location(loc))
|
||||
log.debug('ConditionalDescriptor required_module_locations=%s' % self.required_module_locations)
|
||||
|
||||
|
||||
def get_required_module_descriptors(self):
|
||||
"""Returns a list of XModuleDescritpor instances upon which this module depends, but are
|
||||
not children of this module"""
|
||||
return [self.system.load_item(loc) for loc in self.required_module_locations]
|
||||
|
||||
|
||||
@@ -11,15 +11,16 @@ from xmodule.modulestore import Location
|
||||
from .django import contentstore
|
||||
from PIL import Image
|
||||
|
||||
|
||||
class StaticContent(object):
|
||||
def __init__(self, loc, name, content_type, data, last_modified_at=None, thumbnail_location=None, import_path=None):
|
||||
self.location = loc
|
||||
self.name = name #a display string which can be edited, and thus not part of the location which needs to be fixed
|
||||
self.name = name # a display string which can be edited, and thus not part of the location which needs to be fixed
|
||||
self.content_type = content_type
|
||||
self.data = data
|
||||
self.last_modified_at = last_modified_at
|
||||
self.thumbnail_location = Location(thumbnail_location) if thumbnail_location is not None else None
|
||||
# optional information about where this file was imported from. This is needed to support import/export
|
||||
# optional information about where this file was imported from. This is needed to support import/export
|
||||
# cycles
|
||||
self.import_path = import_path
|
||||
|
||||
@@ -29,7 +30,7 @@ class StaticContent(object):
|
||||
|
||||
@staticmethod
|
||||
def generate_thumbnail_name(original_name):
|
||||
return ('{0}'+XASSET_THUMBNAIL_TAIL_NAME).format(os.path.splitext(original_name)[0])
|
||||
return ('{0}' + XASSET_THUMBNAIL_TAIL_NAME).format(os.path.splitext(original_name)[0])
|
||||
|
||||
@staticmethod
|
||||
def compute_location(org, course, name, revision=None, is_thumbnail=False):
|
||||
@@ -41,7 +42,7 @@ class StaticContent(object):
|
||||
|
||||
def get_url_path(self):
|
||||
return StaticContent.get_url_path_from_location(self.location)
|
||||
|
||||
|
||||
@staticmethod
|
||||
def get_url_path_from_location(location):
|
||||
if location is not None:
|
||||
@@ -56,15 +57,15 @@ class StaticContent(object):
|
||||
|
||||
@staticmethod
|
||||
def get_id_from_location(location):
|
||||
return { 'tag':location.tag, 'org' : location.org, 'course' : location.course,
|
||||
'category' : location.category, 'name' : location.name,
|
||||
'revision' : location.revision}
|
||||
return {'tag': location.tag, 'org': location.org, 'course': location.course,
|
||||
'category': location.category, 'name': location.name,
|
||||
'revision': location.revision}
|
||||
@staticmethod
|
||||
def get_location_from_path(path):
|
||||
# remove leading / character if it is there one
|
||||
if path.startswith('/'):
|
||||
path = path[1:]
|
||||
|
||||
|
||||
return Location(path.split('/'))
|
||||
|
||||
@staticmethod
|
||||
@@ -77,7 +78,7 @@ class StaticContent(object):
|
||||
return StaticContent.get_url_path_from_location(loc)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
class ContentStore(object):
|
||||
'''
|
||||
@@ -95,14 +96,14 @@ class ContentStore(object):
|
||||
|
||||
[
|
||||
|
||||
{u'displayname': u'profile.jpg', u'chunkSize': 262144, u'length': 85374,
|
||||
u'uploadDate': datetime.datetime(2012, 10, 3, 5, 41, 54, 183000), u'contentType': u'image/jpeg',
|
||||
u'_id': {u'category': u'asset', u'name': u'profile.jpg', u'course': u'6.002x', u'tag': u'c4x',
|
||||
u'org': u'MITx', u'revision': None}, u'md5': u'36dc53519d4b735eb6beba51cd686a0e'},
|
||||
{u'displayname': u'profile.jpg', u'chunkSize': 262144, u'length': 85374,
|
||||
u'uploadDate': datetime.datetime(2012, 10, 3, 5, 41, 54, 183000), u'contentType': u'image/jpeg',
|
||||
u'_id': {u'category': u'asset', u'name': u'profile.jpg', u'course': u'6.002x', u'tag': u'c4x',
|
||||
u'org': u'MITx', u'revision': None}, u'md5': u'36dc53519d4b735eb6beba51cd686a0e'},
|
||||
|
||||
{u'displayname': u'profile.thumbnail.jpg', u'chunkSize': 262144, u'length': 4073,
|
||||
u'uploadDate': datetime.datetime(2012, 10, 3, 5, 41, 54, 196000), u'contentType': u'image/jpeg',
|
||||
u'_id': {u'category': u'asset', u'name': u'profile.thumbnail.jpg', u'course': u'6.002x', u'tag': u'c4x',
|
||||
{u'displayname': u'profile.thumbnail.jpg', u'chunkSize': 262144, u'length': 4073,
|
||||
u'uploadDate': datetime.datetime(2012, 10, 3, 5, 41, 54, 196000), u'contentType': u'image/jpeg',
|
||||
u'_id': {u'category': u'asset', u'name': u'profile.thumbnail.jpg', u'course': u'6.002x', u'tag': u'c4x',
|
||||
u'org': u'MITx', u'revision': None}, u'md5': u'ff1532598830e3feac91c2449eaa60d6'},
|
||||
|
||||
....
|
||||
@@ -117,7 +118,7 @@ class ContentStore(object):
|
||||
thumbnail_name = StaticContent.generate_thumbnail_name(content.location.name)
|
||||
|
||||
thumbnail_file_location = StaticContent.compute_location(content.location.org, content.location.course,
|
||||
thumbnail_name, is_thumbnail = True)
|
||||
thumbnail_name, is_thumbnail=True)
|
||||
|
||||
# if we're uploading an image, then let's generate a thumbnail so that we can
|
||||
# serve it up when needed without having to rescale on the fly
|
||||
@@ -129,7 +130,7 @@ class ContentStore(object):
|
||||
# @todo: move the thumbnail size to a configuration setting?!?
|
||||
im = Image.open(StringIO.StringIO(content.data))
|
||||
|
||||
# I've seen some exceptions from the PIL library when trying to save palletted
|
||||
# I've seen some exceptions from the PIL library when trying to save palletted
|
||||
# PNG files to JPEG. Per the google-universe, they suggest converting to RGB first.
|
||||
im = im.convert('RGB')
|
||||
size = 128, 128
|
||||
@@ -139,7 +140,7 @@ class ContentStore(object):
|
||||
thumbnail_file.seek(0)
|
||||
|
||||
# store this thumbnail as any other piece of content
|
||||
thumbnail_content = StaticContent(thumbnail_file_location, thumbnail_name,
|
||||
thumbnail_content = StaticContent(thumbnail_file_location, thumbnail_name,
|
||||
'image/jpeg', thumbnail_file)
|
||||
|
||||
contentstore().save(thumbnail_content)
|
||||
@@ -149,7 +150,3 @@ class ContentStore(object):
|
||||
logging.exception("Failed to generate thumbnail for {0}. Exception: {1}".format(content.location, str(e)))
|
||||
|
||||
return thumbnail_content, thumbnail_file_location
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ from django.conf import settings
|
||||
|
||||
_CONTENTSTORE = None
|
||||
|
||||
|
||||
def load_function(path):
|
||||
"""
|
||||
Load a function by name.
|
||||
|
||||
@@ -17,14 +17,14 @@ import os
|
||||
|
||||
class MongoContentStore(ContentStore):
|
||||
def __init__(self, host, db, port=27017, user=None, password=None, **kwargs):
|
||||
logging.debug( 'Using MongoDB for static content serving at host={0} db={1}'.format(host,db))
|
||||
logging.debug('Using MongoDB for static content serving at host={0} db={1}'.format(host, db))
|
||||
_db = Connection(host=host, port=port, **kwargs)[db]
|
||||
|
||||
if user is not None and password is not None:
|
||||
_db.authenticate(user, password)
|
||||
|
||||
self.fs = gridfs.GridFS(_db)
|
||||
self.fs_files = _db["fs.files"] # the underlying collection GridFS uses
|
||||
self.fs_files = _db["fs.files"] # the underlying collection GridFS uses
|
||||
|
||||
|
||||
def save(self, content):
|
||||
@@ -33,24 +33,24 @@ class MongoContentStore(ContentStore):
|
||||
# Seems like with the GridFS we can't update existing ID's we have to do a delete/add pair
|
||||
self.delete(id)
|
||||
|
||||
with self.fs.new_file(_id = id, filename=content.get_url_path(), content_type=content.content_type,
|
||||
with self.fs.new_file(_id=id, filename=content.get_url_path(), content_type=content.content_type,
|
||||
displayname=content.name, thumbnail_location=content.thumbnail_location, import_path=content.import_path) as fp:
|
||||
|
||||
fp.write(content.data)
|
||||
|
||||
|
||||
return content
|
||||
|
||||
|
||||
def delete(self, id):
|
||||
if self.fs.exists({"_id" : id}):
|
||||
if self.fs.exists({"_id": id}):
|
||||
self.fs.delete(id)
|
||||
|
||||
def find(self, location):
|
||||
id = StaticContent.get_id_from_location(location)
|
||||
try:
|
||||
with self.fs.get(id) as fp:
|
||||
return StaticContent(location, fp.displayname, fp.content_type, fp.read(),
|
||||
fp.uploadDate, thumbnail_location = fp.thumbnail_location if hasattr(fp, 'thumbnail_location') else None,
|
||||
import_path = fp.import_path if hasattr(fp, 'import_path') else None)
|
||||
return StaticContent(location, fp.displayname, fp.content_type, fp.read(),
|
||||
fp.uploadDate, thumbnail_location=fp.thumbnail_location if hasattr(fp, 'thumbnail_location') else None,
|
||||
import_path=fp.import_path if hasattr(fp, 'import_path') else None)
|
||||
except NoFile:
|
||||
raise NotFoundError()
|
||||
|
||||
@@ -76,25 +76,25 @@ class MongoContentStore(ContentStore):
|
||||
self.export(asset_location, output_directory)
|
||||
|
||||
def get_all_content_thumbnails_for_course(self, location):
|
||||
return self._get_all_content_for_course(location, get_thumbnails = True)
|
||||
return self._get_all_content_for_course(location, get_thumbnails=True)
|
||||
|
||||
def get_all_content_for_course(self, location):
|
||||
return self._get_all_content_for_course(location, get_thumbnails = False)
|
||||
return self._get_all_content_for_course(location, get_thumbnails=False)
|
||||
|
||||
def _get_all_content_for_course(self, location, get_thumbnails = False):
|
||||
def _get_all_content_for_course(self, location, get_thumbnails=False):
|
||||
'''
|
||||
Returns a list of all static assets for a course. The return format is a list of dictionary elements. Example:
|
||||
|
||||
[
|
||||
|
||||
{u'displayname': u'profile.jpg', u'chunkSize': 262144, u'length': 85374,
|
||||
u'uploadDate': datetime.datetime(2012, 10, 3, 5, 41, 54, 183000), u'contentType': u'image/jpeg',
|
||||
u'_id': {u'category': u'asset', u'name': u'profile.jpg', u'course': u'6.002x', u'tag': u'c4x',
|
||||
u'org': u'MITx', u'revision': None}, u'md5': u'36dc53519d4b735eb6beba51cd686a0e'},
|
||||
{u'displayname': u'profile.jpg', u'chunkSize': 262144, u'length': 85374,
|
||||
u'uploadDate': datetime.datetime(2012, 10, 3, 5, 41, 54, 183000), u'contentType': u'image/jpeg',
|
||||
u'_id': {u'category': u'asset', u'name': u'profile.jpg', u'course': u'6.002x', u'tag': u'c4x',
|
||||
u'org': u'MITx', u'revision': None}, u'md5': u'36dc53519d4b735eb6beba51cd686a0e'},
|
||||
|
||||
{u'displayname': u'profile.thumbnail.jpg', u'chunkSize': 262144, u'length': 4073,
|
||||
u'uploadDate': datetime.datetime(2012, 10, 3, 5, 41, 54, 196000), u'contentType': u'image/jpeg',
|
||||
u'_id': {u'category': u'asset', u'name': u'profile.thumbnail.jpg', u'course': u'6.002x', u'tag': u'c4x',
|
||||
{u'displayname': u'profile.thumbnail.jpg', u'chunkSize': 262144, u'length': 4073,
|
||||
u'uploadDate': datetime.datetime(2012, 10, 3, 5, 41, 54, 196000), u'contentType': u'image/jpeg',
|
||||
u'_id': {u'category': u'asset', u'name': u'profile.thumbnail.jpg', u'course': u'6.002x', u'tag': u'c4x',
|
||||
u'org': u'MITx', u'revision': None}, u'md5': u'ff1532598830e3feac91c2449eaa60d6'},
|
||||
|
||||
....
|
||||
@@ -102,10 +102,7 @@ class MongoContentStore(ContentStore):
|
||||
]
|
||||
'''
|
||||
course_filter = Location(XASSET_LOCATION_TAG, category="asset" if not get_thumbnails else "thumbnail",
|
||||
course=location.course,org=location.org)
|
||||
course=location.course, org=location.org)
|
||||
# 'borrow' the function 'location_to_query' from the Mongo modulestore implementation
|
||||
items = self.fs_files.find(location_to_query(course_filter))
|
||||
return list(items)
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -147,37 +147,37 @@ class CourseDescriptor(SequenceDescriptor):
|
||||
"""
|
||||
Return a dict which is a copy of the default grading policy
|
||||
"""
|
||||
default = {"GRADER" : [
|
||||
default = {"GRADER": [
|
||||
{
|
||||
"type" : "Homework",
|
||||
"min_count" : 12,
|
||||
"drop_count" : 2,
|
||||
"short_label" : "HW",
|
||||
"weight" : 0.15
|
||||
"type": "Homework",
|
||||
"min_count": 12,
|
||||
"drop_count": 2,
|
||||
"short_label": "HW",
|
||||
"weight": 0.15
|
||||
},
|
||||
{
|
||||
"type" : "Lab",
|
||||
"min_count" : 12,
|
||||
"drop_count" : 2,
|
||||
"weight" : 0.15
|
||||
"type": "Lab",
|
||||
"min_count": 12,
|
||||
"drop_count": 2,
|
||||
"weight": 0.15
|
||||
},
|
||||
{
|
||||
"type" : "Midterm Exam",
|
||||
"short_label" : "Midterm",
|
||||
"min_count" : 1,
|
||||
"drop_count" : 0,
|
||||
"weight" : 0.3
|
||||
"type": "Midterm Exam",
|
||||
"short_label": "Midterm",
|
||||
"min_count": 1,
|
||||
"drop_count": 0,
|
||||
"weight": 0.3
|
||||
},
|
||||
{
|
||||
"type" : "Final Exam",
|
||||
"short_label" : "Final",
|
||||
"min_count" : 1,
|
||||
"drop_count" : 0,
|
||||
"weight" : 0.4
|
||||
"type": "Final Exam",
|
||||
"short_label": "Final",
|
||||
"min_count": 1,
|
||||
"drop_count": 0,
|
||||
"weight": 0.4
|
||||
}
|
||||
],
|
||||
"GRADE_CUTOFFS" : {
|
||||
"Pass" : 0.5
|
||||
"GRADE_CUTOFFS": {
|
||||
"Pass": 0.5
|
||||
}}
|
||||
return copy.deepcopy(default)
|
||||
|
||||
@@ -230,8 +230,8 @@ class CourseDescriptor(SequenceDescriptor):
|
||||
|
||||
# bleh, have to parse the XML here to just pull out the url_name attribute
|
||||
# I don't think it's stored anywhere in the instance.
|
||||
course_file = StringIO(xml_data.encode('ascii','ignore'))
|
||||
xml_obj = etree.parse(course_file,parser=edx_xml_parser).getroot()
|
||||
course_file = StringIO(xml_data.encode('ascii', 'ignore'))
|
||||
xml_obj = etree.parse(course_file, parser=edx_xml_parser).getroot()
|
||||
|
||||
policy_dir = None
|
||||
url_name = xml_obj.get('url_name', xml_obj.get('slug'))
|
||||
@@ -329,7 +329,7 @@ class CourseDescriptor(SequenceDescriptor):
|
||||
def raw_grader(self, value):
|
||||
# NOTE WELL: this change will not update the processed graders. If we need that, this needs to call grader_from_conf
|
||||
self._grading_policy['RAW_GRADER'] = value
|
||||
self.definition['data'].setdefault('grading_policy',{})['GRADER'] = value
|
||||
self.definition['data'].setdefault('grading_policy', {})['GRADER'] = value
|
||||
|
||||
@property
|
||||
def grade_cutoffs(self):
|
||||
@@ -338,7 +338,7 @@ class CourseDescriptor(SequenceDescriptor):
|
||||
@grade_cutoffs.setter
|
||||
def grade_cutoffs(self, value):
|
||||
self._grading_policy['GRADE_CUTOFFS'] = value
|
||||
self.definition['data'].setdefault('grading_policy',{})['GRADE_CUTOFFS'] = value
|
||||
self.definition['data'].setdefault('grading_policy', {})['GRADE_CUTOFFS'] = value
|
||||
|
||||
|
||||
@property
|
||||
@@ -377,7 +377,7 @@ class CourseDescriptor(SequenceDescriptor):
|
||||
Return list of topic ids defined in course policy.
|
||||
"""
|
||||
topics = self.metadata.get("discussion_topics", {})
|
||||
return [d["id"] for d in topics.values()]
|
||||
return [d["id"] for d in topics.values()]
|
||||
|
||||
|
||||
@property
|
||||
@@ -436,10 +436,10 @@ class CourseDescriptor(SequenceDescriptor):
|
||||
scale = 300.0 # about a year
|
||||
if announcement:
|
||||
days = (now - announcement).days
|
||||
score = -exp(-days/scale)
|
||||
score = -exp(-days / scale)
|
||||
else:
|
||||
days = (now - start).days
|
||||
score = exp(days/scale)
|
||||
score = exp(days / scale)
|
||||
return score
|
||||
|
||||
def _sorting_dates(self):
|
||||
@@ -501,16 +501,16 @@ class CourseDescriptor(SequenceDescriptor):
|
||||
xmoduledescriptors.append(s)
|
||||
|
||||
# The xmoduledescriptors included here are only the ones that have scores.
|
||||
section_description = { 'section_descriptor' : s, 'xmoduledescriptors' : filter(lambda child: child.has_score, xmoduledescriptors) }
|
||||
section_description = {'section_descriptor': s, 'xmoduledescriptors': filter(lambda child: child.has_score, xmoduledescriptors)}
|
||||
|
||||
section_format = s.metadata.get('format', "")
|
||||
graded_sections[ section_format ] = graded_sections.get( section_format, [] ) + [section_description]
|
||||
graded_sections[section_format] = graded_sections.get(section_format, []) + [section_description]
|
||||
|
||||
all_descriptors.extend(xmoduledescriptors)
|
||||
all_descriptors.append(s)
|
||||
|
||||
return { 'graded_sections' : graded_sections,
|
||||
'all_descriptors' : all_descriptors,}
|
||||
return {'graded_sections': graded_sections,
|
||||
'all_descriptors': all_descriptors, }
|
||||
|
||||
|
||||
@staticmethod
|
||||
@@ -636,7 +636,7 @@ class CourseDescriptor(SequenceDescriptor):
|
||||
# *end* of the same day, not the same time. It's going to be used as the
|
||||
# end of the exam overall, so we don't want the exam to disappear too soon.
|
||||
# It's also used optionally as the registration end date, so time matters there too.
|
||||
self.last_eligible_appointment_date = self._try_parse_time('Last_Eligible_Appointment_Date') # or self.first_eligible_appointment_date
|
||||
self.last_eligible_appointment_date = self._try_parse_time('Last_Eligible_Appointment_Date') # or self.first_eligible_appointment_date
|
||||
if self.last_eligible_appointment_date is None:
|
||||
raise ValueError("Last appointment date must be specified")
|
||||
self.registration_start_date = self._try_parse_time('Registration_Start_Date') or time.gmtime(0)
|
||||
@@ -715,4 +715,3 @@ class CourseDescriptor(SequenceDescriptor):
|
||||
@property
|
||||
def org(self):
|
||||
return self.location.org
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ from xmodule.raw_module import RawDescriptor
|
||||
|
||||
import json
|
||||
|
||||
|
||||
class DiscussionModule(XModule):
|
||||
js = {'coffee':
|
||||
[resource_string(__name__, 'js/src/time.coffee'),
|
||||
@@ -30,6 +31,7 @@ class DiscussionModule(XModule):
|
||||
self.title = xml_data.attrib['for']
|
||||
self.discussion_category = xml_data.attrib['discussion_category']
|
||||
|
||||
|
||||
class DiscussionDescriptor(RawDescriptor):
|
||||
module_class = DiscussionModule
|
||||
template_dir_name = "discussion"
|
||||
|
||||
@@ -8,12 +8,14 @@ log = logging.getLogger(__name__)
|
||||
|
||||
ErrorLog = namedtuple('ErrorLog', 'tracker errors')
|
||||
|
||||
|
||||
def exc_info_to_str(exc_info):
|
||||
"""Given some exception info, convert it into a string using
|
||||
the traceback.format_exception() function.
|
||||
"""
|
||||
return ''.join(traceback.format_exception(*exc_info))
|
||||
|
||||
|
||||
def in_exception_handler():
|
||||
'''Is there an active exception?'''
|
||||
return sys.exc_info() != (None, None, None)
|
||||
@@ -44,6 +46,7 @@ def make_error_tracker():
|
||||
|
||||
return ErrorLog(error_tracker, errors)
|
||||
|
||||
|
||||
def null_error_tracker(msg):
|
||||
'''A dummy error tracker that just ignores the messages'''
|
||||
pass
|
||||
|
||||
@@ -49,6 +49,7 @@ def invalid_args(func, argdict):
|
||||
if keywords: return set() # All accepted
|
||||
return set(argdict) - set(args)
|
||||
|
||||
|
||||
def grader_from_conf(conf):
|
||||
"""
|
||||
This creates a CourseGrader from a configuration (such as in course_settings.py).
|
||||
@@ -80,7 +81,7 @@ def grader_from_conf(conf):
|
||||
subgrader_class = SingleSectionGrader
|
||||
else:
|
||||
raise ValueError("Configuration has no appropriate grader class.")
|
||||
|
||||
|
||||
bad_args = invalid_args(subgrader_class.__init__, subgraderconf)
|
||||
# See note above concerning 'name'.
|
||||
if bad_args.issuperset({name}):
|
||||
@@ -90,7 +91,7 @@ def grader_from_conf(conf):
|
||||
log.warning("Invalid arguments for a subgrader: %s", bad_args)
|
||||
for key in bad_args:
|
||||
del subgraderconf[key]
|
||||
|
||||
|
||||
subgrader = subgrader_class(**subgraderconf)
|
||||
subgraders.append((subgrader, subgrader.category, weight))
|
||||
|
||||
@@ -210,13 +211,13 @@ class SingleSectionGrader(CourseGrader):
|
||||
break
|
||||
|
||||
if foundScore or generate_random_scores:
|
||||
if generate_random_scores: # for debugging!
|
||||
earned = random.randint(2,15)
|
||||
if generate_random_scores: # for debugging!
|
||||
earned = random.randint(2, 15)
|
||||
possible = random.randint(earned, 15)
|
||||
else: # We found the score
|
||||
else: # We found the score
|
||||
earned = foundScore.earned
|
||||
possible = foundScore.possible
|
||||
|
||||
|
||||
percent = earned / float(possible)
|
||||
detail = "{name} - {percent:.0%} ({earned:.3n}/{possible:.3n})".format(name=self.name,
|
||||
percent=percent,
|
||||
@@ -245,7 +246,7 @@ class AssignmentFormatGrader(CourseGrader):
|
||||
min_count defines how many assignments are expected throughout the course. Placeholder
|
||||
scores (of 0) will be inserted if the number of matching sections in the course is < min_count.
|
||||
If there number of matching sections in the course is > min_count, min_count will be ignored.
|
||||
|
||||
|
||||
show_only_average is to suppress the display of each assignment in this grader and instead
|
||||
only show the total score of this grader in the breakdown.
|
||||
|
||||
@@ -257,7 +258,7 @@ class AssignmentFormatGrader(CourseGrader):
|
||||
|
||||
short_label is similar to section_type, but shorter. For example, for Homework it would be
|
||||
"HW".
|
||||
|
||||
|
||||
starting_index is the first number that will appear. For example, starting_index=3 and
|
||||
min_count = 2 would produce the labels "Assignment 3", "Assignment 4"
|
||||
|
||||
@@ -296,16 +297,16 @@ class AssignmentFormatGrader(CourseGrader):
|
||||
breakdown = []
|
||||
for i in range(max(self.min_count, len(scores))):
|
||||
if i < len(scores) or generate_random_scores:
|
||||
if generate_random_scores: # for debugging!
|
||||
earned = random.randint(2,15)
|
||||
possible = random.randint(earned, 15)
|
||||
if generate_random_scores: # for debugging!
|
||||
earned = random.randint(2, 15)
|
||||
possible = random.randint(earned, 15)
|
||||
section_name = "Generated"
|
||||
|
||||
|
||||
else:
|
||||
earned = scores[i].earned
|
||||
possible = scores[i].possible
|
||||
section_name = scores[i].section
|
||||
|
||||
|
||||
percentage = earned / float(possible)
|
||||
summary = "{section_type} {index} - {name} - {percent:.0%} ({earned:.3n}/{possible:.3n})".format(index=i + self.starting_index,
|
||||
section_type=self.section_type,
|
||||
@@ -318,7 +319,7 @@ class AssignmentFormatGrader(CourseGrader):
|
||||
summary = "{section_type} {index} Unreleased - 0% (?/?)".format(index=i + self.starting_index, section_type=self.section_type)
|
||||
|
||||
short_label = "{short_label} {index:02d}".format(index=i + self.starting_index, short_label=self.short_label)
|
||||
|
||||
|
||||
breakdown.append({'percent': percentage, 'label': short_label, 'detail': summary, 'category': self.category})
|
||||
|
||||
total_percent, dropped_indices = totalWithDrops(breakdown, self.drop_count)
|
||||
@@ -328,13 +329,13 @@ class AssignmentFormatGrader(CourseGrader):
|
||||
|
||||
total_detail = "{section_type} Average = {percent:.0%}".format(percent=total_percent, section_type=self.section_type)
|
||||
total_label = "{short_label} Avg".format(short_label=self.short_label)
|
||||
|
||||
|
||||
if self.show_only_average:
|
||||
breakdown = []
|
||||
|
||||
|
||||
if not self.hide_average:
|
||||
breakdown.append({'percent': total_percent, 'label': total_label, 'detail': total_detail, 'category': self.category, 'prominent': True})
|
||||
|
||||
|
||||
return {'percent': total_percent,
|
||||
'section_breakdown': breakdown,
|
||||
#No grade_breakdown here
|
||||
|
||||
@@ -10,9 +10,11 @@ from lxml import etree
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class GradingServiceError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class GradingService(object):
|
||||
"""
|
||||
Interface to staff grading backend.
|
||||
@@ -35,7 +37,7 @@ class GradingService(object):
|
||||
"""
|
||||
response = self.session.post(self.login_url,
|
||||
{'username': self.username,
|
||||
'password': self.password,})
|
||||
'password': self.password, })
|
||||
|
||||
response.raise_for_status()
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from lxml import etree
|
||||
|
||||
|
||||
def check_html(html):
|
||||
'''
|
||||
Check whether the passed in html string can be parsed by lxml.
|
||||
|
||||
@@ -133,7 +133,7 @@ class HtmlDescriptor(XmlDescriptor, EditingDescriptor):
|
||||
|
||||
# TODO (ichuang): remove this after migration
|
||||
# for Fall 2012 LMS migration: keep filename (and unmangled filename)
|
||||
definition['filename'] = [ filepath, filename ]
|
||||
definition['filename'] = [filepath, filename]
|
||||
|
||||
return definition
|
||||
|
||||
@@ -180,6 +180,7 @@ class AboutDescriptor(HtmlDescriptor):
|
||||
"""
|
||||
template_dir_name = "about"
|
||||
|
||||
|
||||
class StaticTabDescriptor(HtmlDescriptor):
|
||||
"""
|
||||
These pieces of course content are treated as HtmlModules but we need to overload where the templates are located
|
||||
@@ -187,6 +188,7 @@ class StaticTabDescriptor(HtmlDescriptor):
|
||||
"""
|
||||
template_dir_name = "statictab"
|
||||
|
||||
|
||||
class CourseInfoDescriptor(HtmlDescriptor):
|
||||
"""
|
||||
These pieces of course content are treated as HtmlModules but we need to overload where the templates are located
|
||||
|
||||
@@ -34,7 +34,7 @@ class MakoModuleDescriptor(XModuleDescriptor):
|
||||
"""
|
||||
return {'module': self,
|
||||
'metadata': self.metadata,
|
||||
'editable_metadata_fields' : self.editable_metadata_fields
|
||||
'editable_metadata_fields': self.editable_metadata_fields
|
||||
}
|
||||
|
||||
def get_html(self):
|
||||
@@ -46,4 +46,3 @@ class MakoModuleDescriptor(XModuleDescriptor):
|
||||
def editable_metadata_fields(self):
|
||||
subset = [name for name in self.metadata.keys() if name not in self.system_metadata_fields]
|
||||
return subset
|
||||
|
||||
|
||||
@@ -365,7 +365,7 @@ class ModuleStore(object):
|
||||
raise NotImplementedError
|
||||
|
||||
def get_parent_locations(self, location, course_id):
|
||||
'''Find all locations that are the parents of this location in this
|
||||
'''Find all locations that are the parents of this location in this
|
||||
course. Needed for path_to_location().
|
||||
|
||||
returns an iterable of things that can be passed to Location.
|
||||
|
||||
@@ -67,7 +67,7 @@ class DraftModuleStore(ModuleStoreBase):
|
||||
TODO (vshnayder): this may want to live outside the modulestore eventually
|
||||
"""
|
||||
|
||||
# cdodge: we're forcing depth=0 here as the Draft store is not handling caching well
|
||||
# cdodge: we're forcing depth=0 here as the Draft store is not handling caching well
|
||||
try:
|
||||
return wrap_draft(super(DraftModuleStore, self).get_instance(course_id, as_draft(location), depth=0))
|
||||
except ItemNotFoundError:
|
||||
|
||||
@@ -304,7 +304,7 @@ class MongoModuleStore(ModuleStoreBase):
|
||||
if location.category == 'static_tab':
|
||||
course = self.get_course_for_item(item.location)
|
||||
existing_tabs = course.tabs or []
|
||||
existing_tabs.append({'type':'static_tab', 'name' : item.metadata.get('display_name'), 'url_slug' : item.location.name})
|
||||
existing_tabs.append({'type': 'static_tab', 'name': item.metadata.get('display_name'), 'url_slug': item.location.name})
|
||||
course.tabs = existing_tabs
|
||||
self.update_metadata(course.location, course.metadata)
|
||||
|
||||
@@ -423,7 +423,7 @@ class MongoModuleStore(ModuleStoreBase):
|
||||
|
||||
|
||||
def get_parent_locations(self, location, course_id):
|
||||
'''Find all locations that are the parents of this location in this
|
||||
'''Find all locations that are the parents of this location in this
|
||||
course. Needed for path_to_location().
|
||||
'''
|
||||
location = Location.ensure_fully_specified(location)
|
||||
|
||||
@@ -104,14 +104,14 @@ def path_to_location(modulestore, course_id, location):
|
||||
# module nested in more than one positional module will work.
|
||||
if n > 3:
|
||||
position_list = []
|
||||
for path_index in range(2, n-1):
|
||||
for path_index in range(2, n - 1):
|
||||
category = path[path_index].category
|
||||
if category == 'sequential' or category == 'videosequence':
|
||||
section_desc = modulestore.get_instance(course_id, path[path_index])
|
||||
child_locs = [c.location for c in section_desc.get_children()]
|
||||
# positions are 1-indexed, and should be strings to be consistent with
|
||||
# url parsing.
|
||||
position_list.append(str(child_locs.index(path[path_index+1]) + 1))
|
||||
position_list.append(str(child_locs.index(path[path_index + 1]) + 1))
|
||||
position = "_".join(position_list)
|
||||
|
||||
return (course_id, chapter, section, position)
|
||||
|
||||
@@ -3,6 +3,7 @@ from xmodule.contentstore.content import StaticContent
|
||||
from xmodule.modulestore import Location
|
||||
from xmodule.modulestore.mongo import MongoModuleStore
|
||||
|
||||
|
||||
def clone_course(modulestore, contentstore, source_location, dest_location, delete_original=False):
|
||||
# first check to see if the modulestore is Mongo backed
|
||||
if not isinstance(modulestore, MongoModuleStore):
|
||||
@@ -13,7 +14,7 @@ def clone_course(modulestore, contentstore, source_location, dest_location, dele
|
||||
if not modulestore.has_item(dest_location):
|
||||
raise Exception("An empty course at {0} must have already been created. Aborting...".format(dest_location))
|
||||
|
||||
# verify that the dest_location really is an empty course, which means only one
|
||||
# verify that the dest_location really is an empty course, which means only one
|
||||
dest_modules = modulestore.get_items([dest_location.tag, dest_location.org, dest_location.course, None, None, None])
|
||||
|
||||
if len(dest_modules) != 1:
|
||||
@@ -31,12 +32,12 @@ def clone_course(modulestore, contentstore, source_location, dest_location, dele
|
||||
original_loc = Location(module.location)
|
||||
|
||||
if original_loc.category != 'course':
|
||||
module.location = module.location._replace(tag = dest_location.tag, org = dest_location.org,
|
||||
course = dest_location.course)
|
||||
module.location = module.location._replace(tag=dest_location.tag, org=dest_location.org,
|
||||
course=dest_location.course)
|
||||
else:
|
||||
# on the course module we also have to update the module name
|
||||
module.location = module.location._replace(tag = dest_location.tag, org = dest_location.org,
|
||||
course = dest_location.course, name=dest_location.name)
|
||||
module.location = module.location._replace(tag=dest_location.tag, org=dest_location.org,
|
||||
course=dest_location.course, name=dest_location.name)
|
||||
|
||||
print "Cloning module {0} to {1}....".format(original_loc, module.location)
|
||||
|
||||
@@ -48,8 +49,8 @@ def clone_course(modulestore, contentstore, source_location, dest_location, dele
|
||||
new_children = []
|
||||
for child_loc_url in module.definition['children']:
|
||||
child_loc = Location(child_loc_url)
|
||||
child_loc = child_loc._replace(tag = dest_location.tag, org = dest_location.org,
|
||||
course = dest_location.course)
|
||||
child_loc = child_loc._replace(tag=dest_location.tag, org=dest_location.org,
|
||||
course=dest_location.course)
|
||||
new_children = new_children + [child_loc.url()]
|
||||
|
||||
modulestore.update_children(module.location, new_children)
|
||||
@@ -63,8 +64,8 @@ def clone_course(modulestore, contentstore, source_location, dest_location, dele
|
||||
for thumb in thumbs:
|
||||
thumb_loc = Location(thumb["_id"])
|
||||
content = contentstore.find(thumb_loc)
|
||||
content.location = content.location._replace(org = dest_location.org,
|
||||
course = dest_location.course)
|
||||
content.location = content.location._replace(org=dest_location.org,
|
||||
course=dest_location.course)
|
||||
|
||||
print "Cloning thumbnail {0} to {1}".format(thumb_loc, content.location)
|
||||
|
||||
@@ -76,13 +77,13 @@ def clone_course(modulestore, contentstore, source_location, dest_location, dele
|
||||
for asset in assets:
|
||||
asset_loc = Location(asset["_id"])
|
||||
content = contentstore.find(asset_loc)
|
||||
content.location = content.location._replace(org = dest_location.org,
|
||||
course = dest_location.course)
|
||||
content.location = content.location._replace(org=dest_location.org,
|
||||
course=dest_location.course)
|
||||
|
||||
# be sure to update the pointer to the thumbnail
|
||||
if content.thumbnail_location is not None:
|
||||
content.thumbnail_location = content.thumbnail_location._replace(org = dest_location.org,
|
||||
course = dest_location.course)
|
||||
content.thumbnail_location = content.thumbnail_location._replace(org=dest_location.org,
|
||||
course=dest_location.course)
|
||||
|
||||
print "Cloning asset {0} to {1}".format(asset_loc, content.location)
|
||||
|
||||
@@ -90,6 +91,7 @@ def clone_course(modulestore, contentstore, source_location, dest_location, dele
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def delete_course(modulestore, contentstore, source_location):
|
||||
# first check to see if the modulestore is Mongo backed
|
||||
if not isinstance(modulestore, MongoModuleStore):
|
||||
@@ -119,7 +121,7 @@ def delete_course(modulestore, contentstore, source_location):
|
||||
modules = modulestore.get_items([source_location.tag, source_location.org, source_location.course, None, None, None])
|
||||
|
||||
for module in modules:
|
||||
if module.category != 'course': # save deleting the course module for last
|
||||
if module.category != 'course': # save deleting the course module for last
|
||||
print "Deleting {0}...".format(module.location)
|
||||
modulestore.delete_item(module.location)
|
||||
|
||||
@@ -127,4 +129,4 @@ def delete_course(modulestore, contentstore, source_location):
|
||||
print "Deleting {0}...".format(source_location)
|
||||
modulestore.delete_item(source_location)
|
||||
|
||||
return True
|
||||
return True
|
||||
|
||||
@@ -8,5 +8,3 @@ for i in range(5):
|
||||
TEST_DIR = TEST_DIR / 'test'
|
||||
|
||||
DATA_DIR = TEST_DIR / 'data'
|
||||
|
||||
|
||||
|
||||
@@ -5,12 +5,15 @@ from xmodule.modulestore import Location
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule.timeparse import stringify_time
|
||||
|
||||
def XMODULE_COURSE_CREATION(class_to_create, **kwargs):
|
||||
|
||||
def XMODULE_COURSE_CREATION(class_to_create, **kwargs):
|
||||
return XModuleCourseFactory._create(class_to_create, **kwargs)
|
||||
|
||||
|
||||
def XMODULE_ITEM_CREATION(class_to_create, **kwargs):
|
||||
return XModuleItemFactory._create(class_to_create, **kwargs)
|
||||
|
||||
|
||||
class XModuleCourseFactory(Factory):
|
||||
"""
|
||||
Factory for XModule courses.
|
||||
@@ -27,7 +30,7 @@ class XModuleCourseFactory(Factory):
|
||||
org = kwargs.get('org')
|
||||
number = kwargs.get('number')
|
||||
display_name = kwargs.get('display_name')
|
||||
location = Location('i4x', org, number,
|
||||
location = Location('i4x', org, number,
|
||||
'course', Location.clean(display_name))
|
||||
|
||||
store = modulestore('direct')
|
||||
@@ -42,20 +45,22 @@ class XModuleCourseFactory(Factory):
|
||||
new_course.metadata['data_dir'] = uuid4().hex
|
||||
new_course.metadata['start'] = stringify_time(gmtime())
|
||||
|
||||
new_course.tabs = [{"type": "courseware"},
|
||||
new_course.tabs = [{"type": "courseware"},
|
||||
{"type": "course_info", "name": "Course Info"},
|
||||
{"type": "discussion", "name": "Discussion"},
|
||||
{"type": "wiki", "name": "Wiki"},
|
||||
{"type": "progress", "name": "Progress"}]
|
||||
|
||||
# Update the data in the mongo datastore
|
||||
store.update_metadata(new_course.location.url(), new_course.own_metadata)
|
||||
store.update_metadata(new_course.location.url(), new_course.own_metadata)
|
||||
|
||||
return new_course
|
||||
|
||||
|
||||
class Course:
|
||||
pass
|
||||
|
||||
|
||||
class CourseFactory(XModuleCourseFactory):
|
||||
FACTORY_FOR = Course
|
||||
|
||||
@@ -64,6 +69,7 @@ class CourseFactory(XModuleCourseFactory):
|
||||
number = '999'
|
||||
display_name = 'Robot Super Course'
|
||||
|
||||
|
||||
class XModuleItemFactory(Factory):
|
||||
"""
|
||||
Factory for XModule items.
|
||||
@@ -80,7 +86,7 @@ class XModuleItemFactory(Factory):
|
||||
"""
|
||||
|
||||
DETACHED_CATEGORIES = ['about', 'static_tab', 'course_info']
|
||||
|
||||
|
||||
parent_location = Location(kwargs.get('parent_location'))
|
||||
template = Location(kwargs.get('template'))
|
||||
display_name = kwargs.get('display_name')
|
||||
@@ -107,9 +113,11 @@ class XModuleItemFactory(Factory):
|
||||
|
||||
return new_item
|
||||
|
||||
|
||||
class Item:
|
||||
pass
|
||||
|
||||
|
||||
class ItemFactory(XModuleItemFactory):
|
||||
FACTORY_FOR = Item
|
||||
|
||||
|
||||
@@ -61,6 +61,7 @@ invalid = ("foo", ["foo"], ["foo", "bar"],
|
||||
invalid_dict,
|
||||
invalid_dict2)
|
||||
|
||||
|
||||
def test_is_valid():
|
||||
for v in valid:
|
||||
assert_equals(Location.is_valid(v), True)
|
||||
@@ -68,6 +69,7 @@ def test_is_valid():
|
||||
for v in invalid:
|
||||
assert_equals(Location.is_valid(v), False)
|
||||
|
||||
|
||||
def test_dict():
|
||||
assert_equals("tag://org/course/category/name", Location(input_dict).url())
|
||||
assert_equals(dict(revision=None, **input_dict), Location(input_dict).dict())
|
||||
@@ -76,6 +78,7 @@ def test_dict():
|
||||
assert_equals("tag://org/course/category/name@revision", Location(input_dict).url())
|
||||
assert_equals(input_dict, Location(input_dict).dict())
|
||||
|
||||
|
||||
def test_list():
|
||||
assert_equals("tag://org/course/category/name", Location(input_list).url())
|
||||
assert_equals(input_list + [None], Location(input_list).list())
|
||||
@@ -115,17 +118,18 @@ def test_equality():
|
||||
)
|
||||
|
||||
# All the cleaning functions should do the same thing with these
|
||||
general_pairs = [ ('',''),
|
||||
general_pairs = [('', ''),
|
||||
(' ', '_'),
|
||||
('abc,', 'abc_'),
|
||||
('ab fg!@//\\aj', 'ab_fg_aj'),
|
||||
(u"ab\xA9", "ab_"), # no unicode allowed for now
|
||||
]
|
||||
|
||||
|
||||
def test_clean():
|
||||
pairs = general_pairs + [
|
||||
('a:b', 'a_b'), # no colons in non-name components
|
||||
('a-b', 'a-b'), # dashes ok
|
||||
('a-b', 'a-b'), # dashes ok
|
||||
('a.b', 'a.b'), # dot ok
|
||||
]
|
||||
for input, output in pairs:
|
||||
|
||||
@@ -3,6 +3,7 @@ from nose.tools import assert_equals, assert_raises, assert_not_equals, with_set
|
||||
from xmodule.modulestore.exceptions import ItemNotFoundError, NoPathToItem
|
||||
from xmodule.modulestore.search import path_to_location
|
||||
|
||||
|
||||
def check_path_to_location(modulestore):
|
||||
'''Make sure that path_to_location works: should be passed a modulestore
|
||||
with the toy and simple courses loaded.'''
|
||||
@@ -22,4 +23,3 @@ def check_path_to_location(modulestore):
|
||||
)
|
||||
for location in not_found:
|
||||
assert_raises(ItemNotFoundError, path_to_location, modulestore, course_id, location)
|
||||
|
||||
|
||||
@@ -102,4 +102,3 @@ class TestMongoModuleStore(object):
|
||||
def test_path_to_location(self):
|
||||
'''Make sure that path_to_location works'''
|
||||
check_path_to_location(self.store)
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ from xmodule.modulestore.xml_importer import import_from_xml
|
||||
from .test_modulestore import check_path_to_location
|
||||
from . import DATA_DIR
|
||||
|
||||
|
||||
class TestXMLModuleStore(object):
|
||||
def test_path_to_location(self):
|
||||
"""Make sure that path_to_location works properly"""
|
||||
@@ -12,5 +13,5 @@ class TestXMLModuleStore(object):
|
||||
print "Starting import"
|
||||
modulestore = XMLModuleStore(DATA_DIR, course_dirs=['toy', 'simple'])
|
||||
print "finished import"
|
||||
|
||||
|
||||
check_path_to_location(modulestore)
|
||||
|
||||
@@ -363,7 +363,7 @@ class XMLModuleStore(ModuleStoreBase):
|
||||
# been imported into the cms from xml
|
||||
course_file = StringIO(clean_out_mako_templating(course_file.read()))
|
||||
|
||||
course_data = etree.parse(course_file,parser=edx_xml_parser).getroot()
|
||||
course_data = etree.parse(course_file, parser=edx_xml_parser).getroot()
|
||||
|
||||
org = course_data.get('org')
|
||||
|
||||
@@ -437,7 +437,7 @@ class XMLModuleStore(ModuleStoreBase):
|
||||
self.load_extra_content(system, course_descriptor, 'course_info', self.data_dir / course_dir / 'info', course_dir, url_name)
|
||||
|
||||
# now import all static tabs which are expected to be stored in
|
||||
# in <content_dir>/tabs or <content_dir>/tabs/<url_name>
|
||||
# in <content_dir>/tabs or <content_dir>/tabs/<url_name>
|
||||
self.load_extra_content(system, course_descriptor, 'static_tab', self.data_dir / course_dir / 'tabs', course_dir, url_name)
|
||||
|
||||
self.load_extra_content(system, course_descriptor, 'custom_tag_template', self.data_dir / course_dir / 'custom_tags', course_dir, url_name)
|
||||
@@ -454,12 +454,12 @@ class XMLModuleStore(ModuleStoreBase):
|
||||
|
||||
# then look in a override folder based on the course run
|
||||
if os.path.isdir(base_dir / url_name):
|
||||
self._load_extra_content(system, course_descriptor, category, base_dir / url_name, course_dir)
|
||||
self._load_extra_content(system, course_descriptor, category, base_dir / url_name, course_dir)
|
||||
|
||||
|
||||
def _load_extra_content(self, system, course_descriptor, category, path, course_dir):
|
||||
|
||||
for filepath in glob.glob(path/ '*'):
|
||||
for filepath in glob.glob(path / '*'):
|
||||
if not os.path.isdir(filepath):
|
||||
with open(filepath) as f:
|
||||
try:
|
||||
@@ -467,7 +467,7 @@ class XMLModuleStore(ModuleStoreBase):
|
||||
# tabs are referenced in policy.json through a 'slug' which is just the filename without the .html suffix
|
||||
slug = os.path.splitext(os.path.basename(filepath))[0]
|
||||
loc = Location('i4x', course_descriptor.location.org, course_descriptor.location.course, category, slug)
|
||||
module = HtmlDescriptor(system, definition={'data' : html}, **{'location' : loc})
|
||||
module = HtmlDescriptor(system, definition={'data': html}, **{'location': loc})
|
||||
# VS[compat]:
|
||||
# Hack because we need to pull in the 'display_name' for static tabs (because we need to edit them)
|
||||
# from the course policy
|
||||
@@ -555,7 +555,7 @@ class XMLModuleStore(ModuleStoreBase):
|
||||
Return a dictionary of course_dir -> [(msg, exception_str)], for each
|
||||
course_dir where course loading failed.
|
||||
"""
|
||||
return dict( (k, self.errored_courses[k].errors) for k in self.errored_courses)
|
||||
return dict((k, self.errored_courses[k].errors) for k in self.errored_courses)
|
||||
|
||||
def update_item(self, location, data):
|
||||
"""
|
||||
@@ -590,7 +590,7 @@ class XMLModuleStore(ModuleStoreBase):
|
||||
raise NotImplementedError("XMLModuleStores are read-only")
|
||||
|
||||
def get_parent_locations(self, location, course_id):
|
||||
'''Find all locations that are the parents of this location in this
|
||||
'''Find all locations that are the parents of this location in this
|
||||
course. Needed for path_to_location().
|
||||
|
||||
returns an iterable of things that can be passed to Location. This may
|
||||
|
||||
@@ -3,6 +3,7 @@ from xmodule.modulestore import Location
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from fs.osfs import OSFS
|
||||
|
||||
|
||||
def export_to_xml(modulestore, contentstore, course_location, root_dir, course_dir):
|
||||
|
||||
course = modulestore.get_item(course_location)
|
||||
@@ -27,7 +28,7 @@ def export_to_xml(modulestore, contentstore, course_location, root_dir, course_d
|
||||
export_extra_content(export_fs, modulestore, course_location, 'course_info', 'info', '.html')
|
||||
|
||||
|
||||
def export_extra_content(export_fs, modulestore, course_location, category_type, dirname, file_suffix = ''):
|
||||
def export_extra_content(export_fs, modulestore, course_location, category_type, dirname, file_suffix=''):
|
||||
query_loc = Location('i4x', course_location.org, course_location.course, category_type, None)
|
||||
items = modulestore.get_items(query_loc)
|
||||
|
||||
@@ -36,7 +37,3 @@ def export_extra_content(export_fs, modulestore, course_location, category_type,
|
||||
for item in items:
|
||||
with item_dir.open(item.location.name + file_suffix, 'w') as item_file:
|
||||
item_file.write(item.definition['data'].encode('utf8'))
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -11,9 +11,10 @@ from xmodule.contentstore.content import StaticContent, XASSET_SRCREF_PREFIX
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
def import_static_content(modules, course_loc, course_data_path, static_content_store, target_location_namespace,
|
||||
subpath = 'static', verbose=False):
|
||||
|
||||
|
||||
def import_static_content(modules, course_loc, course_data_path, static_content_store, target_location_namespace,
|
||||
subpath='static', verbose=False):
|
||||
|
||||
remap_dict = {}
|
||||
|
||||
# now import all static assets
|
||||
@@ -36,7 +37,7 @@ def import_static_content(modules, course_loc, course_data_path, static_content_
|
||||
with open(content_path, 'rb') as f:
|
||||
data = f.read()
|
||||
|
||||
content = StaticContent(content_loc, filename, mime_type, data, import_path = fullname_with_subpath)
|
||||
content = StaticContent(content_loc, filename, mime_type, data, import_path=fullname_with_subpath)
|
||||
|
||||
# first let's save a thumbnail so we can get back a thumbnail location
|
||||
(thumbnail_content, thumbnail_location) = static_content_store.generate_thumbnail(content)
|
||||
@@ -50,11 +51,12 @@ def import_static_content(modules, course_loc, course_data_path, static_content_
|
||||
#store the remapping information which will be needed to subsitute in the module data
|
||||
remap_dict[fullname_with_subpath] = content_loc.name
|
||||
except:
|
||||
raise
|
||||
raise
|
||||
|
||||
return remap_dict
|
||||
|
||||
def verify_content_links(module, base_dir, static_content_store, link, remap_dict = None):
|
||||
|
||||
def verify_content_links(module, base_dir, static_content_store, link, remap_dict=None):
|
||||
if link.startswith('/static/'):
|
||||
# yes, then parse out the name
|
||||
path = link[len('/static/'):]
|
||||
@@ -70,7 +72,7 @@ def verify_content_links(module, base_dir, static_content_store, link, remap_dic
|
||||
with open(static_pathname, 'rb') as f:
|
||||
data = f.read()
|
||||
|
||||
content = StaticContent(content_loc, filename, mime_type, data, import_path = path)
|
||||
content = StaticContent(content_loc, filename, mime_type, data, import_path=path)
|
||||
|
||||
# first let's save a thumbnail so we can get back a thumbnail location
|
||||
(thumbnail_content, thumbnail_location) = static_content_store.generate_thumbnail(content)
|
||||
@@ -79,20 +81,21 @@ def verify_content_links(module, base_dir, static_content_store, link, remap_dic
|
||||
content.thumbnail_location = thumbnail_location
|
||||
|
||||
#then commit the content
|
||||
static_content_store.save(content)
|
||||
static_content_store.save(content)
|
||||
|
||||
new_link = StaticContent.get_url_path_from_location(content_loc)
|
||||
new_link = StaticContent.get_url_path_from_location(content_loc)
|
||||
|
||||
if remap_dict is not None:
|
||||
remap_dict[link] = new_link
|
||||
|
||||
return new_link
|
||||
return new_link
|
||||
except Exception, e:
|
||||
logging.exception('Skipping failed content load from {0}. Exception: {1}'.format(path, e))
|
||||
|
||||
return link
|
||||
|
||||
def import_from_xml(store, data_dir, course_dirs=None,
|
||||
|
||||
def import_from_xml(store, data_dir, course_dirs=None,
|
||||
default_class='xmodule.raw_module.RawDescriptor',
|
||||
load_error_modules=True, static_content_store=None, target_location_namespace=None, verbose=False):
|
||||
"""
|
||||
@@ -108,7 +111,7 @@ def import_from_xml(store, data_dir, course_dirs=None,
|
||||
the policy.json. so we need to keep the original url_name during import
|
||||
|
||||
"""
|
||||
|
||||
|
||||
module_store = XMLModuleStore(
|
||||
data_dir,
|
||||
default_class=default_class,
|
||||
@@ -137,12 +140,12 @@ def import_from_xml(store, data_dir, course_dirs=None,
|
||||
|
||||
module = remap_namespace(module, target_location_namespace)
|
||||
|
||||
# cdodge: more hacks (what else). Seems like we have a problem when importing a course (like 6.002) which
|
||||
# does not have any tabs defined in the policy file. The import goes fine and then displays fine in LMS,
|
||||
# but if someone tries to add a new tab in the CMS, then the LMS barfs because it expects that -
|
||||
# cdodge: more hacks (what else). Seems like we have a problem when importing a course (like 6.002) which
|
||||
# does not have any tabs defined in the policy file. The import goes fine and then displays fine in LMS,
|
||||
# but if someone tries to add a new tab in the CMS, then the LMS barfs because it expects that -
|
||||
# if there is *any* tabs - then there at least needs to be some predefined ones
|
||||
if module.tabs is None or len(module.tabs) == 0:
|
||||
module.tabs = [{"type": "courseware"},
|
||||
module.tabs = [{"type": "courseware"},
|
||||
{"type": "course_info", "name": "Course Info"},
|
||||
{"type": "discussion", "name": "Discussion"},
|
||||
{"type": "wiki", "name": "Wiki"}] # note, add 'progress' when we can support it on Edge
|
||||
@@ -159,13 +162,13 @@ def import_from_xml(store, data_dir, course_dirs=None,
|
||||
|
||||
course_items.append(module)
|
||||
|
||||
|
||||
|
||||
# then import all the static content
|
||||
if static_content_store is not None:
|
||||
_namespace_rename = target_location_namespace if target_location_namespace is not None else course_location
|
||||
|
||||
|
||||
# first pass to find everything in /static/
|
||||
import_static_content(module_store.modules[course_id], course_location, course_data_path, static_content_store,
|
||||
import_static_content(module_store.modules[course_id], course_location, course_data_path, static_content_store,
|
||||
_namespace_rename, subpath='static', verbose=verbose)
|
||||
|
||||
# finally loop through all the modules
|
||||
@@ -188,18 +191,18 @@ def import_from_xml(store, data_dir, course_dirs=None,
|
||||
|
||||
# cdodge: now go through any link references to '/static/' and make sure we've imported
|
||||
# it as a StaticContent asset
|
||||
try:
|
||||
try:
|
||||
remap_dict = {}
|
||||
|
||||
# use the rewrite_links as a utility means to enumerate through all links
|
||||
# in the module data. We use that to load that reference into our asset store
|
||||
# IMPORTANT: There appears to be a bug in lxml.rewrite_link which makes us not be able to
|
||||
# do the rewrites natively in that code.
|
||||
# For example, what I'm seeing is <img src='foo.jpg' /> -> <img src='bar.jpg'>
|
||||
# For example, what I'm seeing is <img src='foo.jpg' /> -> <img src='bar.jpg'>
|
||||
# Note the dropped element closing tag. This causes the LMS to fail when rendering modules - that's
|
||||
# no good, so we have to do this kludge
|
||||
if isinstance(module_data, str) or isinstance(module_data, unicode): # some module 'data' fields are non strings which blows up the link traversal code
|
||||
lxml_rewrite_links(module_data, lambda link: verify_content_links(module, course_data_path,
|
||||
lxml_rewrite_links(module_data, lambda link: verify_content_links(module, course_data_path,
|
||||
static_content_store, link, remap_dict))
|
||||
|
||||
for key in remap_dict.keys():
|
||||
@@ -219,17 +222,18 @@ def import_from_xml(store, data_dir, course_dirs=None,
|
||||
|
||||
return module_store, course_items
|
||||
|
||||
|
||||
def remap_namespace(module, target_location_namespace):
|
||||
if target_location_namespace is None:
|
||||
return module
|
||||
|
||||
|
||||
# This looks a bit wonky as we need to also change the 'name' of the imported course to be what
|
||||
# the caller passed in
|
||||
if module.location.category != 'course':
|
||||
module.location = module.location._replace(tag=target_location_namespace.tag, org=target_location_namespace.org,
|
||||
module.location = module.location._replace(tag=target_location_namespace.tag, org=target_location_namespace.org,
|
||||
course=target_location_namespace.course)
|
||||
else:
|
||||
module.location = module.location._replace(tag=target_location_namespace.tag, org=target_location_namespace.org,
|
||||
module.location = module.location._replace(tag=target_location_namespace.tag, org=target_location_namespace.org,
|
||||
course=target_location_namespace.course, name=target_location_namespace.name)
|
||||
|
||||
# then remap children pointers since they too will be re-namespaced
|
||||
@@ -238,15 +242,16 @@ def remap_namespace(module, target_location_namespace):
|
||||
new_locs = []
|
||||
for child in children_locs:
|
||||
child_loc = Location(child)
|
||||
new_child_loc = child_loc._replace(tag=target_location_namespace.tag, org=target_location_namespace.org,
|
||||
new_child_loc = child_loc._replace(tag=target_location_namespace.tag, org=target_location_namespace.org,
|
||||
course=target_location_namespace.course)
|
||||
|
||||
new_locs.append(new_child_loc.url())
|
||||
|
||||
module.definition['children'] = new_locs
|
||||
module.definition['children'] = new_locs
|
||||
|
||||
return module
|
||||
|
||||
|
||||
def validate_category_hierarchy(module_store, course_id, parent_category, expected_child_category):
|
||||
err_cnt = 0
|
||||
|
||||
@@ -265,7 +270,8 @@ def validate_category_hierarchy(module_store, course_id, parent_category, expect
|
||||
|
||||
return err_cnt
|
||||
|
||||
def validate_data_source_path_existence(path, is_err = True, extra_msg = None):
|
||||
|
||||
def validate_data_source_path_existence(path, is_err=True, extra_msg=None):
|
||||
_cnt = 0
|
||||
if not os.path.exists(path):
|
||||
print ("{0}: Expected folder at {1}. {2}".format('ERROR' if is_err == True else 'WARNING', path, extra_msg if
|
||||
@@ -273,18 +279,19 @@ def validate_data_source_path_existence(path, is_err = True, extra_msg = None):
|
||||
_cnt = 1
|
||||
return _cnt
|
||||
|
||||
|
||||
def validate_data_source_paths(data_dir, course_dir):
|
||||
# check that there is a '/static/' directory
|
||||
course_path = data_dir / course_dir
|
||||
err_cnt = 0
|
||||
warn_cnt = 0
|
||||
err_cnt += validate_data_source_path_existence(course_path / 'static')
|
||||
warn_cnt += validate_data_source_path_existence(course_path / 'static/subs', is_err = False,
|
||||
extra_msg = 'Video captions (if they are used) will not work unless they are static/subs.')
|
||||
warn_cnt += validate_data_source_path_existence(course_path / 'static/subs', is_err=False,
|
||||
extra_msg='Video captions (if they are used) will not work unless they are static/subs.')
|
||||
return err_cnt, warn_cnt
|
||||
|
||||
|
||||
def perform_xlint(data_dir, course_dirs,
|
||||
def perform_xlint(data_dir, course_dirs,
|
||||
default_class='xmodule.raw_module.RawDescriptor',
|
||||
load_error_modules=True):
|
||||
err_cnt = 0
|
||||
@@ -308,9 +315,9 @@ def perform_xlint(data_dir, course_dirs,
|
||||
for err_log_entry in err_log.errors:
|
||||
msg = err_log_entry[0]
|
||||
if msg.startswith('ERROR:'):
|
||||
err_cnt+=1
|
||||
err_cnt += 1
|
||||
else:
|
||||
warn_cnt+=1
|
||||
warn_cnt += 1
|
||||
|
||||
# then count outright all courses that failed to load at all
|
||||
for err_log in module_store.errored_courses.itervalues():
|
||||
@@ -318,9 +325,9 @@ def perform_xlint(data_dir, course_dirs,
|
||||
msg = err_log_entry[0]
|
||||
print msg
|
||||
if msg.startswith('ERROR:'):
|
||||
err_cnt+=1
|
||||
err_cnt += 1
|
||||
else:
|
||||
warn_cnt+=1
|
||||
warn_cnt += 1
|
||||
|
||||
for course_id in module_store.modules.keys():
|
||||
# constrain that courses only have 'chapter' children
|
||||
@@ -345,6 +352,3 @@ def perform_xlint(data_dir, course_dirs,
|
||||
print "This course can be imported, but some errors may occur during the run of the course. It is recommend that you fix your courseware before importing"
|
||||
else:
|
||||
print "This course can be imported successfully."
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -51,6 +51,7 @@ MAX_COLORS_TO_COUNT = 16
|
||||
#Maximum number of colors allowed in an uploaded image
|
||||
MAX_COLORS = 400
|
||||
|
||||
|
||||
class ImageProperties(object):
|
||||
"""
|
||||
Class to check properties of an image and to validate if they are allowed.
|
||||
@@ -187,6 +188,7 @@ class URLProperties(object):
|
||||
return success
|
||||
return success
|
||||
|
||||
|
||||
def run_url_tests(url_string):
|
||||
"""
|
||||
Creates a URLProperties object and runs all tests
|
||||
@@ -244,7 +246,7 @@ def upload_to_s3(file_to_upload, keyname):
|
||||
#k.set_metadata("Content-Type", 'images/png')
|
||||
|
||||
k.set_acl("public-read")
|
||||
public_url = k.generate_url(60 * 60 * 24 * 365) # URL timeout in seconds.
|
||||
public_url = k.generate_url(60 * 60 * 24 * 365) # URL timeout in seconds.
|
||||
|
||||
return True, public_url
|
||||
except:
|
||||
@@ -260,6 +262,3 @@ def get_from_s3(s3_public_url):
|
||||
r = requests.get(s3_public_url, timeout=2)
|
||||
data = r.text
|
||||
return data
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -38,6 +38,7 @@ from combined_open_ended_rubric import CombinedOpenEndedRubric
|
||||
|
||||
log = logging.getLogger("mitx.courseware")
|
||||
|
||||
|
||||
class OpenEndedModule(openendedchild.OpenEndedChild):
|
||||
"""
|
||||
The open ended module supports all external open ended grader problems.
|
||||
@@ -300,7 +301,7 @@ class OpenEndedModule(openendedchild.OpenEndedChild):
|
||||
|
||||
# We want to display available feedback in a particular order.
|
||||
# This dictionary specifies which goes first--lower first.
|
||||
priorities = {# These go at the start of the feedback
|
||||
priorities = { # These go at the start of the feedback
|
||||
'spelling': 0,
|
||||
'grammar': 1,
|
||||
# needs to be after all the other feedback
|
||||
@@ -674,5 +675,3 @@ class OpenEndedDescriptor(XmlDescriptor, EditingDescriptor):
|
||||
add_child(child)
|
||||
|
||||
return elt
|
||||
|
||||
|
||||
|
||||
@@ -38,6 +38,7 @@ MAX_ATTEMPTS = 1
|
||||
# Overriden by max_score specified in xml.
|
||||
MAX_SCORE = 1
|
||||
|
||||
|
||||
class OpenEndedChild(object):
|
||||
"""
|
||||
States:
|
||||
@@ -375,18 +376,13 @@ class OpenEndedChild(object):
|
||||
"""
|
||||
success = False
|
||||
links = re.findall(r'(https?://\S+)', string)
|
||||
if len(links)>0:
|
||||
if len(links) > 0:
|
||||
for link in links:
|
||||
success = open_ended_image_submission.run_url_tests(link)
|
||||
if not success:
|
||||
string = re.sub(link, '', string)
|
||||
else:
|
||||
string = re.sub(link, self.generate_image_tag_from_url(link,link), string)
|
||||
string = re.sub(link, self.generate_image_tag_from_url(link, link), string)
|
||||
success = True
|
||||
|
||||
return success, string
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -43,6 +43,7 @@ TRUE_DICT = [True, "True", "true", "TRUE"]
|
||||
MAX_SCORE = 1
|
||||
IS_GRADED = True
|
||||
|
||||
|
||||
class PeerGradingModule(XModule):
|
||||
_VERSION = 1
|
||||
|
||||
@@ -80,7 +81,7 @@ class PeerGradingModule(XModule):
|
||||
self.is_graded = (self.is_graded in TRUE_DICT)
|
||||
|
||||
self.link_to_location = self.metadata.get('link_to_location', USE_FOR_SINGLE_LOCATION)
|
||||
if self.use_for_single_location ==True:
|
||||
if self.use_for_single_location == True:
|
||||
#This will raise an exception if the location is invalid
|
||||
link_to_location_object = Location(self.link_to_location)
|
||||
|
||||
@@ -116,7 +117,7 @@ class PeerGradingModule(XModule):
|
||||
if not self.use_for_single_location:
|
||||
return self.peer_grading()
|
||||
else:
|
||||
return self.peer_grading_problem({'location' : self.link_to_location})['html']
|
||||
return self.peer_grading_problem({'location': self.link_to_location})['html']
|
||||
|
||||
def handle_ajax(self, dispatch, get):
|
||||
"""
|
||||
@@ -128,8 +129,8 @@ class PeerGradingModule(XModule):
|
||||
'show_calibration_essay': self.show_calibration_essay,
|
||||
'is_student_calibrated': self.is_student_calibrated,
|
||||
'save_grade': self.save_grade,
|
||||
'save_calibration_essay' : self.save_calibration_essay,
|
||||
'problem' : self.peer_grading_problem,
|
||||
'save_calibration_essay': self.save_calibration_essay,
|
||||
'problem': self.peer_grading_problem,
|
||||
}
|
||||
|
||||
if dispatch not in handlers:
|
||||
@@ -175,11 +176,11 @@ class PeerGradingModule(XModule):
|
||||
return None
|
||||
count_graded = response['count_graded']
|
||||
count_required = response['count_required']
|
||||
if count_required>0 and count_graded>=count_required:
|
||||
if count_required > 0 and count_graded >= count_required:
|
||||
self.student_data_for_location = response
|
||||
|
||||
score_dict = {
|
||||
'score': int(count_graded>=count_required),
|
||||
'score': int(count_graded >= count_required),
|
||||
'total': self.max_grade,
|
||||
}
|
||||
|
||||
@@ -399,7 +400,7 @@ class PeerGradingModule(XModule):
|
||||
log.exception("Error saving calibration grade, location: {0}, submission_id: {1}, submission_key: {2}, grader_id: {3}".format(location, submission_id, submission_key, grader_id))
|
||||
return self._err_response('Could not connect to grading service')
|
||||
|
||||
def peer_grading(self, get = None):
|
||||
def peer_grading(self, get=None):
|
||||
'''
|
||||
Show a peer grading interface
|
||||
'''
|
||||
@@ -434,19 +435,19 @@ class PeerGradingModule(XModule):
|
||||
'error_text': error_text,
|
||||
# Checked above
|
||||
'staff_access': False,
|
||||
'use_single_location' : self.use_for_single_location,
|
||||
'use_single_location': self.use_for_single_location,
|
||||
})
|
||||
|
||||
return html
|
||||
|
||||
def peer_grading_problem(self, get = None):
|
||||
def peer_grading_problem(self, get=None):
|
||||
'''
|
||||
Show individual problem interface
|
||||
'''
|
||||
if get == None or get.get('location')==None:
|
||||
if get == None or get.get('location') == None:
|
||||
if not self.use_for_single_location:
|
||||
#This is an error case, because it must be set to use a single location to be called without get parameters
|
||||
return {'html' : "", 'success' : False}
|
||||
return {'html': "", 'success': False}
|
||||
problem_location = self.link_to_location
|
||||
|
||||
elif get.get('location') is not None:
|
||||
@@ -460,10 +461,10 @@ class PeerGradingModule(XModule):
|
||||
'ajax_url': ajax_url,
|
||||
# Checked above
|
||||
'staff_access': False,
|
||||
'use_single_location' : self.use_for_single_location,
|
||||
'use_single_location': self.use_for_single_location,
|
||||
})
|
||||
|
||||
return {'html' : html, 'success' : True}
|
||||
return {'html': html, 'success': True}
|
||||
|
||||
def get_instance_state(self):
|
||||
"""
|
||||
@@ -473,11 +474,12 @@ class PeerGradingModule(XModule):
|
||||
"""
|
||||
|
||||
state = {
|
||||
'student_data_for_location' : self.student_data_for_location,
|
||||
'student_data_for_location': self.student_data_for_location,
|
||||
}
|
||||
|
||||
return json.dumps(state)
|
||||
|
||||
|
||||
class PeerGradingDescriptor(XmlDescriptor, EditingDescriptor):
|
||||
"""
|
||||
Module for adding combined open ended questions
|
||||
@@ -534,4 +536,4 @@ class PeerGradingDescriptor(XmlDescriptor, EditingDescriptor):
|
||||
for child in ['task']:
|
||||
add_child(child)
|
||||
|
||||
return elt
|
||||
return elt
|
||||
|
||||
@@ -14,11 +14,13 @@ from combined_open_ended_rubric import CombinedOpenEndedRubric, RubricParsingErr
|
||||
from lxml import etree
|
||||
from grading_service_module import GradingService, GradingServiceError
|
||||
|
||||
log=logging.getLogger(__name__)
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class GradingServiceError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class PeerGradingService(GradingService):
|
||||
"""
|
||||
Interface with the grading controller for peer grading
|
||||
@@ -47,23 +49,23 @@ class PeerGradingService(GradingService):
|
||||
return self.try_to_decode(self._render_rubric(response))
|
||||
|
||||
def save_grade(self, location, grader_id, submission_id, score, feedback, submission_key, rubric_scores, submission_flagged):
|
||||
data = {'grader_id' : grader_id,
|
||||
'submission_id' : submission_id,
|
||||
'score' : score,
|
||||
'feedback' : feedback,
|
||||
data = {'grader_id': grader_id,
|
||||
'submission_id': submission_id,
|
||||
'score': score,
|
||||
'feedback': feedback,
|
||||
'submission_key': submission_key,
|
||||
'location': location,
|
||||
'rubric_scores': rubric_scores,
|
||||
'rubric_scores_complete': True,
|
||||
'submission_flagged' : submission_flagged}
|
||||
'submission_flagged': submission_flagged}
|
||||
return self.try_to_decode(self.post(self.save_grade_url, data))
|
||||
|
||||
def is_student_calibrated(self, problem_location, grader_id):
|
||||
params = {'problem_id' : problem_location, 'student_id': grader_id}
|
||||
params = {'problem_id': problem_location, 'student_id': grader_id}
|
||||
return self.try_to_decode(self.get(self.is_student_calibrated_url, params))
|
||||
|
||||
def show_calibration_essay(self, problem_location, grader_id):
|
||||
params = {'problem_id' : problem_location, 'student_id': grader_id}
|
||||
params = {'problem_id': problem_location, 'student_id': grader_id}
|
||||
response = self.get(self.show_calibration_essay_url, params)
|
||||
return self.try_to_decode(self._render_rubric(response))
|
||||
|
||||
@@ -100,10 +102,12 @@ class PeerGradingService(GradingService):
|
||||
This is a mock peer grading service that can be used for unit tests
|
||||
without making actual service calls to the grading controller
|
||||
"""
|
||||
|
||||
|
||||
class MockPeerGradingService(object):
|
||||
def get_next_submission(self, problem_location, grader_id):
|
||||
return json.dumps({'success': True,
|
||||
'submission_id':1,
|
||||
'submission_id': 1,
|
||||
'submission_key': "",
|
||||
'student_response': 'fake student response',
|
||||
'prompt': 'fake submission prompt',
|
||||
@@ -119,7 +123,7 @@ class MockPeerGradingService(object):
|
||||
|
||||
def show_calibration_essay(self, problem_location, grader_id):
|
||||
return json.dumps({'success': True,
|
||||
'submission_id':1,
|
||||
'submission_id': 1,
|
||||
'submission_key': '',
|
||||
'student_response': 'fake student response',
|
||||
'prompt': 'fake submission prompt',
|
||||
@@ -140,6 +144,8 @@ class MockPeerGradingService(object):
|
||||
]})
|
||||
|
||||
_service = None
|
||||
|
||||
|
||||
def peer_grading_service(system):
|
||||
"""
|
||||
Return a peer grading service instance--if settings.MOCK_PEER_GRADING is True,
|
||||
|
||||
@@ -12,6 +12,7 @@ from pkg_resources import resource_string
|
||||
|
||||
log = logging.getLogger('mitx.' + __name__)
|
||||
|
||||
|
||||
class RandomizeModule(XModule):
|
||||
"""
|
||||
Chooses a random child module. Chooses the same one every time for each student.
|
||||
@@ -96,7 +97,7 @@ class RandomizeModule(XModule):
|
||||
def get_icon_class(self):
|
||||
return self.child.get_icon_class() if self.child else 'other'
|
||||
|
||||
|
||||
|
||||
class RandomizeDescriptor(SequenceDescriptor):
|
||||
# the editing interface can be the same as for sequences -- just a container
|
||||
module_class = RandomizeModule
|
||||
@@ -118,4 +119,3 @@ class RandomizeDescriptor(SequenceDescriptor):
|
||||
makes it use module.get_child_descriptors().
|
||||
"""
|
||||
return True
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import sys
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class RawDescriptor(XmlDescriptor, XMLEditingDescriptor):
|
||||
"""
|
||||
Module that provides a raw editing view of its data and children. It
|
||||
@@ -13,7 +14,7 @@ class RawDescriptor(XmlDescriptor, XMLEditingDescriptor):
|
||||
"""
|
||||
@classmethod
|
||||
def definition_from_xml(cls, xml_object, system):
|
||||
return {'data': etree.tostring(xml_object, pretty_print=True,encoding='unicode')}
|
||||
return {'data': etree.tostring(xml_object, pretty_print=True, encoding='unicode')}
|
||||
|
||||
def definition_to_xml(self, resource_fs):
|
||||
try:
|
||||
|
||||
@@ -25,6 +25,7 @@ from combined_open_ended_rubric import CombinedOpenEndedRubric
|
||||
|
||||
log = logging.getLogger("mitx.courseware")
|
||||
|
||||
|
||||
class SelfAssessmentModule(openendedchild.OpenEndedChild):
|
||||
"""
|
||||
A Self Assessment module that allows students to write open-ended responses,
|
||||
|
||||
@@ -88,8 +88,8 @@ class SequenceModule(XModule):
|
||||
'type': child.get_icon_class(),
|
||||
'id': child.id,
|
||||
}
|
||||
if childinfo['title']=='':
|
||||
childinfo['title'] = child.metadata.get('display_name','')
|
||||
if childinfo['title'] == '':
|
||||
childinfo['title'] = child.metadata.get('display_name', '')
|
||||
contents.append(childinfo)
|
||||
|
||||
params = {'items': contents,
|
||||
@@ -116,7 +116,7 @@ class SequenceDescriptor(MakoModuleDescriptor, XmlDescriptor):
|
||||
mako_template = 'widgets/sequence-edit.html'
|
||||
module_class = SequenceModule
|
||||
|
||||
stores_state = True # For remembering where in the sequence the student is
|
||||
stores_state = True # For remembering where in the sequence the student is
|
||||
|
||||
js = {'coffee': [resource_string(__name__, 'js/src/sequence/edit.coffee')]}
|
||||
js_module_name = "SequenceDescriptor"
|
||||
@@ -140,4 +140,3 @@ class SequenceDescriptor(MakoModuleDescriptor, XmlDescriptor):
|
||||
xml_object.append(
|
||||
etree.fromstring(child.export_to_xml(resource_fs)))
|
||||
return xml_object
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from itertools import chain
|
||||
from lxml import etree
|
||||
|
||||
|
||||
def stringify_children(node):
|
||||
'''
|
||||
Return all contents of an xml tree, without the outside tags.
|
||||
|
||||
@@ -5,6 +5,7 @@ from mako.template import Template
|
||||
from xmodule.modulestore.django import modulestore
|
||||
import logging
|
||||
|
||||
|
||||
class CustomTagModule(XModule):
|
||||
"""
|
||||
This module supports tags of the form
|
||||
@@ -81,4 +82,3 @@ class CustomTagDescriptor(RawDescriptor):
|
||||
to export them in a file with yet another layer of indirection.
|
||||
"""
|
||||
return False
|
||||
|
||||
|
||||
@@ -29,9 +29,9 @@ test_system = ModuleSystem(
|
||||
user=Mock(is_staff=False),
|
||||
filestore=Mock(),
|
||||
debug=True,
|
||||
xqueue={'interface':None, 'callback_url':'/', 'default_queuename': 'testqueue', 'waittime': 10},
|
||||
xqueue={'interface': None, 'callback_url': '/', 'default_queuename': 'testqueue', 'waittime': 10},
|
||||
node_path=os.environ.get("NODE_PATH", "/usr/local/lib/node_modules"),
|
||||
anonymous_student_id = 'student'
|
||||
anonymous_student_id='student'
|
||||
)
|
||||
|
||||
|
||||
@@ -85,4 +85,3 @@ class ModelsTest(unittest.TestCase):
|
||||
except:
|
||||
exception_happened = True
|
||||
self.assertTrue(exception_happened)
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ from lxml import etree
|
||||
|
||||
from . import test_system
|
||||
|
||||
|
||||
class CapaFactory(object):
|
||||
"""
|
||||
A helper class to create problem modules with various parameters for testing.
|
||||
@@ -58,7 +59,7 @@ class CapaFactory(object):
|
||||
|
||||
attempts: also added to instance state. Will be converted to an int.
|
||||
"""
|
||||
definition = {'data': CapaFactory.sample_problem_xml,}
|
||||
definition = {'data': CapaFactory.sample_problem_xml, }
|
||||
location = Location(["i4x", "edX", "capa_test", "problem",
|
||||
"SampleProblem{0}".format(CapaFactory.next_num())])
|
||||
metadata = {}
|
||||
@@ -208,8 +209,3 @@ class CapaModuleTest(unittest.TestCase):
|
||||
due=self.yesterday_str,
|
||||
graceperiod=self.two_day_delta_str)
|
||||
self.assertFalse(still_in_grace.answer_available())
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ OpenEndedModule
|
||||
|
||||
"""
|
||||
|
||||
|
||||
class OpenEndedChildTest(unittest.TestCase):
|
||||
location = Location(["i4x", "edX", "sa_test", "selfassessment",
|
||||
"SampleQuestion"])
|
||||
@@ -39,17 +40,17 @@ class OpenEndedChildTest(unittest.TestCase):
|
||||
'max_attempts': 20,
|
||||
'prompt': prompt,
|
||||
'rubric': rubric,
|
||||
'max_score': max_score,
|
||||
'max_score': max_score,
|
||||
'display_name': 'Name',
|
||||
'accept_file_upload' : False,
|
||||
'accept_file_upload': False,
|
||||
}
|
||||
definition = Mock()
|
||||
descriptor = Mock()
|
||||
|
||||
def setUp(self):
|
||||
self.openendedchild = OpenEndedChild(test_system, self.location,
|
||||
self.openendedchild = OpenEndedChild(test_system, self.location,
|
||||
self.definition, self.descriptor, self.static_data, self.metadata)
|
||||
|
||||
|
||||
|
||||
def test_latest_answer_empty(self):
|
||||
answer = self.openendedchild.latest_answer()
|
||||
@@ -116,12 +117,12 @@ class OpenEndedChildTest(unittest.TestCase):
|
||||
self.assertEqual(score['score'], new_score)
|
||||
self.assertEqual(score['total'], self.static_data['max_score'])
|
||||
|
||||
|
||||
|
||||
def test_reset(self):
|
||||
self.openendedchild.reset(test_system)
|
||||
state = json.loads(self.openendedchild.get_instance_state())
|
||||
self.assertEqual(state['state'], OpenEndedChild.INITIAL)
|
||||
|
||||
|
||||
|
||||
def test_is_last_response_correct(self):
|
||||
new_answer = "New Answer"
|
||||
@@ -135,6 +136,7 @@ class OpenEndedChildTest(unittest.TestCase):
|
||||
self.assertEqual(self.openendedchild.is_last_response_correct(),
|
||||
'incorrect')
|
||||
|
||||
|
||||
class OpenEndedModuleTest(unittest.TestCase):
|
||||
location = Location(["i4x", "edX", "sa_test", "selfassessment",
|
||||
"SampleQuestion"])
|
||||
@@ -153,7 +155,7 @@ class OpenEndedModuleTest(unittest.TestCase):
|
||||
'max_attempts': 20,
|
||||
'prompt': prompt,
|
||||
'rubric': rubric,
|
||||
'max_score': max_score,
|
||||
'max_score': max_score,
|
||||
'display_name': 'Name',
|
||||
'accept_file_upload': False,
|
||||
}
|
||||
@@ -171,9 +173,9 @@ class OpenEndedModuleTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
test_system.location = self.location
|
||||
self.mock_xqueue = MagicMock()
|
||||
self.mock_xqueue.send_to_queue.return_value=(None, "Message")
|
||||
test_system.xqueue = {'interface':self.mock_xqueue, 'callback_url':'/', 'default_queuename': 'testqueue', 'waittime': 1}
|
||||
self.openendedmodule = OpenEndedModule(test_system, self.location,
|
||||
self.mock_xqueue.send_to_queue.return_value = (None, "Message")
|
||||
test_system.xqueue = {'interface': self.mock_xqueue, 'callback_url': '/', 'default_queuename': 'testqueue', 'waittime': 1}
|
||||
self.openendedmodule = OpenEndedModule(test_system, self.location,
|
||||
self.definition, self.descriptor, self.static_data, self.metadata)
|
||||
|
||||
def test_message_post(self):
|
||||
@@ -195,8 +197,8 @@ class OpenEndedModuleTest(unittest.TestCase):
|
||||
result = self.openendedmodule.message_post(get, test_system)
|
||||
self.assertTrue(result['success'])
|
||||
# make sure it's actually sending something we want to the queue
|
||||
self.mock_xqueue.send_to_queue.assert_called_with(body = json.dumps(contents), header=ANY)
|
||||
|
||||
self.mock_xqueue.send_to_queue.assert_called_with(body=json.dumps(contents), header=ANY)
|
||||
|
||||
state = json.loads(self.openendedmodule.get_instance_state())
|
||||
self.assertIsNotNone(state['state'], OpenEndedModule.DONE)
|
||||
|
||||
@@ -206,21 +208,21 @@ class OpenEndedModuleTest(unittest.TestCase):
|
||||
student_info = {'anonymous_student_id': test_system.anonymous_student_id,
|
||||
'submission_time': qtime}
|
||||
contents = self.openendedmodule.payload.copy()
|
||||
contents.update({
|
||||
contents.update({
|
||||
'student_info': json.dumps(student_info),
|
||||
'student_response': submission,
|
||||
'student_response': submission,
|
||||
'max_score': self.max_score
|
||||
})
|
||||
result = self.openendedmodule.send_to_grader(submission, test_system)
|
||||
self.assertTrue(result)
|
||||
self.mock_xqueue.send_to_queue.assert_called_with(body = json.dumps(contents), header=ANY)
|
||||
self.mock_xqueue.send_to_queue.assert_called_with(body=json.dumps(contents), header=ANY)
|
||||
|
||||
def update_score_single(self):
|
||||
self.openendedmodule.new_history_entry("New Entry")
|
||||
score_msg = {
|
||||
score_msg = {
|
||||
'correct': True,
|
||||
'score': 4,
|
||||
'msg' : 'Grader Message',
|
||||
'msg': 'Grader Message',
|
||||
'feedback': "Grader Feedback"
|
||||
}
|
||||
get = {'queuekey': "abcd",
|
||||
@@ -233,10 +235,10 @@ class OpenEndedModuleTest(unittest.TestCase):
|
||||
"success": True,
|
||||
"feedback": "Grader Feedback"
|
||||
}
|
||||
score_msg = {
|
||||
score_msg = {
|
||||
'correct': True,
|
||||
'score': 4,
|
||||
'msg' : 'Grader Message',
|
||||
'msg': 'Grader Message',
|
||||
'feedback': json.dumps(feedback),
|
||||
'grader_type': 'IN',
|
||||
'grader_id': '1',
|
||||
@@ -262,6 +264,7 @@ class OpenEndedModuleTest(unittest.TestCase):
|
||||
score = self.openendedmodule.latest_score()
|
||||
self.assertEqual(score, 4)
|
||||
|
||||
|
||||
class CombinedOpenEndedModuleTest(unittest.TestCase):
|
||||
location = Location(["i4x", "edX", "open_ended", "combinedopenended",
|
||||
"SampleQuestion"])
|
||||
@@ -282,7 +285,7 @@ class CombinedOpenEndedModuleTest(unittest.TestCase):
|
||||
'max_attempts': 20,
|
||||
'prompt': prompt,
|
||||
'rubric': rubric,
|
||||
'max_score': max_score,
|
||||
'max_score': max_score,
|
||||
'display_name': 'Name'
|
||||
})
|
||||
|
||||
@@ -337,5 +340,3 @@ class CombinedOpenEndedModuleTest(unittest.TestCase):
|
||||
changed = self.combinedoe.update_task_states()
|
||||
|
||||
self.assertTrue(changed)
|
||||
|
||||
|
||||
|
||||
@@ -17,10 +17,11 @@ from xmodule.modulestore.exceptions import ItemNotFoundError
|
||||
from .test_export import DATA_DIR
|
||||
|
||||
ORG = 'test_org'
|
||||
COURSE = 'conditional' # name of directory with course data
|
||||
COURSE = 'conditional' # name of directory with course data
|
||||
|
||||
from . import test_system
|
||||
|
||||
|
||||
class DummySystem(ImportSystem):
|
||||
|
||||
@patch('xmodule.modulestore.xml.OSFS', lambda dir: MemoryFS())
|
||||
@@ -82,11 +83,11 @@ class ConditionalModuleTest(unittest.TestCase):
|
||||
location = descriptor
|
||||
descriptor = self.modulestore.get_instance(course.id, location, depth=None)
|
||||
location = descriptor.location
|
||||
instance_state = instance_states.get(location.category,None)
|
||||
instance_state = instance_states.get(location.category, None)
|
||||
print "inner_get_module, location.category=%s, inst_state=%s" % (location.category, instance_state)
|
||||
return descriptor.xmodule_constructor(test_system)(instance_state, shared_state)
|
||||
|
||||
location = Location(["i4x", "edX", "cond_test", "conditional","condone"])
|
||||
location = Location(["i4x", "edX", "cond_test", "conditional", "condone"])
|
||||
module = inner_get_module(location)
|
||||
|
||||
def replace_urls(text, staticfiles_prefix=None, replace_prefix='/static/', course_namespace=None):
|
||||
@@ -105,15 +106,13 @@ class ConditionalModuleTest(unittest.TestCase):
|
||||
gdi = module.get_display_items()
|
||||
print "gdi=", gdi
|
||||
|
||||
ajax = json.loads(module.handle_ajax('',''))
|
||||
ajax = json.loads(module.handle_ajax('', ''))
|
||||
self.assertTrue('xmodule.conditional_module' in ajax['html'])
|
||||
print "ajax: ", ajax
|
||||
|
||||
# now change state of the capa problem to make it completed
|
||||
instance_states['problem'] = json.dumps({'attempts':1})
|
||||
instance_states['problem'] = json.dumps({'attempts': 1})
|
||||
|
||||
ajax = json.loads(module.handle_ajax('',''))
|
||||
ajax = json.loads(module.handle_ajax('', ''))
|
||||
self.assertTrue('This is a secret' in ajax['html'])
|
||||
print "post-attempt ajax: ", ajax
|
||||
|
||||
|
||||
|
||||
@@ -3,11 +3,13 @@ from xmodule.contentstore.content import StaticContent
|
||||
from xmodule.contentstore.content import ContentStore
|
||||
from xmodule.modulestore import Location
|
||||
|
||||
|
||||
class Content:
|
||||
def __init__(self, location, content_type):
|
||||
self.location = location
|
||||
self.content_type = content_type
|
||||
|
||||
|
||||
class ContentTest(unittest.TestCase):
|
||||
def test_thumbnail_none(self):
|
||||
# We had a bug where a thumbnail location of None was getting transformed into a Location tuple, with
|
||||
@@ -22,4 +24,4 @@ class ContentTest(unittest.TestCase):
|
||||
content = Content(Location(u'c4x', u'mitX', u'800', u'asset', u'monsters.jpg'), None)
|
||||
(thumbnail_content, thumbnail_file_location) = contentStore.generate_thumbnail(content)
|
||||
self.assertIsNone(thumbnail_content)
|
||||
self.assertEqual(Location(u'c4x', u'mitX', u'800', u'thumbnail', u'monsters.jpg'), thumbnail_file_location)
|
||||
self.assertEqual(Location(u'c4x', u'mitX', u'800', u'thumbnail', u'monsters.jpg'), thumbnail_file_location)
|
||||
|
||||
@@ -27,6 +27,7 @@ def strip_metadata(descriptor, key):
|
||||
for d in descriptor.get_children():
|
||||
strip_metadata(d, key)
|
||||
|
||||
|
||||
def strip_filenames(descriptor):
|
||||
"""
|
||||
Recursively strips 'filename' from all children's definitions.
|
||||
@@ -119,12 +120,12 @@ class RoundTripTestCase(unittest.TestCase):
|
||||
|
||||
def test_selfassessment_roundtrip(self):
|
||||
#Test selfassessment xmodule to see if it exports correctly
|
||||
self.check_export_roundtrip(DATA_DIR,"self_assessment")
|
||||
self.check_export_roundtrip(DATA_DIR, "self_assessment")
|
||||
|
||||
def test_graphicslidertool_roundtrip(self):
|
||||
#Test graphicslidertool xmodule to see if it exports correctly
|
||||
self.check_export_roundtrip(DATA_DIR,"graphic_slider_tool")
|
||||
self.check_export_roundtrip(DATA_DIR, "graphic_slider_tool")
|
||||
|
||||
def test_exam_registration_roundtrip(self):
|
||||
# Test exam_registration xmodule to see if it exports correctly
|
||||
self.check_export_roundtrip(DATA_DIR,"test_exam_registration")
|
||||
self.check_export_roundtrip(DATA_DIR, "test_exam_registration")
|
||||
|
||||
@@ -4,6 +4,7 @@ import unittest
|
||||
from xmodule import graders
|
||||
from xmodule.graders import Score, aggregate_scores
|
||||
|
||||
|
||||
class GradesheetTest(unittest.TestCase):
|
||||
|
||||
def test_weighted_grading(self):
|
||||
@@ -217,4 +218,3 @@ class GraderTest(unittest.TestCase):
|
||||
|
||||
#TODO: How do we test failure cases? The parser only logs an error when
|
||||
#it can't parse something. Maybe it should throw exceptions?
|
||||
|
||||
|
||||
@@ -61,6 +61,7 @@ class BaseCourseTestCase(unittest.TestCase):
|
||||
self.assertEquals(len(courses), 1)
|
||||
return courses[0]
|
||||
|
||||
|
||||
class ImportTestCase(BaseCourseTestCase):
|
||||
|
||||
def test_fallback(self):
|
||||
@@ -323,7 +324,7 @@ class ImportTestCase(BaseCourseTestCase):
|
||||
|
||||
self.assertEqual(len(sections), 4)
|
||||
|
||||
for i in (2,3):
|
||||
for i in (2, 3):
|
||||
video = sections[i]
|
||||
# Name should be 'video_{hash}'
|
||||
print "video {0} url_name: {1}".format(i, video.url_name)
|
||||
@@ -379,5 +380,3 @@ class ImportTestCase(BaseCourseTestCase):
|
||||
# and finally...
|
||||
course.metadata['cohort_config'] = {'cohorted': True}
|
||||
self.assertTrue(course.is_cohorted)
|
||||
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ from xmodule import x_module
|
||||
|
||||
from . import test_system
|
||||
|
||||
|
||||
class ProgressTest(unittest.TestCase):
|
||||
''' Test that basic Progress objects work. A Progress represents a
|
||||
fraction between 0 and 1.
|
||||
|
||||
@@ -52,4 +52,3 @@ class RandomizeModuleTestCase(unittest.TestCase):
|
||||
|
||||
# TODO: add tests that create a module and check. Passing state is a good way to
|
||||
# check that child access works...
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ from lxml import etree
|
||||
|
||||
from . import test_system
|
||||
|
||||
|
||||
class SelfAssessmentTest(unittest.TestCase):
|
||||
|
||||
rubric = '''<rubric><rubric>
|
||||
@@ -44,7 +45,7 @@ class SelfAssessmentTest(unittest.TestCase):
|
||||
'prompt': self.prompt,
|
||||
'max_score': 1,
|
||||
'display_name': "Name",
|
||||
'accept_file_upload' : False,
|
||||
'accept_file_upload': False,
|
||||
}
|
||||
|
||||
self.module = SelfAssessmentModule(test_system, self.location,
|
||||
@@ -74,4 +75,3 @@ class SelfAssessmentTest(unittest.TestCase):
|
||||
self.module.save_answer({'student_answer': 'answer 4'}, test_system)
|
||||
self.module.save_assessment({'assessment': '1'}, test_system)
|
||||
self.assertEqual(self.module.state, self.module.DONE)
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ from nose.tools import assert_equals, assert_true, assert_false
|
||||
from lxml import etree
|
||||
from xmodule.stringify import stringify_children
|
||||
|
||||
|
||||
def test_stringify():
|
||||
text = 'Hi <div x="foo">there <span>Bruce</span><b>!</b></div>'
|
||||
html = '''<html a="b" foo="bar">{0}</html>'''.format(text)
|
||||
@@ -9,6 +10,7 @@ def test_stringify():
|
||||
out = stringify_children(xml)
|
||||
assert_equals(out, text)
|
||||
|
||||
|
||||
def test_stringify_again():
|
||||
html = """<html name="Voltage Source Answer" >A voltage source is non-linear!
|
||||
<div align="center">
|
||||
|
||||
@@ -5,6 +5,7 @@ import time
|
||||
|
||||
TIME_FORMAT = "%Y-%m-%dT%H:%M"
|
||||
|
||||
|
||||
def parse_time(time_str):
|
||||
"""
|
||||
Takes a time string in TIME_FORMAT
|
||||
@@ -15,6 +16,7 @@ def parse_time(time_str):
|
||||
"""
|
||||
return time.strptime(time_str, TIME_FORMAT)
|
||||
|
||||
|
||||
def stringify_time(time_struct):
|
||||
"""
|
||||
Convert a time struct to a string
|
||||
|
||||
@@ -4,8 +4,8 @@ def lazyproperty(fn):
|
||||
"""
|
||||
Use this decorator for lazy generation of properties that
|
||||
are expensive to compute. From http://stackoverflow.com/a/3013910/86828
|
||||
|
||||
|
||||
|
||||
|
||||
Example:
|
||||
class Test(object):
|
||||
|
||||
@@ -13,7 +13,7 @@ def lazyproperty(fn):
|
||||
def a(self):
|
||||
print 'generating "a"'
|
||||
return range(5)
|
||||
|
||||
|
||||
Interactive Session:
|
||||
>>> t = Test()
|
||||
>>> t.__dict__
|
||||
@@ -26,11 +26,11 @@ def lazyproperty(fn):
|
||||
>>> t.a
|
||||
[0, 1, 2, 3, 4]
|
||||
"""
|
||||
|
||||
|
||||
attr_name = '_lazy_' + fn.__name__
|
||||
@property
|
||||
def _lazyprop(self):
|
||||
if not hasattr(self, attr_name):
|
||||
setattr(self, attr_name, fn(self))
|
||||
return getattr(self, attr_name)
|
||||
return _lazyprop
|
||||
return _lazyprop
|
||||
|
||||
@@ -47,6 +47,6 @@ class VerticalDescriptor(SequenceDescriptor):
|
||||
|
||||
js = {'coffee': [resource_string(__name__, 'js/src/vertical/edit.coffee')]}
|
||||
js_module_name = "VerticalDescriptor"
|
||||
|
||||
|
||||
# TODO (victor): Does this need its own definition_to_xml method? Otherwise it looks
|
||||
# like verticals will get exported as sequentials...
|
||||
|
||||
@@ -121,7 +121,7 @@ class VideoModule(XModule):
|
||||
return self.youtube
|
||||
|
||||
def get_html(self):
|
||||
if isinstance(modulestore(), XMLModuleStore) :
|
||||
if isinstance(modulestore(), XMLModuleStore):
|
||||
# VS[compat]
|
||||
# cdodge: filesystem static content support.
|
||||
caption_asset_path = "/static/{0}/subs/".format(self.metadata['data_dir'])
|
||||
|
||||
@@ -109,16 +109,16 @@ class HTMLSnippet(object):
|
||||
All of these will be loaded onto the page in the CMS
|
||||
"""
|
||||
# cdodge: We've moved the xmodule.coffee script from an outside directory into the xmodule area of common
|
||||
# this means we need to make sure that all xmodules include this dependency which had been previously implicitly
|
||||
# this means we need to make sure that all xmodules include this dependency which had been previously implicitly
|
||||
# fulfilled in a different area of code
|
||||
js = cls.js
|
||||
|
||||
|
||||
if js is None:
|
||||
js = {}
|
||||
|
||||
if 'coffee' not in js:
|
||||
js['coffee'] = []
|
||||
|
||||
|
||||
js['coffee'].append(resource_string(__name__, 'js/src/xmodule.coffee'))
|
||||
|
||||
return js
|
||||
@@ -538,7 +538,7 @@ class XModuleDescriptor(Plugin, HTMLSnippet, ResourceTemplates):
|
||||
def start(self, value):
|
||||
if isinstance(value, time.struct_time):
|
||||
self.metadata['start'] = stringify_time(value)
|
||||
|
||||
|
||||
@property
|
||||
def days_early_for_beta(self):
|
||||
"""
|
||||
|
||||
@@ -16,6 +16,7 @@ edx_xml_parser = etree.XMLParser(dtd_validation=False, load_dtd=False,
|
||||
remove_comments=True, remove_blank_text=True,
|
||||
encoding='utf-8')
|
||||
|
||||
|
||||
def name_to_pathname(name):
|
||||
"""
|
||||
Convert a location name for use in a path: replace ':' with '/'.
|
||||
@@ -23,6 +24,7 @@ def name_to_pathname(name):
|
||||
"""
|
||||
return name.replace(':', '/')
|
||||
|
||||
|
||||
def is_pointer_tag(xml_obj):
|
||||
"""
|
||||
Check if xml_obj is a pointer tag: <blah url_name="something" />.
|
||||
@@ -46,6 +48,7 @@ def is_pointer_tag(xml_obj):
|
||||
|
||||
return len(xml_obj) == 0 and actual_attr == expected_attr and not has_text
|
||||
|
||||
|
||||
def get_metadata_from_xml(xml_object, remove=True):
|
||||
meta = xml_object.find('meta')
|
||||
if meta is None:
|
||||
@@ -58,6 +61,7 @@ def get_metadata_from_xml(xml_object, remove=True):
|
||||
|
||||
_AttrMapBase = namedtuple('_AttrMap', 'from_xml to_xml')
|
||||
|
||||
|
||||
class AttrMap(_AttrMapBase):
|
||||
"""
|
||||
A class that specifies two functions:
|
||||
@@ -93,16 +97,16 @@ class XmlDescriptor(XModuleDescriptor):
|
||||
metadata_attributes = ('format', 'graceperiod', 'showanswer', 'rerandomize',
|
||||
'start', 'due', 'graded', 'display_name', 'url_name', 'hide_from_toc',
|
||||
'ispublic', # if True, then course is listed for all users; see
|
||||
'xqa_key', # for xqaa server access
|
||||
# information about testcenter exams is a dict (of dicts), not a string,
|
||||
# so it cannot be easily exportable as a course element's attribute.
|
||||
'xqa_key', # for xqaa server access
|
||||
# information about testcenter exams is a dict (of dicts), not a string,
|
||||
# so it cannot be easily exportable as a course element's attribute.
|
||||
'testcenter_info',
|
||||
# VS[compat] Remove once unused.
|
||||
'name', 'slug')
|
||||
|
||||
metadata_to_strip = ('data_dir',
|
||||
metadata_to_strip = ('data_dir',
|
||||
# cdodge: @TODO: We need to figure out a way to export out 'tabs' and 'grading_policy' which is on the course
|
||||
'tabs', 'grading_policy', 'is_draft', 'published_by', 'published_date',
|
||||
'tabs', 'grading_policy', 'is_draft', 'published_by', 'published_date',
|
||||
'discussion_blackouts', 'testcenter_info',
|
||||
# VS[compat] -- remove the below attrs once everything is in the CMS
|
||||
'course', 'org', 'url_name', 'filename')
|
||||
@@ -117,7 +121,7 @@ class XmlDescriptor(XModuleDescriptor):
|
||||
bool_map = AttrMap(to_bool, from_bool)
|
||||
|
||||
to_int = lambda val: int(val)
|
||||
from_int = lambda val: str(val)
|
||||
from_int = lambda val: str(val)
|
||||
int_map = AttrMap(to_int, from_int)
|
||||
xml_attribute_map = {
|
||||
# type conversion: want True/False in python, "true"/"false" in xml
|
||||
@@ -125,7 +129,7 @@ class XmlDescriptor(XModuleDescriptor):
|
||||
'hide_progress_tab': bool_map,
|
||||
'allow_anonymous': bool_map,
|
||||
'allow_anonymous_to_peers': bool_map,
|
||||
'weight':int_map
|
||||
'weight': int_map
|
||||
}
|
||||
|
||||
|
||||
@@ -133,8 +137,8 @@ class XmlDescriptor(XModuleDescriptor):
|
||||
# importing 2012 courses.
|
||||
# A set of metadata key conversions that we want to make
|
||||
metadata_translations = {
|
||||
'slug' : 'url_name',
|
||||
'name' : 'display_name',
|
||||
'slug': 'url_name',
|
||||
'name': 'display_name',
|
||||
}
|
||||
|
||||
@classmethod
|
||||
@@ -230,7 +234,7 @@ class XmlDescriptor(XModuleDescriptor):
|
||||
|
||||
# TODO (ichuang): remove this after migration
|
||||
# for Fall 2012 LMS migration: keep filename (and unmangled filename)
|
||||
definition['filename'] = [ filepath, filename ]
|
||||
definition['filename'] = [filepath, filename]
|
||||
|
||||
return definition
|
||||
|
||||
@@ -291,9 +295,9 @@ class XmlDescriptor(XModuleDescriptor):
|
||||
filepath = cls._format_filepath(xml_object.tag, name_to_pathname(url_name))
|
||||
definition_xml = cls.load_file(filepath, system.resources_fs, location)
|
||||
else:
|
||||
definition_xml = xml_object # this is just a pointer, not the real definition content
|
||||
definition_xml = xml_object # this is just a pointer, not the real definition content
|
||||
|
||||
definition = cls.load_definition(definition_xml, system, location) # note this removes metadata
|
||||
definition = cls.load_definition(definition_xml, system, location) # note this removes metadata
|
||||
# VS[compat] -- make Ike's github preview links work in both old and
|
||||
# new file layouts
|
||||
if is_pointer_tag(xml_object):
|
||||
@@ -303,13 +307,13 @@ class XmlDescriptor(XModuleDescriptor):
|
||||
metadata = cls.load_metadata(definition_xml)
|
||||
|
||||
# move definition metadata into dict
|
||||
dmdata = definition.get('definition_metadata','')
|
||||
dmdata = definition.get('definition_metadata', '')
|
||||
if dmdata:
|
||||
metadata['definition_metadata_raw'] = dmdata
|
||||
try:
|
||||
metadata.update(json.loads(dmdata))
|
||||
except Exception as err:
|
||||
log.debug('Error %s in loading metadata %s' % (err,dmdata))
|
||||
log.debug('Error %s in loading metadata %s' % (err, dmdata))
|
||||
metadata['definition_metadata_err'] = str(err)
|
||||
|
||||
# Set/override any metadata specified by policy
|
||||
|
||||
Reference in New Issue
Block a user