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:
Diana Huang
2013-02-06 13:16:24 -05:00
273 changed files with 2398 additions and 1824 deletions

View File

@@ -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'''

View File

@@ -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('<','&lt;')
msg = "Error while executing script code: %s" % str(err).replace('<', '&lt;')
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)

View File

@@ -1 +0,0 @@

View File

@@ -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

View File

@@ -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())

View File

@@ -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)

View File

@@ -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

View File

@@ -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:

View File

@@ -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'
)

View File

@@ -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.

View File

@@ -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()

View File

@@ -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')

View File

@@ -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

View File

@@ -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)

View File

@@ -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>"

View File

@@ -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",

View File

@@ -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

View File

@@ -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

View File

@@ -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):

View File

@@ -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})

View File

@@ -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]

View File

@@ -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

View File

@@ -6,6 +6,7 @@ from django.conf import settings
_CONTENTSTORE = None
def load_function(path):
"""
Load a function by name.

View File

@@ -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)

View File

@@ -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

View File

@@ -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"

View File

@@ -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

View File

@@ -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

View File

@@ -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()

View File

@@ -1,5 +1,6 @@
from lxml import etree
def check_html(html):
'''
Check whether the passed in html string can be parsed by lxml.

View File

@@ -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

View File

@@ -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

View File

@@ -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.

View File

@@ -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:

View File

@@ -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)

View File

@@ -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)

View File

@@ -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

View File

@@ -8,5 +8,3 @@ for i in range(5):
TEST_DIR = TEST_DIR / 'test'
DATA_DIR = TEST_DIR / 'data'

View File

@@ -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

View File

@@ -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:

View File

@@ -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)

View File

@@ -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)

View File

@@ -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)

View File

@@ -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

View File

@@ -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'))

View File

@@ -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."

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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,

View File

@@ -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

View File

@@ -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:

View File

@@ -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,

View File

@@ -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

View File

@@ -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.

View File

@@ -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

View File

@@ -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)

View File

@@ -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())

View File

@@ -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)

View File

@@ -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

View File

@@ -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)

View File

@@ -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")

View File

@@ -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?

View File

@@ -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)

View File

@@ -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.

View File

@@ -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...

View File

@@ -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)

View File

@@ -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">

View File

@@ -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

View File

@@ -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

View File

@@ -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...

View File

@@ -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'])

View File

@@ -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):
"""

View File

@@ -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