Merge branch 'master' of github.com:MITx/mitx into fix/cdodge/studio-forum-improvements

This commit is contained in:
Chris Dodge
2013-04-05 13:20:02 -04:00
73 changed files with 936 additions and 287 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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=[])

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

@@ -0,0 +1 @@
Learning never ends

View File

@@ -0,0 +1 @@
<course org="edX" course="test_about_blob_end_date" url_name="2012_Fall"/>

View File

@@ -0,0 +1,2 @@
<course>
</course>

View 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 About Blob End Date",
"graded": "true"
}
}

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

View File

@@ -0,0 +1 @@
<course org="edX" course="test_end" url_name="2012_Fall"/>

View File

@@ -0,0 +1,2 @@
<course>
</course>

View 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"
}
}