Merge branch 'master' of github.com:MITx/mitx into fix/cdodge/studio-forum-improvements
This commit is contained in:
@@ -655,9 +655,9 @@ class MatlabInput(CodeInput):
|
||||
# Check if problem has been queued
|
||||
self.queuename = 'matlab'
|
||||
self.queue_msg = ''
|
||||
if 'queue_msg' in self.input_state and self.status in ['queued','incomplete', 'unsubmitted']:
|
||||
if 'queue_msg' in self.input_state and self.status in ['queued', 'incomplete', 'unsubmitted']:
|
||||
self.queue_msg = self.input_state['queue_msg']
|
||||
if 'queued' in self.input_state and self.input_state['queuestate'] is not None:
|
||||
if 'queuestate' in self.input_state and self.input_state['queuestate'] == 'queued':
|
||||
self.status = 'queued'
|
||||
self.queue_len = 1
|
||||
self.msg = self.plot_submitted_msg
|
||||
@@ -702,7 +702,7 @@ class MatlabInput(CodeInput):
|
||||
def _extra_context(self):
|
||||
''' Set up additional context variables'''
|
||||
extra_context = {
|
||||
'queue_len': self.queue_len,
|
||||
'queue_len': str(self.queue_len),
|
||||
'queue_msg': self.queue_msg
|
||||
}
|
||||
return extra_context
|
||||
|
||||
@@ -361,7 +361,6 @@ class MatlabTest(unittest.TestCase):
|
||||
'feedback': {'message': '3'}, }
|
||||
elt = etree.fromstring(self.xml)
|
||||
|
||||
input_class = lookup_tag('matlabinput')
|
||||
the_input = self.input_class(test_system, elt, state)
|
||||
context = the_input._get_render_context()
|
||||
|
||||
@@ -381,6 +380,31 @@ class MatlabTest(unittest.TestCase):
|
||||
|
||||
self.assertEqual(context, expected)
|
||||
|
||||
def test_rendering_while_queued(self):
|
||||
state = {'value': 'print "good evening"',
|
||||
'status': 'incomplete',
|
||||
'input_state': {'queuestate': 'queued'},
|
||||
}
|
||||
elt = etree.fromstring(self.xml)
|
||||
|
||||
the_input = self.input_class(test_system, elt, state)
|
||||
context = the_input._get_render_context()
|
||||
expected = {'id': 'prob_1_2',
|
||||
'value': 'print "good evening"',
|
||||
'status': 'queued',
|
||||
'msg': self.input_class.plot_submitted_msg,
|
||||
'mode': self.mode,
|
||||
'rows': self.rows,
|
||||
'cols': self.cols,
|
||||
'queue_msg': '',
|
||||
'linenumbers': 'true',
|
||||
'hidden': '',
|
||||
'tabsize': int(self.tabsize),
|
||||
'queue_len': '1',
|
||||
}
|
||||
|
||||
self.assertEqual(context, expected)
|
||||
|
||||
def test_plot_data(self):
|
||||
get = {'submission': 'x = 1234;'}
|
||||
response = self.the_input.handle_ajax("plot", get)
|
||||
@@ -391,6 +415,43 @@ class MatlabTest(unittest.TestCase):
|
||||
self.assertTrue(self.the_input.input_state['queuekey'] is not None)
|
||||
self.assertEqual(self.the_input.input_state['queuestate'], 'queued')
|
||||
|
||||
def test_ungraded_response_success(self):
|
||||
queuekey = 'abcd'
|
||||
input_state = {'queuekey': queuekey, 'queuestate': 'queued'}
|
||||
state = {'value': 'print "good evening"',
|
||||
'status': 'incomplete',
|
||||
'input_state': input_state,
|
||||
'feedback': {'message': '3'}, }
|
||||
elt = etree.fromstring(self.xml)
|
||||
|
||||
the_input = self.input_class(test_system, elt, state)
|
||||
inner_msg = 'hello!'
|
||||
queue_msg = json.dumps({'msg': inner_msg})
|
||||
|
||||
the_input.ungraded_response(queue_msg, queuekey)
|
||||
self.assertTrue(input_state['queuekey'] is None)
|
||||
self.assertTrue(input_state['queuestate'] is None)
|
||||
self.assertEqual(input_state['queue_msg'], inner_msg)
|
||||
|
||||
def test_ungraded_response_key_mismatch(self):
|
||||
queuekey = 'abcd'
|
||||
input_state = {'queuekey': queuekey, 'queuestate': 'queued'}
|
||||
state = {'value': 'print "good evening"',
|
||||
'status': 'incomplete',
|
||||
'input_state': input_state,
|
||||
'feedback': {'message': '3'}, }
|
||||
elt = etree.fromstring(self.xml)
|
||||
|
||||
the_input = self.input_class(test_system, elt, state)
|
||||
inner_msg = 'hello!'
|
||||
queue_msg = json.dumps({'msg': inner_msg})
|
||||
|
||||
the_input.ungraded_response(queue_msg, 'abc')
|
||||
self.assertEqual(input_state['queuekey'], queuekey)
|
||||
self.assertEqual(input_state['queuestate'], 'queued')
|
||||
self.assertFalse('queue_msg' in input_state)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ def group_from_value(groups, v):
|
||||
|
||||
class ABTestFields(object):
|
||||
group_portions = Object(help="What proportions of students should go in each group", default={DEFAULT: 1}, scope=Scope.content)
|
||||
group_assignments = Object(help="What group this user belongs to", scope=Scope.student_preferences, default={})
|
||||
group_assignments = Object(help="What group this user belongs to", scope=Scope.preferences, default={})
|
||||
group_content = Object(help="What content to display to each group", scope=Scope.content, default={DEFAULT: []})
|
||||
experiment = String(help="Experiment that this A/B test belongs to", scope=Scope.content)
|
||||
has_children = True
|
||||
|
||||
@@ -83,7 +83,7 @@ class ComplexEncoder(json.JSONEncoder):
|
||||
|
||||
|
||||
class CapaFields(object):
|
||||
attempts = StringyInteger(help="Number of attempts taken by the student on this problem", default=0, scope=Scope.student_state)
|
||||
attempts = StringyInteger(help="Number of attempts taken by the student on this problem", default=0, scope=Scope.user_state)
|
||||
max_attempts = StringyInteger(help="Maximum number of attempts that a student is allowed", scope=Scope.settings)
|
||||
due = Date(help="Date that this problem is due by", scope=Scope.settings)
|
||||
graceperiod = Timedelta(help="Amount of time after the due date that submissions will be accepted", scope=Scope.settings)
|
||||
@@ -91,12 +91,12 @@ class CapaFields(object):
|
||||
force_save_button = Boolean(help="Whether to force the save button to appear on the page", scope=Scope.settings, default=False)
|
||||
rerandomize = Randomization(help="When to rerandomize the problem", default="always", scope=Scope.settings)
|
||||
data = String(help="XML data for the problem", scope=Scope.content)
|
||||
correct_map = Object(help="Dictionary with the correctness of current student answers", scope=Scope.student_state, default={})
|
||||
input_state = Object(help="Dictionary for maintaining the state of inputtypes", scope=Scope.student_state)
|
||||
student_answers = Object(help="Dictionary with the current student responses", scope=Scope.student_state)
|
||||
done = Boolean(help="Whether the student has answered the problem", scope=Scope.student_state)
|
||||
correct_map = Object(help="Dictionary with the correctness of current student answers", scope=Scope.user_state, default={})
|
||||
input_state = Object(help="Dictionary for maintaining the state of inputtypes", scope=Scope.user_state)
|
||||
student_answers = Object(help="Dictionary with the current student responses", scope=Scope.user_state)
|
||||
done = Boolean(help="Whether the student has answered the problem", scope=Scope.user_state)
|
||||
display_name = String(help="Display name for this module", scope=Scope.settings)
|
||||
seed = StringyInteger(help="Random seed for this student", scope=Scope.student_state)
|
||||
seed = StringyInteger(help="Random seed for this student", scope=Scope.user_state)
|
||||
weight = StringyFloat(help="How much to weight this problem by", scope=Scope.settings)
|
||||
markdown = String(help="Markdown source of this module", scope=Scope.settings)
|
||||
|
||||
@@ -108,11 +108,10 @@ class CapaModule(CapaFields, XModule):
|
||||
'''
|
||||
icon_class = 'problem'
|
||||
|
||||
|
||||
js = {'coffee': [resource_string(__name__, 'js/src/capa/display.coffee'),
|
||||
resource_string(__name__, 'js/src/collapsible.coffee'),
|
||||
resource_string(__name__, 'js/src/javascript_loader.coffee'),
|
||||
],
|
||||
],
|
||||
'js': [resource_string(__name__, 'js/src/capa/imageinput.js'),
|
||||
resource_string(__name__, 'js/src/capa/schematic.js')
|
||||
]}
|
||||
@@ -367,11 +366,11 @@ class CapaModule(CapaFields, XModule):
|
||||
self.set_state_from_lcp()
|
||||
|
||||
# Prepend a scary warning to the student
|
||||
warning = '<div class="capa_reset">'\
|
||||
'<h2>Warning: The problem has been reset to its initial state!</h2>'\
|
||||
'The problem\'s state was corrupted by an invalid submission. ' \
|
||||
'The submission consisted of:'\
|
||||
'<ul>'
|
||||
warning = '<div class="capa_reset">'\
|
||||
'<h2>Warning: The problem has been reset to its initial state!</h2>'\
|
||||
'The problem\'s state was corrupted by an invalid submission. ' \
|
||||
'The submission consisted of:'\
|
||||
'<ul>'
|
||||
for student_answer in student_answers.values():
|
||||
if student_answer != '':
|
||||
warning += '<li>' + cgi.escape(student_answer) + '</li>'
|
||||
@@ -388,7 +387,6 @@ class CapaModule(CapaFields, XModule):
|
||||
|
||||
return html
|
||||
|
||||
|
||||
def get_problem_html(self, encapsulate=True):
|
||||
'''Return html for the problem. Adds check, reset, save buttons
|
||||
as necessary based on the problem config and state.'''
|
||||
@@ -401,7 +399,6 @@ class CapaModule(CapaFields, XModule):
|
||||
except Exception, err:
|
||||
html = self.handle_problem_html_error(err)
|
||||
|
||||
|
||||
# The convention is to pass the name of the check button
|
||||
# if we want to show a check button, and False otherwise
|
||||
# This works because non-empty strings evaluate to True
|
||||
@@ -454,7 +451,7 @@ class CapaModule(CapaFields, XModule):
|
||||
'score_update': self.update_score,
|
||||
'input_ajax': self.handle_input_ajax,
|
||||
'ungraded_response': self.handle_ungraded_response
|
||||
}
|
||||
}
|
||||
|
||||
if dispatch not in handlers:
|
||||
return 'Error'
|
||||
@@ -472,7 +469,7 @@ class CapaModule(CapaFields, XModule):
|
||||
d.update({
|
||||
'progress_changed': after != before,
|
||||
'progress_status': Progress.to_js_status_str(after),
|
||||
})
|
||||
})
|
||||
return json.dumps(d, cls=ComplexEncoder)
|
||||
|
||||
def is_past_due(self):
|
||||
@@ -535,7 +532,6 @@ class CapaModule(CapaFields, XModule):
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def update_score(self, get):
|
||||
"""
|
||||
Delivers grading response (e.g. from asynchronous code checking) to
|
||||
@@ -590,7 +586,6 @@ class CapaModule(CapaFields, XModule):
|
||||
self.set_state_from_lcp()
|
||||
return response
|
||||
|
||||
|
||||
def get_answer(self, get):
|
||||
'''
|
||||
For the "show answer" button.
|
||||
@@ -700,7 +695,6 @@ class CapaModule(CapaFields, XModule):
|
||||
'max_value': score['total'],
|
||||
})
|
||||
|
||||
|
||||
def check_problem(self, get):
|
||||
''' Checks whether answers to a problem are correct, and
|
||||
returns a map of correct/incorrect answers:
|
||||
@@ -783,7 +777,7 @@ class CapaModule(CapaFields, XModule):
|
||||
self.system.track_function('save_problem_check', event_info)
|
||||
|
||||
if hasattr(self.system, 'psychometrics_handler'): # update PsychometricsData using callback
|
||||
self.system.psychometrics_handler(self.get_instance_state())
|
||||
self.system.psychometrics_handler(self.get_state_for_lcp())
|
||||
|
||||
# render problem into HTML
|
||||
html = self.get_problem_html(encapsulate=False)
|
||||
|
||||
@@ -50,14 +50,14 @@ class VersionInteger(Integer):
|
||||
|
||||
class CombinedOpenEndedFields(object):
|
||||
display_name = String(help="Display name for this module", default="Open Ended Grading", scope=Scope.settings)
|
||||
current_task_number = Integer(help="Current task that the student is on.", default=0, scope=Scope.student_state)
|
||||
task_states = List(help="List of state dictionaries of each task within this module.", scope=Scope.student_state)
|
||||
current_task_number = Integer(help="Current task that the student is on.", default=0, scope=Scope.user_state)
|
||||
task_states = List(help="List of state dictionaries of each task within this module.", scope=Scope.user_state)
|
||||
state = String(help="Which step within the current task that the student is on.", default="initial",
|
||||
scope=Scope.student_state)
|
||||
scope=Scope.user_state)
|
||||
student_attempts = Integer(help="Number of attempts taken by the student on this problem", default=0,
|
||||
scope=Scope.student_state)
|
||||
scope=Scope.user_state)
|
||||
ready_to_reset = Boolean(help="If the problem is ready to be reset or not.", default=False,
|
||||
scope=Scope.student_state)
|
||||
scope=Scope.user_state)
|
||||
attempts = Integer(help="Maximum number of attempts that a student is allowed.", default=1, scope=Scope.settings)
|
||||
is_graded = Boolean(help="Whether or not the problem is graded.", default=False, scope=Scope.settings)
|
||||
accept_file_upload = Boolean(help="Whether or not the problem accepts file uploads.", default=False,
|
||||
@@ -219,4 +219,5 @@ class CombinedOpenEndedDescriptor(CombinedOpenEndedFields, RawDescriptor):
|
||||
|
||||
stores_state = True
|
||||
has_score = True
|
||||
always_recalculate_grades=True
|
||||
template_dir_name = "combinedopenended"
|
||||
|
||||
@@ -125,7 +125,8 @@ class ConditionalModule(ConditionalFields, XModule):
|
||||
an AJAX call.
|
||||
"""
|
||||
if not self.is_condition_satisfied():
|
||||
message = self.descriptor.xml_attributes.get('message')
|
||||
defmsg = "{link} must be attempted before this will become visible."
|
||||
message = self.descriptor.xml_attributes.get('message', defmsg)
|
||||
context = {'module': self,
|
||||
'message': message}
|
||||
html = self.system.render_template('conditional_module.html',
|
||||
|
||||
@@ -652,7 +652,12 @@ class CourseDescriptor(CourseFields, SequenceDescriptor):
|
||||
|
||||
@property
|
||||
def end_date_text(self):
|
||||
return time.strftime("%b %d, %Y", self.end)
|
||||
"""
|
||||
Returns the end date for the course formatted as a string.
|
||||
|
||||
If the course does not have an end date set (course.end is None), an empty string will be returned.
|
||||
"""
|
||||
return '' if self.end is None else time.strftime("%b %d, %Y", self.end)
|
||||
|
||||
@property
|
||||
def forum_posts_allowed(self):
|
||||
|
||||
@@ -356,22 +356,44 @@ def remap_namespace(module, target_location_namespace):
|
||||
|
||||
return module
|
||||
|
||||
def validate_no_non_editable_metadata(module_store, course_id, category, allowed=None):
|
||||
|
||||
def allowed_metadata_by_category(category):
|
||||
# should this be in the descriptors?!?
|
||||
return {
|
||||
'vertical': [],
|
||||
'chapter': ['start'],
|
||||
'sequential': ['due', 'format', 'start', 'graded']
|
||||
}.get(category,['*'])
|
||||
|
||||
|
||||
def check_module_metadata_editability(module):
|
||||
'''
|
||||
Assert that there is no metadata within a particular category that we can't support editing
|
||||
Assert that there is no metadata within a particular module that we can't support editing
|
||||
However we always allow 'display_name' and 'xml_attribtues'
|
||||
'''
|
||||
_allowed = (allowed if allowed is not None else []) + ['xml_attributes', 'display_name']
|
||||
allowed = allowed_metadata_by_category(module.location.category)
|
||||
if '*' in allowed:
|
||||
# everything is allowed
|
||||
return 0
|
||||
|
||||
allowed = allowed + ['xml_attributes', 'display_name']
|
||||
err_cnt = 0
|
||||
my_metadata = dict(own_metadata(module))
|
||||
illegal_keys = set(own_metadata(module).keys()) - set(allowed)
|
||||
|
||||
if len(illegal_keys) > 0:
|
||||
err_cnt = err_cnt + 1
|
||||
print ': found non-editable metadata on {0}. These metadata keys are not supported = {1}'. format(module.location.url(), illegal_keys)
|
||||
|
||||
return err_cnt
|
||||
|
||||
|
||||
def validate_no_non_editable_metadata(module_store, course_id, category):
|
||||
err_cnt = 0
|
||||
for module_loc in module_store.modules[course_id]:
|
||||
module = module_store.modules[course_id][module_loc]
|
||||
if module.location.category == category:
|
||||
my_metadata = dict(own_metadata(module))
|
||||
for key in my_metadata.keys():
|
||||
if key not in _allowed:
|
||||
err_cnt = err_cnt + 1
|
||||
print ': found metadata on {0}. Studio will not support editing this piece of metadata, so it is not allowed. Metadata: {1} = {2}'. format(module.location.url(), key, my_metadata[key])
|
||||
err_cnt = err_cnt + check_module_metadata_editability(module)
|
||||
|
||||
return err_cnt
|
||||
|
||||
@@ -463,10 +485,9 @@ def perform_xlint(data_dir, course_dirs,
|
||||
# don't allow metadata on verticals, since we can't edit them in studio
|
||||
err_cnt += validate_no_non_editable_metadata(module_store, course_id, "vertical")
|
||||
# don't allow metadata on chapters, since we can't edit them in studio
|
||||
err_cnt += validate_no_non_editable_metadata(module_store, course_id, "chapter",['start'])
|
||||
err_cnt += validate_no_non_editable_metadata(module_store, course_id, "chapter")
|
||||
# don't allow metadata on sequences that we can't edit
|
||||
err_cnt += validate_no_non_editable_metadata(module_store, course_id, "sequential",
|
||||
['due','format','start','graded'])
|
||||
err_cnt += validate_no_non_editable_metadata(module_store, course_id, "sequential")
|
||||
|
||||
# check for a presence of a course marketing video
|
||||
location_elements = course_id.split('/')
|
||||
|
||||
@@ -37,8 +37,8 @@ class PeerGradingFields(object):
|
||||
grace_period_string = String(help="Amount of grace to give on the due date.", default=None, scope=Scope.settings)
|
||||
max_grade = Integer(help="The maximum grade that a student can receieve for this problem.", default=MAX_SCORE,
|
||||
scope=Scope.settings)
|
||||
student_data_for_location = Object(help="Student data for a given peer grading problem.", default=json.dumps({}),
|
||||
scope=Scope.student_state)
|
||||
student_data_for_location = Object(help="Student data for a given peer grading problem.",
|
||||
scope=Scope.user_state)
|
||||
weight = StringyFloat(help="How much to weight this problem by", scope=Scope.settings)
|
||||
|
||||
|
||||
@@ -577,4 +577,5 @@ class PeerGradingDescriptor(PeerGradingFields, RawDescriptor):
|
||||
|
||||
stores_state = True
|
||||
has_score = True
|
||||
always_recalculate_grades=True
|
||||
template_dir_name = "peer_grading"
|
||||
|
||||
@@ -30,8 +30,8 @@ class PollFields(object):
|
||||
# Name of poll to use in links to this poll
|
||||
display_name = String(help="Display name for this module", scope=Scope.settings)
|
||||
|
||||
voted = Boolean(help="Whether this student has voted on the poll", scope=Scope.student_state, default=False)
|
||||
poll_answer = String(help="Student answer", scope=Scope.student_state, default='')
|
||||
voted = Boolean(help="Whether this student has voted on the poll", scope=Scope.user_state, default=False)
|
||||
poll_answer = String(help="Student answer", scope=Scope.user_state, default='')
|
||||
poll_answers = Object(help="All possible answers for the poll fro other students", scope=Scope.content)
|
||||
|
||||
answers = List(help="Poll answers from xml", scope=Scope.content, default=[])
|
||||
|
||||
@@ -10,7 +10,7 @@ log = logging.getLogger('mitx.' + __name__)
|
||||
|
||||
|
||||
class RandomizeFields(object):
|
||||
choice = Integer(help="Which random child was chosen", scope=Scope.student_state)
|
||||
choice = Integer(help="Which random child was chosen", scope=Scope.user_state)
|
||||
|
||||
|
||||
class RandomizeModule(RandomizeFields, XModule):
|
||||
|
||||
@@ -23,7 +23,7 @@ class SequenceFields(object):
|
||||
|
||||
# NOTE: Position is 1-indexed. This is silly, but there are now student
|
||||
# positions saved on prod, so it's not easy to fix.
|
||||
position = Integer(help="Last tab viewed in this sequence", scope=Scope.student_state)
|
||||
position = Integer(help="Last tab viewed in this sequence", scope=Scope.user_state)
|
||||
|
||||
|
||||
class SequenceModule(SequenceFields, XModule):
|
||||
|
||||
@@ -35,6 +35,7 @@ class CapaFactory(object):
|
||||
"""
|
||||
|
||||
num = 0
|
||||
|
||||
@staticmethod
|
||||
def next_num():
|
||||
CapaFactory.num += 1
|
||||
@@ -49,7 +50,7 @@ class CapaFactory(object):
|
||||
def answer_key():
|
||||
""" Return the key stored in the capa problem answer dict """
|
||||
return ("-".join(['i4x', 'edX', 'capa_test', 'problem',
|
||||
'SampleProblem%d' % CapaFactory.num]) +
|
||||
'SampleProblem%d' % CapaFactory.num]) +
|
||||
"_2_1")
|
||||
|
||||
@staticmethod
|
||||
@@ -120,7 +121,6 @@ class CapaFactory(object):
|
||||
return module
|
||||
|
||||
|
||||
|
||||
class CapaModuleTest(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
@@ -142,9 +142,6 @@ class CapaModuleTest(unittest.TestCase):
|
||||
self.assertNotEqual(module.url_name, other_module.url_name,
|
||||
"Factory should be creating unique names for each problem")
|
||||
|
||||
|
||||
|
||||
|
||||
def test_correct(self):
|
||||
"""
|
||||
Check that the factory creates correct and incorrect problems properly.
|
||||
@@ -155,7 +152,6 @@ class CapaModuleTest(unittest.TestCase):
|
||||
other_module = CapaFactory.create(correct=True)
|
||||
self.assertEqual(other_module.get_score()['score'], 1)
|
||||
|
||||
|
||||
def test_showanswer_default(self):
|
||||
"""
|
||||
Make sure the show answer logic does the right thing.
|
||||
@@ -165,14 +161,12 @@ class CapaModuleTest(unittest.TestCase):
|
||||
problem = CapaFactory.create()
|
||||
self.assertFalse(problem.answer_available())
|
||||
|
||||
|
||||
def test_showanswer_attempted(self):
|
||||
problem = CapaFactory.create(showanswer='attempted')
|
||||
self.assertFalse(problem.answer_available())
|
||||
problem.attempts = 1
|
||||
self.assertTrue(problem.answer_available())
|
||||
|
||||
|
||||
def test_showanswer_closed(self):
|
||||
|
||||
# can see after attempts used up, even with due date in the future
|
||||
@@ -182,21 +176,19 @@ class CapaModuleTest(unittest.TestCase):
|
||||
due=self.tomorrow_str)
|
||||
self.assertTrue(used_all_attempts.answer_available())
|
||||
|
||||
|
||||
# can see after due date
|
||||
after_due_date = CapaFactory.create(showanswer='closed',
|
||||
max_attempts="1",
|
||||
attempts="0",
|
||||
due=self.yesterday_str)
|
||||
max_attempts="1",
|
||||
attempts="0",
|
||||
due=self.yesterday_str)
|
||||
|
||||
self.assertTrue(after_due_date.answer_available())
|
||||
|
||||
|
||||
# can't see because attempts left
|
||||
attempts_left_open = CapaFactory.create(showanswer='closed',
|
||||
max_attempts="1",
|
||||
attempts="0",
|
||||
due=self.tomorrow_str)
|
||||
max_attempts="1",
|
||||
attempts="0",
|
||||
due=self.tomorrow_str)
|
||||
self.assertFalse(attempts_left_open.answer_available())
|
||||
|
||||
# Can't see because grace period hasn't expired
|
||||
@@ -207,8 +199,6 @@ class CapaModuleTest(unittest.TestCase):
|
||||
graceperiod=self.two_day_delta_str)
|
||||
self.assertFalse(still_in_grace.answer_available())
|
||||
|
||||
|
||||
|
||||
def test_showanswer_past_due(self):
|
||||
"""
|
||||
With showanswer="past_due" should only show answer after the problem is closed
|
||||
@@ -222,20 +212,18 @@ class CapaModuleTest(unittest.TestCase):
|
||||
due=self.tomorrow_str)
|
||||
self.assertFalse(used_all_attempts.answer_available())
|
||||
|
||||
|
||||
# can see after due date
|
||||
past_due_date = CapaFactory.create(showanswer='past_due',
|
||||
max_attempts="1",
|
||||
attempts="0",
|
||||
due=self.yesterday_str)
|
||||
max_attempts="1",
|
||||
attempts="0",
|
||||
due=self.yesterday_str)
|
||||
self.assertTrue(past_due_date.answer_available())
|
||||
|
||||
|
||||
# can't see because attempts left
|
||||
attempts_left_open = CapaFactory.create(showanswer='past_due',
|
||||
max_attempts="1",
|
||||
attempts="0",
|
||||
due=self.tomorrow_str)
|
||||
max_attempts="1",
|
||||
attempts="0",
|
||||
due=self.tomorrow_str)
|
||||
self.assertFalse(attempts_left_open.answer_available())
|
||||
|
||||
# Can't see because grace period hasn't expired, even though have no more
|
||||
@@ -260,31 +248,28 @@ class CapaModuleTest(unittest.TestCase):
|
||||
due=self.tomorrow_str)
|
||||
self.assertTrue(used_all_attempts.answer_available())
|
||||
|
||||
|
||||
# can see after due date
|
||||
past_due_date = CapaFactory.create(showanswer='finished',
|
||||
max_attempts="1",
|
||||
attempts="0",
|
||||
due=self.yesterday_str)
|
||||
max_attempts="1",
|
||||
attempts="0",
|
||||
due=self.yesterday_str)
|
||||
self.assertTrue(past_due_date.answer_available())
|
||||
|
||||
|
||||
# can't see because attempts left and wrong
|
||||
attempts_left_open = CapaFactory.create(showanswer='finished',
|
||||
max_attempts="1",
|
||||
attempts="0",
|
||||
due=self.tomorrow_str)
|
||||
max_attempts="1",
|
||||
attempts="0",
|
||||
due=self.tomorrow_str)
|
||||
self.assertFalse(attempts_left_open.answer_available())
|
||||
|
||||
# _can_ see because attempts left and right
|
||||
correct_ans = CapaFactory.create(showanswer='finished',
|
||||
max_attempts="1",
|
||||
attempts="0",
|
||||
due=self.tomorrow_str,
|
||||
correct=True)
|
||||
max_attempts="1",
|
||||
attempts="0",
|
||||
due=self.tomorrow_str,
|
||||
correct=True)
|
||||
self.assertTrue(correct_ans.answer_available())
|
||||
|
||||
|
||||
# Can see even though grace period hasn't expired, because have no more
|
||||
# attempts.
|
||||
still_in_grace = CapaFactory.create(showanswer='finished',
|
||||
@@ -294,7 +279,6 @@ class CapaModuleTest(unittest.TestCase):
|
||||
graceperiod=self.two_day_delta_str)
|
||||
self.assertTrue(still_in_grace.answer_available())
|
||||
|
||||
|
||||
def test_closed(self):
|
||||
|
||||
# Attempts < Max attempts --> NOT closed
|
||||
@@ -322,7 +306,6 @@ class CapaModuleTest(unittest.TestCase):
|
||||
due=self.yesterday_str)
|
||||
self.assertTrue(module.closed())
|
||||
|
||||
|
||||
def test_parse_get_params(self):
|
||||
|
||||
# We have to set up Django settings in order to use QueryDict
|
||||
@@ -348,7 +331,6 @@ class CapaModuleTest(unittest.TestCase):
|
||||
"Output dict should have key %s" % original_key)
|
||||
self.assertEqual(valid_get_dict[original_key], result[key])
|
||||
|
||||
|
||||
# Valid GET param dict with list keys
|
||||
valid_get_dict = self._querydict_from_dict({'input_2[]': ['test1', 'test2']})
|
||||
result = CapaModule.make_dict_of_responses(valid_get_dict)
|
||||
@@ -366,12 +348,11 @@ class CapaModuleTest(unittest.TestCase):
|
||||
with self.assertRaises(ValueError):
|
||||
result = CapaModule.make_dict_of_responses(invalid_get_dict)
|
||||
|
||||
|
||||
# Two equivalent names (one list, one non-list)
|
||||
# One of the values would overwrite the other, so detect this
|
||||
# and raise an exception
|
||||
invalid_get_dict = self._querydict_from_dict({'input_1[]': 'test 1',
|
||||
'input_1': 'test 2'})
|
||||
'input_1': 'test 2'})
|
||||
with self.assertRaises(ValueError):
|
||||
result = CapaModule.make_dict_of_responses(invalid_get_dict)
|
||||
|
||||
@@ -395,7 +376,6 @@ class CapaModuleTest(unittest.TestCase):
|
||||
|
||||
return copyDict
|
||||
|
||||
|
||||
def test_check_problem_correct(self):
|
||||
|
||||
module = CapaFactory.create(attempts=1)
|
||||
@@ -403,6 +383,7 @@ class CapaModuleTest(unittest.TestCase):
|
||||
# Simulate that all answers are marked correct, no matter
|
||||
# what the input is, by patching CorrectMap.is_correct()
|
||||
# Also simulate rendering the HTML
|
||||
# TODO: pep8 thinks the following line has invalid syntax
|
||||
with patch('capa.correctmap.CorrectMap.is_correct') as mock_is_correct,\
|
||||
patch('xmodule.capa_module.CapaModule.get_problem_html') as mock_html:
|
||||
mock_is_correct.return_value = True
|
||||
@@ -439,7 +420,6 @@ class CapaModuleTest(unittest.TestCase):
|
||||
# Expect that the number of attempts is incremented by 1
|
||||
self.assertEqual(module.attempts, 1)
|
||||
|
||||
|
||||
def test_check_problem_closed(self):
|
||||
module = CapaFactory.create(attempts=3)
|
||||
|
||||
@@ -503,12 +483,11 @@ class CapaModuleTest(unittest.TestCase):
|
||||
# Expect that the number of attempts is NOT incremented
|
||||
self.assertEqual(module.attempts, 1)
|
||||
|
||||
|
||||
def test_check_problem_error(self):
|
||||
|
||||
# Try each exception that capa_module should handle
|
||||
for exception_class in [StudentInputError,
|
||||
LoncapaProblemError,
|
||||
for exception_class in [StudentInputError,
|
||||
LoncapaProblemError,
|
||||
ResponseError]:
|
||||
|
||||
# Create the module
|
||||
@@ -532,9 +511,9 @@ class CapaModuleTest(unittest.TestCase):
|
||||
self.assertEqual(module.attempts, 1)
|
||||
|
||||
def test_check_problem_error_with_staff_user(self):
|
||||
|
||||
|
||||
# Try each exception that capa module should handle
|
||||
for exception_class in [StudentInputError,
|
||||
for exception_class in [StudentInputError,
|
||||
LoncapaProblemError,
|
||||
ResponseError]:
|
||||
|
||||
@@ -560,7 +539,6 @@ class CapaModuleTest(unittest.TestCase):
|
||||
# Expect that the number of attempts is NOT incremented
|
||||
self.assertEqual(module.attempts, 1)
|
||||
|
||||
|
||||
def test_reset_problem(self):
|
||||
module = CapaFactory.create(done=True)
|
||||
module.new_lcp = Mock(wraps=module.new_lcp)
|
||||
@@ -583,7 +561,6 @@ class CapaModuleTest(unittest.TestCase):
|
||||
# Expect that the problem was reset
|
||||
module.new_lcp.assert_called_once_with({'seed': None})
|
||||
|
||||
|
||||
def test_reset_problem_closed(self):
|
||||
module = CapaFactory.create()
|
||||
|
||||
@@ -598,7 +575,6 @@ class CapaModuleTest(unittest.TestCase):
|
||||
# Expect that the problem was NOT reset
|
||||
self.assertTrue('success' in result and not result['success'])
|
||||
|
||||
|
||||
def test_reset_problem_not_done(self):
|
||||
# Simulate that the problem is NOT done
|
||||
module = CapaFactory.create(done=False)
|
||||
@@ -610,7 +586,6 @@ class CapaModuleTest(unittest.TestCase):
|
||||
# Expect that the problem was NOT reset
|
||||
self.assertTrue('success' in result and not result['success'])
|
||||
|
||||
|
||||
def test_save_problem(self):
|
||||
module = CapaFactory.create(done=False)
|
||||
|
||||
@@ -625,7 +600,6 @@ class CapaModuleTest(unittest.TestCase):
|
||||
# Expect that the result is success
|
||||
self.assertTrue('success' in result and result['success'])
|
||||
|
||||
|
||||
def test_save_problem_closed(self):
|
||||
module = CapaFactory.create(done=False)
|
||||
|
||||
@@ -640,7 +614,6 @@ class CapaModuleTest(unittest.TestCase):
|
||||
# Expect that the result is failure
|
||||
self.assertTrue('success' in result and not result['success'])
|
||||
|
||||
|
||||
def test_save_problem_submitted_with_randomize(self):
|
||||
module = CapaFactory.create(rerandomize='always', done=True)
|
||||
|
||||
@@ -651,7 +624,6 @@ class CapaModuleTest(unittest.TestCase):
|
||||
# Expect that we cannot save
|
||||
self.assertTrue('success' in result and not result['success'])
|
||||
|
||||
|
||||
def test_save_problem_submitted_no_randomize(self):
|
||||
module = CapaFactory.create(rerandomize='never', done=True)
|
||||
|
||||
@@ -724,7 +696,6 @@ class CapaModuleTest(unittest.TestCase):
|
||||
module = CapaFactory.create(rerandomize="never", done=True)
|
||||
self.assertTrue(module.should_show_check_button())
|
||||
|
||||
|
||||
def test_should_show_reset_button(self):
|
||||
|
||||
attempts = random.randint(1, 10)
|
||||
@@ -755,7 +726,6 @@ class CapaModuleTest(unittest.TestCase):
|
||||
module = CapaFactory.create(max_attempts=0, done=True)
|
||||
self.assertTrue(module.should_show_reset_button())
|
||||
|
||||
|
||||
def test_should_show_save_button(self):
|
||||
|
||||
attempts = random.randint(1, 10)
|
||||
@@ -823,7 +793,6 @@ class CapaModuleTest(unittest.TestCase):
|
||||
html = module.get_problem_html()
|
||||
# assert that we got here without exploding
|
||||
|
||||
|
||||
def test_get_problem_html(self):
|
||||
module = CapaFactory.create()
|
||||
|
||||
@@ -869,6 +838,18 @@ class CapaModuleTest(unittest.TestCase):
|
||||
# Assert that the encapsulated html contains the original html
|
||||
self.assertTrue(html in html_encapsulated)
|
||||
|
||||
def test_input_state_consistency(self):
|
||||
module1 = CapaFactory.create()
|
||||
module2 = CapaFactory.create()
|
||||
|
||||
# check to make sure that the input_state and the keys have the same values
|
||||
module1.set_state_from_lcp()
|
||||
self.assertEqual(module1.lcp.inputs.keys(), module1.input_state.keys())
|
||||
|
||||
module2.set_state_from_lcp()
|
||||
|
||||
intersection = set(module2.input_state.keys()).intersection(set(module1.input_state.keys()))
|
||||
self.assertEqual(len(intersection), 0)
|
||||
|
||||
def test_get_problem_html_error(self):
|
||||
"""
|
||||
@@ -902,7 +883,6 @@ class CapaModuleTest(unittest.TestCase):
|
||||
# Expect that the module has created a new dummy problem with the error
|
||||
self.assertNotEqual(original_problem, module.lcp)
|
||||
|
||||
|
||||
def test_random_seed_no_change(self):
|
||||
|
||||
# Run the test for each possible rerandomize value
|
||||
@@ -920,10 +900,10 @@ class CapaModuleTest(unittest.TestCase):
|
||||
self.assertEqual(seed, 1)
|
||||
|
||||
# Check the problem
|
||||
get_request_dict = { CapaFactory.input_key(): '3.14'}
|
||||
get_request_dict = {CapaFactory.input_key(): '3.14'}
|
||||
module.check_problem(get_request_dict)
|
||||
|
||||
# Expect that the seed is the same
|
||||
# Expect that the seed is the same
|
||||
self.assertEqual(seed, module.seed)
|
||||
|
||||
# Save the problem
|
||||
@@ -933,7 +913,7 @@ class CapaModuleTest(unittest.TestCase):
|
||||
self.assertEqual(seed, module.seed)
|
||||
|
||||
def test_random_seed_with_reset(self):
|
||||
|
||||
|
||||
def _reset_and_get_seed(module):
|
||||
'''
|
||||
Reset the XModule and return the module's seed
|
||||
@@ -956,7 +936,7 @@ class CapaModuleTest(unittest.TestCase):
|
||||
Returns True if *test_func* was successful
|
||||
(returned True) within *num_tries* attempts
|
||||
|
||||
*test_func* must be a function
|
||||
*test_func* must be a function
|
||||
of the form test_func() -> bool
|
||||
'''
|
||||
success = False
|
||||
@@ -989,9 +969,10 @@ class CapaModuleTest(unittest.TestCase):
|
||||
# Since there's a small chance we might get the
|
||||
# same seed again, give it 5 chances
|
||||
# to generate a different seed
|
||||
success = _retry_and_check(5,
|
||||
lambda: _reset_and_get_seed(module) != seed)
|
||||
|
||||
success = _retry_and_check(5,
|
||||
lambda: _reset_and_get_seed(module) != seed)
|
||||
|
||||
# TODO: change this comparison to module.seed is not None?
|
||||
self.assertTrue(module.seed != None)
|
||||
msg = 'Could not get a new seed from reset after 5 tries'
|
||||
self.assertTrue(success, msg)
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
import unittest
|
||||
from time import strptime
|
||||
import datetime
|
||||
|
||||
from fs.memoryfs import MemoryFS
|
||||
|
||||
from mock import Mock, patch
|
||||
|
||||
from xmodule.modulestore.xml import ImportSystem, XMLModuleStore
|
||||
import xmodule.course_module
|
||||
from xmodule.util.date_utils import time_to_datetime
|
||||
|
||||
|
||||
ORG = 'test_org'
|
||||
@@ -39,8 +42,19 @@ class DummySystem(ImportSystem):
|
||||
|
||||
class IsNewCourseTestCase(unittest.TestCase):
|
||||
"""Make sure the property is_new works on courses"""
|
||||
|
||||
def setUp(self):
|
||||
# Needed for test_is_newish
|
||||
datetime_patcher = patch.object(
|
||||
xmodule.course_module, 'datetime',
|
||||
Mock(wraps=datetime.datetime)
|
||||
)
|
||||
mocked_datetime = datetime_patcher.start()
|
||||
mocked_datetime.utcnow.return_value = time_to_datetime(NOW)
|
||||
self.addCleanup(datetime_patcher.stop)
|
||||
|
||||
@staticmethod
|
||||
def get_dummy_course(start, announcement=None, is_new=None, advertised_start=None):
|
||||
def get_dummy_course(start, announcement=None, is_new=None, advertised_start=None, end=None):
|
||||
"""Get a dummy course"""
|
||||
|
||||
system = DummySystem(load_error_modules=True)
|
||||
@@ -51,6 +65,7 @@ class IsNewCourseTestCase(unittest.TestCase):
|
||||
is_new = to_attrb('is_new', is_new)
|
||||
announcement = to_attrb('announcement', announcement)
|
||||
advertised_start = to_attrb('advertised_start', advertised_start)
|
||||
end = to_attrb('end', end)
|
||||
|
||||
start_xml = '''
|
||||
<course org="{org}" course="{course}"
|
||||
@@ -58,13 +73,14 @@ class IsNewCourseTestCase(unittest.TestCase):
|
||||
start="{start}"
|
||||
{announcement}
|
||||
{is_new}
|
||||
{advertised_start}>
|
||||
{advertised_start}
|
||||
{end}>
|
||||
<chapter url="hi" url_name="ch" display_name="CH">
|
||||
<html url_name="h" display_name="H">Two houses, ...</html>
|
||||
</chapter>
|
||||
</course>
|
||||
'''.format(org=ORG, course=COURSE, start=start, is_new=is_new,
|
||||
announcement=announcement, advertised_start=advertised_start)
|
||||
announcement=announcement, advertised_start=advertised_start, end=end)
|
||||
|
||||
return system.process_xml(start_xml)
|
||||
|
||||
@@ -126,10 +142,7 @@ class IsNewCourseTestCase(unittest.TestCase):
|
||||
print "Checking start=%s advertised=%s" % (s[0], s[1])
|
||||
self.assertEqual(d.start_date_text, s[2])
|
||||
|
||||
@patch('xmodule.course_module.time.gmtime')
|
||||
def test_is_newish(self, gmtime_mock):
|
||||
gmtime_mock.return_value = NOW
|
||||
|
||||
def test_is_newish(self):
|
||||
descriptor = self.get_dummy_course(start='2012-12-02T12:00', is_new=True)
|
||||
assert(descriptor.is_newish is True)
|
||||
|
||||
@@ -150,3 +163,11 @@ class IsNewCourseTestCase(unittest.TestCase):
|
||||
|
||||
descriptor = self.get_dummy_course(start='2012-12-31T12:00')
|
||||
assert(descriptor.is_newish is True)
|
||||
|
||||
def test_end_date_text(self):
|
||||
# No end date set, returns empty string.
|
||||
d = self.get_dummy_course('2012-12-02T12:00')
|
||||
self.assertEqual('', d.end_date_text)
|
||||
|
||||
d = self.get_dummy_course('2012-12-02T12:00', end='2014-9-04T12:00')
|
||||
self.assertEqual('Sep 04, 2014', d.end_date_text)
|
||||
|
||||
@@ -16,9 +16,9 @@ log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class TimeLimitFields(object):
|
||||
beginning_at = Float(help="The time this timer was started", scope=Scope.student_state)
|
||||
ending_at = Float(help="The time this timer will end", scope=Scope.student_state)
|
||||
accomodation_code = String(help="A code indicating accommodations to be given the student", scope=Scope.student_state)
|
||||
beginning_at = Float(help="The time this timer was started", scope=Scope.user_state)
|
||||
ending_at = Float(help="The time this timer will end", scope=Scope.user_state)
|
||||
accomodation_code = String(help="A code indicating accommodations to be given the student", scope=Scope.user_state)
|
||||
time_expired_redirect_url = String(help="Url to redirect users to after the timelimit has expired", scope=Scope.settings)
|
||||
duration = Float(help="The length of this timer", scope=Scope.settings)
|
||||
suppress_toplevel_navigation = Boolean(help="Whether the toplevel navigation should be suppressed when viewing this module", scope=Scope.settings)
|
||||
|
||||
@@ -19,7 +19,7 @@ log = logging.getLogger(__name__)
|
||||
|
||||
class VideoFields(object):
|
||||
data = String(help="XML data for the problem", scope=Scope.content)
|
||||
position = Integer(help="Current position in the video", scope=Scope.student_state, default=0)
|
||||
position = Integer(help="Current position in the video", scope=Scope.user_state, default=0)
|
||||
display_name = String(help="Display name for this module", scope=Scope.settings)
|
||||
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ log = logging.getLogger(__name__)
|
||||
|
||||
class VideoAlphaFields(object):
|
||||
data = String(help="XML data for the problem", scope=Scope.content)
|
||||
position = Integer(help="Current position in the video", scope=Scope.student_state, default=0)
|
||||
position = Integer(help="Current position in the video", scope=Scope.user_state, default=0)
|
||||
display_name = String(help="Display name for this module", scope=Scope.settings)
|
||||
|
||||
|
||||
|
||||
5
common/test/data/test_about_blob_end_date/README.md
Normal file
5
common/test/data/test_about_blob_end_date/README.md
Normal file
@@ -0,0 +1,5 @@
|
||||
Test course for checking the end date displayed on the course about page.
|
||||
This course has both an end_date HTML "blob", and it also has a course end date set.
|
||||
The end_date "blob" has higher precedence and will show.
|
||||
|
||||
See also test_end course.
|
||||
@@ -0,0 +1 @@
|
||||
Learning never ends
|
||||
1
common/test/data/test_about_blob_end_date/course.xml
Normal file
1
common/test/data/test_about_blob_end_date/course.xml
Normal file
@@ -0,0 +1 @@
|
||||
<course org="edX" course="test_about_blob_end_date" url_name="2012_Fall"/>
|
||||
@@ -0,0 +1,2 @@
|
||||
<course>
|
||||
</course>
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"course/2012_Fall": {
|
||||
"graceperiod": "2 days 5 hours 59 minutes 59 seconds",
|
||||
"start": "2015-07-17T12:00",
|
||||
"end": "2015-09-17T12:00",
|
||||
"display_name": "Test About Blob End Date",
|
||||
"graded": "true"
|
||||
}
|
||||
}
|
||||
5
common/test/data/test_end/README.md
Normal file
5
common/test/data/test_end/README.md
Normal file
@@ -0,0 +1,5 @@
|
||||
Test course for checking the end date displayed on the course about page.
|
||||
This course does not have an end_date HTML "blob", but it does have a course end date set.
|
||||
Therefore the course end date should show on the course about page.
|
||||
|
||||
See also test_about_blob_end_date course.
|
||||
1
common/test/data/test_end/course.xml
Normal file
1
common/test/data/test_end/course.xml
Normal file
@@ -0,0 +1 @@
|
||||
<course org="edX" course="test_end" url_name="2012_Fall"/>
|
||||
2
common/test/data/test_end/course/2012_Fall.xml
Normal file
2
common/test/data/test_end/course/2012_Fall.xml
Normal file
@@ -0,0 +1,2 @@
|
||||
<course>
|
||||
</course>
|
||||
9
common/test/data/test_end/policies/2012_Fall.json
Normal file
9
common/test/data/test_end/policies/2012_Fall.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"course/2012_Fall": {
|
||||
"graceperiod": "2 days 5 hours 59 minutes 59 seconds",
|
||||
"start": "2015-07-17T12:00",
|
||||
"end": "2015-09-17T12:00",
|
||||
"display_name": "Test End",
|
||||
"graded": "true"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user