Merged master to rc/2013-11-21
This commit is contained in:
@@ -59,23 +59,28 @@ class ResetPasswordTests(TestCase):
|
||||
self.user_bad_passwd.password = UNUSABLE_PASSWORD
|
||||
self.user_bad_passwd.save()
|
||||
|
||||
@patch('student.views.render_to_string', Mock(side_effect=mock_render_to_string, autospec=True))
|
||||
def test_user_bad_password_reset(self):
|
||||
"""Tests password reset behavior for user with password marked UNUSABLE_PASSWORD"""
|
||||
|
||||
bad_pwd_req = self.request_factory.post('/password_reset/', {'email': self.user_bad_passwd.email})
|
||||
bad_pwd_resp = password_reset(bad_pwd_req)
|
||||
# If they've got an unusable password, we return a successful response code
|
||||
self.assertEquals(bad_pwd_resp.status_code, 200)
|
||||
self.assertEquals(bad_pwd_resp.content, json.dumps({'success': False,
|
||||
'error': 'Invalid e-mail or user'}))
|
||||
self.assertEquals(bad_pwd_resp.content, json.dumps({'success': True,
|
||||
'value': "('registration/password_reset_done.html', [])"}))
|
||||
|
||||
@patch('student.views.render_to_string', Mock(side_effect=mock_render_to_string, autospec=True))
|
||||
def test_nonexist_email_password_reset(self):
|
||||
"""Now test the exception cases with of reset_password called with invalid email."""
|
||||
|
||||
bad_email_req = self.request_factory.post('/password_reset/', {'email': self.user.email+"makeItFail"})
|
||||
bad_email_resp = password_reset(bad_email_req)
|
||||
# Note: even if the email is bad, we return a successful response code
|
||||
# This prevents someone potentially trying to "brute-force" find out which emails are and aren't registered with edX
|
||||
self.assertEquals(bad_email_resp.status_code, 200)
|
||||
self.assertEquals(bad_email_resp.content, json.dumps({'success': False,
|
||||
'error': 'Invalid e-mail or user'}))
|
||||
self.assertEquals(bad_email_resp.content, json.dumps({'success': True,
|
||||
'value': "('registration/password_reset_done.html', [])"}))
|
||||
|
||||
@unittest.skipUnless(not settings.MITX_FEATURES.get('DISABLE_PASSWORD_RESET_EMAIL_TEST', False),
|
||||
dedent("""Skipping Test because CMS has not provided necessary templates for password reset.
|
||||
@@ -152,38 +157,43 @@ class CourseEndingTest(TestCase):
|
||||
{'status': 'processing',
|
||||
'show_disabled_download_button': False,
|
||||
'show_download_url': False,
|
||||
'show_survey_button': False, })
|
||||
'show_survey_button': False,
|
||||
})
|
||||
|
||||
cert_status = {'status': 'unavailable'}
|
||||
self.assertEqual(_cert_info(user, course, cert_status),
|
||||
{'status': 'processing',
|
||||
'show_disabled_download_button': False,
|
||||
'show_download_url': False,
|
||||
'show_survey_button': False})
|
||||
|
||||
cert_status = {'status': 'generating', 'grade': '67'}
|
||||
self.assertEqual(_cert_info(user, course, cert_status),
|
||||
{'status': 'generating',
|
||||
'show_disabled_download_button': True,
|
||||
'show_download_url': False,
|
||||
'show_survey_button': True,
|
||||
'survey_url': survey_url,
|
||||
'grade': '67'
|
||||
'show_survey_button': False,
|
||||
'mode': None
|
||||
})
|
||||
|
||||
cert_status = {'status': 'regenerating', 'grade': '67'}
|
||||
cert_status = {'status': 'generating', 'grade': '67', 'mode': 'honor'}
|
||||
self.assertEqual(_cert_info(user, course, cert_status),
|
||||
{'status': 'generating',
|
||||
'show_disabled_download_button': True,
|
||||
'show_download_url': False,
|
||||
'show_survey_button': True,
|
||||
'survey_url': survey_url,
|
||||
'grade': '67'
|
||||
'grade': '67',
|
||||
'mode': 'honor'
|
||||
})
|
||||
|
||||
cert_status = {'status': 'regenerating', 'grade': '67', 'mode': 'verified'}
|
||||
self.assertEqual(_cert_info(user, course, cert_status),
|
||||
{'status': 'generating',
|
||||
'show_disabled_download_button': True,
|
||||
'show_download_url': False,
|
||||
'show_survey_button': True,
|
||||
'survey_url': survey_url,
|
||||
'grade': '67',
|
||||
'mode': 'verified'
|
||||
})
|
||||
|
||||
download_url = 'http://s3.edx/cert'
|
||||
cert_status = {'status': 'downloadable', 'grade': '67',
|
||||
'download_url': download_url}
|
||||
'download_url': download_url, 'mode': 'honor'}
|
||||
self.assertEqual(_cert_info(user, course, cert_status),
|
||||
{'status': 'ready',
|
||||
'show_disabled_download_button': False,
|
||||
@@ -191,30 +201,33 @@ class CourseEndingTest(TestCase):
|
||||
'download_url': download_url,
|
||||
'show_survey_button': True,
|
||||
'survey_url': survey_url,
|
||||
'grade': '67'
|
||||
'grade': '67',
|
||||
'mode': 'honor'
|
||||
})
|
||||
|
||||
cert_status = {'status': 'notpassing', 'grade': '67',
|
||||
'download_url': download_url}
|
||||
'download_url': download_url, 'mode': 'honor'}
|
||||
self.assertEqual(_cert_info(user, course, cert_status),
|
||||
{'status': 'notpassing',
|
||||
'show_disabled_download_button': False,
|
||||
'show_download_url': False,
|
||||
'show_survey_button': True,
|
||||
'survey_url': survey_url,
|
||||
'grade': '67'
|
||||
'grade': '67',
|
||||
'mode': 'honor'
|
||||
})
|
||||
|
||||
# Test a course that doesn't have a survey specified
|
||||
course2 = Mock(end_of_course_survey_url=None)
|
||||
cert_status = {'status': 'notpassing', 'grade': '67',
|
||||
'download_url': download_url}
|
||||
'download_url': download_url, 'mode': 'honor'}
|
||||
self.assertEqual(_cert_info(user, course2, cert_status),
|
||||
{'status': 'notpassing',
|
||||
'show_disabled_download_button': False,
|
||||
'show_download_url': False,
|
||||
'show_survey_button': False,
|
||||
'grade': '67'
|
||||
'grade': '67',
|
||||
'mode': 'honor'
|
||||
})
|
||||
|
||||
|
||||
|
||||
@@ -185,7 +185,8 @@ def _cert_info(user, course, cert_status):
|
||||
default_info = {'status': default_status,
|
||||
'show_disabled_download_button': False,
|
||||
'show_download_url': False,
|
||||
'show_survey_button': False}
|
||||
'show_survey_button': False,
|
||||
}
|
||||
|
||||
if cert_status is None:
|
||||
return default_info
|
||||
@@ -203,7 +204,8 @@ def _cert_info(user, course, cert_status):
|
||||
|
||||
d = {'status': status,
|
||||
'show_download_url': status == 'ready',
|
||||
'show_disabled_download_button': status == 'generating', }
|
||||
'show_disabled_download_button': status == 'generating',
|
||||
'mode': cert_status.get('mode', None)}
|
||||
|
||||
if (status in ('generating', 'ready', 'notpassing', 'restricted') and
|
||||
course.end_of_course_survey_url is not None):
|
||||
@@ -296,7 +298,7 @@ def complete_course_mode_info(course_id, enrollment):
|
||||
def dashboard(request):
|
||||
user = request.user
|
||||
|
||||
# Build our (course, enorllment) list for the user, but ignore any courses that no
|
||||
# Build our (course, enrollment) list for the user, but ignore any courses that no
|
||||
# longer exist (because the course IDs have changed). Still, we don't delete those
|
||||
# enrollments, because it could have been a data push snafu.
|
||||
course_enrollment_pairs = []
|
||||
@@ -1229,11 +1231,8 @@ def password_reset(request):
|
||||
from_email=settings.DEFAULT_FROM_EMAIL,
|
||||
request=request,
|
||||
domain_override=request.get_host())
|
||||
return HttpResponse(json.dumps({'success': True,
|
||||
return HttpResponse(json.dumps({'success': True,
|
||||
'value': render_to_string('registration/password_reset_done.html', {})}))
|
||||
else:
|
||||
return HttpResponse(json.dumps({'success': False,
|
||||
'error': _('Invalid e-mail or user')}))
|
||||
|
||||
|
||||
def password_reset_confirm_wrapper(
|
||||
@@ -1515,4 +1514,4 @@ def change_email_settings(request):
|
||||
log.info(u"User {0} ({1}) opted out of receiving emails from course {2}".format(user.username, user.email, course_id))
|
||||
track.views.server_track(request, "change-email-settings", {"receive_emails": "no", "course": course_id}, page='dashboard')
|
||||
|
||||
return HttpResponse(json.dumps({'success': True}))
|
||||
return HttpResponse(json.dumps({'success': True}))
|
||||
|
||||
@@ -946,17 +946,34 @@ class NumericalResponse(LoncapaResponse):
|
||||
|
||||
|
||||
class StringResponse(LoncapaResponse):
|
||||
'''
|
||||
This response type allows one or more answers. Use `_or_` separator to set
|
||||
more than 1 answer.
|
||||
|
||||
Example:
|
||||
|
||||
# One answer
|
||||
<stringresponse answer="Michigan">
|
||||
<textline size="20" />
|
||||
</stringresponse >
|
||||
|
||||
# Multiple answers
|
||||
<stringresponse answer="Martin Luther King_or_Dr. Martin Luther King Jr.">
|
||||
<textline size="20" />
|
||||
</stringresponse >
|
||||
|
||||
'''
|
||||
response_tag = 'stringresponse'
|
||||
hint_tag = 'stringhint'
|
||||
allowed_inputfields = ['textline']
|
||||
required_attributes = ['answer']
|
||||
max_inputfields = 1
|
||||
correct_answer = None
|
||||
correct_answer = []
|
||||
SEPARATOR = '_or_'
|
||||
|
||||
def setup_response(self):
|
||||
self.correct_answer = contextualize_text(
|
||||
self.xml.get('answer'), self.context).strip()
|
||||
self.correct_answer = [contextualize_text(answer, self.context).strip()
|
||||
for answer in self.xml.get('answer').split(self.SEPARATOR)]
|
||||
|
||||
def get_score(self, student_answers):
|
||||
'''Grade a string response '''
|
||||
@@ -966,23 +983,25 @@ class StringResponse(LoncapaResponse):
|
||||
|
||||
def check_string(self, expected, given):
|
||||
if self.xml.get('type') == 'ci':
|
||||
return given.lower() == expected.lower()
|
||||
return given == expected
|
||||
return given.lower() in [i.lower() for i in expected]
|
||||
return given in expected
|
||||
|
||||
def check_hint_condition(self, hxml_set, student_answers):
|
||||
given = student_answers[self.answer_id].strip()
|
||||
hints_to_show = []
|
||||
for hxml in hxml_set:
|
||||
name = hxml.get('name')
|
||||
correct_answer = contextualize_text(
|
||||
hxml.get('answer'), self.context).strip()
|
||||
|
||||
correct_answer = [contextualize_text(answer, self.context).strip()
|
||||
for answer in hxml.get('answer').split(self.SEPARATOR)]
|
||||
|
||||
if self.check_string(correct_answer, given):
|
||||
hints_to_show.append(name)
|
||||
log.debug('hints_to_show = %s', hints_to_show)
|
||||
return hints_to_show
|
||||
|
||||
def get_answers(self):
|
||||
return {self.answer_id: self.correct_answer}
|
||||
return {self.answer_id: ' <b>or</b> '.join(self.correct_answer)}
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
|
||||
@@ -55,7 +55,7 @@
|
||||
% else:
|
||||
<% my_id = content_node.get('contents','') %>
|
||||
<% my_val = value.get(my_id,'') %>
|
||||
<input class="ctinput" type="text" name="${content_node['contents']}" id="${content_node['contents']}" value="${my_val|h} "/>
|
||||
<input class="ctinput" type="text" name="${content_node['contents']}" id="${content_node['contents']}" value="${my_val|h}"/>
|
||||
%endif
|
||||
<span class="mock_label">
|
||||
${content_node['tail_text']}
|
||||
|
||||
@@ -500,6 +500,7 @@ class StringResponseTest(ResponseTest):
|
||||
xml_factory_class = StringResponseXMLFactory
|
||||
|
||||
def test_case_sensitive(self):
|
||||
# Test single answer
|
||||
problem = self.build_problem(answer="Second", case_sensitive=True)
|
||||
|
||||
# Exact string should be correct
|
||||
@@ -509,7 +510,20 @@ class StringResponseTest(ResponseTest):
|
||||
self.assert_grade(problem, "Other String", "incorrect")
|
||||
self.assert_grade(problem, "second", "incorrect")
|
||||
|
||||
# Test multiple answers
|
||||
answers = ["Second", "Third", "Fourth"]
|
||||
problem = self.build_problem(answer="_or_".join(answers), case_sensitive=True)
|
||||
|
||||
for answer in answers:
|
||||
# Exact string should be correct
|
||||
self.assert_grade(problem, answer, "correct")
|
||||
|
||||
# Other strings and the lowercase version of the string are incorrect
|
||||
self.assert_grade(problem, "Other String", "incorrect")
|
||||
self.assert_grade(problem, "second", "incorrect")
|
||||
|
||||
def test_case_insensitive(self):
|
||||
# Test single answer
|
||||
problem = self.build_problem(answer="Second", case_sensitive=False)
|
||||
|
||||
# Both versions of the string should be allowed, regardless
|
||||
@@ -520,9 +534,28 @@ class StringResponseTest(ResponseTest):
|
||||
# Other strings are not allowed
|
||||
self.assert_grade(problem, "Other String", "incorrect")
|
||||
|
||||
# Test multiple answers
|
||||
answers = ["Second", "Third", "Fourth"]
|
||||
problem = self.build_problem(answer="_or_".join(answers), case_sensitive=False)
|
||||
|
||||
for answer in answers:
|
||||
# Exact string should be correct
|
||||
self.assert_grade(problem, answer, "correct")
|
||||
self.assert_grade(problem, answer.lower(), "correct")
|
||||
|
||||
# Other strings and the lowercase version of the string are incorrect
|
||||
self.assert_grade(problem, "Other String", "incorrect")
|
||||
|
||||
def test_hints(self):
|
||||
multiple_answers = [
|
||||
"Martin Luther King Junior",
|
||||
"Doctor Martin Luther King Junior",
|
||||
"Dr. Martin Luther King Jr.",
|
||||
"Martin Luther King"
|
||||
]
|
||||
hints = [("wisconsin", "wisc", "The state capital of Wisconsin is Madison"),
|
||||
("minnesota", "minn", "The state capital of Minnesota is St. Paul")]
|
||||
("minnesota", "minn", "The state capital of Minnesota is St. Paul"),
|
||||
("_or_".join(multiple_answers), "mlk", "He lead the civil right movement in the United States of America.")]
|
||||
|
||||
problem = self.build_problem(answer="Michigan",
|
||||
case_sensitive=False,
|
||||
@@ -550,6 +583,14 @@ class StringResponseTest(ResponseTest):
|
||||
correct_map = problem.grade_answers(input_dict)
|
||||
self.assertEquals(correct_map.get_hint('1_2_1'), "")
|
||||
|
||||
# We should get the same hint for each answer
|
||||
for answer in multiple_answers:
|
||||
input_dict = {'1_2_1': answer}
|
||||
correct_map = problem.grade_answers(input_dict)
|
||||
self.assertEquals(correct_map.get_hint('1_2_1'),
|
||||
"He lead the civil right movement in the United States of America.")
|
||||
|
||||
|
||||
def test_computed_hints(self):
|
||||
problem = self.build_problem(
|
||||
answer="Michigan",
|
||||
|
||||
@@ -597,6 +597,9 @@ class CourseDescriptor(CourseFields, SequenceDescriptor):
|
||||
|
||||
@property
|
||||
def raw_grader(self):
|
||||
# force the caching of the xblock value so that it can detect the change
|
||||
# pylint: disable=pointless-statement
|
||||
self.grading_policy['GRADER']
|
||||
return self._grading_policy['RAW_GRADER']
|
||||
|
||||
@raw_grader.setter
|
||||
|
||||
@@ -726,21 +726,24 @@ section.problem {
|
||||
}
|
||||
|
||||
a.full {
|
||||
@include position(absolute, 0 0 1px 0);
|
||||
@include position(absolute, 0 0px 1px 0px);
|
||||
@include box-sizing(border-box);
|
||||
display: block;
|
||||
padding: 4px;
|
||||
width: 100%;
|
||||
background: #f3f3f3;
|
||||
text-align: right;
|
||||
font-size: .8em;
|
||||
font-size: 1em;
|
||||
|
||||
&.full-top{
|
||||
@include position(absolute, 1px 0px auto 0px);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.external-grader-message {
|
||||
section {
|
||||
padding-top: $baseline/2;
|
||||
padding-top: ($baseline*1.5);
|
||||
padding-left: $baseline;
|
||||
background-color: #fafafa;
|
||||
color: #2c2c2c;
|
||||
|
||||
115
common/lib/xmodule/xmodule/js/spec/collapsible.coffee
Normal file
115
common/lib/xmodule/xmodule/js/spec/collapsible.coffee
Normal file
@@ -0,0 +1,115 @@
|
||||
describe 'Collapsible', ->
|
||||
html = custom_labels = html_custom = el = undefined
|
||||
|
||||
initialize = (template) =>
|
||||
setFixtures(template)
|
||||
el = $('.collapsible')
|
||||
Collapsible.setCollapsibles(el)
|
||||
|
||||
disableFx = () =>
|
||||
$.fx.off = true
|
||||
|
||||
enableFx = () =>
|
||||
$.fx.off = false
|
||||
|
||||
beforeEach ->
|
||||
html = '''
|
||||
<section class="collapsible">
|
||||
<div class="shortform">
|
||||
shortform message
|
||||
</div>
|
||||
<div class="longform">
|
||||
<p>longform is visible</p>
|
||||
</div>
|
||||
</section>
|
||||
'''
|
||||
html_custom = '''
|
||||
<section class="collapsible">
|
||||
<div class="shortform-custom" data-open-text="Show shortform-custom" data-close-text="Hide shortform-custom">
|
||||
shortform message
|
||||
</div>
|
||||
<div class="longform">
|
||||
<p>longform is visible</p>
|
||||
</div>
|
||||
</section>
|
||||
'''
|
||||
|
||||
describe 'setCollapsibles', ->
|
||||
|
||||
it 'Default container initialized correctly', ->
|
||||
initialize(html)
|
||||
|
||||
expect(el.find('.shortform')).toContain '.full-top'
|
||||
expect(el.find('.shortform')).toContain '.full-bottom'
|
||||
expect(el.find('.longform')).toBeHidden()
|
||||
expect(el.find('.full')).toHandle('click')
|
||||
|
||||
it 'Custom container initialized correctly', ->
|
||||
initialize(html_custom)
|
||||
|
||||
expect(el.find('.shortform-custom')).toContain '.full-custom'
|
||||
expect(el.find('.full-custom')).toHaveText "Show shortform-custom"
|
||||
expect(el.find('.longform')).toBeHidden()
|
||||
expect(el.find('.full-custom')).toHandle('click')
|
||||
|
||||
describe 'toggleFull', ->
|
||||
|
||||
beforeEach ->
|
||||
disableFx()
|
||||
|
||||
afterEach ->
|
||||
enableFx()
|
||||
|
||||
it 'Default container', ->
|
||||
initialize(html)
|
||||
|
||||
event = jQuery.Event('click', {
|
||||
target: el.find('.full').get(0)
|
||||
})
|
||||
|
||||
assertChanges = (state='closed') =>
|
||||
anchors = el.find('.full')
|
||||
|
||||
if state is 'closed'
|
||||
expect(el.find('.longform')).toBeHidden()
|
||||
expect(el).not.toHaveClass('open')
|
||||
text = "See full output"
|
||||
else
|
||||
expect(el.find('.longform')).toBeVisible()
|
||||
expect(el).toHaveClass('open')
|
||||
text = "Hide output"
|
||||
|
||||
$.each anchors, (index, el) =>
|
||||
expect(el).toHaveText text
|
||||
|
||||
Collapsible.toggleFull(event, "See full output", "Hide output")
|
||||
assertChanges('opened')
|
||||
Collapsible.toggleFull(event, "See full output", "Hide output")
|
||||
assertChanges('closed')
|
||||
|
||||
it 'Custom container', ->
|
||||
initialize(html_custom)
|
||||
|
||||
event = jQuery.Event('click', {
|
||||
target: el.find('.full-custom').get(0)
|
||||
})
|
||||
|
||||
assertChanges = (state='closed') =>
|
||||
anchors = el.find('.full-custom')
|
||||
|
||||
if state is 'closed'
|
||||
expect(el.find('.longform')).toBeHidden()
|
||||
expect(el).not.toHaveClass('open')
|
||||
text = "Show shortform-custom"
|
||||
else
|
||||
expect(el.find('.longform')).toBeVisible()
|
||||
expect(el).toHaveClass('open')
|
||||
text = "Hide shortform-custom"
|
||||
|
||||
$.each anchors, (index, el) =>
|
||||
expect(el).toHaveText text
|
||||
|
||||
Collapsible.toggleFull(event, "Show shortform-custom", "Hide shortform-custom")
|
||||
assertChanges('opened')
|
||||
Collapsible.toggleFull(event, "Show shortform-custom", "Hide shortform-custom")
|
||||
assertChanges('closed')
|
||||
@@ -104,45 +104,45 @@ describe 'MarkdownEditingDescriptor', ->
|
||||
|
||||
Enter the number of fingers on a human hand:
|
||||
= 5
|
||||
|
||||
|
||||
[Explanation]
|
||||
Pi, or the the ratio between a circle's circumference to its diameter, is an irrational number known to extreme precision. It is value is approximately equal to 3.14.
|
||||
|
||||
|
||||
Although you can get an exact value by typing 502*9 into a calculator, the result will be close to 500*10, or 5,000. The grader accepts any response within 15% of the true value, 4518, so that you can use any estimation technique that you like.
|
||||
|
||||
|
||||
If you look at your hand, you can count that you have five fingers.
|
||||
[Explanation]
|
||||
""")
|
||||
expect(data).toEqual("""<problem>
|
||||
<p>A numerical response problem accepts a line of text input from the student, and evaluates the input for correctness based on its numerical value.</p>
|
||||
|
||||
|
||||
<p>The answer is correct if it is within a specified numerical tolerance of the expected answer.</p>
|
||||
|
||||
|
||||
<p>Enter the numerical value of Pi:</p>
|
||||
<numericalresponse answer="3.14159">
|
||||
<responseparam type="tolerance" default=".02" />
|
||||
<formulaequationinput />
|
||||
</numericalresponse>
|
||||
|
||||
|
||||
<p>Enter the approximate value of 502*9:</p>
|
||||
<numericalresponse answer="4518">
|
||||
<responseparam type="tolerance" default="15%" />
|
||||
<formulaequationinput />
|
||||
</numericalresponse>
|
||||
|
||||
|
||||
<p>Enter the number of fingers on a human hand:</p>
|
||||
<numericalresponse answer="5">
|
||||
<formulaequationinput />
|
||||
</numericalresponse>
|
||||
|
||||
|
||||
<solution>
|
||||
<div class="detailed-solution">
|
||||
<p>Explanation</p>
|
||||
|
||||
|
||||
<p>Pi, or the the ratio between a circle's circumference to its diameter, is an irrational number known to extreme precision. It is value is approximately equal to 3.14.</p>
|
||||
|
||||
|
||||
<p>Although you can get an exact value by typing 502*9 into a calculator, the result will be close to 500*10, or 5,000. The grader accepts any response within 15% of the true value, 4518, so that you can use any estimation technique that you like.</p>
|
||||
|
||||
|
||||
<p>If you look at your hand, you can count that you have five fingers.</p>
|
||||
|
||||
</div>
|
||||
@@ -161,12 +161,27 @@ describe 'MarkdownEditingDescriptor', ->
|
||||
</numericalresponse>
|
||||
|
||||
|
||||
</problem>""")
|
||||
it 'markup with multiple answers doesn\'t break numerical response', ->
|
||||
data = MarkdownEditingDescriptor.markdownToXml("""
|
||||
Enter 1 with a tolerance:
|
||||
= 1 +- .02
|
||||
or= 2 +- 5%
|
||||
""")
|
||||
expect(data).toEqual("""<problem>
|
||||
<p>Enter 1 with a tolerance:</p>
|
||||
<numericalresponse answer="1">
|
||||
<responseparam type="tolerance" default=".02" />
|
||||
<formulaequationinput />
|
||||
</numericalresponse>
|
||||
|
||||
|
||||
</problem>""")
|
||||
it 'converts multiple choice to xml', ->
|
||||
data = MarkdownEditingDescriptor.markdownToXml("""A multiple choice problem presents radio buttons for student input. Students can only select a single option presented. Multiple Choice questions have been the subject of many areas of research due to the early invention and adoption of bubble sheets.
|
||||
|
||||
|
||||
One of the main elements that goes into a good multiple choice question is the existence of good distractors. That is, each of the alternate responses presented to the student should be the result of a plausible mistake that a student might make.
|
||||
|
||||
|
||||
What Apple device competed with the portable CD player?
|
||||
( ) The iPad
|
||||
( ) Napster
|
||||
@@ -174,16 +189,16 @@ describe 'MarkdownEditingDescriptor', ->
|
||||
( ) The vegetable peeler
|
||||
( ) Android
|
||||
( ) The Beatles
|
||||
|
||||
|
||||
[Explanation]
|
||||
The release of the iPod allowed consumers to carry their entire music library with them in a format that did not rely on fragile and energy-intensive spinning disks.
|
||||
[Explanation]
|
||||
""")
|
||||
expect(data).toEqual("""<problem>
|
||||
<p>A multiple choice problem presents radio buttons for student input. Students can only select a single option presented. Multiple Choice questions have been the subject of many areas of research due to the early invention and adoption of bubble sheets.</p>
|
||||
|
||||
|
||||
<p>One of the main elements that goes into a good multiple choice question is the existence of good distractors. That is, each of the alternate responses presented to the student should be the result of a plausible mistake that a student might make.</p>
|
||||
|
||||
|
||||
<p>What Apple device competed with the portable CD player?</p>
|
||||
<multiplechoiceresponse>
|
||||
<choicegroup type="MultipleChoice">
|
||||
@@ -195,76 +210,102 @@ describe 'MarkdownEditingDescriptor', ->
|
||||
<choice correct="false">The Beatles</choice>
|
||||
</choicegroup>
|
||||
</multiplechoiceresponse>
|
||||
|
||||
|
||||
<solution>
|
||||
<div class="detailed-solution">
|
||||
<p>Explanation</p>
|
||||
|
||||
|
||||
<p>The release of the iPod allowed consumers to carry their entire music library with them in a format that did not rely on fragile and energy-intensive spinning disks.</p>
|
||||
|
||||
</div>
|
||||
</solution>
|
||||
</problem>""")
|
||||
</problem>""")
|
||||
it 'converts OptionResponse to xml', ->
|
||||
data = MarkdownEditingDescriptor.markdownToXml("""OptionResponse gives a limited set of options for students to respond with, and presents those options in a format that encourages them to search for a specific answer rather than being immediately presented with options from which to recognize the correct answer.
|
||||
|
||||
|
||||
The answer options and the identification of the correct answer is defined in the <b>optioninput</b> tag.
|
||||
|
||||
|
||||
Translation between Option Response and __________ is extremely straightforward:
|
||||
[[(Multiple Choice), String Response, Numerical Response, External Response, Image Response]]
|
||||
|
||||
|
||||
[Explanation]
|
||||
Multiple Choice also allows students to select from a variety of pre-written responses, although the format makes it easier for students to read very long response options. Optionresponse also differs slightly because students are more likely to think of an answer and then search for it rather than relying purely on recognition to answer the question.
|
||||
[Explanation]
|
||||
""")
|
||||
expect(data).toEqual("""<problem>
|
||||
<p>OptionResponse gives a limited set of options for students to respond with, and presents those options in a format that encourages them to search for a specific answer rather than being immediately presented with options from which to recognize the correct answer.</p>
|
||||
|
||||
|
||||
<p>The answer options and the identification of the correct answer is defined in the <b>optioninput</b> tag.</p>
|
||||
|
||||
|
||||
<p>Translation between Option Response and __________ is extremely straightforward:</p>
|
||||
|
||||
|
||||
<optionresponse>
|
||||
<optioninput options="('Multiple Choice','String Response','Numerical Response','External Response','Image Response')" correct="Multiple Choice"></optioninput>
|
||||
</optionresponse>
|
||||
|
||||
|
||||
<solution>
|
||||
<div class="detailed-solution">
|
||||
<p>Explanation</p>
|
||||
|
||||
|
||||
<p>Multiple Choice also allows students to select from a variety of pre-written responses, although the format makes it easier for students to read very long response options. Optionresponse also differs slightly because students are more likely to think of an answer and then search for it rather than relying purely on recognition to answer the question.</p>
|
||||
|
||||
</div>
|
||||
</solution>
|
||||
</problem>""")
|
||||
</problem>""")
|
||||
it 'converts StringResponse to xml', ->
|
||||
data = MarkdownEditingDescriptor.markdownToXml("""A string response problem accepts a line of text input from the student, and evaluates the input for correctness based on an expected answer within each input box.
|
||||
|
||||
|
||||
The answer is correct if it matches every character of the expected answer. This can be a problem with international spelling, dates, or anything where the format of the answer is not clear.
|
||||
|
||||
|
||||
Which US state has Lansing as its capital?
|
||||
= Michigan
|
||||
|
||||
|
||||
[Explanation]
|
||||
Lansing is the capital of Michigan, although it is not Michgan's largest city, or even the seat of the county in which it resides.
|
||||
[Explanation]
|
||||
""")
|
||||
expect(data).toEqual("""<problem>
|
||||
<p>A string response problem accepts a line of text input from the student, and evaluates the input for correctness based on an expected answer within each input box.</p>
|
||||
|
||||
|
||||
<p>The answer is correct if it matches every character of the expected answer. This can be a problem with international spelling, dates, or anything where the format of the answer is not clear.</p>
|
||||
|
||||
|
||||
<p>Which US state has Lansing as its capital?</p>
|
||||
<stringresponse answer="Michigan" type="ci">
|
||||
<textline size="20"/>
|
||||
</stringresponse>
|
||||
|
||||
|
||||
<solution>
|
||||
<div class="detailed-solution">
|
||||
<p>Explanation</p>
|
||||
|
||||
|
||||
<p>Lansing is the capital of Michigan, although it is not Michgan's largest city, or even the seat of the county in which it resides.</p>
|
||||
|
||||
</div>
|
||||
</solution>
|
||||
</problem>""")
|
||||
it 'converts StringResponse with multiple answers to xml', ->
|
||||
data = MarkdownEditingDescriptor.markdownToXml("""Who lead the civil right movement in the United States of America?
|
||||
= Dr. Martin Luther King Jr.
|
||||
or= Doctor Martin Luther King Junior
|
||||
or= Martin Luther King
|
||||
or= Martin Luther King Junior
|
||||
|
||||
[Explanation]
|
||||
Test Explanation.
|
||||
[Explanation]
|
||||
""")
|
||||
expect(data).toEqual("""<problem>
|
||||
<p>Who lead the civil right movement in the United States of America?</p>
|
||||
<stringresponse answer="Dr. Martin Luther King Jr._or_Doctor Martin Luther King Junior_or_Martin Luther King_or_Martin Luther King Junior" type="ci">
|
||||
<textline size="20"/>
|
||||
</stringresponse>
|
||||
|
||||
<solution>
|
||||
<div class="detailed-solution">
|
||||
<p>Explanation</p>
|
||||
|
||||
<p>Test Explanation.</p>
|
||||
|
||||
</div>
|
||||
</solution>
|
||||
</problem>""")
|
||||
@@ -273,26 +314,26 @@ describe 'MarkdownEditingDescriptor', ->
|
||||
data = MarkdownEditingDescriptor.markdownToXml("""Not a header
|
||||
A header
|
||||
==============
|
||||
|
||||
|
||||
Multiple choice w/ parentheticals
|
||||
( ) option (with parens)
|
||||
( ) xd option (x)
|
||||
()) parentheses inside
|
||||
() no space b4 close paren
|
||||
|
||||
|
||||
Choice checks
|
||||
[ ] option1 [x]
|
||||
[x] correct
|
||||
[x] redundant
|
||||
[(] distractor
|
||||
[] no space
|
||||
|
||||
|
||||
Option with multiple correct ones
|
||||
[[one option, (correct one), (should not be correct)]]
|
||||
|
||||
|
||||
Option with embedded parens
|
||||
[[My (heart), another, (correct)]]
|
||||
|
||||
|
||||
What happens w/ empty correct options?
|
||||
[[()]]
|
||||
|
||||
@@ -300,21 +341,21 @@ describe 'MarkdownEditingDescriptor', ->
|
||||
|
||||
[explanation]
|
||||
orphaned start
|
||||
|
||||
|
||||
No p tags in the below
|
||||
<script type='javascript'>
|
||||
var two = 2;
|
||||
|
||||
console.log(two * 2);
|
||||
</script>
|
||||
|
||||
|
||||
But in this there should be
|
||||
<div>
|
||||
Great ideas require offsetting.
|
||||
|
||||
|
||||
bad tests require drivel
|
||||
</div>
|
||||
|
||||
|
||||
[code]
|
||||
Code should be nicely monospaced.
|
||||
[/code]
|
||||
@@ -322,7 +363,7 @@ describe 'MarkdownEditingDescriptor', ->
|
||||
expect(data).toEqual("""<problem>
|
||||
<p>Not a header</p>
|
||||
<h1>A header</h1>
|
||||
|
||||
|
||||
<p>Multiple choice w/ parentheticals</p>
|
||||
<multiplechoiceresponse>
|
||||
<choicegroup type="MultipleChoice">
|
||||
@@ -332,7 +373,7 @@ describe 'MarkdownEditingDescriptor', ->
|
||||
<choice correct="false">no space b4 close paren</choice>
|
||||
</choicegroup>
|
||||
</multiplechoiceresponse>
|
||||
|
||||
|
||||
<p>Choice checks</p>
|
||||
<choiceresponse>
|
||||
<checkboxgroup direction="vertical">
|
||||
@@ -343,25 +384,25 @@ describe 'MarkdownEditingDescriptor', ->
|
||||
<choice correct="false">no space</choice>
|
||||
</checkboxgroup>
|
||||
</choiceresponse>
|
||||
|
||||
|
||||
<p>Option with multiple correct ones</p>
|
||||
|
||||
|
||||
<optionresponse>
|
||||
<optioninput options="('one option','correct one','should not be correct')" correct="correct one"></optioninput>
|
||||
</optionresponse>
|
||||
|
||||
|
||||
<p>Option with embedded parens</p>
|
||||
|
||||
|
||||
<optionresponse>
|
||||
<optioninput options="('My (heart)','another','correct')" correct="correct"></optioninput>
|
||||
</optionresponse>
|
||||
|
||||
|
||||
<p>What happens w/ empty correct options?</p>
|
||||
|
||||
|
||||
<optionresponse>
|
||||
<optioninput options="('')" correct=""></optioninput>
|
||||
</optionresponse>
|
||||
|
||||
|
||||
<solution>
|
||||
<div class="detailed-solution">
|
||||
<p>Explanation</p>
|
||||
@@ -379,14 +420,14 @@ describe 'MarkdownEditingDescriptor', ->
|
||||
|
||||
console.log(two * 2);
|
||||
</script>
|
||||
|
||||
|
||||
<p>But in this there should be</p>
|
||||
<div>
|
||||
<p>Great ideas require offsetting.</p>
|
||||
|
||||
|
||||
<p>bad tests require drivel</p>
|
||||
</div>
|
||||
|
||||
|
||||
<pre><code>
|
||||
Code should be nicely monospaced.
|
||||
</code></pre>
|
||||
|
||||
@@ -271,7 +271,17 @@
|
||||
});
|
||||
|
||||
// Disabled 10/29/13 due to flakiness in master
|
||||
xdescribe('multiple YT on page', function () {
|
||||
//
|
||||
// Update: Turned on test back again. Passing locally and on
|
||||
// Jenkins in a large number of runs.
|
||||
//
|
||||
// Will observe for a little while to see if any failures arise.
|
||||
// Most probable cause of test passing is:
|
||||
//
|
||||
// https://github.com/edx/edx-platform/pull/1642
|
||||
//
|
||||
// : )
|
||||
describe('multiple YT on page', function () {
|
||||
var state1, state2, state3;
|
||||
|
||||
beforeEach(function () {
|
||||
|
||||
@@ -457,7 +457,17 @@
|
||||
});
|
||||
|
||||
// Disabled 10/25/13 due to flakiness in master
|
||||
xit('scroll caption to new position', function () {
|
||||
//
|
||||
// Update: Turned on test back again. Passing locally and on
|
||||
// Jenkins in a large number of runs.
|
||||
//
|
||||
// Will observe for a little while to see if any failures arise.
|
||||
// Most probable cause of test passing is:
|
||||
//
|
||||
// https://github.com/edx/edx-platform/pull/1642
|
||||
//
|
||||
// : )
|
||||
it('scroll caption to new position', function () {
|
||||
expect($.fn.scrollTo).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -538,7 +548,17 @@
|
||||
});
|
||||
|
||||
// Disabled 10/23/13 due to flakiness in master
|
||||
xdescribe('scrollCaption', function () {
|
||||
//
|
||||
// Update: Turned on test back again. Passing locally and on
|
||||
// Jenkins in a large number of runs.
|
||||
//
|
||||
// Will observe for a little while to see if any failures arise.
|
||||
// Most probable cause of test passing is:
|
||||
//
|
||||
// https://github.com/edx/edx-platform/pull/1642
|
||||
//
|
||||
// : )
|
||||
describe('scrollCaption', function () {
|
||||
beforeEach(function () {
|
||||
initialize();
|
||||
});
|
||||
@@ -683,7 +703,17 @@
|
||||
});
|
||||
|
||||
// Test turned off due to flakiness (30.10.2013).
|
||||
xit('scroll the caption', function () {
|
||||
//
|
||||
// Update: Turned on test back again. Passing locally and on
|
||||
// Jenkins in a large number of runs.
|
||||
//
|
||||
// Will observe for a little while to see if any failures arise.
|
||||
// Most probable cause of test passing is:
|
||||
//
|
||||
// https://github.com/edx/edx-platform/pull/1642
|
||||
//
|
||||
// : )
|
||||
it('scroll the caption', function () {
|
||||
// After transcripts are shown, and the video plays for a
|
||||
// bit.
|
||||
jasmine.Clock.tick(1000);
|
||||
|
||||
@@ -72,7 +72,17 @@
|
||||
expect(state.focusGrabber.disableFocusGrabber).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('after controls hide focus grabbers are enabled', function () {
|
||||
// Disabled on 18.11.2013 due to flakiness on local dev machine.
|
||||
//
|
||||
// Video FocusGrabber: after controls hide focus grabbers are
|
||||
// enabled [fail]
|
||||
// Expected spy enableFocusGrabber to have been called.
|
||||
//
|
||||
// Approximately 1 in 8 times this test fails.
|
||||
//
|
||||
// TODO: Most likely, focusGrabber will be disabled in the future. This
|
||||
// test could become unneeded in the future.
|
||||
xit('after controls hide focus grabbers are enabled', function () {
|
||||
runs(function () {
|
||||
// Captions should not be "sticky" for the autohide mechanism
|
||||
// to work.
|
||||
|
||||
@@ -4,11 +4,21 @@
|
||||
videoProgressSlider, videoSpeedControl, videoVolumeControl,
|
||||
oldOTBD;
|
||||
|
||||
function initialize(fixture) {
|
||||
if (typeof fixture === 'undefined') {
|
||||
loadFixtures('video_all.html');
|
||||
} else {
|
||||
function initialize(fixture, params) {
|
||||
if (_.isString(fixture)) {
|
||||
loadFixtures(fixture);
|
||||
} else {
|
||||
if (_.isObject(fixture)) {
|
||||
params = fixture;
|
||||
}
|
||||
|
||||
loadFixtures('video_all.html');
|
||||
}
|
||||
|
||||
if (_.isObject(params)) {
|
||||
$('#example')
|
||||
.find('#video_id')
|
||||
.data(params);
|
||||
}
|
||||
|
||||
state = new Video('#example');
|
||||
@@ -532,8 +542,54 @@
|
||||
});
|
||||
});
|
||||
|
||||
// Disabled 10/24/13 due to flakiness in master
|
||||
xdescribe('updatePlayTime', function () {
|
||||
describe('update with start & end time', function () {
|
||||
var START_TIME = 1, END_TIME = 2;
|
||||
|
||||
beforeEach(function () {
|
||||
initialize({start: START_TIME, end: END_TIME});
|
||||
|
||||
spyOn(videoPlayer, 'update').andCallThrough();
|
||||
spyOn(videoPlayer, 'pause').andCallThrough();
|
||||
spyOn(videoProgressSlider, 'notifyThroughHandleEnd')
|
||||
.andCallThrough();
|
||||
});
|
||||
|
||||
it('video is paused on first endTime, start & end time are reset', function () {
|
||||
var checkForStartEndTimeSet = true;
|
||||
|
||||
videoProgressSlider.notifyThroughHandleEnd.reset();
|
||||
videoPlayer.pause.reset();
|
||||
videoPlayer.play();
|
||||
|
||||
waitsFor(function () {
|
||||
if (
|
||||
!isFinite(videoPlayer.currentTime) ||
|
||||
videoPlayer.currentTime <= 0
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (checkForStartEndTimeSet) {
|
||||
checkForStartEndTimeSet = false;
|
||||
|
||||
expect(videoPlayer.startTime).toBe(START_TIME);
|
||||
expect(videoPlayer.endTime).toBe(END_TIME);
|
||||
}
|
||||
|
||||
return videoPlayer.pause.calls.length === 1
|
||||
}, 5000, 'pause() has been called');
|
||||
|
||||
runs(function () {
|
||||
expect(videoPlayer.startTime).toBe(0);
|
||||
expect(videoPlayer.endTime).toBe(null);
|
||||
|
||||
expect(videoProgressSlider.notifyThroughHandleEnd)
|
||||
.toHaveBeenCalledWith({end: true});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('updatePlayTime', function () {
|
||||
beforeEach(function () {
|
||||
initialize();
|
||||
|
||||
@@ -548,7 +604,7 @@
|
||||
duration = videoPlayer.duration();
|
||||
|
||||
if (duration > 0) {
|
||||
return true;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
@@ -612,6 +668,74 @@
|
||||
});
|
||||
});
|
||||
|
||||
describe('updatePlayTime when start & end times are defined', function () {
|
||||
var START_TIME = 1,
|
||||
END_TIME = 2;
|
||||
|
||||
beforeEach(function () {
|
||||
initialize({start: START_TIME, end: END_TIME});
|
||||
|
||||
spyOn(videoPlayer, 'updatePlayTime').andCallThrough();
|
||||
spyOn(videoPlayer.player, 'seekTo').andCallThrough();
|
||||
spyOn(videoProgressSlider, 'updateStartEndTimeRegion')
|
||||
.andCallThrough();
|
||||
});
|
||||
|
||||
it('when duration becomes available, updatePlayTime() is called', function () {
|
||||
var duration;
|
||||
|
||||
expect(videoPlayer.initialSeekToStartTime).toBeTruthy();
|
||||
expect(videoPlayer.seekToStartTimeOldSpeed).toBe('void');
|
||||
|
||||
videoPlayer.play();
|
||||
|
||||
waitsFor(function () {
|
||||
duration = videoPlayer.duration();
|
||||
|
||||
return duration > 0 &&
|
||||
videoPlayer.initialSeekToStartTime === false;
|
||||
}, 'duration becomes available', 1000);
|
||||
|
||||
runs(function () {
|
||||
expect(videoPlayer.startTime).toBe(START_TIME);
|
||||
expect(videoPlayer.endTime).toBe(END_TIME);
|
||||
|
||||
expect(videoPlayer.player.seekTo).toHaveBeenCalledWith(START_TIME);
|
||||
|
||||
expect(videoProgressSlider.updateStartEndTimeRegion)
|
||||
.toHaveBeenCalledWith({duration: duration});
|
||||
|
||||
expect(videoPlayer.seekToStartTimeOldSpeed).toBe(state.speed);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('updatePlayTime with invalid endTime', function () {
|
||||
beforeEach(function () {
|
||||
initialize({end: 100000});
|
||||
|
||||
spyOn(videoPlayer, 'updatePlayTime').andCallThrough();
|
||||
});
|
||||
|
||||
it('invalid endTime is reset to null', function () {
|
||||
var duration;
|
||||
|
||||
videoPlayer.updatePlayTime.reset();
|
||||
videoPlayer.play();
|
||||
|
||||
waitsFor(function () {
|
||||
duration = videoPlayer.duration();
|
||||
|
||||
return duration > 0 &&
|
||||
videoPlayer.initialSeekToStartTime === false;
|
||||
}, 'updatePlayTime was invoked and duration is set', 5000);
|
||||
|
||||
runs(function () {
|
||||
expect(videoPlayer.endTime).toBe(null);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('toggleFullScreen', function () {
|
||||
describe('when the video player is not full screen', function () {
|
||||
beforeEach(function () {
|
||||
|
||||
@@ -154,7 +154,17 @@
|
||||
});
|
||||
|
||||
// Turned off test due to flakiness (30.10.2013).
|
||||
xit('trigger seek event', function() {
|
||||
//
|
||||
// Update: Turned on test back again. Passing locally and on
|
||||
// Jenkins in a large number of runs.
|
||||
//
|
||||
// Will observe for a little while to see if any failures arise.
|
||||
// Most probable cause of test passing is:
|
||||
//
|
||||
// https://github.com/edx/edx-platform/pull/1642
|
||||
//
|
||||
// : )
|
||||
it('trigger seek event', function() {
|
||||
runs(function () {
|
||||
videoProgressSlider.onSlide(
|
||||
jQuery.Event('slide'), { value: 20 }
|
||||
@@ -220,7 +230,17 @@
|
||||
});
|
||||
|
||||
// Turned off test due to flakiness (30.10.2013).
|
||||
xit('trigger seek event', function() {
|
||||
//
|
||||
// Update: Turned on test back again. Passing locally and on
|
||||
// Jenkins in a large number of runs.
|
||||
//
|
||||
// Will observe for a little while to see if any failures arise.
|
||||
// Most probable cause of test passing is:
|
||||
//
|
||||
// https://github.com/edx/edx-platform/pull/1642
|
||||
//
|
||||
// : )
|
||||
it('trigger seek event', function() {
|
||||
runs(function () {
|
||||
videoProgressSlider.onStop(
|
||||
jQuery.Event('stop'), { value: 20 }
|
||||
@@ -285,6 +305,55 @@
|
||||
expect(params).toEqual(expectedParams);
|
||||
});
|
||||
});
|
||||
|
||||
describe('notifyThroughHandleEnd', function () {
|
||||
beforeEach(function () {
|
||||
initialize();
|
||||
|
||||
spyOnEvent(videoProgressSlider.handle, 'focus');
|
||||
spyOn(videoProgressSlider, 'notifyThroughHandleEnd')
|
||||
.andCallThrough();
|
||||
});
|
||||
|
||||
it('params.end = true', function () {
|
||||
videoProgressSlider.notifyThroughHandleEnd({end: true});
|
||||
|
||||
expect(videoProgressSlider.handle.attr('title'))
|
||||
.toBe('video ended');
|
||||
|
||||
expect('focus').toHaveBeenTriggeredOn(videoProgressSlider.handle);
|
||||
});
|
||||
|
||||
it('params.end = false', function () {
|
||||
videoProgressSlider.notifyThroughHandleEnd({end: false});
|
||||
|
||||
expect(videoProgressSlider.handle.attr('title'))
|
||||
.toBe('video position');
|
||||
|
||||
expect('focus').not.toHaveBeenTriggeredOn(videoProgressSlider.handle);
|
||||
});
|
||||
|
||||
it('is called when video plays', function () {
|
||||
videoPlayer.play();
|
||||
|
||||
waitsFor(function () {
|
||||
var duration = videoPlayer.duration(),
|
||||
currentTime = videoPlayer.currentTime;
|
||||
|
||||
return (
|
||||
isFinite(duration) &&
|
||||
duration > 0 &&
|
||||
isFinite(currentTime) &&
|
||||
currentTime > 0
|
||||
);
|
||||
}, 'duration is set, video is playing', 5000);
|
||||
|
||||
runs(function () {
|
||||
expect(videoProgressSlider.notifyThroughHandleEnd)
|
||||
.toHaveBeenCalledWith({end: false});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
}).call(this);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
class @Collapsible
|
||||
|
||||
# Set of library functions that provide a simple way to add collapsible
|
||||
# functionality to elements.
|
||||
# functionality to elements.
|
||||
|
||||
# setCollapsibles:
|
||||
# Scan element's content for generic collapsible containers
|
||||
@@ -9,12 +9,15 @@ class @Collapsible
|
||||
###
|
||||
el: container
|
||||
###
|
||||
linkTop = '<a href="#" class="full full-top">See full output</a>'
|
||||
linkBottom = '<a href="#" class="full full-bottom">See full output</a>'
|
||||
|
||||
# standard longform + shortfom pattern
|
||||
el.find('.longform').hide()
|
||||
el.find('.shortform').append('<a href="#" class="full">See full output</a>')
|
||||
el.find('.shortform').append(linkTop, linkBottom)
|
||||
|
||||
# custom longform + shortform text pattern
|
||||
short_custom = el.find('.shortform-custom')
|
||||
short_custom = el.find('.shortform-custom')
|
||||
# set up each one individually
|
||||
short_custom.each (index, elt) =>
|
||||
open_text = $(elt).data('open-text')
|
||||
@@ -31,13 +34,18 @@ class @Collapsible
|
||||
|
||||
@toggleFull: (event, open_text, close_text) =>
|
||||
event.preventDefault()
|
||||
$(event.target).parent().siblings().slideToggle()
|
||||
$(event.target).parent().parent().toggleClass('open')
|
||||
parent = $(event.target).parent()
|
||||
parent.siblings().slideToggle()
|
||||
parent.parent().toggleClass('open')
|
||||
if $(event.target).text() == open_text
|
||||
new_text = close_text
|
||||
else
|
||||
new_text = open_text
|
||||
$(event.target).text(new_text)
|
||||
if $(event.target).hasClass('full')
|
||||
el = parent.find('.full')
|
||||
else
|
||||
el = $(event.target)
|
||||
el.text(new_text)
|
||||
|
||||
@toggleHint: (event) =>
|
||||
event.preventDefault()
|
||||
|
||||
@@ -228,11 +228,13 @@ class @MarkdownEditingDescriptor extends XModule.Descriptor
|
||||
});
|
||||
|
||||
// replace string and numerical
|
||||
xml = xml.replace(/^\=\s*(.*?$)/gm, function(match, p) {
|
||||
var string;
|
||||
var floatValue = parseFloat(p);
|
||||
xml = xml.replace(/(^\=\s*(.*?$)(\n*or\=\s*(.*?$))*)+/gm, function(match, p) {
|
||||
var string,
|
||||
answersList = p.replace(/^(or)?=\s*/gm, '').split('\n'),
|
||||
floatValue = parseFloat(answersList[0]);
|
||||
|
||||
if(!isNaN(floatValue)) {
|
||||
var params = /(.*?)\+\-\s*(.*?$)/.exec(p);
|
||||
var params = /(.*?)\+\-\s*(.*?$)/.exec(answersList[0]);
|
||||
if(params) {
|
||||
string = '<numericalresponse answer="' + floatValue + '">\n';
|
||||
string += ' <responseparam type="tolerance" default="' + params[2] + '" />\n';
|
||||
@@ -242,10 +244,16 @@ class @MarkdownEditingDescriptor extends XModule.Descriptor
|
||||
string += ' <formulaequationinput />\n';
|
||||
string += '</numericalresponse>\n\n';
|
||||
} else {
|
||||
string = '<stringresponse answer="' + p + '" type="ci">\n <textline size="20"/>\n</stringresponse>\n\n';
|
||||
var answers = [];
|
||||
|
||||
for(var i = 0; i < answersList.length; i++) {
|
||||
answers.push(answersList[i])
|
||||
}
|
||||
|
||||
string = '<stringresponse answer="' + answers.join('_or_') + '" type="ci">\n <textline size="20"/>\n</stringresponse>\n\n';
|
||||
}
|
||||
return string;
|
||||
});
|
||||
});
|
||||
|
||||
// replace selects
|
||||
xml = xml.replace(/\[\[(.+?)\]\]/g, function(match, p) {
|
||||
@@ -262,13 +270,13 @@ class @MarkdownEditingDescriptor extends XModule.Descriptor
|
||||
selectString += '</optionresponse>\n\n';
|
||||
return selectString;
|
||||
});
|
||||
|
||||
|
||||
// replace explanations
|
||||
xml = xml.replace(/\[explanation\]\n?([^\]]*)\[\/?explanation\]/gmi, function(match, p1) {
|
||||
var selectString = '<solution>\n<div class="detailed-solution">\nExplanation\n\n' + p1 + '\n</div>\n</solution>';
|
||||
return selectString;
|
||||
});
|
||||
|
||||
|
||||
// replace code blocks
|
||||
xml = xml.replace(/\[code\]\n?([^\]]*)\[\/?code\]/gmi, function(match, p1) {
|
||||
var selectString = '<pre><code>\n' + p1 + '</code></pre>';
|
||||
@@ -293,7 +301,7 @@ class @MarkdownEditingDescriptor extends XModule.Descriptor
|
||||
|
||||
// rid white space
|
||||
xml = xml.replace(/\n\n\n/g, '\n');
|
||||
|
||||
|
||||
// surround w/ problem tag
|
||||
xml = '<problem>\n' + xml + '\n</problem>';
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
// VideoPlayer module.
|
||||
define(
|
||||
'video/03_video_player.js',
|
||||
['video/02_html5_video.js', 'video/00_resizer.js' ],
|
||||
['video/02_html5_video.js', 'video/00_resizer.js'],
|
||||
function (HTML5Video, Resizer) {
|
||||
var dfd = $.Deferred();
|
||||
|
||||
@@ -83,11 +83,9 @@ function (HTML5Video, Resizer) {
|
||||
|
||||
state.videoPlayer.initialSeekToStartTime = true;
|
||||
|
||||
state.videoPlayer.oneTimePauseAtEndTime = true;
|
||||
|
||||
// The initial value of the variable `seekToStartTimeOldSpeed`
|
||||
// should always differ from the value returned by the duration
|
||||
// function.
|
||||
// At the start, the initial value of the variable
|
||||
// `seekToStartTimeOldSpeed` should always differ from the value
|
||||
// returned by the duration function.
|
||||
state.videoPlayer.seekToStartTimeOldSpeed = 'void';
|
||||
|
||||
state.videoPlayer.playerVars = {
|
||||
@@ -215,8 +213,7 @@ function (HTML5Video, Resizer) {
|
||||
|
||||
// This function gets the video's current play position in time
|
||||
// (currentTime) and its duration.
|
||||
// It is called at a regular interval when the video is playing (see
|
||||
// below).
|
||||
// It is called at a regular interval when the video is playing.
|
||||
function update() {
|
||||
this.videoPlayer.currentTime = this.videoPlayer.player
|
||||
.getCurrentTime();
|
||||
@@ -224,22 +221,28 @@ function (HTML5Video, Resizer) {
|
||||
if (isFinite(this.videoPlayer.currentTime)) {
|
||||
this.videoPlayer.updatePlayTime(this.videoPlayer.currentTime);
|
||||
|
||||
// We need to pause the video is current time is smaller (or equal)
|
||||
// than end time. Also, we must make sure that the end time is the
|
||||
// one that was set in the configuration parameter. If it differs,
|
||||
// this means that it was either reset to the end, or the duration
|
||||
// changed it's value.
|
||||
// We need to pause the video if current time is smaller (or equal)
|
||||
// than end time. Also, we must make sure that this is only done
|
||||
// once.
|
||||
//
|
||||
// In the case of YouTube Flash mode, we must remember that the
|
||||
// start and end times are rescaled based on the current speed of
|
||||
// the video.
|
||||
// If `endTime` is not `null`, then we are safe to pause the
|
||||
// video. `endTime` will be set to `null`, and this if statement
|
||||
// will not be executed on next runs.
|
||||
if (
|
||||
this.videoPlayer.endTime <= this.videoPlayer.currentTime &&
|
||||
this.videoPlayer.oneTimePauseAtEndTime
|
||||
this.videoPlayer.endTime != null &&
|
||||
this.videoPlayer.endTime <= this.videoPlayer.currentTime
|
||||
) {
|
||||
this.videoPlayer.oneTimePauseAtEndTime = false;
|
||||
this.videoPlayer.pause();
|
||||
this.videoPlayer.endTime = this.videoPlayer.duration();
|
||||
|
||||
// After the first time the video reached the `endTime`,
|
||||
// `startTime` and `endTime` are disabled. The video will play
|
||||
// from start to the end on subsequent runs.
|
||||
this.videoPlayer.startTime = 0;
|
||||
this.videoPlayer.endTime = null;
|
||||
|
||||
this.trigger('videoProgressSlider.notifyThroughHandleEnd', {
|
||||
end: true
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -321,8 +324,10 @@ function (HTML5Video, Resizer) {
|
||||
}
|
||||
);
|
||||
|
||||
// After the user seeks, startTime and endTime are disabled. The video
|
||||
// will play from start to the end on subsequent runs.
|
||||
this.videoPlayer.startTime = 0;
|
||||
this.videoPlayer.endTime = duration;
|
||||
this.videoPlayer.endTime = null;
|
||||
|
||||
this.videoPlayer.player.seekTo(newTime, true);
|
||||
|
||||
@@ -344,11 +349,21 @@ function (HTML5Video, Resizer) {
|
||||
var time = this.videoPlayer.duration();
|
||||
|
||||
this.trigger('videoControl.pause', null);
|
||||
this.trigger('videoProgressSlider.notifyThroughHandleEnd', {
|
||||
end: true
|
||||
});
|
||||
|
||||
if (this.config.show_captions) {
|
||||
this.trigger('videoCaption.pause', null);
|
||||
}
|
||||
|
||||
// When only `startTime` is set, the video will play to the end
|
||||
// starting at `startTime`. After the first time the video reaches the
|
||||
// end, `startTime` and `endTime` are disabled. The video will play
|
||||
// from start to the end on subsequent runs.
|
||||
this.videoPlayer.startTime = 0;
|
||||
this.videoPlayer.endTime = null;
|
||||
|
||||
// Sometimes `onEnded` events fires when `currentTime` not equal
|
||||
// `duration`. In this case, slider doesn't reach the end point of
|
||||
// timeline.
|
||||
@@ -391,6 +406,10 @@ function (HTML5Video, Resizer) {
|
||||
|
||||
this.trigger('videoControl.play', null);
|
||||
|
||||
this.trigger('videoProgressSlider.notifyThroughHandleEnd', {
|
||||
end: false
|
||||
});
|
||||
|
||||
if (this.config.show_captions) {
|
||||
this.trigger('videoCaption.play', null);
|
||||
}
|
||||
@@ -531,7 +550,7 @@ function (HTML5Video, Resizer) {
|
||||
|
||||
function updatePlayTime(time) {
|
||||
var duration = this.videoPlayer.duration(),
|
||||
durationChange;
|
||||
durationChange, tempStartTime, tempEndTime;
|
||||
|
||||
if (
|
||||
duration > 0 &&
|
||||
@@ -545,13 +564,23 @@ function (HTML5Video, Resizer) {
|
||||
this.videoPlayer.initialSeekToStartTime === false
|
||||
) {
|
||||
durationChange = true;
|
||||
} else {
|
||||
} else { // this.videoPlayer.initialSeekToStartTime === true
|
||||
this.videoPlayer.initialSeekToStartTime = false;
|
||||
|
||||
durationChange = false;
|
||||
}
|
||||
|
||||
this.videoPlayer.initialSeekToStartTime = false;
|
||||
this.videoPlayer.seekToStartTimeOldSpeed = this.speed;
|
||||
|
||||
// Current startTime and endTime could have already been reset.
|
||||
// We will remember their current values, and reset them at the
|
||||
// end. We need to perform the below calculations on start and end
|
||||
// times so that the range on the slider gets correctly updated in
|
||||
// the case of speed change in Flash player mode (for YouTube
|
||||
// videos).
|
||||
tempStartTime = this.videoPlayer.startTime;
|
||||
tempEndTime = this.videoPlayer.endTime;
|
||||
|
||||
// We retrieve the original times. They could have been changed due
|
||||
// to the fact of speed change (duration change). This happens when
|
||||
// in YouTube Flash mode. There each speed is a different video,
|
||||
@@ -566,31 +595,33 @@ function (HTML5Video, Resizer) {
|
||||
this.videoPlayer.startTime /= Number(this.speed);
|
||||
}
|
||||
}
|
||||
|
||||
// An `endTime` of `null` means that either the user didn't set
|
||||
// and `endTime`, or it was set to a value greater than the
|
||||
// duration of the video.
|
||||
//
|
||||
// If `endTime` is `null`, the video will play to the end. We do
|
||||
// not set the `endTime` to the duration of the video because
|
||||
// sometimes in YouTube mode the duration changes slightly during
|
||||
// the course of playback. This would cause the video to pause just
|
||||
// before the actual end of the video.
|
||||
if (
|
||||
this.videoPlayer.endTime === null ||
|
||||
this.videoPlayer.endTime !== null &&
|
||||
this.videoPlayer.endTime > duration
|
||||
) {
|
||||
this.videoPlayer.endTime = duration;
|
||||
} else {
|
||||
this.videoPlayer.endTime = null;
|
||||
} else if (this.videoPlayer.endTime !== null) {
|
||||
if (this.currentPlayerMode === 'flash') {
|
||||
this.videoPlayer.endTime /= Number(this.speed);
|
||||
}
|
||||
}
|
||||
|
||||
// If this is not a duration change (if it is, we continue playing
|
||||
// from current time), then we need to seek the video to the start
|
||||
// time.
|
||||
//
|
||||
// We seek only if start time differs from zero.
|
||||
if (durationChange === false && this.videoPlayer.startTime > 0) {
|
||||
this.videoPlayer.player.seekTo(this.videoPlayer.startTime);
|
||||
}
|
||||
|
||||
// Rebuild the slider start-end range (if it doesn't take up the
|
||||
// whole slider).
|
||||
// whole slider). Remember that endTime === null means the end time
|
||||
// is set to the end of video by default.
|
||||
if (!(
|
||||
this.videoPlayer.startTime === 0 &&
|
||||
this.videoPlayer.endTime === duration
|
||||
this.videoPlayer.endTime === null
|
||||
)) {
|
||||
this.trigger(
|
||||
'videoProgressSlider.updateStartEndTimeRegion',
|
||||
@@ -599,6 +630,28 @@ function (HTML5Video, Resizer) {
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// If this is not a duration change (if it is, we continue playing
|
||||
// from current time), then we need to seek the video to the start
|
||||
// time.
|
||||
//
|
||||
// We seek only if start time differs from zero, and we haven't
|
||||
// performed already such a seek.
|
||||
if (
|
||||
durationChange === false &&
|
||||
this.videoPlayer.startTime > 0 &&
|
||||
!(tempStartTime === 0 && tempEndTime === null)
|
||||
) {
|
||||
this.videoPlayer.player.seekTo(this.videoPlayer.startTime);
|
||||
}
|
||||
|
||||
// Reset back the actual startTime and endTime if they have been
|
||||
// already reset (a seek event happened, the video already ended
|
||||
// once, or endTime has already been reached once).
|
||||
if (tempStartTime === 0 && tempEndTime === null) {
|
||||
this.videoPlayer.startTime = 0;
|
||||
this.videoPlayer.endTime = null;
|
||||
}
|
||||
}
|
||||
|
||||
this.trigger(
|
||||
|
||||
@@ -41,7 +41,8 @@ function () {
|
||||
onSlide: onSlide,
|
||||
onStop: onStop,
|
||||
updatePlayTime: updatePlayTime,
|
||||
updateStartEndTimeRegion: updateStartEndTimeRegion
|
||||
updateStartEndTimeRegion: updateStartEndTimeRegion,
|
||||
notifyThroughHandleEnd: notifyThroughHandleEnd
|
||||
};
|
||||
|
||||
state.bindTo(methodsDict, state.videoProgressSlider, state);
|
||||
@@ -111,11 +112,6 @@ function () {
|
||||
duration = params.duration;
|
||||
}
|
||||
|
||||
// If the range spans the entire length of video, we don't do anything.
|
||||
if (!this.videoPlayer.startTime && !this.videoPlayer.endTime) {
|
||||
return;
|
||||
}
|
||||
|
||||
start = this.videoPlayer.startTime;
|
||||
|
||||
// If end is set to null, then we set it to the end of the video. We
|
||||
@@ -199,8 +195,6 @@ function () {
|
||||
}, 200);
|
||||
}
|
||||
|
||||
// Changed for tests -- JM: Check if it is the cause of Chrome Bug Valera
|
||||
// noticed
|
||||
function updatePlayTime(params) {
|
||||
var time = Math.floor(params.time),
|
||||
duration = Math.floor(params.duration);
|
||||
@@ -215,6 +209,33 @@ function () {
|
||||
}
|
||||
}
|
||||
|
||||
// When the video stops playing (either because the end was reached, or
|
||||
// because endTime was reached), the screen reader must be notified that
|
||||
// the video is no longer playing. We do this by a little trick. Setting
|
||||
// the title attribute of the slider know to "video ended", and focusing
|
||||
// on it. The screen reader will read the attr text.
|
||||
//
|
||||
// The user can then tab his way forward, landing on the next control
|
||||
// element, the Play button.
|
||||
//
|
||||
// @param params - object with property `end`. If set to true, the
|
||||
// function must set the title attribute to
|
||||
// `video ended`;
|
||||
// if set to false, the function must reset the attr to
|
||||
// it's original state.
|
||||
//
|
||||
// This function will be triggered from VideoPlayer methods onEnded(),
|
||||
// onPlay(), and update() (update method handles endTime).
|
||||
function notifyThroughHandleEnd(params) {
|
||||
if (params.end) {
|
||||
this.videoProgressSlider.handle
|
||||
.attr('title', 'video ended')
|
||||
.focus();
|
||||
} else {
|
||||
this.videoProgressSlider.handle.attr('title', 'video position');
|
||||
}
|
||||
}
|
||||
|
||||
// Returns a string describing the current time of video in hh:mm:ss
|
||||
// format.
|
||||
function getTimeDescription(time) {
|
||||
|
||||
@@ -204,21 +204,16 @@ class LocMapperStore(object):
|
||||
self._decode_from_mongo(old_name),
|
||||
None)
|
||||
elif usage_id == locator.usage_id:
|
||||
# figure out revision
|
||||
# enforce the draft only if category in [..] logic
|
||||
if category in draft.DIRECT_ONLY_CATEGORIES:
|
||||
revision = None
|
||||
elif locator.branch == candidate['draft_branch']:
|
||||
revision = draft.DRAFT
|
||||
else:
|
||||
revision = None
|
||||
# Always return revision=None because the
|
||||
# old draft module store wraps locations as draft before
|
||||
# trying to access things.
|
||||
return Location(
|
||||
'i4x',
|
||||
candidate['_id']['org'],
|
||||
candidate['_id']['course'],
|
||||
category,
|
||||
self._decode_from_mongo(old_name),
|
||||
revision)
|
||||
None)
|
||||
return None
|
||||
|
||||
def add_block_location_translator(self, location, old_course_id=None, usage_id=None):
|
||||
|
||||
@@ -778,11 +778,7 @@ class MongoModuleStore(ModuleStoreWriteBase):
|
||||
children: A list of child item identifiers
|
||||
"""
|
||||
|
||||
# We expect the children IDs to always be the non-draft version. With view refactoring
|
||||
# for split, we are now passing the draft version in some cases.
|
||||
children_ids = [Location(child).replace(revision=None).url() for child in children]
|
||||
|
||||
self._update_single_item(location, {'definition.children': children_ids})
|
||||
self._update_single_item(location, {'definition.children': children})
|
||||
# recompute (and update) the metadata inheritance tree which is cached
|
||||
self.refresh_cached_metadata_inheritance_tree(Location(location))
|
||||
# fire signal that we've written to DB
|
||||
|
||||
@@ -81,7 +81,7 @@ class DraftModuleStore(MongoModuleStore):
|
||||
try:
|
||||
return wrap_draft(super(DraftModuleStore, self).get_item(as_draft(location), depth=depth))
|
||||
except ItemNotFoundError:
|
||||
return wrap_draft(super(DraftModuleStore, self).get_item(as_published(location), depth=depth))
|
||||
return wrap_draft(super(DraftModuleStore, self).get_item(location, depth=depth))
|
||||
|
||||
def get_instance(self, course_id, location, depth=0):
|
||||
"""
|
||||
@@ -169,7 +169,7 @@ class DraftModuleStore(MongoModuleStore):
|
||||
try:
|
||||
draft_item = self.get_item(location)
|
||||
if not getattr(draft_item, 'is_draft', False):
|
||||
self.convert_to_draft(as_published(location))
|
||||
self.convert_to_draft(location)
|
||||
except ItemNotFoundError, e:
|
||||
if not allow_not_found:
|
||||
raise e
|
||||
@@ -187,7 +187,7 @@ class DraftModuleStore(MongoModuleStore):
|
||||
draft_loc = as_draft(location)
|
||||
draft_item = self.get_item(location)
|
||||
if not getattr(draft_item, 'is_draft', False):
|
||||
self.convert_to_draft(as_published(location))
|
||||
self.convert_to_draft(location)
|
||||
|
||||
return super(DraftModuleStore, self).update_children(draft_loc, children)
|
||||
|
||||
@@ -203,7 +203,7 @@ class DraftModuleStore(MongoModuleStore):
|
||||
draft_item = self.get_item(location)
|
||||
|
||||
if not getattr(draft_item, 'is_draft', False):
|
||||
self.convert_to_draft(as_published(location))
|
||||
self.convert_to_draft(location)
|
||||
|
||||
if 'is_draft' in metadata:
|
||||
del metadata['is_draft']
|
||||
@@ -262,7 +262,7 @@ class DraftModuleStore(MongoModuleStore):
|
||||
"""
|
||||
Turn the published version into a draft, removing the published version
|
||||
"""
|
||||
self.convert_to_draft(as_published(location))
|
||||
self.convert_to_draft(location)
|
||||
super(DraftModuleStore, self).delete_item(location)
|
||||
|
||||
def _query_children_for_cache_children(self, items):
|
||||
|
||||
@@ -88,7 +88,7 @@ class SplitMigrator(object):
|
||||
index_info = self.split_modulestore.get_course_index_info(course_version_locator)
|
||||
versions = index_info['versions']
|
||||
versions['draft'] = versions['published']
|
||||
self.split_modulestore.update_course_index(course_version_locator, {'versions': versions}, update_versions=True)
|
||||
self.split_modulestore.update_course_index(index_info)
|
||||
|
||||
# clean up orphans in published version: in old mongo, parents pointed to the union of their published and draft
|
||||
# children which meant some pointers were to non-existent locations in 'direct'
|
||||
|
||||
@@ -22,5 +22,4 @@ class DefinitionLazyLoader(object):
|
||||
Fetch the definition. Note, the caller should replace this lazy
|
||||
loader pointer with the result so as not to fetch more than once
|
||||
"""
|
||||
return self.modulestore.definitions.find_one(
|
||||
{'_id': self.definition_locator.definition_id})
|
||||
return self.modulestore.db_connection.get_definition(self.definition_locator.definition_id)
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
"""
|
||||
Segregation of pymongo functions from the data modeling mechanisms for split modulestore.
|
||||
"""
|
||||
import pymongo
|
||||
|
||||
class MongoConnection(object):
|
||||
"""
|
||||
Segregation of pymongo functions from the data modeling mechanisms for split modulestore.
|
||||
"""
|
||||
def __init__(
|
||||
self, db, collection, host, port=27017, tz_aware=True, user=None, password=None, **kwargs
|
||||
):
|
||||
"""
|
||||
Create & open the connection, authenticate, and provide pointers to the collections
|
||||
"""
|
||||
self.database = pymongo.database.Database(
|
||||
pymongo.MongoClient(
|
||||
host=host,
|
||||
port=port,
|
||||
tz_aware=tz_aware,
|
||||
**kwargs
|
||||
),
|
||||
db
|
||||
)
|
||||
|
||||
if user is not None and password is not None:
|
||||
self.database.authenticate(user, password)
|
||||
|
||||
self.course_index = self.database[collection + '.active_versions']
|
||||
self.structures = self.database[collection + '.structures']
|
||||
self.definitions = self.database[collection + '.definitions']
|
||||
|
||||
# every app has write access to the db (v having a flag to indicate r/o v write)
|
||||
# Force mongo to report errors, at the expense of performance
|
||||
# pymongo docs suck but explanation:
|
||||
# http://api.mongodb.org/java/2.10.1/com/mongodb/WriteConcern.html
|
||||
self.course_index.write_concern = {'w': 1}
|
||||
self.structures.write_concern = {'w': 1}
|
||||
self.definitions.write_concern = {'w': 1}
|
||||
|
||||
def get_structure(self, key):
|
||||
"""
|
||||
Get the structure from the persistence mechanism whose id is the given key
|
||||
"""
|
||||
return self.structures.find_one({'_id': key})
|
||||
|
||||
def find_matching_structures(self, query):
|
||||
"""
|
||||
Find the structure matching the query. Right now the query must be a legal mongo query
|
||||
:param query: a mongo-style query of {key: [value|{$in ..}|..], ..}
|
||||
"""
|
||||
return self.structures.find(query)
|
||||
|
||||
def insert_structure(self, structure):
|
||||
"""
|
||||
Create the structure in the db
|
||||
"""
|
||||
self.structures.insert(structure)
|
||||
|
||||
def update_structure(self, structure):
|
||||
"""
|
||||
Update the db record for structure
|
||||
"""
|
||||
self.structures.update({'_id': structure['_id']}, structure)
|
||||
|
||||
def get_course_index(self, key):
|
||||
"""
|
||||
Get the course_index from the persistence mechanism whose id is the given key
|
||||
"""
|
||||
return self.course_index.find_one({'_id': key})
|
||||
|
||||
def find_matching_course_indexes(self, query):
|
||||
"""
|
||||
Find the course_index matching the query. Right now the query must be a legal mongo query
|
||||
:param query: a mongo-style query of {key: [value|{$in ..}|..], ..}
|
||||
"""
|
||||
return self.course_index.find(query)
|
||||
|
||||
def insert_course_index(self, course_index):
|
||||
"""
|
||||
Create the course_index in the db
|
||||
"""
|
||||
self.course_index.insert(course_index)
|
||||
|
||||
def update_course_index(self, course_index):
|
||||
"""
|
||||
Update the db record for course_index
|
||||
"""
|
||||
self.course_index.update({'_id': course_index['_id']}, course_index)
|
||||
|
||||
def delete_course_index(self, key):
|
||||
"""
|
||||
Delete the course_index from the persistence mechanism whose id is the given key
|
||||
"""
|
||||
return self.course_index.remove({'_id': key})
|
||||
|
||||
def get_definition(self, key):
|
||||
"""
|
||||
Get the definition from the persistence mechanism whose id is the given key
|
||||
"""
|
||||
return self.definitions.find_one({'_id': key})
|
||||
|
||||
def find_matching_definitions(self, query):
|
||||
"""
|
||||
Find the definitions matching the query. Right now the query must be a legal mongo query
|
||||
:param query: a mongo-style query of {key: [value|{$in ..}|..], ..}
|
||||
"""
|
||||
return self.definitions.find(query)
|
||||
|
||||
def insert_definition(self, definition):
|
||||
"""
|
||||
Create the definition in the db
|
||||
"""
|
||||
self.definitions.insert(definition)
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import threading
|
||||
import datetime
|
||||
import logging
|
||||
import pymongo
|
||||
import re
|
||||
from importlib import import_module
|
||||
from path import path
|
||||
@@ -21,6 +20,7 @@ from .caching_descriptor_system import CachingDescriptorSystem
|
||||
from xblock.fields import Scope
|
||||
from xblock.runtime import Mixologist
|
||||
from bson.objectid import ObjectId
|
||||
from xmodule.modulestore.split_mongo.mongo_connection import MongoConnection
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
#==============================================================================
|
||||
@@ -49,7 +49,6 @@ class SplitMongoModuleStore(ModuleStoreWriteBase):
|
||||
A Mongodb backed ModuleStore supporting versions, inheritance,
|
||||
and sharing.
|
||||
"""
|
||||
# pylint: disable=W0201
|
||||
def __init__(self, doc_store_config, fs_root, render_template,
|
||||
default_class=None,
|
||||
error_tracker=null_error_tracker,
|
||||
@@ -62,44 +61,13 @@ class SplitMongoModuleStore(ModuleStoreWriteBase):
|
||||
super(SplitMongoModuleStore, self).__init__(**kwargs)
|
||||
self.loc_mapper = loc_mapper
|
||||
|
||||
def do_connection(
|
||||
db, collection, host, port=27017, tz_aware=True, user=None, password=None, **kwargs
|
||||
):
|
||||
"""
|
||||
Create & open the connection, authenticate, and provide pointers to the collections
|
||||
"""
|
||||
self.db = pymongo.database.Database(
|
||||
pymongo.MongoClient(
|
||||
host=host,
|
||||
port=port,
|
||||
tz_aware=tz_aware,
|
||||
**kwargs
|
||||
),
|
||||
db
|
||||
)
|
||||
|
||||
if user is not None and password is not None:
|
||||
self.db.authenticate(user, password)
|
||||
|
||||
self.course_index = self.db[collection + '.active_versions']
|
||||
self.structures = self.db[collection + '.structures']
|
||||
self.definitions = self.db[collection + '.definitions']
|
||||
|
||||
do_connection(**doc_store_config)
|
||||
self.db_connection = MongoConnection(**doc_store_config)
|
||||
self.db = self.db_connection.database
|
||||
|
||||
# Code review question: How should I expire entries?
|
||||
# _add_cache could use a lru mechanism to control the cache size?
|
||||
self.thread_cache = threading.local()
|
||||
|
||||
|
||||
# every app has write access to the db (v having a flag to indicate r/o v write)
|
||||
# Force mongo to report errors, at the expense of performance
|
||||
# pymongo docs suck but explanation:
|
||||
# http://api.mongodb.org/java/2.10.1/com/mongodb/WriteConcern.html
|
||||
self.course_index.write_concern = {'w': 1}
|
||||
self.structures.write_concern = {'w': 1}
|
||||
self.definitions.write_concern = {'w': 1}
|
||||
|
||||
if default_class is not None:
|
||||
module_path, _, class_name = default_class.rpartition('.')
|
||||
class_ = getattr(import_module(module_path), class_name)
|
||||
@@ -138,7 +106,7 @@ class SplitMongoModuleStore(ModuleStoreWriteBase):
|
||||
block['definition'] = DefinitionLazyLoader(self, block['definition'])
|
||||
else:
|
||||
# Load all descendants by id
|
||||
descendent_definitions = self.definitions.find({
|
||||
descendent_definitions = self.db_connection.find_matching_definitions({
|
||||
'_id': {'$in': [block['definition']
|
||||
for block in new_module_data.itervalues()]}})
|
||||
# turn into a map
|
||||
@@ -226,7 +194,7 @@ class SplitMongoModuleStore(ModuleStoreWriteBase):
|
||||
|
||||
if course_locator.course_id is not None and course_locator.branch is not None:
|
||||
# use the course_id
|
||||
index = self.course_index.find_one({'_id': course_locator.course_id})
|
||||
index = self.db_connection.get_course_index(course_locator.course_id)
|
||||
if index is None:
|
||||
raise ItemNotFoundError(course_locator)
|
||||
if course_locator.branch not in index['versions']:
|
||||
@@ -241,7 +209,7 @@ class SplitMongoModuleStore(ModuleStoreWriteBase):
|
||||
|
||||
# cast string to ObjectId if necessary
|
||||
version_guid = course_locator.as_object_id(version_guid)
|
||||
entry = self.structures.find_one({'_id': version_guid})
|
||||
entry = self.db_connection.get_structure(version_guid)
|
||||
|
||||
# b/c more than one course can use same structure, the 'course_id' and 'branch' are not intrinsic to structure
|
||||
# and the one assoc'd w/ it by another fetch may not be the one relevant to this fetch; so,
|
||||
@@ -269,7 +237,7 @@ class SplitMongoModuleStore(ModuleStoreWriteBase):
|
||||
if qualifiers is None:
|
||||
qualifiers = {}
|
||||
qualifiers.update({"versions.{}".format(branch): {"$exists": True}})
|
||||
matching = self.course_index.find(qualifiers)
|
||||
matching = self.db_connection.find_matching_course_indexes(qualifiers)
|
||||
|
||||
# collect ids and then query for those
|
||||
version_guids = []
|
||||
@@ -279,7 +247,7 @@ class SplitMongoModuleStore(ModuleStoreWriteBase):
|
||||
version_guids.append(version_guid)
|
||||
id_version_map[version_guid] = structure['_id']
|
||||
|
||||
course_entries = self.structures.find({'_id': {'$in': version_guids}})
|
||||
course_entries = self.db_connection.find_matching_structures({'_id': {'$in': version_guids}})
|
||||
|
||||
# get the block for the course element (s/b the root)
|
||||
result = []
|
||||
@@ -455,7 +423,7 @@ class SplitMongoModuleStore(ModuleStoreWriteBase):
|
||||
"""
|
||||
if course_locator.course_id is None:
|
||||
return None
|
||||
index = self.course_index.find_one({'_id': course_locator.course_id})
|
||||
index = self.db_connection.get_course_index(course_locator.course_id)
|
||||
return index
|
||||
|
||||
# TODO figure out a way to make this info accessible from the course descriptor
|
||||
@@ -487,7 +455,7 @@ class SplitMongoModuleStore(ModuleStoreWriteBase):
|
||||
'edited_on': when the change was made
|
||||
}
|
||||
"""
|
||||
definition = self.definitions.find_one({'_id': definition_locator.definition_id})
|
||||
definition = self.db_connection.get_definition(definition_locator.definition_id)
|
||||
if definition is None:
|
||||
return None
|
||||
return definition['edit_info']
|
||||
@@ -509,14 +477,14 @@ class SplitMongoModuleStore(ModuleStoreWriteBase):
|
||||
|
||||
# TODO if depth is significant, it may make sense to get all that have the same original_version
|
||||
# and reconstruct the subtree from version_guid
|
||||
next_entries = self.structures.find({'previous_version' : version_guid})
|
||||
next_entries = self.db_connection.find_matching_structures({'previous_version' : version_guid})
|
||||
# must only scan cursor's once
|
||||
next_versions = [struct for struct in next_entries]
|
||||
result = {version_guid: [CourseLocator(version_guid=struct['_id']) for struct in next_versions]}
|
||||
depth = 1
|
||||
while depth < version_history_depth and len(next_versions) > 0:
|
||||
depth += 1
|
||||
next_entries = self.structures.find({'previous_version':
|
||||
next_entries = self.db_connection.find_matching_structures({'previous_version':
|
||||
{'$in': [struct['_id'] for struct in next_versions]}})
|
||||
next_versions = [struct for struct in next_entries]
|
||||
for course_structure in next_versions:
|
||||
@@ -537,7 +505,7 @@ class SplitMongoModuleStore(ModuleStoreWriteBase):
|
||||
course_struct = self._lookup_course(block_locator.version_agnostic())['structure']
|
||||
usage_id = block_locator.usage_id
|
||||
update_version_field = 'blocks.{}.edit_info.update_version'.format(usage_id)
|
||||
all_versions_with_block = self.structures.find({'original_version': course_struct['original_version'],
|
||||
all_versions_with_block = self.db_connection.find_matching_structures({'original_version': course_struct['original_version'],
|
||||
update_version_field: {'$exists': True}})
|
||||
# find (all) root versions and build map previous: [successors]
|
||||
possible_roots = []
|
||||
@@ -596,7 +564,7 @@ class SplitMongoModuleStore(ModuleStoreWriteBase):
|
||||
"original_version": new_id,
|
||||
}
|
||||
}
|
||||
self.definitions.insert(document)
|
||||
self.db_connection.insert_definition(document)
|
||||
definition_locator = DefinitionLocator(new_id)
|
||||
return definition_locator
|
||||
|
||||
@@ -618,7 +586,7 @@ class SplitMongoModuleStore(ModuleStoreWriteBase):
|
||||
|
||||
# if this looks in cache rather than fresh fetches, then it will probably not detect
|
||||
# actual change b/c the descriptor and cache probably point to the same objects
|
||||
old_definition = self.definitions.find_one({'_id': definition_locator.definition_id})
|
||||
old_definition = self.db_connection.get_definition(definition_locator.definition_id)
|
||||
if old_definition is None:
|
||||
raise ItemNotFoundError(definition_locator.url())
|
||||
|
||||
@@ -630,7 +598,7 @@ class SplitMongoModuleStore(ModuleStoreWriteBase):
|
||||
old_definition['edit_info']['edited_on'] = datetime.datetime.now(UTC)
|
||||
# previous version id
|
||||
old_definition['edit_info']['previous_version'] = definition_locator.definition_id
|
||||
self.definitions.insert(old_definition)
|
||||
self.db_connection.insert_definition(old_definition)
|
||||
return DefinitionLocator(old_definition['_id']), True
|
||||
else:
|
||||
return definition_locator, False
|
||||
@@ -657,7 +625,7 @@ class SplitMongoModuleStore(ModuleStoreWriteBase):
|
||||
:param course_blocks: the current list of blocks.
|
||||
:param category:
|
||||
"""
|
||||
existing_uses = self.course_index.find({"_id": {"$regex": id_root}})
|
||||
existing_uses = self.db_connection.find_matching_course_indexes({"_id": {"$regex": id_root}})
|
||||
if existing_uses.count() > 0:
|
||||
max_found = 0
|
||||
matcher = re.compile(id_root + r'(\d+)')
|
||||
@@ -779,11 +747,11 @@ class SplitMongoModuleStore(ModuleStoreWriteBase):
|
||||
parent['edit_info']['update_version'] = new_id
|
||||
if continue_version:
|
||||
# db update
|
||||
self.structures.update({'_id': new_id}, new_structure)
|
||||
self.db_connection.update_structure(new_structure)
|
||||
# clear cache so things get refetched and inheritance recomputed
|
||||
self._clear_cache(new_id)
|
||||
else:
|
||||
self.structures.insert(new_structure)
|
||||
self.db_connection.insert_structure(new_structure)
|
||||
|
||||
# update the index entry if appropriate
|
||||
if index_entry is not None:
|
||||
@@ -856,7 +824,7 @@ class SplitMongoModuleStore(ModuleStoreWriteBase):
|
||||
'original_version': definition_id,
|
||||
}
|
||||
}
|
||||
self.definitions.insert(definition_entry)
|
||||
self.db_connection.insert_definition(definition_entry)
|
||||
|
||||
new_id = ObjectId()
|
||||
draft_structure = {
|
||||
@@ -880,7 +848,7 @@ class SplitMongoModuleStore(ModuleStoreWriteBase):
|
||||
}
|
||||
}
|
||||
}
|
||||
self.structures.insert(draft_structure)
|
||||
self.db_connection.insert_structure(draft_structure)
|
||||
|
||||
if versions_dict is None:
|
||||
versions_dict = {master_branch: new_id}
|
||||
@@ -898,20 +866,20 @@ class SplitMongoModuleStore(ModuleStoreWriteBase):
|
||||
if block_fields is not None:
|
||||
root_block['fields'].update(block_fields)
|
||||
if definition_fields is not None:
|
||||
definition = self.definitions.find_one({'_id': root_block['definition']})
|
||||
definition = self.db_connection.get_definition(root_block['definition'])
|
||||
definition['fields'].update(definition_fields)
|
||||
definition['edit_info']['previous_version'] = definition['_id']
|
||||
definition['edit_info']['edited_by'] = user_id
|
||||
definition['edit_info']['edited_on'] = datetime.datetime.now(UTC)
|
||||
definition['_id'] = ObjectId()
|
||||
self.definitions.insert(definition)
|
||||
self.db_connection.insert_definition(definition)
|
||||
root_block['definition'] = definition['_id']
|
||||
root_block['edit_info']['edited_on'] = datetime.datetime.now(UTC)
|
||||
root_block['edit_info']['edited_by'] = user_id
|
||||
root_block['edit_info']['previous_version'] = root_block['edit_info'].get('update_version')
|
||||
root_block['edit_info']['update_version'] = new_id
|
||||
|
||||
self.structures.insert(draft_structure)
|
||||
self.db_connection.insert_structure(draft_structure)
|
||||
versions_dict[master_branch] = new_id
|
||||
|
||||
# create the index entry
|
||||
@@ -926,7 +894,7 @@ class SplitMongoModuleStore(ModuleStoreWriteBase):
|
||||
'edited_by': user_id,
|
||||
'edited_on': datetime.datetime.now(UTC),
|
||||
'versions': versions_dict}
|
||||
self.course_index.insert(index_entry)
|
||||
self.db_connection.insert_course_index(index_entry)
|
||||
return self.get_course(CourseLocator(course_id=new_id, branch=master_branch))
|
||||
|
||||
def update_item(self, descriptor, user_id, force=False):
|
||||
@@ -978,7 +946,7 @@ class SplitMongoModuleStore(ModuleStoreWriteBase):
|
||||
'previous_version': block_data['edit_info']['update_version'],
|
||||
'update_version': new_id,
|
||||
}
|
||||
self.structures.insert(new_structure)
|
||||
self.db_connection.insert_structure(new_structure)
|
||||
# update the index entry if appropriate
|
||||
if index_entry is not None:
|
||||
self._update_head(index_entry, descriptor.location.branch, new_id)
|
||||
@@ -1016,7 +984,7 @@ class SplitMongoModuleStore(ModuleStoreWriteBase):
|
||||
is_updated = self._persist_subdag(xblock, user_id, new_structure['blocks'], new_id)
|
||||
|
||||
if is_updated:
|
||||
self.structures.insert(new_structure)
|
||||
self.db_connection.insert_structure(new_structure)
|
||||
|
||||
# update the index entry if appropriate
|
||||
if index_entry is not None:
|
||||
@@ -1115,31 +1083,18 @@ class SplitMongoModuleStore(ModuleStoreWriteBase):
|
||||
'''Deprecated, use update_item.'''
|
||||
raise NotImplementedError('use update_item')
|
||||
|
||||
def update_course_index(self, course_locator, new_values_dict, update_versions=False):
|
||||
def update_course_index(self, updated_index_entry):
|
||||
"""
|
||||
Change the given course's index entry for the given fields. new_values_dict
|
||||
should be a subset of the dict returned by get_course_index_info.
|
||||
It cannot include '_id' (will raise IllegalArgument).
|
||||
Provide update_versions=True if you intend this to replace the versions hash.
|
||||
Change the given course's index entry.
|
||||
|
||||
Note, this operation can be dangerous and break running courses.
|
||||
|
||||
If the dict includes versions and not update_versions, it will raise an exception.
|
||||
|
||||
If the dict includes edited_on or edited_by, it will raise an exception
|
||||
|
||||
Does not return anything useful.
|
||||
"""
|
||||
# TODO how should this log the change? edited_on and edited_by for this entry
|
||||
# has the semantic of who created the course and when; so, changing those will lose
|
||||
# that information.
|
||||
if '_id' in new_values_dict:
|
||||
raise ValueError("Cannot override _id")
|
||||
if 'edited_on' in new_values_dict or 'edited_by' in new_values_dict:
|
||||
raise ValueError("Cannot set edited_on or edited_by")
|
||||
if not update_versions and 'versions' in new_values_dict:
|
||||
raise ValueError("Cannot override versions without setting update_versions")
|
||||
self.course_index.update({'_id': course_locator.course_id},
|
||||
{'$set': new_values_dict})
|
||||
self.db_connection.update_course_index(updated_index_entry)
|
||||
|
||||
def delete_item(self, usage_locator, user_id, delete_children=False, force=False):
|
||||
"""
|
||||
@@ -1182,7 +1137,7 @@ class SplitMongoModuleStore(ModuleStoreWriteBase):
|
||||
remove_subtree(usage_locator.usage_id)
|
||||
|
||||
# update index if appropriate and structures
|
||||
self.structures.insert(new_structure)
|
||||
self.db_connection.insert_structure(new_structure)
|
||||
|
||||
result = CourseLocator(version_guid=new_id)
|
||||
|
||||
@@ -1204,11 +1159,11 @@ class SplitMongoModuleStore(ModuleStoreWriteBase):
|
||||
|
||||
:param course_id: uses course_id rather than locator to emphasize its global effect
|
||||
"""
|
||||
index = self.course_index.find_one({'_id': course_id})
|
||||
index = self.db_connection.get_course_index(course_id)
|
||||
if index is None:
|
||||
raise ItemNotFoundError(course_id)
|
||||
# this is the only real delete in the system. should it do something else?
|
||||
self.course_index.remove(index['_id'])
|
||||
self.db_connection.delete_course_index(index['_id'])
|
||||
|
||||
def get_errored_courses(self):
|
||||
"""
|
||||
@@ -1296,7 +1251,7 @@ class SplitMongoModuleStore(ModuleStoreWriteBase):
|
||||
block['fields']["children"] = [
|
||||
usage_id for usage_id in block['fields']["children"] if usage_id in original_structure['blocks']
|
||||
]
|
||||
self.structures.update({'_id': original_structure['_id']}, original_structure)
|
||||
self.db_connection.update_structure(original_structure)
|
||||
# clear cache again b/c inheritance may be wrong over orphans
|
||||
self._clear_cache(original_structure['_id'])
|
||||
|
||||
@@ -1379,7 +1334,7 @@ class SplitMongoModuleStore(ModuleStoreWriteBase):
|
||||
else:
|
||||
return None
|
||||
else:
|
||||
index_entry = self.course_index.find_one({'_id': locator.course_id})
|
||||
index_entry = self.db_connection.get_course_index(locator.course_id)
|
||||
is_head = (
|
||||
locator.version_guid is None or
|
||||
index_entry['versions'][locator.branch] == locator.version_guid
|
||||
@@ -1424,9 +1379,8 @@ class SplitMongoModuleStore(ModuleStoreWriteBase):
|
||||
:param course_locator:
|
||||
:param new_id:
|
||||
"""
|
||||
self.course_index.update(
|
||||
{"_id": index_entry["_id"]},
|
||||
{"$set": {"versions.{}".format(branch): new_id}})
|
||||
index_entry['versions'][branch] = new_id
|
||||
self.db_connection.update_course_index(index_entry)
|
||||
|
||||
def _partition_fields_by_scope(self, category, fields):
|
||||
"""
|
||||
|
||||
@@ -274,7 +274,9 @@ class TestLocationMapper(unittest.TestCase):
|
||||
course_id=prob_locator.course_id, branch='draft', usage_id=prob_locator.usage_id
|
||||
)
|
||||
prob_location = loc_mapper().translate_locator_to_location(prob_locator)
|
||||
self.assertEqual(prob_location, Location('i4x', org, course, 'problem', 'abc123', 'draft'))
|
||||
# Even though the problem was set as draft, we always return revision=None to work
|
||||
# with old mongo/draft modulestores.
|
||||
self.assertEqual(prob_location, Location('i4x', org, course, 'problem', 'abc123', None))
|
||||
prob_locator = BlockUsageLocator(
|
||||
course_id=new_style_course_id, usage_id='problem2', branch='production'
|
||||
)
|
||||
|
||||
@@ -59,9 +59,9 @@ class TestMigration(unittest.TestCase):
|
||||
dbref = self.loc_mapper.db
|
||||
dbref.drop_collection(self.loc_mapper.location_map)
|
||||
split_db = self.split_mongo.db
|
||||
split_db.drop_collection(split_db.course_index)
|
||||
split_db.drop_collection(split_db.structures)
|
||||
split_db.drop_collection(split_db.definitions)
|
||||
split_db.drop_collection(self.split_mongo.db_connection.course_index)
|
||||
split_db.drop_collection(self.split_mongo.db_connection.structures)
|
||||
split_db.drop_collection(self.split_mongo.db_connection.definitions)
|
||||
# old_mongo doesn't give a db attr, but all of the dbs are the same
|
||||
dbref.drop_collection(self.old_mongo.collection)
|
||||
|
||||
|
||||
@@ -1018,41 +1018,29 @@ class TestCourseCreation(SplitModuleTest):
|
||||
Test changing the org, pretty id, etc of a course. Test that it doesn't allow changing the id, etc.
|
||||
"""
|
||||
locator = CourseLocator(course_id="GreekHero", branch='draft')
|
||||
modulestore().update_course_index(locator, {'org': 'funkyU'})
|
||||
course_info = modulestore().get_course_index_info(locator)
|
||||
course_info['org'] = 'funkyU'
|
||||
modulestore().update_course_index(course_info)
|
||||
course_info = modulestore().get_course_index_info(locator)
|
||||
self.assertEqual(course_info['org'], 'funkyU')
|
||||
|
||||
modulestore().update_course_index(locator, {'org': 'moreFunky', 'prettyid': 'Ancient Greek Demagods'})
|
||||
course_info['org'] = 'moreFunky'
|
||||
course_info['prettyid'] = 'Ancient Greek Demagods'
|
||||
modulestore().update_course_index(course_info)
|
||||
course_info = modulestore().get_course_index_info(locator)
|
||||
self.assertEqual(course_info['org'], 'moreFunky')
|
||||
self.assertEqual(course_info['prettyid'], 'Ancient Greek Demagods')
|
||||
|
||||
self.assertRaises(ValueError, modulestore().update_course_index, locator, {'_id': 'funkygreeks'})
|
||||
|
||||
with self.assertRaises(ValueError):
|
||||
modulestore().update_course_index(
|
||||
locator,
|
||||
{'edited_on': datetime.datetime.now(UTC)}
|
||||
)
|
||||
with self.assertRaises(ValueError):
|
||||
modulestore().update_course_index(
|
||||
locator,
|
||||
{'edited_by': 'sneak'}
|
||||
)
|
||||
|
||||
self.assertRaises(ValueError, modulestore().update_course_index, locator,
|
||||
{'versions': {'draft': self.GUID_D1}})
|
||||
|
||||
# an allowed but not necessarily recommended way to revert the draft version
|
||||
versions = course_info['versions']
|
||||
versions['draft'] = self.GUID_D1
|
||||
modulestore().update_course_index(locator, {'versions': versions}, update_versions=True)
|
||||
modulestore().update_course_index(course_info)
|
||||
course = modulestore().get_course(locator)
|
||||
self.assertEqual(str(course.location.version_guid), self.GUID_D1)
|
||||
|
||||
# an allowed but not recommended way to publish a course
|
||||
versions['published'] = self.GUID_D1
|
||||
modulestore().update_course_index(locator, {'versions': versions}, update_versions=True)
|
||||
modulestore().update_course_index(course_info)
|
||||
course = modulestore().get_course(CourseLocator(course_id=locator.course_id, branch="published"))
|
||||
self.assertEqual(str(course.location.version_guid), self.GUID_D1)
|
||||
|
||||
@@ -1068,9 +1056,9 @@ class TestCourseCreation(SplitModuleTest):
|
||||
self.assertEqual(new_course.location.usage_id, 'top')
|
||||
self.assertEqual(new_course.category, 'chapter')
|
||||
# look at db to verify
|
||||
db_structure = modulestore().structures.find_one({
|
||||
'_id': new_course.location.as_object_id(new_course.location.version_guid)
|
||||
})
|
||||
db_structure = modulestore().db_connection.get_structure(
|
||||
new_course.location.as_object_id(new_course.location.version_guid)
|
||||
)
|
||||
self.assertIsNotNone(db_structure, "Didn't find course")
|
||||
self.assertNotIn('course', db_structure['blocks'])
|
||||
self.assertIn('top', db_structure['blocks'])
|
||||
|
||||
@@ -97,16 +97,15 @@ def export_to_xml(modulestore, contentstore, course_location, root_dir, course_d
|
||||
if len(draft_verticals) > 0:
|
||||
draft_course_dir = export_fs.makeopendir('drafts')
|
||||
for draft_vertical in draft_verticals:
|
||||
if getattr(draft_vertical, 'is_draft', False):
|
||||
parent_locs = draft_modulestore.get_parent_locations(draft_vertical.location, course.location.course_id)
|
||||
# Don't try to export orphaned items.
|
||||
if len(parent_locs) > 0:
|
||||
logging.debug('parent_locs = {0}'.format(parent_locs))
|
||||
draft_vertical.xml_attributes['parent_sequential_url'] = Location(parent_locs[0]).url()
|
||||
sequential = modulestore.get_item(Location(parent_locs[0]))
|
||||
index = sequential.children.index(draft_vertical.location.url())
|
||||
draft_vertical.xml_attributes['index_in_children_list'] = str(index)
|
||||
draft_vertical.export_to_xml(draft_course_dir)
|
||||
parent_locs = draft_modulestore.get_parent_locations(draft_vertical.location, course.location.course_id)
|
||||
# Don't try to export orphaned items.
|
||||
if len(parent_locs) > 0:
|
||||
logging.debug('parent_locs = {0}'.format(parent_locs))
|
||||
draft_vertical.xml_attributes['parent_sequential_url'] = Location(parent_locs[0]).url()
|
||||
sequential = modulestore.get_item(Location(parent_locs[0]))
|
||||
index = sequential.children.index(draft_vertical.location.url())
|
||||
draft_vertical.xml_attributes['index_in_children_list'] = str(index)
|
||||
draft_vertical.export_to_xml(draft_course_dir)
|
||||
|
||||
|
||||
def export_extra_content(export_fs, modulestore, course_id, course_location, category_type, dirname, file_suffix=''):
|
||||
|
||||
@@ -25,9 +25,8 @@ if Backbone?
|
||||
@add model
|
||||
model
|
||||
|
||||
retrieveAnotherPage: (mode, options={}, sort_options={})->
|
||||
@current_page += 1
|
||||
data = { page: @current_page }
|
||||
retrieveAnotherPage: (mode, options={}, sort_options={}, error=null)->
|
||||
data = { page: @current_page + 1 }
|
||||
switch mode
|
||||
when 'search'
|
||||
url = DiscussionUtil.urlFor 'search'
|
||||
@@ -59,6 +58,7 @@ if Backbone?
|
||||
@reset new_collection
|
||||
@pages = response.num_pages
|
||||
@current_page = response.page
|
||||
error: error
|
||||
|
||||
sortByDate: (thread) ->
|
||||
#
|
||||
|
||||
@@ -36,12 +36,15 @@ if Backbone?
|
||||
event.preventDefault()
|
||||
@newPostForm.slideUp(300)
|
||||
|
||||
hideDiscussion: ->
|
||||
@$("section.discussion").slideUp()
|
||||
@toggleDiscussionBtn.removeClass('shown')
|
||||
@toggleDiscussionBtn.find('.button-text').html("Show Discussion")
|
||||
@showed = false
|
||||
|
||||
toggleDiscussion: (event) ->
|
||||
if @showed
|
||||
@$("section.discussion").slideUp()
|
||||
@toggleDiscussionBtn.removeClass('shown')
|
||||
@toggleDiscussionBtn.find('.button-text').html("Show Discussion")
|
||||
@showed = false
|
||||
@hideDiscussion()
|
||||
else
|
||||
@toggleDiscussionBtn.addClass('shown')
|
||||
@toggleDiscussionBtn.find('.button-text').html("Hide Discussion")
|
||||
@@ -51,9 +54,17 @@ if Backbone?
|
||||
@showed = true
|
||||
else
|
||||
$elem = @toggleDiscussionBtn
|
||||
@loadPage $elem
|
||||
@loadPage(
|
||||
$elem,
|
||||
=>
|
||||
@hideDiscussion()
|
||||
DiscussionUtil.discussionAlert(
|
||||
"Sorry",
|
||||
"We had some trouble loading the discussion. Please try again."
|
||||
)
|
||||
)
|
||||
|
||||
loadPage: ($elem)=>
|
||||
loadPage: ($elem, error) =>
|
||||
discussionId = @$el.data("discussion-id")
|
||||
url = DiscussionUtil.urlFor('retrieve_discussion', discussionId) + "?page=#{@page}"
|
||||
DiscussionUtil.safeAjax
|
||||
@@ -63,6 +74,7 @@ if Backbone?
|
||||
type: "GET"
|
||||
dataType: 'json'
|
||||
success: (response, textStatus, jqXHR) => @renderDiscussion($elem, response, textStatus, discussionId)
|
||||
error: error
|
||||
|
||||
renderDiscussion: ($elem, response, textStatus, discussionId) =>
|
||||
window.user = new DiscussionUser(response.user_info)
|
||||
@@ -131,5 +143,14 @@ if Backbone?
|
||||
navigateToPage: (event) =>
|
||||
event.preventDefault()
|
||||
window.history.pushState({}, window.document.title, event.target.href)
|
||||
currPage = @page
|
||||
@page = $(event.target).data('page-number')
|
||||
@loadPage($(event.target))
|
||||
@loadPage(
|
||||
$(event.target),
|
||||
=>
|
||||
@page = currPage
|
||||
DiscussionUtil.discussionAlert(
|
||||
"Sorry",
|
||||
"We had some trouble loading the threads you requested. Please try again."
|
||||
)
|
||||
)
|
||||
|
||||
@@ -87,6 +87,13 @@ class @DiscussionUtil
|
||||
"notifications_status" : "/notification_prefs/status"
|
||||
}[name]
|
||||
|
||||
@makeFocusTrap: (elem) ->
|
||||
elem.keydown(
|
||||
(event) ->
|
||||
if event.which == 9 # Tab
|
||||
event.preventDefault()
|
||||
)
|
||||
|
||||
@discussionAlert: (header, body) ->
|
||||
if $("#discussion-alert").length == 0
|
||||
alertDiv = $("<div class='modal' role='alertdialog' id='discussion-alert' aria-describedby='discussion-alert-message'/>").css("display", "none")
|
||||
@@ -99,12 +106,7 @@ class @DiscussionUtil
|
||||
" <button class='dismiss'>OK</button>" +
|
||||
"</div>"
|
||||
)
|
||||
# Capture focus
|
||||
alertDiv.find("button").keydown(
|
||||
(event) ->
|
||||
if event.which == 9 # Tab
|
||||
event.preventDefault()
|
||||
)
|
||||
@makeFocusTrap(alertDiv.find("button"))
|
||||
alertTrigger = $("<a href='#discussion-alert' id='discussion-alert-trigger'/>").css("display", "none")
|
||||
alertTrigger.leanModal({closeButton: "#discussion-alert .dismiss", overlay: 1, top: 200})
|
||||
$("body").append(alertDiv).append(alertTrigger)
|
||||
|
||||
@@ -124,8 +124,11 @@ if Backbone?
|
||||
loadMorePages: (event) ->
|
||||
if event
|
||||
event.preventDefault()
|
||||
@$(".more-pages").html('<div class="loading-animation"><span class="sr">Loading more threads</span></div>')
|
||||
@$(".more-pages").html('<div class="loading-animation" tabindex=0><span class="sr" role="alert">Loading more threads</span></div>')
|
||||
@$(".more-pages").addClass("loading")
|
||||
loadingDiv = @$(".more-pages .loading-animation")
|
||||
DiscussionUtil.makeFocusTrap(loadingDiv)
|
||||
loadingDiv.focus()
|
||||
options = {}
|
||||
switch @mode
|
||||
when 'search'
|
||||
@@ -156,7 +159,11 @@ if Backbone?
|
||||
$(".post-list a").first()?.focus()
|
||||
)
|
||||
|
||||
@collection.retrieveAnotherPage(@mode, options, {sort_key: @sortBy})
|
||||
error = =>
|
||||
@renderThreads()
|
||||
DiscussionUtil.discussionAlert("Sorry", "We had some trouble loading more threads. Please try again.")
|
||||
|
||||
@collection.retrieveAnotherPage(@mode, options, {sort_key: @sortBy}, error)
|
||||
|
||||
renderThread: (thread) =>
|
||||
content = $(_.template($("#thread-list-item-template").html())(thread.toJSON()))
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
(function () {
|
||||
var update = function () {
|
||||
// Whenever a value changes create a new serialized version of this
|
||||
// problem's inputs and set the hidden input fields value to equal it.
|
||||
var parent = $(this).closest('.problems-wrapper');
|
||||
// problem's inputs and set the hidden input field's value to equal it.
|
||||
var parent = $(this).closest('section.choicetextinput');
|
||||
// find the closest parent problems-wrapper and use that as the problem
|
||||
// grab the input id from the input
|
||||
// real_input is the hidden input field
|
||||
var real_input = $('input.choicetextvalue', parent);
|
||||
var all_inputs = $('.choicetextinput .ctinput', parent);
|
||||
var all_inputs = $('input.ctinput', parent);
|
||||
var user_inputs = {};
|
||||
$(all_inputs).each(function (index, elt) {
|
||||
var node = $(elt);
|
||||
|
||||
3
common/static/js/vendor/backbone-min.js
vendored
3
common/static/js/vendor/backbone-min.js
vendored
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user