Merge branch 'master' into feature/btalbot/studio-alerts

This commit is contained in:
Brian Talbot
2013-04-04 16:55:57 -04:00
48 changed files with 480 additions and 219 deletions

View File

@@ -34,6 +34,7 @@ MITX_FEATURES = {
'ENABLE_DISCUSSION_SERVICE': False,
'AUTH_USE_MIT_CERTIFICATES': False,
'STUB_VIDEO_FOR_TESTING': False, # do not display video when running automated acceptance tests
'STAFF_EMAIL': '', # email address for staff (eg to request course creation)
'STUDIO_NPS_SURVEY': True,
}
ENABLE_JASMINE = False

View File

@@ -46,6 +46,8 @@
<li class="nav-item">
% if not disable_course_creation:
<a href="#" class="button new-button new-course-button"><i class="ss-icon ss-symbolicons-standard icon icon-create">&#x002B;</i> New Course</a>
% elif settings.MITX_FEATURES.get('STAFF_EMAIL',''):
<a href="mailto:${settings.MITX_FEATURES.get('STAFF_EMAIL','')}">Email staff to create course</a>
% endif
</li>
</ul>
@@ -67,7 +69,7 @@
<article class="my-classes">
% if user.is_active:
<ul class="class-list">
%for course, url, lms_link in courses:
%for course, url, lms_link in sorted(courses, key=lambda s: s[0].lower()):
<li>
<a class="class-link" href="${url}" class="class-name">
<span class="class-name">${course}</span>

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,

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

@@ -38,7 +38,7 @@ class PeerGradingFields(object):
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)
scope=Scope.user_state)
weight = StringyFloat(help="How much to weight this problem by", scope=Scope.settings)

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

@@ -54,7 +54,7 @@ class IsNewCourseTestCase(unittest.TestCase):
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)
@@ -65,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}"
@@ -72,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)
@@ -161,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"
}
}

View File

@@ -165,7 +165,7 @@ def grade(student, request, course, model_data_cache=None, keep_raw_scores=False
# Create a fake key to pull out a StudentModule object from the ModelDataCache
key = LmsKeyValueStore.Key(
Scope.student_state,
Scope.user_state,
student.id,
moduledescriptor.location,
None
@@ -370,7 +370,7 @@ def get_score(course_id, user, problem_descriptor, module_creator, model_data_ca
# Create a fake KeyValueStore key to pull out the StudentModule
key = LmsKeyValueStore.Key(
Scope.student_state,
Scope.user_state,
user.id,
problem_descriptor.location,
None

View File

@@ -76,6 +76,11 @@ class Command(BaseCommand):
for hist_module in hist_modules:
self.remove_studentmodulehistory_input_state(hist_module, save_changes)
if self.num_visited % 1000 == 0:
LOG.info(" Progress: updated {0} of {1} student modules".format(self.num_changed, self.num_visited))
LOG.info(" Progress: updated {0} of {1} student history modules".format(self.num_hist_changed,
self.num_hist_visited))
@transaction.autocommit
def remove_studentmodule_input_state(self, module, save_changes):
''' Fix the grade assigned to a StudentModule'''

View File

@@ -134,7 +134,7 @@ class ModelDataCache(object):
"""
if scope in (Scope.children, Scope.parent):
return []
elif scope == Scope.student_state:
elif scope == Scope.user_state:
return self._chunked_query(
StudentModule,
'module_state_key__in',
@@ -159,7 +159,7 @@ class ModelDataCache(object):
),
field_name__in=set(field.name for field in fields),
)
elif scope == Scope.student_preferences:
elif scope == Scope.preferences:
return self._chunked_query(
XModuleStudentPrefsField,
'module_type__in',
@@ -167,7 +167,7 @@ class ModelDataCache(object):
student=self.user.pk,
field_name__in=set(field.name for field in fields),
)
elif scope == Scope.student_info:
elif scope == Scope.user_info:
return self._query(
XModuleStudentInfoField,
student=self.user.pk,
@@ -190,15 +190,15 @@ class ModelDataCache(object):
"""
Return the key used in the ModelDataCache for the specified KeyValueStore key
"""
if key.scope == Scope.student_state:
if key.scope == Scope.user_state:
return (key.scope, key.block_scope_id.url())
elif key.scope == Scope.content:
return (key.scope, key.block_scope_id.url(), key.field_name)
elif key.scope == Scope.settings:
return (key.scope, '%s-%s' % (self.course_id, key.block_scope_id.url()), key.field_name)
elif key.scope == Scope.student_preferences:
elif key.scope == Scope.preferences:
return (key.scope, key.block_scope_id, key.field_name)
elif key.scope == Scope.student_info:
elif key.scope == Scope.user_info:
return (key.scope, key.field_name)
def _cache_key_from_field_object(self, scope, field_object):
@@ -206,15 +206,15 @@ class ModelDataCache(object):
Return the key used in the ModelDataCache for the specified scope and
field
"""
if scope == Scope.student_state:
if scope == Scope.user_state:
return (scope, field_object.module_state_key)
elif scope == Scope.content:
return (scope, field_object.definition_id, field_object.field_name)
elif scope == Scope.settings:
return (scope, field_object.usage_id, field_object.field_name)
elif scope == Scope.student_preferences:
elif scope == Scope.preferences:
return (scope, field_object.module_type, field_object.field_name)
elif scope == Scope.student_info:
elif scope == Scope.user_info:
return (scope, field_object.field_name)
def find(self, key):
@@ -237,13 +237,14 @@ class ModelDataCache(object):
if field_object is not None:
return field_object
if key.scope == Scope.student_state:
if key.scope == Scope.user_state:
field_object, _ = StudentModule.objects.get_or_create(
course_id=self.course_id,
student=self.user,
module_type=key.block_scope_id.category,
module_state_key=key.block_scope_id.url(),
defaults={'state': json.dumps({})},
defaults={'state': json.dumps({}),
'module_type': key.block_scope_id.category,
},
)
elif key.scope == Scope.content:
field_object, _ = XModuleContentField.objects.get_or_create(
@@ -255,13 +256,13 @@ class ModelDataCache(object):
field_name=key.field_name,
usage_id='%s-%s' % (self.course_id, key.block_scope_id.url()),
)
elif key.scope == Scope.student_preferences:
elif key.scope == Scope.preferences:
field_object, _ = XModuleStudentPrefsField.objects.get_or_create(
field_name=key.field_name,
module_type=key.block_scope_id,
student=self.user,
)
elif key.scope == Scope.student_info:
elif key.scope == Scope.user_info:
field_object, _ = XModuleStudentInfoField.objects.get_or_create(
field_name=key.field_name,
student=self.user,
@@ -281,12 +282,12 @@ class LmsKeyValueStore(KeyValueStore):
If the scope to write to is not one of the 5 named scopes:
Scope.content
Scope.settings
Scope.student_state
Scope.student_preferences
Scope.student_info
Scope.user_state
Scope.preferences
Scope.user_info
then an InvalidScopeError will be raised.
Data for Scope.student_state is stored as StudentModule objects via the django orm.
Data for Scope.user_state is stored as StudentModule objects via the django orm.
Data for the other scopes is stored in individual objects that are named for the
scope involved and have the field name as a key
@@ -297,9 +298,9 @@ class LmsKeyValueStore(KeyValueStore):
_allowed_scopes = (
Scope.content,
Scope.settings,
Scope.student_state,
Scope.student_preferences,
Scope.student_info,
Scope.user_state,
Scope.preferences,
Scope.user_info,
Scope.children,
)
@@ -321,7 +322,7 @@ class LmsKeyValueStore(KeyValueStore):
if field_object is None:
raise KeyError(key.field_name)
if key.scope == Scope.student_state:
if key.scope == Scope.user_state:
return json.loads(field_object.state)[key.field_name]
else:
return json.loads(field_object.value)
@@ -335,7 +336,7 @@ class LmsKeyValueStore(KeyValueStore):
if key.scope not in self._allowed_scopes:
raise InvalidScopeError(key.scope)
if key.scope == Scope.student_state:
if key.scope == Scope.user_state:
state = json.loads(field_object.state)
state[key.field_name] = value
field_object.state = json.dumps(state)
@@ -355,7 +356,7 @@ class LmsKeyValueStore(KeyValueStore):
if field_object is None:
raise KeyError(key.field_name)
if key.scope == Scope.student_state:
if key.scope == Scope.user_state:
state = json.loads(field_object.state)
del state[key.field_name]
field_object.state = json.dumps(state)
@@ -377,7 +378,7 @@ class LmsKeyValueStore(KeyValueStore):
if field_object is None:
return False
if key.scope == Scope.student_state:
if key.scope == Scope.user_state:
return key.field_name in json.loads(field_object.state)
else:
return True

View File

@@ -165,7 +165,7 @@ class XModuleSettingsField(models.Model):
class XModuleStudentPrefsField(models.Model):
"""
Stores data set in the Scope.student_preferences scope by an xmodule field
Stores data set in the Scope.preferences scope by an xmodule field
"""
class Meta:
@@ -199,7 +199,7 @@ class XModuleStudentPrefsField(models.Model):
class XModuleStudentInfoField(models.Model):
"""
Stores data set in the Scope.student_preferences scope by an xmodule field
Stores data set in the Scope.preferences scope by an xmodule field
"""
class Meta:

View File

@@ -177,18 +177,13 @@ def get_module_for_descriptor(user, request, descriptor, model_data_cache, cours
# Intended use is as {ajax_url}/{dispatch_command}, so get rid of the trailing slash.
ajax_url = ajax_url.rstrip('/')
# Fully qualified callback URL for external queueing system
xqueue_callback_url = '{proto}://{host}'.format(
host=request.get_host(),
proto=request.META.get('HTTP_X_FORWARDED_PROTO', 'https' if request.is_secure() else 'http')
)
def make_xqueue_callback(dispatch='score_update'):
# Fully qualified callback URL for external queueing system
xqueue_callback_url = '{proto}://{host}'.format(
host=request.get_host(),
proto=request.META.get('HTTP_X_FORWARDED_PROTO', 'https' if request.is_secure() else 'http')
)
xqueue_callback_url = settings.XQUEUE_INTERFACE.get('callback_url',xqueue_callback_url) # allow override
xqueue_callback_url += reverse('xqueue_callback',
kwargs=dict(course_id=course_id,

View File

@@ -32,9 +32,9 @@ course_id = 'edX/test_course/test'
content_key = partial(LmsKeyValueStore.Key, Scope.content, None, location('def_id'))
settings_key = partial(LmsKeyValueStore.Key, Scope.settings, None, location('def_id'))
student_state_key = partial(LmsKeyValueStore.Key, Scope.student_state, 'user', location('def_id'))
student_prefs_key = partial(LmsKeyValueStore.Key, Scope.student_preferences, 'user', 'problem')
student_info_key = partial(LmsKeyValueStore.Key, Scope.student_info, 'user', None)
user_state_key = partial(LmsKeyValueStore.Key, Scope.user_state, 'user', location('def_id'))
prefs_key = partial(LmsKeyValueStore.Key, Scope.preferences, 'user', 'problem')
user_info_key = partial(LmsKeyValueStore.Key, Scope.user_info, 'user', None)
class UserFactory(factory.Factory):
@@ -115,13 +115,13 @@ class TestInvalidScopes(TestCase):
def setUp(self):
self.desc_md = {}
self.user = UserFactory.create()
self.mdc = ModelDataCache([mock_descriptor([mock_field(Scope.student_state, 'a_field')])], course_id, self.user)
self.mdc = ModelDataCache([mock_descriptor([mock_field(Scope.user_state, 'a_field')])], course_id, self.user)
self.kvs = LmsKeyValueStore(self.desc_md, self.mdc)
def test_invalid_scopes(self):
for scope in (Scope(student=True, block=BlockScope.DEFINITION),
Scope(student=False, block=BlockScope.TYPE),
Scope(student=False, block=BlockScope.ALL)):
for scope in (Scope(user=True, block=BlockScope.DEFINITION),
Scope(user=False, block=BlockScope.TYPE),
Scope(user=False, block=BlockScope.ALL)):
self.assertRaises(InvalidScopeError, self.kvs.get, LmsKeyValueStore.Key(scope, None, None, 'field'))
self.assertRaises(InvalidScopeError, self.kvs.set, LmsKeyValueStore.Key(scope, None, None, 'field'), 'value')
self.assertRaises(InvalidScopeError, self.kvs.delete, LmsKeyValueStore.Key(scope, None, None, 'field'))
@@ -134,48 +134,48 @@ class TestStudentModuleStorage(TestCase):
self.desc_md = {}
student_module = StudentModuleFactory(state=json.dumps({'a_field': 'a_value'}))
self.user = student_module.student
self.mdc = ModelDataCache([mock_descriptor([mock_field(Scope.student_state, 'a_field')])], course_id, self.user)
self.mdc = ModelDataCache([mock_descriptor([mock_field(Scope.user_state, 'a_field')])], course_id, self.user)
self.kvs = LmsKeyValueStore(self.desc_md, self.mdc)
def test_get_existing_field(self):
"Test that getting an existing field in an existing StudentModule works"
self.assertEquals('a_value', self.kvs.get(student_state_key('a_field')))
self.assertEquals('a_value', self.kvs.get(user_state_key('a_field')))
def test_get_missing_field(self):
"Test that getting a missing field from an existing StudentModule raises a KeyError"
self.assertRaises(KeyError, self.kvs.get, student_state_key('not_a_field'))
self.assertRaises(KeyError, self.kvs.get, user_state_key('not_a_field'))
def test_set_existing_field(self):
"Test that setting an existing student_state field changes the value"
self.kvs.set(student_state_key('a_field'), 'new_value')
"Test that setting an existing user_state field changes the value"
self.kvs.set(user_state_key('a_field'), 'new_value')
self.assertEquals(1, StudentModule.objects.all().count())
self.assertEquals({'a_field': 'new_value'}, json.loads(StudentModule.objects.all()[0].state))
def test_set_missing_field(self):
"Test that setting a new student_state field changes the value"
self.kvs.set(student_state_key('not_a_field'), 'new_value')
"Test that setting a new user_state field changes the value"
self.kvs.set(user_state_key('not_a_field'), 'new_value')
self.assertEquals(1, StudentModule.objects.all().count())
self.assertEquals({'a_field': 'a_value', 'not_a_field': 'new_value'}, json.loads(StudentModule.objects.all()[0].state))
def test_delete_existing_field(self):
"Test that deleting an existing field removes it from the StudentModule"
self.kvs.delete(student_state_key('a_field'))
self.kvs.delete(user_state_key('a_field'))
self.assertEquals(1, StudentModule.objects.all().count())
self.assertRaises(KeyError, self.kvs.get, student_state_key('not_a_field'))
self.assertRaises(KeyError, self.kvs.get, user_state_key('not_a_field'))
def test_delete_missing_field(self):
"Test that deleting a missing field from an existing StudentModule raises a KeyError"
self.assertRaises(KeyError, self.kvs.delete, student_state_key('not_a_field'))
self.assertRaises(KeyError, self.kvs.delete, user_state_key('not_a_field'))
self.assertEquals(1, StudentModule.objects.all().count())
self.assertEquals({'a_field': 'a_value'}, json.loads(StudentModule.objects.all()[0].state))
def test_has_existing_field(self):
"Test that `has` returns True for existing fields in StudentModules"
self.assertTrue(self.kvs.has(student_state_key('a_field')))
self.assertTrue(self.kvs.has(user_state_key('a_field')))
def test_has_missing_field(self):
"Test that `has` returns False for missing fields in StudentModule"
self.assertFalse(self.kvs.has(student_state_key('not_a_field')))
self.assertFalse(self.kvs.has(user_state_key('not_a_field')))
class TestMissingStudentModule(TestCase):
@@ -187,14 +187,14 @@ class TestMissingStudentModule(TestCase):
def test_get_field_from_missing_student_module(self):
"Test that getting a field from a missing StudentModule raises a KeyError"
self.assertRaises(KeyError, self.kvs.get, student_state_key('a_field'))
self.assertRaises(KeyError, self.kvs.get, user_state_key('a_field'))
def test_set_field_in_missing_student_module(self):
"Test that setting a field in a missing StudentModule creates the student module"
self.assertEquals(0, len(self.mdc.cache))
self.assertEquals(0, StudentModule.objects.all().count())
self.kvs.set(student_state_key('a_field'), 'a_value')
self.kvs.set(user_state_key('a_field'), 'a_value')
self.assertEquals(1, len(self.mdc.cache))
self.assertEquals(1, StudentModule.objects.all().count())
@@ -207,11 +207,11 @@ class TestMissingStudentModule(TestCase):
def test_delete_field_from_missing_student_module(self):
"Test that deleting a field from a missing StudentModule raises a KeyError"
self.assertRaises(KeyError, self.kvs.delete, student_state_key('a_field'))
self.assertRaises(KeyError, self.kvs.delete, user_state_key('a_field'))
def test_has_field_for_missing_student_module(self):
"Test that `has` returns False for missing StudentModules"
self.assertFalse(self.kvs.has(student_state_key('a_field')))
self.assertFalse(self.kvs.has(user_state_key('a_field')))
class StorageTestBase(object):
@@ -286,13 +286,13 @@ class TestContentStorage(StorageTestBase, TestCase):
class TestStudentPrefsStorage(StorageTestBase, TestCase):
factory = StudentPrefsFactory
scope = Scope.student_preferences
key_factory = student_prefs_key
scope = Scope.preferences
key_factory = prefs_key
storage_class = XModuleStudentPrefsField
class TestStudentInfoStorage(StorageTestBase, TestCase):
factory = StudentInfoFactory
scope = Scope.student_info
key_factory = student_info_key
scope = Scope.user_info
key_factory = user_info_key
storage_class = XModuleStudentInfoField

View File

@@ -1,26 +1,19 @@
import logging
from mock import MagicMock, patch
from mock import MagicMock
import datetime
import factory
import unittest
import os
from django.test import TestCase
from django.http import Http404, HttpResponse
from django.http import Http404
from django.conf import settings
from django.test.utils import override_settings
from django.contrib.auth.models import User
from django.test.client import RequestFactory
from student.models import CourseEnrollment
from xmodule.modulestore.django import modulestore, _MODULESTORES
from xmodule.modulestore.exceptions import InvalidLocationError,\
ItemNotFoundError, NoPathToItem
from xmodule.modulestore.django import modulestore
import courseware.views as views
from xmodule.modulestore import Location
from .factories import UserFactory
class Stub():
pass
@@ -55,7 +48,6 @@ class TestJumpTo(TestCase):
def test_jumpto_invalid_location(self):
location = Location('i4x', 'edX', 'toy', 'NoSuchPlace', None)
jumpto_url = '%s/%s/jump_to/%s' % ('/courses', self.course_name, location)
expected = 'courses/edX/toy/2012_Fall/courseware/Overview/'
response = self.client.get(jumpto_url)
self.assertEqual(response.status_code, 404)
@@ -124,3 +116,26 @@ class ViewsTestCase(TestCase):
request, 'bar', ())
self.assertRaisesRegexp(Http404, 'No data*', views.jump_to, request,
'dummy', self.location)
def test_no_end_on_about_page(self):
# Toy course has no course end date or about/end_date blob
self.verify_end_date(self.course_id)
def test_no_end_about_blob(self):
# test_end has a course end date, no end_date HTML blob
self.verify_end_date("edX/test_end/2012_Fall", "Sep 17, 2015")
def test_about_blob_end_date(self):
# test_about_blob_end_date has both a course end date and an end_date HTML blob.
# HTML blob wins
self.verify_end_date("edX/test_about_blob_end_date/2012_Fall", "Learning never ends")
def verify_end_date(self, course_id, expected_end_text=None):
request = self.request_factory.get("foo")
request.user = self.user
result = views.course_about(request, course_id)
if expected_end_text is not None:
self.assertContains(result, "Classes End")
self.assertContains(result, expected_end_text)
else:
self.assertNotContains(result, "Classes End")

View File

@@ -630,6 +630,7 @@ def progress(request, course_id, student_id=None):
'courseware_summary': courseware_summary,
'grade_summary': grade_summary,
'staff_access': staff_access,
'student': student,
}
context.update()

View File

@@ -6,7 +6,8 @@ Enrollments.
"""
from django.core.management.base import BaseCommand, CommandError
from student.models import CourseEnrollment, assign_default_role
from student.models import CourseEnrollment
from django_comment_client.models import assign_default_role
class Command(BaseCommand):

View File

@@ -6,7 +6,8 @@ Enrollments.
"""
from django.core.management.base import BaseCommand, CommandError
from student.models import CourseEnrollment, assign_default_role
from student.models import CourseEnrollment
from django_comment_client.models import assign_default_role
class Command(BaseCommand):

View File

@@ -0,0 +1,29 @@
"""
Reload forum (comment client) users from existing users.
"""
from django.core.management.base import BaseCommand, CommandError
from django.contrib.auth.models import User
import comment_client as cc
class Command(BaseCommand):
help = 'Reload forum (comment client) users from existing users'
def adduser(self,user):
print user
try:
cc_user = cc.User.from_django_user(user)
cc_user.save()
except Exception as err:
print "update user info to discussion failed for user with id: %s" % user
def handle(self, *args, **options):
if len(args) != 0:
uset = [User.objects.get(username=x) for x in args]
else:
uset = User.objects.all()
for user in uset:
self.adduser(user)

View File

@@ -39,12 +39,14 @@ def getip(request):
def get_commit_id(course):
return course.metadata.get('GIT_COMMIT_ID', 'No commit id')
#return course.metadata.get('GIT_COMMIT_ID', 'No commit id')
return getattr(course, 'GIT_COMMIT_ID', 'No commit id')
# getattr(def_ms.courses[reload_dir], 'GIT_COMMIT_ID','No commit id')
def set_commit_id(course, commit_id):
course.metadata['GIT_COMMIT_ID'] = commit_id
#course.metadata['GIT_COMMIT_ID'] = commit_id
setattr(course, 'GIT_COMMIT_ID', commit_id)
# setattr(def_ms.courses[reload_dir], 'GIT_COMMIT_ID', new_commit_id)
@@ -124,7 +126,8 @@ def manage_modulestores(request, reload_dir=None, commit_id=None):
#----------------------------------------
dumpfields = ['definition', 'location', 'metadata']
#dumpfields = ['definition', 'location', 'metadata']
dumpfields = ['location', 'metadata']
for cdir, course in def_ms.courses.items():
html += '<hr width="100%"/>'
@@ -133,7 +136,7 @@ def manage_modulestores(request, reload_dir=None, commit_id=None):
html += '<p>commit_id=%s</p>' % get_commit_id(course)
for field in dumpfields:
data = getattr(course, field)
data = getattr(course, field, None)
html += '<h3>%s</h3>' % field
if type(data) == dict:
html += '<ul>'

View File

@@ -15,7 +15,6 @@ from scipy.optimize import curve_fit
from django.conf import settings
from django.db.models import Sum, Max
from psychometrics.models import *
from xmodule.modulestore import Location
log = logging.getLogger("mitx.psychometrics")
@@ -246,13 +245,16 @@ def generate_plots_for_problem(problem):
yset['ydat'] = ydat
if len(ydat) > 3: # try to fit to logistic function if enough data points
cfp = curve_fit(func_2pl, xdat, ydat, [1.0, max_attempts / 2.0])
yset['fitparam'] = cfp
yset['fitpts'] = func_2pl(np.array(xdat), *cfp[0])
yset['fiterr'] = [yd - yf for (yd, yf) in zip(ydat, yset['fitpts'])]
fitx = np.linspace(xdat[0], xdat[-1], 100)
yset['fitx'] = fitx
yset['fity'] = func_2pl(np.array(fitx), *cfp[0])
try:
cfp = curve_fit(func_2pl, xdat, ydat, [1.0, max_attempts / 2.0])
yset['fitparam'] = cfp
yset['fitpts'] = func_2pl(np.array(xdat), *cfp[0])
yset['fiterr'] = [yd - yf for (yd, yf) in zip(ydat, yset['fitpts'])]
fitx = np.linspace(xdat[0], xdat[-1], 100)
yset['fitx'] = fitx
yset['fity'] = func_2pl(np.array(fitx), *cfp[0])
except Exception as err:
log.debug('Error in psychoanalyze curve fitting: %s' % err)
dataset['grade_%d' % grade] = yset
@@ -289,7 +291,7 @@ def generate_plots_for_problem(problem):
'info': '',
'data': jsdata,
'cmd': '[%s], %s' % (','.join(jsplots), axisopts),
})
})
#log.debug('plots = %s' % plots)
return msg, plots
@@ -302,12 +304,12 @@ def make_psychometrics_data_update_handler(course_id, user, module_state_key):
Construct and return a procedure which may be called to update
the PsychometricsData instance for the given StudentModule instance.
"""
sm = studentmodule.objects.get_or_create(
course_id=course_id,
student=user,
module_state_key=module_state_key,
defaults={'state': '{}', 'module_type': 'problem'},
)
sm, status = StudentModule.objects.get_or_create(
course_id=course_id,
student=user,
module_state_key=module_state_key,
defaults={'state': '{}', 'module_type': 'problem'},
)
try:
pmd = PsychometricData.objects.using(db).get(studentmodule=sm)
@@ -329,7 +331,11 @@ def make_psychometrics_data_update_handler(course_id, user, module_state_key):
return
pmd.done = done
pmd.attempts = state['attempts']
try:
pmd.attempts = state.get('attempts', 0)
except:
log.exception("no attempts for %s (state=%s)" % (sm, sm.state))
try:
checktimes = eval(pmd.checktimes) # update log of attempt timestamps
except:

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

View File

@@ -144,10 +144,20 @@
<li><div class="icon course-number"></div><p>Course Number</p><span class="course-number">${course.number}</span></li>
<li><div class="icon start"></div><p>Classes Start</p><span class="start-date">${course.start_date_text}</span></li>
## End date should come from course.xml, but this is a quick hack
% if get_course_about_section(course, "end_date"):
<li><div class="icon end"></div><p>Classes End</p><span class="final-date">${get_course_about_section(course, "end_date")}</span></li>
% endif
## We plan to ditch end_date (which is not stored in course metadata),
## but for backwards compatibility, show about/end_date blob if it exists.
% if get_course_about_section(course, "end_date") or course.end:
<li>
<div class="icon end"></div>
<p>Classes End</p><span class="final-date">
% if get_course_about_section(course, "end_date"):
${get_course_about_section(course, "end_date")}
% else:
${course.end_date_text}
% endif
</span>
</li>
% endif
% if get_course_about_section(course, "effort"):
<li><div class="icon effort"></div><p>Estimated Effort</p><span class="start-date">${get_course_about_section(course, "effort")}</span></li>

View File

@@ -31,7 +31,7 @@ ${progress_graph.body(grade_summary, course.grade_cutoffs, "grade-detail-graph",
<section class="course-info">
<header>
<h1>Course Progress</h1>
<h1>Course Progress for Student '${student.username}' (${student.email})</h1>
</header>
%if not course.disable_progress_graph:

View File

@@ -6,7 +6,16 @@
<link type="text/html" rel="alternate" href="http://blog.edx.org/"/>
<link type="application/atom+xml" rel="self" href="https://github.com/blog.atom"/>
<title>EdX Blog</title>
<updated>2013-03-15T14:00:12-07:00</updated>
<updated>2013-04-03T14:00:12-07:00</updated>
<entry>
<id>tag:www.edx.org,2012:Post/17</id>
<published>2012-12-19T14:00:00-07:00</published>
<updated>2012-12-19T14:00:00-07:00</updated>
<link type="text/html" rel="alternate" href="${reverse('press/stanford-to-work-with-edx')}"/>
<title>Stanford University to Collaborate with edX on Development of Non-Profit Open Source edX Platform</title>
<content type="html">&lt;img src=&quot;${static.url('images/press/releases/stanford-university_102x57.png')}&quot; /&gt;
&lt;p&gt;&lt;/p&gt;</content>
</entry>
<entry>
<id>tag:www.edx.org,2013:Post/16</id>
<published>2013-03-15T10:00:00-07:00</published>

View File

@@ -0,0 +1,92 @@
<%! from django.core.urlresolvers import reverse %>
<%inherit file="../../main.html" />
<%namespace name='static' file='../../static_content.html'/>
<%block name="title"><title>Stanford University to Collaborate with edX on Development of Non-Profit Open Source edX Platform</title></%block>
<div id="fb-root"></div>
<script>(function(d, s, id) {
var js, fjs = d.getElementsByTagName(s)[0];
if (d.getElementById(id)) return;
js = d.createElement(s); js.id = id;
js.src = "//connect.facebook.net/en_US/all.js#xfbml=1";
fjs.parentNode.insertBefore(js, fjs);
}(document, 'script', 'facebook-jssdk'));</script>
<section class="pressrelease">
<section class="container">
<h1>Stanford University to Collaborate with edX on Development of Non-Profit Open Source edX Platform</h1>
<hr class="horizontal-divider">
<article>
<h2>edX Learning Platform to be open source and available on June 1</h2>
<p><strong>CAMBRIDGE, MA and STANFORD, CA &ndash; April 3, 2013 &ndash;</strong>
Stanford University and <a href="https://www.edx.org">edX</a>, the not-for-profit online learning enterprise founded by Harvard University and the Massachusetts Institute of Technology (MIT), today announced their collaboration to advance the development of edXs open source learning platform and provide free and open online learning tools for institutions around the world.</p>
<p>As part of this announcement, edX will release the source code for its entire online learning platform on June 1, 2013. In support of that move, Stanford will integrate features of its existing Class2Go platform into the edX platform, use the integration as an internal platform for online coursework for on-campus and distance learners, and work collaboratively with edX and other institutions to further develop the edX platform.</p>
<p>“This collaboration brings together two leaders in online education in a common effort to ensure that the worlds universities have the strongest possible not-for-profit, open source platform available to them,” said John Mitchell, vice provost for online learning at Stanford University. “A not-for-profit, open source platform will help universities experiment with different ways to produce and share content, fostering continued innovation through a vibrant community of contributors.”</p>
<p>EdX and Stanford will collaborate along with others around the globe on the ongoing development and refinement of the edX online learning platform. As of June 1, developers everywhere will be able to freely access the source code of the edX learning platform, including code for its Learning Management System (LMS); Studio, a course authoring tool; xBlock, an application programming interface (API) for integrating third-party learning objects; and machine grading APIs. EdX will support and nurture the community of developers contributing to the enhancement of the edX platform by providing a rich environment for developer collaboration as well as technical and process guidelines to facilitate developer contributions.</p>
<p>“It has been our vision to offer our platform as open source since edXs founding by Harvard and MIT,” stated Anant Agarwal, president of edX. “We are now realizing that vision, and I am pleased to welcome Stanford University, one of the worlds leading institutions of higher education, to further this global open source solution. I want to acknowledge the key role played by our X Consortium member UC Berkeley, which was instrumental in fostering this collaboration. We believe the edX platform—the Linux of learning—will benefit from all the worlds institutions and communities.”</p>
<p>EdX is pursuing an open source vision to enhance access to higher education for the entire world. One of the chief benefits of massive open online courses (MOOCs) is that they bring together a tremendously diverse student body to learn with and from each other. EdX has chosen to extend that perspective to its learning platform as well, knowing that drawing upon the global community of developers is an effective route to both transform and deliver the worlds best and most accessible online and blended learning experience.</p>
<p>MOOCs and innovative online teaching approaches on college campuses, such as the “flipped classroom,” use web environments that support interactive video, online discussion, social/cohort interaction, assessment and other functions. Open source online learning platforms will allow universities to develop their own delivery methods, partner with other universities and institutions as they choose, collect data, and control branding of their educational material. Further developing online opportunities through open source technology is a key objective of the partnership between edX and Stanford.</p>
<p>Stanford will continue to provide a range of platforms for its instructors to choose from in hosting their online coursework, including continued partnerships with Coursera and other providers. The university will focus its ongoing platform development efforts on the new platform, combining key features from the Class2Go open source platform with the open source edX code base.</p>
<p>The edX learning platform source code, as well as platform developments from Stanford, edX and other contributors, will be available on June 1, 2013 and can be accessed from the edX Platform Repository located at <a href="https://github.com/edX">https://github.com/edX</a>.</p>
<h2>About edX</h2>
<p><a href="https://www.edx.org/">EdX</a> is a not-for-profit enterprise of its founding partners <a href="http://www.harvard.edu">Harvard University</a> and the <a href="http://www.mit.edu">Massachusetts Institute of Technology</a> focused on transforming online and on-campus learning through groundbreaking methodologies, game-like experiences and cutting-edge research. EdX provides inspirational and transformative knowledge to students of all ages, social status, and income who form worldwide communities of learners. EdX uses its open source technology to transcend physical and social borders. Were focused on people, not profit. EdX is based in Cambridge, Massachusetts in the USA.</p>
<h2>About Stanford University</h2>
<p>
<a href="http://www.stanford.edu">Stanford University</a> is engaged in a variety of efforts to develop online learning experimenting with coursework for both on-campus and off-campus students, researching key questions around what a digital environment means for teaching and learning, and pursuing platform development. More information on Stanfords online learning activities is available at <a href="http://online.stanford.edu">http://online.stanford.edu</a>
<section class="contact">
<p><strong>Media Contact:</strong></p>
<p>Dan O'Connell</p>
<p>oconnell@edx.org</p>
<p>(617) 480-6585</p>
</section>
<section class="contact">
<p>Brad Hayward</p>
<p>bhayward@stanford.edu</p>
<p>650-724-0199</p>
</section>
<section class="contact">
<p>Lisa Lapin</p>
<p>lapin@stanford.edu</p>
<p>650-725-8396</p>
</section>
<section class="footer">
<hr class="horizontal-divider">
<div class="logo"></div><h3 class="date">DATE: 01 - 29 - 2013</h3>
<div class="social-sharing">
<hr class="horizontal-divider">
<p>Share with friends and family:</p>
<a href="http://twitter.com/intent/tweet?text=:Stanford+to+work+with+edX+http://www.edx.org/press/stanford-to-work-with-edx" class="share">
<img src="${static.url('images/social/twitter-sharing.png')}">
</a>
</a>
<a href="mailto:?subject=Stanford%20to%20work%20with%20EdX…http://edx.org/press/stanford-to-work-with-edx" class="share">
<img src="${static.url('images/social/email-sharing.png')}">
</a>
<div class="fb-like" data-href="http://edx.org/press/stanford-to-work-with-edx" data-send="true" data-width="450" data-show-faces="true"></div>
</div>
</section>
</article>
</section>
</section>

View File

@@ -153,6 +153,9 @@ urlpatterns = ('',
url(r'^press/xblock_announcement$', 'static_template_view.views.render',
{'template': 'press_releases/xblock_announcement.html'},
name="press/xblock-announcement"),
url(r'^press/stanford-to-work-with-edx$', 'static_template_view.views.render',
{'template': 'press_releases/stanford_announcement.html'},
name="press/stanford-to-work-with-edx"),
# Should this always update to point to the latest press release?
(r'^pressrelease$', 'django.views.generic.simple.redirect_to',

View File

@@ -6,4 +6,4 @@
# XBlock:
# Might change frequently, so put it in local-requirements.txt,
# but conceptually is an external package, so it is in a separate repo.
-e git+ssh://git@github.com/MITx/xmodule-debugger@9a4f883a#egg=XBlock
-e git+https://github.com/edx/XBlock.git@96d8f5f4#egg=XBlock