Commit 1eab25f2 by Matt Drayer

mattdrayer/increment-edx-lint: Bump to v0.2.9 and address pylint/pep8 violations

* Fix paver violations to stablize edx-lint update
* Parens, Line2Long
* Fix missing docstrings
* Fix PEP8 issues
* Address PR feedback (thanks @nedbat!)
parent d7c8cb80
...@@ -173,9 +173,9 @@ def remove_transcripts_from_store(_step, subs_id): ...@@ -173,9 +173,9 @@ def remove_transcripts_from_store(_step, subs_id):
try: try:
content = contentstore().find(content_location) content = contentstore().find(content_location)
contentstore().delete(content.location) contentstore().delete(content.location)
print('Transcript file was removed from store.') print 'Transcript file was removed from store.'
except NotFoundError: except NotFoundError:
print('Transcript file was NOT found and not removed.') print 'Transcript file was NOT found and not removed.'
@step('I enter a "([^"]+)" source to field number (\d+)$') @step('I enter a "([^"]+)" source to field number (\d+)$')
......
...@@ -36,11 +36,11 @@ class Command(BaseCommand): ...@@ -36,11 +36,11 @@ class Command(BaseCommand):
mstore = modulestore() mstore = modulestore()
print("Cloning course {0} to {1}".format(source_course_id, dest_course_id)) print "Cloning course {0} to {1}".format(source_course_id, dest_course_id)
with mstore.bulk_operations(dest_course_id): with mstore.bulk_operations(dest_course_id):
if mstore.clone_course(source_course_id, dest_course_id, ModuleStoreEnum.UserID.mgmt_command): if mstore.clone_course(source_course_id, dest_course_id, ModuleStoreEnum.UserID.mgmt_command):
print("copying User permissions...") print "copying User permissions..."
# purposely avoids auth.add_user b/c it doesn't have a caller to authorize # purposely avoids auth.add_user b/c it doesn't have a caller to authorize
CourseInstructorRole(dest_course_id).add_users( CourseInstructorRole(dest_course_id).add_users(
*CourseInstructorRole(source_course_id).users_with_role() *CourseInstructorRole(source_course_id).users_with_role()
......
...@@ -23,13 +23,13 @@ class Command(BaseCommand): ...@@ -23,13 +23,13 @@ class Command(BaseCommand):
output_path = args[0] output_path = args[0]
courses, failed_export_courses = export_courses_to_output_path(output_path) courses, failed_export_courses = export_courses_to_output_path(output_path)
print("=" * 80) print "=" * 80
print(u"=" * 30 + u"> Export summary") print u"=" * 30 + u"> Export summary"
print(u"Total number of courses to export: {0}".format(len(courses))) print u"Total number of courses to export: {0}".format(len(courses))
print(u"Total number of courses which failed to export: {0}".format(len(failed_export_courses))) print u"Total number of courses which failed to export: {0}".format(len(failed_export_courses))
print(u"List of export failed courses ids:") print u"List of export failed courses ids:"
print(u"\n".join(failed_export_courses)) print u"\n".join(failed_export_courses)
print("=" * 80) print "=" * 80
def export_courses_to_output_path(output_path): def export_courses_to_output_path(output_path):
...@@ -45,15 +45,15 @@ def export_courses_to_output_path(output_path): ...@@ -45,15 +45,15 @@ def export_courses_to_output_path(output_path):
failed_export_courses = [] failed_export_courses = []
for course_id in course_ids: for course_id in course_ids:
print(u"-" * 80) print u"-" * 80
print(u"Exporting course id = {0} to {1}".format(course_id, output_path)) print u"Exporting course id = {0} to {1}".format(course_id, output_path)
try: try:
course_dir = course_id.to_deprecated_string().replace('/', '...') course_dir = course_id.to_deprecated_string().replace('/', '...')
export_course_to_xml(module_store, content_store, course_id, root_dir, course_dir) export_course_to_xml(module_store, content_store, course_id, root_dir, course_dir)
except Exception as err: # pylint: disable=broad-except except Exception as err: # pylint: disable=broad-except
failed_export_courses.append(unicode(course_id)) failed_export_courses.append(unicode(course_id))
print(u"=" * 30 + u"> Oops, failed to export {0}".format(course_id)) print u"=" * 30 + u"> Oops, failed to export {0}".format(course_id)
print(u"Error:") print u"Error:"
print(err) print err
return courses, failed_export_courses return courses, failed_export_courses
...@@ -54,7 +54,7 @@ class Command(BaseCommand): ...@@ -54,7 +54,7 @@ class Command(BaseCommand):
finally: finally:
tar_file.close() tar_file.close()
print("Created archive {0}".format(archive_name)) print "Created archive {0}".format(archive_name)
except ValueError as err: except ValueError as err:
raise CommandError(err) raise CommandError(err)
......
...@@ -111,7 +111,7 @@ class AuthTestCase(ContentStoreTestCase): ...@@ -111,7 +111,7 @@ class AuthTestCase(ContentStoreTestCase):
reverse('signup'), reverse('signup'),
) )
for page in pages: for page in pages:
print("Checking '{0}'".format(page)) print "Checking '{0}'".format(page)
self.check_page_get(page, 200) self.check_page_get(page, 200)
def test_create_account_errors(self): def test_create_account_errors(self):
...@@ -254,17 +254,17 @@ class AuthTestCase(ContentStoreTestCase): ...@@ -254,17 +254,17 @@ class AuthTestCase(ContentStoreTestCase):
self.client = AjaxEnabledTestClient() self.client = AjaxEnabledTestClient()
# Not logged in. Should redirect to login. # Not logged in. Should redirect to login.
print('Not logged in') print 'Not logged in'
for page in auth_pages: for page in auth_pages:
print("Checking '{0}'".format(page)) print "Checking '{0}'".format(page)
self.check_page_get(page, expected=302) self.check_page_get(page, expected=302)
# Logged in should work. # Logged in should work.
self.login(self.email, self.pw) self.login(self.email, self.pw)
print('Logged in') print 'Logged in'
for page in simple_auth_pages: for page in simple_auth_pages:
print("Checking '{0}'".format(page)) print "Checking '{0}'".format(page)
self.check_page_get(page, expected=200) self.check_page_get(page, expected=200)
def test_index_auth(self): def test_index_auth(self):
......
...@@ -73,7 +73,7 @@ class Command(TrackedCommand): ...@@ -73,7 +73,7 @@ class Command(TrackedCommand):
for user in source_students: for user in source_students:
with transaction.commit_on_success(): with transaction.commit_on_success():
print("Moving {}.".format(user.username)) print "Moving {}.".format(user.username)
# Find the old enrollment. # Find the old enrollment.
enrollment = CourseEnrollment.objects.get( enrollment = CourseEnrollment.objects.get(
user=user, user=user,
...@@ -84,14 +84,14 @@ class Command(TrackedCommand): ...@@ -84,14 +84,14 @@ class Command(TrackedCommand):
mode = enrollment.mode mode = enrollment.mode
old_is_active = enrollment.is_active old_is_active = enrollment.is_active
CourseEnrollment.unenroll(user, source_key, skip_refund=True) CourseEnrollment.unenroll(user, source_key, skip_refund=True)
print(u"Unenrolled {} from {}".format(user.username, unicode(source_key))) print u"Unenrolled {} from {}".format(user.username, unicode(source_key))
for dest_key in dest_keys: for dest_key in dest_keys:
if CourseEnrollment.is_enrolled(user, dest_key): if CourseEnrollment.is_enrolled(user, dest_key):
# Un Enroll from source course but don't mess # Un Enroll from source course but don't mess
# with the enrollment in the destination course. # with the enrollment in the destination course.
msg = u"Skipping {}, already enrolled in destination course {}" msg = u"Skipping {}, already enrolled in destination course {}"
print(msg.format(user.username, unicode(dest_key))) print msg.format(user.username, unicode(dest_key))
else: else:
new_enrollment = CourseEnrollment.enroll(user, dest_key, mode=mode) new_enrollment = CourseEnrollment.enroll(user, dest_key, mode=mode)
...@@ -128,7 +128,7 @@ class Command(TrackedCommand): ...@@ -128,7 +128,7 @@ class Command(TrackedCommand):
course_enrollment=enrollment course_enrollment=enrollment
) )
except CertificateItem.DoesNotExist: except CertificateItem.DoesNotExist:
print(u"No certificate for {}".format(user)) print u"No certificate for {}".format(user)
return return
certificate_item.course_id = dest_keys[0] certificate_item.course_id = dest_keys[0]
......
...@@ -1090,7 +1090,7 @@ def perform_xlint( ...@@ -1090,7 +1090,7 @@ def perform_xlint(
for err_log in module_store.errored_courses.itervalues(): for err_log in module_store.errored_courses.itervalues():
for err_log_entry in err_log.errors: for err_log_entry in err_log.errors:
msg = err_log_entry[0] msg = err_log_entry[0]
print(msg) print msg
if msg.startswith('ERROR:'): if msg.startswith('ERROR:'):
err_cnt += 1 err_cnt += 1
else: else:
...@@ -1133,10 +1133,11 @@ def perform_xlint( ...@@ -1133,10 +1133,11 @@ def perform_xlint(
) )
warn_cnt += 1 warn_cnt += 1
print("\n") print "\n"
print("------------------------------------------") print "------------------------------------------"
print("VALIDATION SUMMARY: {err} Errors {warn} Warnings".format( print "VALIDATION SUMMARY: {err} Errors {warn} Warnings".format(
err=err_cnt, warn=warn_cnt) err=err_cnt,
warn=warn_cnt
) )
if err_cnt > 0: if err_cnt > 0:
...@@ -1151,7 +1152,7 @@ def perform_xlint( ...@@ -1151,7 +1152,7 @@ def perform_xlint(
"your courseware before importing" "your courseware before importing"
) )
else: else:
print("This course can be imported successfully.") print "This course can be imported successfully."
return err_cnt return err_cnt
......
"""
Package declaration for content assertions test helper module
"""
import core import core
...@@ -37,7 +37,7 @@ def strip_filenames(descriptor): ...@@ -37,7 +37,7 @@ def strip_filenames(descriptor):
""" """
Recursively strips 'filename' from all children's definitions. Recursively strips 'filename' from all children's definitions.
""" """
print("strip filename from {desc}".format(desc=descriptor.location.to_deprecated_string())) print "strip filename from {desc}".format(desc=descriptor.location.to_deprecated_string())
if descriptor._field_data.has(descriptor, 'filename'): if descriptor._field_data.has(descriptor, 'filename'):
descriptor._field_data.delete(descriptor, 'filename') descriptor._field_data.delete(descriptor, 'filename')
...@@ -98,12 +98,12 @@ class RoundTripTestCase(unittest.TestCase): ...@@ -98,12 +98,12 @@ class RoundTripTestCase(unittest.TestCase):
""").strip() """).strip()
root_dir = path(self.temp_dir) root_dir = path(self.temp_dir)
print("Copying test course to temp dir {0}".format(root_dir)) print "Copying test course to temp dir {0}".format(root_dir)
data_dir = path(DATA_DIR) data_dir = path(DATA_DIR)
shutil.copytree(data_dir / course_dir, root_dir / course_dir) shutil.copytree(data_dir / course_dir, root_dir / course_dir)
print("Starting import") print "Starting import"
initial_import = XMLModuleStore(root_dir, source_dirs=[course_dir], xblock_mixins=(XModuleMixin,)) initial_import = XMLModuleStore(root_dir, source_dirs=[course_dir], xblock_mixins=(XModuleMixin,))
courses = initial_import.get_courses() courses = initial_import.get_courses()
...@@ -112,7 +112,7 @@ class RoundTripTestCase(unittest.TestCase): ...@@ -112,7 +112,7 @@ class RoundTripTestCase(unittest.TestCase):
# export to the same directory--that way things like the custom_tags/ folder # export to the same directory--that way things like the custom_tags/ folder
# will still be there. # will still be there.
print("Starting export") print "Starting export"
file_system = OSFS(root_dir) file_system = OSFS(root_dir)
initial_course.runtime.export_fs = file_system.makeopendir(course_dir) initial_course.runtime.export_fs = file_system.makeopendir(course_dir)
root = lxml.etree.Element('root') root = lxml.etree.Element('root')
...@@ -121,14 +121,14 @@ class RoundTripTestCase(unittest.TestCase): ...@@ -121,14 +121,14 @@ class RoundTripTestCase(unittest.TestCase):
with initial_course.runtime.export_fs.open('course.xml', 'w') as course_xml: with initial_course.runtime.export_fs.open('course.xml', 'w') as course_xml:
lxml.etree.ElementTree(root).write(course_xml) lxml.etree.ElementTree(root).write(course_xml)
print("Starting second import") print "Starting second import"
second_import = XMLModuleStore(root_dir, source_dirs=[course_dir], xblock_mixins=(XModuleMixin,)) second_import = XMLModuleStore(root_dir, source_dirs=[course_dir], xblock_mixins=(XModuleMixin,))
courses2 = second_import.get_courses() courses2 = second_import.get_courses()
self.assertEquals(len(courses2), 1) self.assertEquals(len(courses2), 1)
exported_course = courses2[0] exported_course = courses2[0]
print("Checking course equality") print "Checking course equality"
# HACK: filenames change when changing file formats # HACK: filenames change when changing file formats
# during imports from old-style courses. Ignore them. # during imports from old-style courses. Ignore them.
...@@ -139,13 +139,13 @@ class RoundTripTestCase(unittest.TestCase): ...@@ -139,13 +139,13 @@ class RoundTripTestCase(unittest.TestCase):
self.assertEquals(initial_course.id, exported_course.id) self.assertEquals(initial_course.id, exported_course.id)
course_id = initial_course.id course_id = initial_course.id
print("Checking key equality") print "Checking key equality"
self.assertItemsEqual( self.assertItemsEqual(
initial_import.modules[course_id].keys(), initial_import.modules[course_id].keys(),
second_import.modules[course_id].keys() second_import.modules[course_id].keys()
) )
print("Checking module equality") print "Checking module equality"
for location in initial_import.modules[course_id].keys(): for location in initial_import.modules[course_id].keys():
print("Checking", location) print("Checking", location)
self.assertTrue(blocks_are_equivalent( self.assertTrue(blocks_are_equivalent(
......
...@@ -218,7 +218,8 @@ class RelativeTimeTest(unittest.TestCase): ...@@ -218,7 +218,8 @@ class RelativeTimeTest(unittest.TestCase):
RelativeTimeTest.delta.to_json(100.0) RelativeTimeTest.delta.to_json(100.0)
) )
with self.assertRaisesRegexp(ValueError, "RelativeTime max value is 23:59:59=86400.0 seconds, but 90000.0 seconds is passed"): error_msg = "RelativeTime max value is 23:59:59=86400.0 seconds, but 90000.0 seconds is passed"
with self.assertRaisesRegexp(ValueError, error_msg):
RelativeTimeTest.delta.to_json(datetime.timedelta(seconds=90000)) RelativeTimeTest.delta.to_json(datetime.timedelta(seconds=90000))
with self.assertRaises(TypeError): with self.assertRaises(TypeError):
......
...@@ -87,8 +87,19 @@ class ImageAnnotationModuleTestCase(unittest.TestCase): ...@@ -87,8 +87,19 @@ class ImageAnnotationModuleTestCase(unittest.TestCase):
def test_student_view(self): def test_student_view(self):
""" """
Tests the function that passes in all the information in the context that will be used in templates/textannotation.html Tests the function that passes in all the information in the context
that will be used in templates/imageannotation.html
""" """
context = self.mod.student_view({}).content context = self.mod.student_view({}).content
for key in ['display_name', 'instructions_html', 'annotation_storage', 'token', 'tag', 'openseadragonjson', 'default_tab', 'instructor_email', 'annotation_mode', 'is_course_staff']:
for key in ['display_name',
'instructions_html',
'annotation_storage',
'token',
'tag',
'openseadragonjson',
'default_tab',
'instructor_email',
'annotation_mode',
'is_course_staff']:
self.assertIn(key, context) self.assertIn(key, context)
...@@ -66,7 +66,7 @@ class BaseCourseTestCase(unittest.TestCase): ...@@ -66,7 +66,7 @@ class BaseCourseTestCase(unittest.TestCase):
def get_course(self, name): def get_course(self, name):
"""Get a test course by directory name. If there's more than one, error.""" """Get a test course by directory name. If there's more than one, error."""
print("Importing {0}".format(name)) print "Importing {0}".format(name)
modulestore = XMLModuleStore( modulestore = XMLModuleStore(
DATA_DIR, DATA_DIR,
...@@ -431,22 +431,22 @@ class ImportTestCase(BaseCourseTestCase): ...@@ -431,22 +431,22 @@ class ImportTestCase(BaseCourseTestCase):
"""] """]
for xml_str in yes: for xml_str in yes:
print("should be True for {0}".format(xml_str)) print "should be True for {0}".format(xml_str)
self.assertTrue(is_pointer_tag(etree.fromstring(xml_str))) self.assertTrue(is_pointer_tag(etree.fromstring(xml_str)))
for xml_str in no: for xml_str in no:
print("should be False for {0}".format(xml_str)) print "should be False for {0}".format(xml_str)
self.assertFalse(is_pointer_tag(etree.fromstring(xml_str))) self.assertFalse(is_pointer_tag(etree.fromstring(xml_str)))
def test_metadata_inherit(self): def test_metadata_inherit(self):
"""Make sure that metadata is inherited properly""" """Make sure that metadata is inherited properly"""
print("Starting import") print "Starting import"
course = self.get_course('toy') course = self.get_course('toy')
def check_for_key(key, node, value): def check_for_key(key, node, value):
"recursive check for presence of key" "recursive check for presence of key"
print("Checking {0}".format(node.location.to_deprecated_string())) print "Checking {0}".format(node.location.to_deprecated_string())
self.assertEqual(getattr(node, key), value) self.assertEqual(getattr(node, key), value)
for c in node.get_children(): for c in node.get_children():
check_for_key(key, c, value) check_for_key(key, c, value)
...@@ -496,17 +496,17 @@ class ImportTestCase(BaseCourseTestCase): ...@@ -496,17 +496,17 @@ class ImportTestCase(BaseCourseTestCase):
def test_colon_in_url_name(self): def test_colon_in_url_name(self):
"""Ensure that colons in url_names convert to file paths properly""" """Ensure that colons in url_names convert to file paths properly"""
print("Starting import") print "Starting import"
# Not using get_courses because we need the modulestore object too afterward # Not using get_courses because we need the modulestore object too afterward
modulestore = XMLModuleStore(DATA_DIR, source_dirs=['toy']) modulestore = XMLModuleStore(DATA_DIR, source_dirs=['toy'])
courses = modulestore.get_courses() courses = modulestore.get_courses()
self.assertEquals(len(courses), 1) self.assertEquals(len(courses), 1)
course = courses[0] course = courses[0]
print("course errors:") print "course errors:"
for (msg, err) in modulestore.get_course_errors(course.id): for (msg, err) in modulestore.get_course_errors(course.id):
print(msg) print msg
print(err) print err
chapters = course.get_children() chapters = course.get_children()
self.assertEquals(len(chapters), 5) self.assertEquals(len(chapters), 5)
...@@ -514,12 +514,12 @@ class ImportTestCase(BaseCourseTestCase): ...@@ -514,12 +514,12 @@ class ImportTestCase(BaseCourseTestCase):
ch2 = chapters[1] ch2 = chapters[1]
self.assertEquals(ch2.url_name, "secret:magic") self.assertEquals(ch2.url_name, "secret:magic")
print("Ch2 location: ", ch2.location) print "Ch2 location: ", ch2.location
also_ch2 = modulestore.get_item(ch2.location) also_ch2 = modulestore.get_item(ch2.location)
self.assertEquals(ch2, also_ch2) self.assertEquals(ch2, also_ch2)
print("making sure html loaded") print "making sure html loaded"
loc = course.id.make_usage_key('html', 'secret:toylab') loc = course.id.make_usage_key('html', 'secret:toylab')
html = modulestore.get_item(loc) html = modulestore.get_item(loc)
self.assertEquals(html.display_name, "Toy lab") self.assertEquals(html.display_name, "Toy lab")
...@@ -531,13 +531,13 @@ class ImportTestCase(BaseCourseTestCase): ...@@ -531,13 +531,13 @@ class ImportTestCase(BaseCourseTestCase):
loaded because of unicode filenames, there are appropriate loaded because of unicode filenames, there are appropriate
exceptions/errors to that effect.""" exceptions/errors to that effect."""
print("Starting import") print "Starting import"
modulestore = XMLModuleStore(DATA_DIR, source_dirs=['test_unicode']) modulestore = XMLModuleStore(DATA_DIR, source_dirs=['test_unicode'])
courses = modulestore.get_courses() courses = modulestore.get_courses()
self.assertEquals(len(courses), 1) self.assertEquals(len(courses), 1)
course = courses[0] course = courses[0]
print("course errors:") print "course errors:"
# Expect to find an error/exception about characters in "®esources" # Expect to find an error/exception about characters in "®esources"
expect = "InvalidKeyError" expect = "InvalidKeyError"
...@@ -573,7 +573,7 @@ class ImportTestCase(BaseCourseTestCase): ...@@ -573,7 +573,7 @@ class ImportTestCase(BaseCourseTestCase):
for i in (2, 3): for i in (2, 3):
video = sections[i] video = sections[i]
# Name should be 'video_{hash}' # Name should be 'video_{hash}'
print("video {0} url_name: {1}".format(i, video.url_name)) print "video {0} url_name: {1}".format(i, video.url_name)
self.assertEqual(len(video.url_name), len('video_') + 12) self.assertEqual(len(video.url_name), len('video_') + 12)
......
...@@ -56,7 +56,9 @@ class LTIModuleTest(LogicTest): ...@@ -56,7 +56,9 @@ class LTIModuleTest(LogicTest):
self.user_id = self.xmodule.runtime.anonymous_student_id self.user_id = self.xmodule.runtime.anonymous_student_id
self.lti_id = self.xmodule.lti_id self.lti_id = self.xmodule.lti_id
self.unquoted_resource_link_id = u'{}-i4x-2-3-lti-31de800015cf4afb973356dbe81496df'.format(self.xmodule.runtime.hostname) self.unquoted_resource_link_id = u'{}-i4x-2-3-lti-31de800015cf4afb973356dbe81496df'.format(
self.xmodule.runtime.hostname
)
sourced_id = u':'.join(urllib.quote(i) for i in (self.lti_id, self.unquoted_resource_link_id, self.user_id)) sourced_id = u':'.join(urllib.quote(i) for i in (self.lti_id, self.unquoted_resource_link_id, self.user_id))
...@@ -104,7 +106,10 @@ class LTIModuleTest(LogicTest): ...@@ -104,7 +106,10 @@ class LTIModuleTest(LogicTest):
'action': action 'action': action
} }
@patch('xmodule.lti_module.LTIModule.get_client_key_secret', return_value=('test_client_key', u'test_client_secret')) @patch(
'xmodule.lti_module.LTIModule.get_client_key_secret',
return_value=('test_client_key', u'test_client_secret')
)
def test_authorization_header_not_present(self, _get_key_secret): def test_authorization_header_not_present(self, _get_key_secret):
""" """
Request has no Authorization header. Request has no Authorization header.
...@@ -125,7 +130,10 @@ class LTIModuleTest(LogicTest): ...@@ -125,7 +130,10 @@ class LTIModuleTest(LogicTest):
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertDictEqual(expected_response, real_response) self.assertDictEqual(expected_response, real_response)
@patch('xmodule.lti_module.LTIModule.get_client_key_secret', return_value=('test_client_key', u'test_client_secret')) @patch(
'xmodule.lti_module.LTIModule.get_client_key_secret',
return_value=('test_client_key', u'test_client_secret')
)
def test_authorization_header_empty(self, _get_key_secret): def test_authorization_header_empty(self, _get_key_secret):
""" """
Request Authorization header has no value. Request Authorization header has no value.
...@@ -350,7 +358,10 @@ class LTIModuleTest(LogicTest): ...@@ -350,7 +358,10 @@ class LTIModuleTest(LogicTest):
self.xmodule.get_client_key_secret() self.xmodule.get_client_key_secret()
@patch('xmodule.lti_module.signature.verify_hmac_sha1', Mock(return_value=True)) @patch('xmodule.lti_module.signature.verify_hmac_sha1', Mock(return_value=True))
@patch('xmodule.lti_module.LTIModule.get_client_key_secret', Mock(return_value=('test_client_key', u'test_client_secret'))) @patch(
'xmodule.lti_module.LTIModule.get_client_key_secret',
Mock(return_value=('test_client_key', u'test_client_secret'))
)
def test_successful_verify_oauth_body_sign(self): def test_successful_verify_oauth_body_sign(self):
""" """
Test if OAuth signing was successful. Test if OAuth signing was successful.
...@@ -397,7 +408,7 @@ class LTIModuleTest(LogicTest): ...@@ -397,7 +408,7 @@ class LTIModuleTest(LogicTest):
'<imsx_POXHeader><imsx_POXRequestHeaderInfo><imsx_version>V1.0</imsx_version>' '<imsx_POXHeader><imsx_POXRequestHeaderInfo><imsx_version>V1.0</imsx_version>'
'<imsx_messageIdentifier>edX_fix</imsx_messageIdentifier></imsx_POXRequestHeaderInfo>' '<imsx_messageIdentifier>edX_fix</imsx_messageIdentifier></imsx_POXRequestHeaderInfo>'
'</imsx_POXHeader><imsx_POXBody><replaceResultRequest><resultRecord><sourcedGUID>' '</imsx_POXHeader><imsx_POXBody><replaceResultRequest><resultRecord><sourcedGUID>'
'<sourcedId>MITxLTI/MITxLTI/201x:localhost%3A8000-i4x-MITxLTI-MITxLTI-lti-3751833a214a4f66a0d18f63234207f2:363979ef768ca171b50f9d1bfb322131</sourcedId>' '<sourcedId>MITxLTI/MITxLTI/201x:localhost%3A8000-i4x-MITxLTI-MITxLTI-lti-3751833a214a4f66a0d18f63234207f2:363979ef768ca171b50f9d1bfb322131</sourcedId>' # pylint: disable=line-too-long
'</sourcedGUID><result><resultScore><language>en</language><textString>0.32</textString></resultScore>' '</sourcedGUID><result><resultScore><language>en</language><textString>0.32</textString></resultScore>'
'</result></resultRecord></replaceResultRequest></imsx_POXBody></imsx_POXEnvelopeRequest>' '</result></resultRecord></replaceResultRequest></imsx_POXBody></imsx_POXEnvelopeRequest>'
) )
...@@ -428,7 +439,10 @@ class LTIModuleTest(LogicTest): ...@@ -428,7 +439,10 @@ class LTIModuleTest(LogicTest):
self.assertEqual(self.defaults['action'], action) self.assertEqual(self.defaults['action'], action)
@patch('xmodule.lti_module.signature.verify_hmac_sha1', Mock(return_value=False)) @patch('xmodule.lti_module.signature.verify_hmac_sha1', Mock(return_value=False))
@patch('xmodule.lti_module.LTIModule.get_client_key_secret', Mock(return_value=('test_client_key', u'test_client_secret'))) @patch(
'xmodule.lti_module.LTIModule.get_client_key_secret',
Mock(return_value=('test_client_key', u'test_client_secret'))
)
def test_failed_verify_oauth_body_sign(self): def test_failed_verify_oauth_body_sign(self):
""" """
Oauth signing verify fail. Oauth signing verify fail.
......
"""
Test cases covering behaviors and workflows of the Peer Grading XBlock
"""
import unittest import unittest
import json import json
import logging import logging
...@@ -219,11 +222,15 @@ class PeerGradingModuleTest(unittest.TestCase, DummyModulestore): ...@@ -219,11 +222,15 @@ class PeerGradingModuleTest(unittest.TestCase, DummyModulestore):
self.assertEqual(score_dict["score"], 0.0) self.assertEqual(score_dict["score"], 0.0)
def get_score(self, success, count_graded, count_required): def get_score(self, success, count_graded, count_required):
"""
Returns the peer-graded score based on the provided graded/required values
"""
self.peer_grading.use_for_single_location_local = True self.peer_grading.use_for_single_location_local = True
self.peer_grading.graded = True self.peer_grading.graded = True
# Patch for external grading service. # Patch for external grading service.
with patch('xmodule.peer_grading_module.PeerGradingModule.query_data_for_location') as mock_query_data_for_location: module_name = 'xmodule.peer_grading_module.PeerGradingModule.query_data_for_location'
with patch(module_name) as mock_query_data_for_location:
mock_query_data_for_location.return_value = ( mock_query_data_for_location.return_value = (
success, success,
{"count_graded": count_graded, "count_required": count_required} {"count_graded": count_graded, "count_required": count_required}
...@@ -249,6 +256,9 @@ class PeerGradingModuleTest(unittest.TestCase, DummyModulestore): ...@@ -249,6 +256,9 @@ class PeerGradingModuleTest(unittest.TestCase, DummyModulestore):
class MockPeerGradingServiceProblemList(MockPeerGradingService): class MockPeerGradingServiceProblemList(MockPeerGradingService):
"""
Mock object representing a set of peer-grading problems
"""
def get_problem_list(self, course_id, grader_id): def get_problem_list(self, course_id, grader_id):
return {'success': True, return {'success': True,
'problem_list': [ 'problem_list': [
...@@ -348,6 +358,9 @@ class PeerGradingModuleLinkedTest(unittest.TestCase, DummyModulestore): ...@@ -348,6 +358,9 @@ class PeerGradingModuleLinkedTest(unittest.TestCase, DummyModulestore):
return ScopeIds(None, None, self.problem_location, self.problem_location) return ScopeIds(None, None, self.problem_location, self.problem_location)
def _create_peer_grading_descriptor_with_linked_problem(self): def _create_peer_grading_descriptor_with_linked_problem(self):
"""
Internal helper method to construct a peer grading XBlock
"""
# Initialize the peer grading module. # Initialize the peer grading module.
system = get_test_descriptor_system() system = get_test_descriptor_system()
......
"""
Test cases covering workflows and behaviors for the Randomize XModule
"""
import unittest import unittest
from datetime import datetime, timedelta from datetime import datetime, timedelta
......
"""
Test cases covering workflows and behaviors of the Self Assessment feature
"""
from datetime import datetime from datetime import datetime
import json import json
import unittest import unittest
...@@ -15,6 +18,9 @@ import test_util_open_ended ...@@ -15,6 +18,9 @@ import test_util_open_ended
class SelfAssessmentTest(unittest.TestCase): class SelfAssessmentTest(unittest.TestCase):
"""
Test cases covering workflows and behaviors of the Self Assessment feature
"""
rubric = '''<rubric><rubric> rubric = '''<rubric><rubric>
<category> <category>
<description>Response Quality</description> <description>Response Quality</description>
...@@ -78,9 +84,15 @@ class SelfAssessmentTest(unittest.TestCase): ...@@ -78,9 +84,15 @@ class SelfAssessmentTest(unittest.TestCase):
responses = {'assessment': '0', 'score_list[]': ['0', '0']} responses = {'assessment': '0', 'score_list[]': ['0', '0']}
def get_fake_item(name): def get_fake_item(name):
"""
Returns the specified key from the parent workflow container
"""
return responses[name] return responses[name]
def get_data_for_location(self, location, student): def get_data_for_location(self, location, student):
"""
Returns a dictionary of keys having zero values
"""
return { return {
'count_graded': 0, 'count_graded': 0,
'count_required': 0, 'count_required': 0,
......
...@@ -68,7 +68,7 @@ class SplitTestModuleTest(XModuleXmlImportTest, PartitionTestCase): ...@@ -68,7 +68,7 @@ class SplitTestModuleTest(XModuleXmlImportTest, PartitionTestCase):
parent=sequence, parent=sequence,
attribs={ attribs={
'user_partition_id': '0', 'user_partition_id': '0',
'group_id_to_child': '{"0": "i4x://edX/xml_test_course/html/split_test_cond0", "1": "i4x://edX/xml_test_course/html/split_test_cond1"}' 'group_id_to_child': '{"0": "i4x://edX/xml_test_course/html/split_test_cond0", "1": "i4x://edX/xml_test_course/html/split_test_cond1"}' # pylint: disable=line-too-long
} }
) )
xml.HtmlFactory(parent=split_test, url_name='split_test_cond0', text='HTML FOR GROUP 0') xml.HtmlFactory(parent=split_test, url_name='split_test_cond0', text='HTML FOR GROUP 0')
...@@ -135,7 +135,10 @@ class SplitTestModuleLMSTest(SplitTestModuleTest): ...@@ -135,7 +135,10 @@ class SplitTestModuleLMSTest(SplitTestModuleTest):
# If a user_tag has a missing value, a group should be saved/persisted for that user. # If a user_tag has a missing value, a group should be saved/persisted for that user.
# So, we check that we get the same url_name when we call on the url_name twice. # So, we check that we get the same url_name when we call on the url_name twice.
# We run the test ten times so that, if our storage is failing, we'll be most likely to notice it. # We run the test ten times so that, if our storage is failing, we'll be most likely to notice it.
self.assertEquals(self.split_test_module.child_descriptor.url_name, self.split_test_module.child_descriptor.url_name) self.assertEquals(
self.split_test_module.child_descriptor.url_name,
self.split_test_module.child_descriptor.url_name
)
# Patch the definition_to_xml for the html children. # Patch the definition_to_xml for the html children.
@patch('xmodule.html_module.HtmlDescriptor.definition_to_xml') @patch('xmodule.html_module.HtmlDescriptor.definition_to_xml')
......
...@@ -72,8 +72,19 @@ class TextAnnotationModuleTestCase(unittest.TestCase): ...@@ -72,8 +72,19 @@ class TextAnnotationModuleTestCase(unittest.TestCase):
def test_student_view(self): def test_student_view(self):
""" """
Tests the function that passes in all the information in the context that will be used in templates/textannotation.html Tests the function that passes in all the information in the context
that will be used in templates/textannotation.html
""" """
context = self.mod.student_view({}).content context = self.mod.student_view({}).content
for key in ['display_name', 'tag', 'source', 'instructions_html', 'content_html', 'annotation_storage', 'token', 'diacritic_marks', 'default_tab', 'annotation_mode', 'is_course_staff']: for key in ['display_name',
'tag',
'source',
'instructions_html',
'content_html',
'annotation_storage',
'token',
'diacritic_marks',
'default_tab',
'annotation_mode',
'is_course_staff']:
self.assertIn(key, context) self.assertIn(key, context)
"""
Utility classes and data structures supporting the Open-Ended Grading feature of Open edX
"""
import json import json
from StringIO import StringIO from StringIO import StringIO
...@@ -29,15 +32,27 @@ class MockS3Key(object): ...@@ -29,15 +32,27 @@ class MockS3Key(object):
pass pass
def set_metadata(self, key, value): def set_metadata(self, key, value):
"""
Appends an attribute the current instance using the provided key/value pair
"""
setattr(self, key, value) setattr(self, key, value)
def set_contents_from_file(self, fileobject): def set_contents_from_file(self, fileobject):
"""
Sets the 'data' parameter to the contents of the provided file object
"""
self.data = fileobject.read() self.data = fileobject.read()
def set_acl(self, acl): def set_acl(self, acl):
"""
Sets the 'acl' metadata parameter to the provided value
"""
self.set_metadata("acl", acl) self.set_metadata("acl", acl)
def generate_url(self, timeout): def generate_url(self, timeout):
"""
Returns a sample URL for use in tests
"""
return "http://www.edx.org/sample_url" return "http://www.edx.org/sample_url"
...@@ -52,9 +67,15 @@ class MockS3Connection(object): ...@@ -52,9 +67,15 @@ class MockS3Connection(object):
pass pass
def create_bucket(self, bucket_name, **kwargs): def create_bucket(self, bucket_name, **kwargs):
"""
Mock boto operation: create_bucket -- returns a simple string value
"""
return "edX Bucket" return "edX Bucket"
def lookup(self, bucket_name): def lookup(self, bucket_name):
"""
Mock boto operation: lookup -- returns None
"""
return None return None
...@@ -70,9 +91,15 @@ class MockUploadedFile(object): ...@@ -70,9 +91,15 @@ class MockUploadedFile(object):
self.name = name self.name = name
def seek(self, index): def seek(self, index):
"""
Returns the file contents at the provided index
"""
return self.mock_file.seek(index) return self.mock_file.seek(index)
def read(self): def read(self):
"""
Returns the contents of the mock file
"""
return self.mock_file.read() return self.mock_file.read()
...@@ -82,9 +109,15 @@ class DummyModulestore(object): ...@@ -82,9 +109,15 @@ class DummyModulestore(object):
""" """
def get_module_system(self, descriptor): def get_module_system(self, descriptor):
"""
Pseudo-abstract method that forces derivatives to devise a module system implementation
"""
raise NotImplementedError("Sub-tests must specify how to generate a module-system") raise NotImplementedError("Sub-tests must specify how to generate a module-system")
def setup_modulestore(self, name): def setup_modulestore(self, name):
"""
Sets the modulestore to an XMLModuleStore instance
"""
# pylint: disable=attribute-defined-outside-init # pylint: disable=attribute-defined-outside-init
self.modulestore = XMLModuleStore(DATA_DIR, source_dirs=[name]) self.modulestore = XMLModuleStore(DATA_DIR, source_dirs=[name])
...@@ -94,6 +127,9 @@ class DummyModulestore(object): ...@@ -94,6 +127,9 @@ class DummyModulestore(object):
return courses[0] return courses[0]
def get_module_from_location(self, usage_key): def get_module_from_location(self, usage_key):
"""
Returns the content descriptor for the given usage key
"""
descriptor = self.modulestore.get_item(usage_key, depth=None) descriptor = self.modulestore.get_item(usage_key, depth=None)
descriptor.xmodule_runtime = self.get_module_system(descriptor) descriptor.xmodule_runtime = self.get_module_system(descriptor)
return descriptor return descriptor
...@@ -921,6 +957,8 @@ STATE_POST_ASSESSMENT = serialize_open_ended_instance_state(""" ...@@ -921,6 +957,8 @@ STATE_POST_ASSESSMENT = serialize_open_ended_instance_state("""
"max_attempts": 1 "max_attempts": 1
}""") }""")
# pylint: disable=line-too-long
# Task state with self assessment only. # Task state with self assessment only.
TEST_STATE_SA = ["{\"child_created\": false, \"child_attempts\": 1, \"version\": 1, \"child_history\": [{\"answer\": \"Censorship in the Libraries\\r<br>'All of us can think of a book that we hope none of our children or any other children have taken off the shelf. But if I have the right to remove that book from the shelf -- that work I abhor -- then you also have exactly the same right and so does everyone else. And then we have no books left on the shelf for any of us.' --Katherine Paterson, Author\\r<br><br>Write a persuasive essay to a newspaper reflecting your views on censorship in libraries. Do you believe that certain materials, such as books, music, movies, magazines, etc., should be removed from the shelves if they are found offensive? Support your position with convincing arguments from your own experience, observations, and/or reading.\", \"post_assessment\": \"[3, 3, 2, 2, 2]\", \"score\": 12}], \"max_score\": 12, \"child_state\": \"done\"}", "{\"child_created\": false, \"child_attempts\": 0, \"version\": 1, \"child_history\": [{\"answer\": \"Censorship in the Libraries\\r<br>'All of us can think of a book that we hope none of our children or any other children have taken off the shelf. But if I have the right to remove that book from the shelf -- that work I abhor -- then you also have exactly the same right and so does everyone else. And then we have no books left on the shelf for any of us.' --Katherine Paterson, Author\\r<br><br>Write a persuasive essay to a newspaper reflecting your views on censorship in libraries. Do you believe that certain materials, such as books, music, movies, magazines, etc., should be removed from the shelves if they are found offensive? Support your position with convincing arguments from your own experience, observations, and/or reading.\", \"post_assessment\": \"{\\\"submission_id\\\": 1461, \\\"score\\\": 12, \\\"feedback\\\": \\\"{\\\\\\\"feedback\\\\\\\": \\\\\\\"\\\\\\\"}\\\", \\\"success\\\": true, \\\"grader_id\\\": 5414, \\\"grader_type\\\": \\\"IN\\\", \\\"rubric_scores_complete\\\": true, \\\"rubric_xml\\\": \\\"<rubric><category><description>\\\\nIdeas\\\\n</description><score>3</score><option points='0'>\\\\nDifficult for the reader to discern the main idea. Too brief or too repetitive to establish or maintain a focus.\\\\n</option><option points='1'>\\\\nAttempts a main idea. Sometimes loses focus or ineffectively displays focus.\\\\n</option><option points='2'>\\\\nPresents a unifying theme or main idea, but may include minor tangents. Stays somewhat focused on topic and task.\\\\n</option><option points='3'>\\\\nPresents a unifying theme or main idea without going off on tangents. Stays completely focused on topic and task.\\\\n</option></category><category><description>\\\\nContent\\\\n</description><score>3</score><option points='0'>\\\\nIncludes little information with few or no details or unrelated details. Unsuccessful in attempts to explore any facets of the topic.\\\\n</option><option points='1'>\\\\nIncludes little information and few or no details. Explores only one or two facets of the topic.\\\\n</option><option points='2'>\\\\nIncludes sufficient information and supporting details. (Details may not be fully developed; ideas may be listed.) Explores some facets of the topic.\\\\n</option><option points='3'>\\\\nIncludes in-depth information and exceptional supporting details that are fully developed. Explores all facets of the topic.\\\\n</option></category><category><description>\\\\nOrganization\\\\n</description><score>2</score><option points='0'>\\\\nIdeas organized illogically, transitions weak, and response difficult to follow.\\\\n</option><option points='1'>\\\\nAttempts to logically organize ideas. Attempts to progress in an order that enhances meaning, and demonstrates use of transitions.\\\\n</option><option points='2'>\\\\nIdeas organized logically. Progresses in an order that enhances meaning. Includes smooth transitions.\\\\n</option></category><category><description>\\\\nStyle\\\\n</description><score>2</score><option points='0'>\\\\nContains limited vocabulary, with many words used incorrectly. Demonstrates problems with sentence patterns.\\\\n</option><option points='1'>\\\\nContains basic vocabulary, with words that are predictable and common. Contains mostly simple sentences (although there may be an attempt at more varied sentence patterns).\\\\n</option><option points='2'>\\\\nIncludes vocabulary to make explanations detailed and precise. Includes varied sentence patterns, including complex sentences.\\\\n</option></category><category><description>\\\\nVoice\\\\n</description><score>2</score><option points='0'>\\\\nDemonstrates language and tone that may be inappropriate to task and reader.\\\\n</option><option points='1'>\\\\nDemonstrates an attempt to adjust language and tone to task and reader.\\\\n</option><option points='2'>\\\\nDemonstrates effective adjustment of language and tone to task and reader.\\\\n</option></category></rubric>\\\"}\", \"score\": 12}], \"max_score\": 12, \"child_state\": \"post_assessment\"}"] TEST_STATE_SA = ["{\"child_created\": false, \"child_attempts\": 1, \"version\": 1, \"child_history\": [{\"answer\": \"Censorship in the Libraries\\r<br>'All of us can think of a book that we hope none of our children or any other children have taken off the shelf. But if I have the right to remove that book from the shelf -- that work I abhor -- then you also have exactly the same right and so does everyone else. And then we have no books left on the shelf for any of us.' --Katherine Paterson, Author\\r<br><br>Write a persuasive essay to a newspaper reflecting your views on censorship in libraries. Do you believe that certain materials, such as books, music, movies, magazines, etc., should be removed from the shelves if they are found offensive? Support your position with convincing arguments from your own experience, observations, and/or reading.\", \"post_assessment\": \"[3, 3, 2, 2, 2]\", \"score\": 12}], \"max_score\": 12, \"child_state\": \"done\"}", "{\"child_created\": false, \"child_attempts\": 0, \"version\": 1, \"child_history\": [{\"answer\": \"Censorship in the Libraries\\r<br>'All of us can think of a book that we hope none of our children or any other children have taken off the shelf. But if I have the right to remove that book from the shelf -- that work I abhor -- then you also have exactly the same right and so does everyone else. And then we have no books left on the shelf for any of us.' --Katherine Paterson, Author\\r<br><br>Write a persuasive essay to a newspaper reflecting your views on censorship in libraries. Do you believe that certain materials, such as books, music, movies, magazines, etc., should be removed from the shelves if they are found offensive? Support your position with convincing arguments from your own experience, observations, and/or reading.\", \"post_assessment\": \"{\\\"submission_id\\\": 1461, \\\"score\\\": 12, \\\"feedback\\\": \\\"{\\\\\\\"feedback\\\\\\\": \\\\\\\"\\\\\\\"}\\\", \\\"success\\\": true, \\\"grader_id\\\": 5414, \\\"grader_type\\\": \\\"IN\\\", \\\"rubric_scores_complete\\\": true, \\\"rubric_xml\\\": \\\"<rubric><category><description>\\\\nIdeas\\\\n</description><score>3</score><option points='0'>\\\\nDifficult for the reader to discern the main idea. Too brief or too repetitive to establish or maintain a focus.\\\\n</option><option points='1'>\\\\nAttempts a main idea. Sometimes loses focus or ineffectively displays focus.\\\\n</option><option points='2'>\\\\nPresents a unifying theme or main idea, but may include minor tangents. Stays somewhat focused on topic and task.\\\\n</option><option points='3'>\\\\nPresents a unifying theme or main idea without going off on tangents. Stays completely focused on topic and task.\\\\n</option></category><category><description>\\\\nContent\\\\n</description><score>3</score><option points='0'>\\\\nIncludes little information with few or no details or unrelated details. Unsuccessful in attempts to explore any facets of the topic.\\\\n</option><option points='1'>\\\\nIncludes little information and few or no details. Explores only one or two facets of the topic.\\\\n</option><option points='2'>\\\\nIncludes sufficient information and supporting details. (Details may not be fully developed; ideas may be listed.) Explores some facets of the topic.\\\\n</option><option points='3'>\\\\nIncludes in-depth information and exceptional supporting details that are fully developed. Explores all facets of the topic.\\\\n</option></category><category><description>\\\\nOrganization\\\\n</description><score>2</score><option points='0'>\\\\nIdeas organized illogically, transitions weak, and response difficult to follow.\\\\n</option><option points='1'>\\\\nAttempts to logically organize ideas. Attempts to progress in an order that enhances meaning, and demonstrates use of transitions.\\\\n</option><option points='2'>\\\\nIdeas organized logically. Progresses in an order that enhances meaning. Includes smooth transitions.\\\\n</option></category><category><description>\\\\nStyle\\\\n</description><score>2</score><option points='0'>\\\\nContains limited vocabulary, with many words used incorrectly. Demonstrates problems with sentence patterns.\\\\n</option><option points='1'>\\\\nContains basic vocabulary, with words that are predictable and common. Contains mostly simple sentences (although there may be an attempt at more varied sentence patterns).\\\\n</option><option points='2'>\\\\nIncludes vocabulary to make explanations detailed and precise. Includes varied sentence patterns, including complex sentences.\\\\n</option></category><category><description>\\\\nVoice\\\\n</description><score>2</score><option points='0'>\\\\nDemonstrates language and tone that may be inappropriate to task and reader.\\\\n</option><option points='1'>\\\\nDemonstrates an attempt to adjust language and tone to task and reader.\\\\n</option><option points='2'>\\\\nDemonstrates effective adjustment of language and tone to task and reader.\\\\n</option></category></rubric>\\\"}\", \"score\": 12}], \"max_score\": 12, \"child_state\": \"post_assessment\"}"]
......
...@@ -209,10 +209,25 @@ class StudioValidationTest(unittest.TestCase): ...@@ -209,10 +209,25 @@ class StudioValidationTest(unittest.TestCase):
expected = { expected = {
"xblock_id": "id", "xblock_id": "id",
"messages": [ "messages": [
{"type": "error", "text": u"Error message", "action_label": u"Action label", "action_class": "edit-button"}, {
{"type": "not-configured", "text": u"Not configured message", "action_label": u"Action label", "action_runtime_event": "make groups"} "type": "error",
"text": u"Error message",
"action_label": u"Action label",
"action_class": "edit-button"
},
{
"type": "not-configured",
"text": u"Not configured message",
"action_label": u"Action label",
"action_runtime_event": "make groups"
}
], ],
"summary": {"type": "warning", "text": u"Summary message", "action_label": u"Summary label", "action_runtime_event": "fix everything"}, "summary": {
"type": "warning",
"text": u"Summary message",
"action_label": u"Summary label",
"action_runtime_event": "fix everything"
},
"empty": False "empty": False
} }
self.assertEqual(expected, validation.to_json()) self.assertEqual(expected, validation.to_json())
...@@ -83,5 +83,14 @@ class VideoAnnotationModuleTestCase(unittest.TestCase): ...@@ -83,5 +83,14 @@ class VideoAnnotationModuleTestCase(unittest.TestCase):
Tests to make sure variables passed in truly exist within the html once it is all rendered. Tests to make sure variables passed in truly exist within the html once it is all rendered.
""" """
context = self.mod.student_view({}).content context = self.mod.student_view({}).content
for key in ['display_name', 'instructions_html', 'sourceUrl', 'typeSource', 'poster', 'annotation_storage', 'default_tab', 'instructor_email', 'annotation_mode', "is_course_staff"]: for key in ['display_name',
'instructions_html',
'sourceUrl',
'typeSource',
'poster',
'annotation_storage',
'default_tab',
'instructor_email',
'annotation_mode',
'is_course_staff']:
self.assertIn(key, context) self.assertIn(key, context)
...@@ -383,7 +383,8 @@ def manage_video_subtitles_save(item, user, old_metadata=None, generate_translat ...@@ -383,7 +383,8 @@ def manage_video_subtitles_save(item, user, old_metadata=None, generate_translat
lang, lang,
) )
except TranscriptException as ex: except TranscriptException as ex:
item.transcripts.pop(lang) # remove key from transcripts because proper srt file does not exist in assets. # remove key from transcripts because proper srt file does not exist in assets.
item.transcripts.pop(lang)
reraised_message += ' ' + ex.message reraised_message += ' ' + ex.message
if reraised_message: if reraised_message:
item.save_with_metadata(user) item.save_with_metadata(user)
......
...@@ -185,7 +185,8 @@ class VideoModule(VideoFields, VideoTranscriptsMixin, VideoStudentViewHandlers, ...@@ -185,7 +185,8 @@ class VideoModule(VideoFields, VideoTranscriptsMixin, VideoStudentViewHandlers,
return track_url, transcript_language, sorted_languages return track_url, transcript_language, sorted_languages
def get_html(self): def get_html(self):
transcript_download_format = self.transcript_download_format if not (self.download_track and self.track) else None track_status = (self.download_track and self.track)
transcript_download_format = self.transcript_download_format if not track_status else None
sources = filter(None, self.html5_sources) sources = filter(None, self.html5_sources)
download_video_link = None download_video_link = None
...@@ -458,7 +459,11 @@ class VideoDescriptor(VideoFields, VideoTranscriptsMixin, VideoStudioViewHandler ...@@ -458,7 +459,11 @@ class VideoDescriptor(VideoFields, VideoTranscriptsMixin, VideoStudioViewHandler
languages.sort(key=lambda l: l['label']) languages.sort(key=lambda l: l['label'])
editable_fields['transcripts']['languages'] = languages editable_fields['transcripts']['languages'] = languages
editable_fields['transcripts']['type'] = 'VideoTranslations' editable_fields['transcripts']['type'] = 'VideoTranslations'
editable_fields['transcripts']['urlRoot'] = self.runtime.handler_url(self, 'studio_transcript', 'translation').rstrip('/?') editable_fields['transcripts']['urlRoot'] = self.runtime.handler_url(
self,
'studio_transcript',
'translation'
).rstrip('/?')
editable_fields['handout']['type'] = 'FileUploader' editable_fields['handout']['type'] = 'FileUploader'
return editable_fields return editable_fields
...@@ -568,6 +573,9 @@ class VideoDescriptor(VideoFields, VideoTranscriptsMixin, VideoStudioViewHandler ...@@ -568,6 +573,9 @@ class VideoDescriptor(VideoFields, VideoTranscriptsMixin, VideoStudioViewHandler
youtube_id_1_0 = metadata_fields['youtube_id_1_0'] youtube_id_1_0 = metadata_fields['youtube_id_1_0']
def get_youtube_link(video_id): def get_youtube_link(video_id):
"""
Returns the fully-qualified YouTube URL for the given video identifier
"""
# First try a lookup in VAL. If we have a YouTube entry there, it overrides the # First try a lookup in VAL. If we have a YouTube entry there, it overrides the
# one passed in. # one passed in.
if self.edx_video_id and edxval_api: if self.edx_video_id and edxval_api:
...@@ -582,7 +590,7 @@ class VideoDescriptor(VideoFields, VideoTranscriptsMixin, VideoStudioViewHandler ...@@ -582,7 +590,7 @@ class VideoDescriptor(VideoFields, VideoTranscriptsMixin, VideoStudioViewHandler
_ = self.runtime.service(self, "i18n").ugettext _ = self.runtime.service(self, "i18n").ugettext
video_url.update({ video_url.update({
'help': _('The URL for your video. This can be a YouTube URL or a link to an .mp4, .ogg, or .webm video file hosted elsewhere on the Internet.'), 'help': _('The URL for your video. This can be a YouTube URL or a link to an .mp4, .ogg, or .webm video file hosted elsewhere on the Internet.'), # pylint: disable=line-too-long
'display_name': _('Default Video URL'), 'display_name': _('Default Video URL'),
'field_name': 'video_url', 'field_name': 'video_url',
'type': 'VideoList', 'type': 'VideoList',
......
...@@ -82,30 +82,30 @@ class VideoFields(object): ...@@ -82,30 +82,30 @@ class VideoFields(object):
default="" default=""
) )
download_video = Boolean( download_video = Boolean(
help=_("Allow students to download versions of this video in different formats if they cannot use the edX video player or do not have access to YouTube. You must add at least one non-YouTube URL in the Video File URLs field."), help=_("Allow students to download versions of this video in different formats if they cannot use the edX video player or do not have access to YouTube. You must add at least one non-YouTube URL in the Video File URLs field."), # pylint: disable=line-too-long
display_name=_("Video Download Allowed"), display_name=_("Video Download Allowed"),
scope=Scope.settings, scope=Scope.settings,
default=False default=False
) )
html5_sources = List( html5_sources = List(
help=_("The URL or URLs where you've posted non-YouTube versions of the video. Each URL must end in .mpeg, .mp4, .ogg, or .webm and cannot be a YouTube URL. (For browser compatibility, we strongly recommend .mp4 and .webm format.) Students will be able to view the first listed video that's compatible with the student's computer. To allow students to download these videos, set Video Download Allowed to True."), help=_("The URL or URLs where you've posted non-YouTube versions of the video. Each URL must end in .mpeg, .mp4, .ogg, or .webm and cannot be a YouTube URL. (For browser compatibility, we strongly recommend .mp4 and .webm format.) Students will be able to view the first listed video that's compatible with the student's computer. To allow students to download these videos, set Video Download Allowed to True."), # pylint: disable=line-too-long
display_name=_("Video File URLs"), display_name=_("Video File URLs"),
scope=Scope.settings, scope=Scope.settings,
) )
track = String( track = String(
help=_("By default, students can download an .srt or .txt transcript when you set Download Transcript Allowed to True. If you want to provide a downloadable transcript in a different format, we recommend that you upload a handout by using the Upload a Handout field. If this isn't possible, you can post a transcript file on the Files & Uploads page or on the Internet, and then add the URL for the transcript here. Students see a link to download that transcript below the video."), help=_("By default, students can download an .srt or .txt transcript when you set Download Transcript Allowed to True. If you want to provide a downloadable transcript in a different format, we recommend that you upload a handout by using the Upload a Handout field. If this isn't possible, you can post a transcript file on the Files & Uploads page or on the Internet, and then add the URL for the transcript here. Students see a link to download that transcript below the video."), # pylint: disable=line-too-long
display_name=_("Downloadable Transcript URL"), display_name=_("Downloadable Transcript URL"),
scope=Scope.settings, scope=Scope.settings,
default='' default=''
) )
download_track = Boolean( download_track = Boolean(
help=_("Allow students to download the timed transcript. A link to download the file appears below the video. By default, the transcript is an .srt or .txt file. If you want to provide the transcript for download in a different format, upload a file by using the Upload Handout field."), help=_("Allow students to download the timed transcript. A link to download the file appears below the video. By default, the transcript is an .srt or .txt file. If you want to provide the transcript for download in a different format, upload a file by using the Upload Handout field."), # pylint: disable=line-too-long
display_name=_("Download Transcript Allowed"), display_name=_("Download Transcript Allowed"),
scope=Scope.settings, scope=Scope.settings,
default=False default=False
) )
sub = String( sub = String(
help=_("The default transcript for the video, from the Default Timed Transcript field on the Basic tab. This transcript should be in English. You don't have to change this setting."), help=_("The default transcript for the video, from the Default Timed Transcript field on the Basic tab. This transcript should be in English. You don't have to change this setting."), # pylint: disable=line-too-long
display_name=_("Default Timed Transcript"), display_name=_("Default Timed Transcript"),
scope=Scope.settings, scope=Scope.settings,
default="" default=""
...@@ -118,7 +118,7 @@ class VideoFields(object): ...@@ -118,7 +118,7 @@ class VideoFields(object):
) )
# Data format: {'de': 'german_translation', 'uk': 'ukrainian_translation'} # Data format: {'de': 'german_translation', 'uk': 'ukrainian_translation'}
transcripts = Dict( transcripts = Dict(
help=_("Add transcripts in different languages. Click below to specify a language and upload an .srt transcript file for that language."), help=_("Add transcripts in different languages. Click below to specify a language and upload an .srt transcript file for that language."), # pylint: disable=line-too-long
display_name=_("Transcript Languages"), display_name=_("Transcript Languages"),
scope=Scope.settings, scope=Scope.settings,
default={} default={}
...@@ -154,7 +154,7 @@ class VideoFields(object): ...@@ -154,7 +154,7 @@ class VideoFields(object):
default=True default=True
) )
handout = String( handout = String(
help=_("Upload a handout to accompany this video. Students can download the handout by clicking Download Handout under the video."), help=_("Upload a handout to accompany this video. Students can download the handout by clicking Download Handout under the video."), # pylint: disable=line-too-long
display_name=_("Upload Handout"), display_name=_("Upload Handout"),
scope=Scope.settings, scope=Scope.settings,
) )
......
...@@ -34,15 +34,27 @@ log = logging.getLogger(__name__) ...@@ -34,15 +34,27 @@ log = logging.getLogger(__name__)
def extract(dic, keys): def extract(dic, keys):
"""
Returns a subset of keys from the provided dictionary
"""
return {k: dic.get(k) for k in keys} return {k: dic.get(k) for k in keys}
def strip_none(dic): def strip_none(dic):
"""
Returns a dictionary stripped of any keys having values of None
"""
return dict([(k, v) for k, v in dic.iteritems() if v is not None]) return dict([(k, v) for k, v in dic.iteritems() if v is not None])
def strip_blank(dic): def strip_blank(dic):
"""
Returns a dictionary stripped of any 'blank' (empty) keys
"""
def _is_blank(v): def _is_blank(v):
"""
Determines if the provided value contains no information
"""
return isinstance(v, str) and len(v.strip()) == 0 return isinstance(v, str) and len(v.strip()) == 0
return dict([(k, v) for k, v in dic.iteritems() if not _is_blank(v)]) return dict([(k, v) for k, v in dic.iteritems() if not _is_blank(v)])
...@@ -50,16 +62,23 @@ def strip_blank(dic): ...@@ -50,16 +62,23 @@ def strip_blank(dic):
def merge_dict(dic1, dic2): def merge_dict(dic1, dic2):
"""
Combines the keys from the two provided dictionaries
"""
return dict(dic1.items() + dic2.items()) return dict(dic1.items() + dic2.items())
def get_role_ids(course_id): def get_role_ids(course_id):
"""
Returns a dictionary having role names as keys and a list of users as values
"""
roles = Role.objects.filter(course_id=course_id).exclude(name=FORUM_ROLE_STUDENT) roles = Role.objects.filter(course_id=course_id).exclude(name=FORUM_ROLE_STUDENT)
return dict([(role.name, list(role.users.values_list('id', flat=True))) for role in roles]) return dict([(role.name, list(role.users.values_list('id', flat=True))) for role in roles])
def has_discussion_privileges(user, course_id): def has_discussion_privileges(user, course_id):
"""Returns True if the user is privileged in teams discussions for """
Returns True if the user is privileged in teams discussions for
this course. The user must be one of Discussion Admin, Moderator, this course. The user must be one of Discussion Admin, Moderator,
or Community TA. or Community TA.
...@@ -79,6 +98,9 @@ def has_discussion_privileges(user, course_id): ...@@ -79,6 +98,9 @@ def has_discussion_privileges(user, course_id):
def has_forum_access(uname, course_id, rolename): def has_forum_access(uname, course_id, rolename):
"""
Boolean operation which tests a user's role-based permissions (not actually forums-specific)
"""
try: try:
role = Role.objects.get(name=rolename, course_id=course_id) role = Role.objects.get(name=rolename, course_id=course_id)
except Role.DoesNotExist: except Role.DoesNotExist:
...@@ -87,10 +109,17 @@ def has_forum_access(uname, course_id, rolename): ...@@ -87,10 +109,17 @@ def has_forum_access(uname, course_id, rolename):
def has_required_keys(module): def has_required_keys(module):
"""Returns True iff module has the proper attributes for generating metadata with get_discussion_id_map_entry()""" """
Returns True iff module has the proper attributes for generating metadata
with get_discussion_id_map_entry()
"""
for key in ('discussion_id', 'discussion_category', 'discussion_target'): for key in ('discussion_id', 'discussion_category', 'discussion_target'):
if getattr(module, key, None) is None: if getattr(module, key, None) is None:
log.debug("Required key '%s' not in discussion %s, leaving out of category map", key, module.location) log.debug(
"Required key '%s' not in discussion %s, leaving out of category map",
key,
module.location
)
return False return False
return True return True
...@@ -170,7 +199,10 @@ def get_discussion_id_map(course, user): ...@@ -170,7 +199,10 @@ def get_discussion_id_map(course, user):
def _filter_unstarted_categories(category_map): def _filter_unstarted_categories(category_map):
"""
Returns a subset of categories from the provided map which have not yet met the start date
Includes information about category children, subcategories (different), and entries
"""
now = datetime.now(UTC()) now = datetime.now(UTC())
result_map = {} result_map = {}
...@@ -208,6 +240,9 @@ def _filter_unstarted_categories(category_map): ...@@ -208,6 +240,9 @@ def _filter_unstarted_categories(category_map):
def _sort_map_entries(category_map, sort_alpha): def _sort_map_entries(category_map, sort_alpha):
"""
Internal helper method to list category entries according to the provided sort order
"""
things = [] things = []
for title, entry in category_map["entries"].items(): for title, entry in category_map["entries"].items():
if entry["sort_key"] is None and sort_alpha: if entry["sort_key"] is None and sort_alpha:
...@@ -387,14 +422,26 @@ def get_discussion_categories_ids(course, user, include_all=False): ...@@ -387,14 +422,26 @@ def get_discussion_categories_ids(course, user, include_all=False):
class JsonResponse(HttpResponse): class JsonResponse(HttpResponse):
"""
Django response object delivering JSON representations
"""
def __init__(self, data=None): def __init__(self, data=None):
"""
Object constructor, converts data (if provided) to JSON
"""
content = json.dumps(data, cls=i4xEncoder) content = json.dumps(data, cls=i4xEncoder)
super(JsonResponse, self).__init__(content, super(JsonResponse, self).__init__(content,
mimetype='application/json; charset=utf-8') mimetype='application/json; charset=utf-8')
class JsonError(HttpResponse): class JsonError(HttpResponse):
"""
Django response object delivering JSON exceptions
"""
def __init__(self, error_messages=[], status=400): def __init__(self, error_messages=[], status=400):
"""
Object constructor, returns an error response containing the provided exception messages
"""
if isinstance(error_messages, basestring): if isinstance(error_messages, basestring):
error_messages = [error_messages] error_messages = [error_messages]
content = json.dumps({'errors': error_messages}, indent=2, ensure_ascii=False) content = json.dumps({'errors': error_messages}, indent=2, ensure_ascii=False)
...@@ -403,12 +450,24 @@ class JsonError(HttpResponse): ...@@ -403,12 +450,24 @@ class JsonError(HttpResponse):
class HtmlResponse(HttpResponse): class HtmlResponse(HttpResponse):
"""
Django response object delivering HTML representations
"""
def __init__(self, html=''): def __init__(self, html=''):
"""
Object constructor, brokers provided HTML to caller
"""
super(HtmlResponse, self).__init__(html, content_type='text/plain') super(HtmlResponse, self).__init__(html, content_type='text/plain')
class ViewNameMiddleware(object): class ViewNameMiddleware(object):
"""
Django middleware object to inject view name into request context
"""
def process_view(self, request, view_func, view_args, view_kwargs): def process_view(self, request, view_func, view_args, view_kwargs):
"""
Injects the view name value into the request context
"""
request.view_name = view_func.__name__ request.view_name = view_func.__name__
...@@ -420,6 +479,9 @@ class QueryCountDebugMiddleware(object): ...@@ -420,6 +479,9 @@ class QueryCountDebugMiddleware(object):
multi-db setups. multi-db setups.
""" """
def process_response(self, request, response): def process_response(self, request, response):
"""
Log information for 200 OK responses as part of the outbound pipeline
"""
if response.status_code == 200: if response.status_code == 200:
total_time = 0 total_time = 0
...@@ -439,6 +501,9 @@ class QueryCountDebugMiddleware(object): ...@@ -439,6 +501,9 @@ class QueryCountDebugMiddleware(object):
def get_ability(course_id, content, user): def get_ability(course_id, content, user):
"""
Return a dictionary of forums-oriented actions and the user's permission to perform them
"""
return { return {
'editable': check_permissions_by_view(user, course_id, content, "update_thread" if content['type'] == 'thread' else "update_comment"), 'editable': check_permissions_by_view(user, course_id, content, "update_thread" if content['type'] == 'thread' else "update_comment"),
'can_reply': check_permissions_by_view(user, course_id, content, "create_comment" if content['type'] == 'thread' else "create_sub_comment"), 'can_reply': check_permissions_by_view(user, course_id, content, "create_comment" if content['type'] == 'thread' else "create_sub_comment"),
...@@ -487,6 +552,10 @@ def get_annotated_content_infos(course_id, thread, user, user_info): ...@@ -487,6 +552,10 @@ def get_annotated_content_infos(course_id, thread, user, user_info):
def get_metadata_for_threads(course_id, threads, user, user_info): def get_metadata_for_threads(course_id, threads, user, user_info):
"""
Returns annotated content information for the specified course, threads, and user information
"""
def infogetter(thread): def infogetter(thread):
return get_annotated_content_infos(course_id, thread, user, user_info) return get_annotated_content_infos(course_id, thread, user, user_info)
......
...@@ -28,14 +28,14 @@ class Command(BaseCommand): ...@@ -28,14 +28,14 @@ class Command(BaseCommand):
attempts_to_retry = SoftwareSecurePhotoVerification.objects.filter(status='must_retry') attempts_to_retry = SoftwareSecurePhotoVerification.objects.filter(status='must_retry')
force_must_retry = False force_must_retry = False
print("Attempting to retry {0} failed PhotoVerification submissions".format(len(attempts_to_retry))) print "Attempting to retry {0} failed PhotoVerification submissions".format(len(attempts_to_retry))
for index, attempt in enumerate(attempts_to_retry): for index, attempt in enumerate(attempts_to_retry):
print("Retrying submission #{0} (ID: {1}, User: {2})".format(index, attempt.id, attempt.user)) print "Retrying submission #{0} (ID: {1}, User: {2})".format(index, attempt.id, attempt.user)
# Set the attempts status to 'must_retry' so that we can re-submit it # Set the attempts status to 'must_retry' so that we can re-submit it
if force_must_retry: if force_must_retry:
attempt.status = 'must_retry' attempt.status = 'must_retry'
attempt.submit(copy_id_photo_from=attempt.copy_id_photo_from) attempt.submit(copy_id_photo_from=attempt.copy_id_photo_from)
print("Retry result: {0}".format(attempt.status)) print "Retry result: {0}".format(attempt.status)
print("Done resubmitting failed photo verifications") print "Done resubmitting failed photo verifications"
"""
Django Admin model registry for Course Structures sub-application
"""
from ratelimitbackend import admin from ratelimitbackend import admin
from .models import CourseStructure from .models import CourseStructure
class CourseStructureAdmin(admin.ModelAdmin): class CourseStructureAdmin(admin.ModelAdmin):
"""
Django Admin class for managing Course Structures model data
"""
search_fields = ('course_id',) search_fields = ('course_id',)
list_display = ('course_id', 'modified') list_display = ('course_id', 'modified')
ordering = ('course_id', '-modified') ordering = ('course_id', '-modified')
......
"""
Django Management Command: Generate Course Structure
Generates and stores course structure information for one or more courses.
"""
import logging import logging
from optparse import make_option from optparse import make_option
...@@ -12,6 +17,9 @@ log = logging.getLogger(__name__) ...@@ -12,6 +17,9 @@ log = logging.getLogger(__name__)
class Command(BaseCommand): class Command(BaseCommand):
"""
Generates and stores course structure information for one or more courses.
"""
args = '<course_id course_id ...>' args = '<course_id course_id ...>'
help = 'Generates and stores course structure for one or more courses.' help = 'Generates and stores course structure for one or more courses.'
...@@ -23,7 +31,9 @@ class Command(BaseCommand): ...@@ -23,7 +31,9 @@ class Command(BaseCommand):
) )
def handle(self, *args, **options): def handle(self, *args, **options):
"""
Perform the course structure generation workflow
"""
if options['all']: if options['all']:
course_keys = [course.id for course in modulestore().get_courses()] course_keys = [course.id for course in modulestore().get_courses()]
else: else:
......
"""
Django ORM model specifications for the Course Structures sub-application
"""
import json import json
import logging import logging
...@@ -12,6 +15,9 @@ logger = logging.getLogger(__name__) # pylint: disable=invalid-name ...@@ -12,6 +15,9 @@ logger = logging.getLogger(__name__) # pylint: disable=invalid-name
class CourseStructure(TimeStampedModel): class CourseStructure(TimeStampedModel):
"""
The CourseStructure model is an aggregated representation of the course content tree
"""
course_id = CourseKeyField(max_length=255, db_index=True, unique=True, verbose_name='Course ID') course_id = CourseKeyField(max_length=255, db_index=True, unique=True, verbose_name='Course ID')
# Right now the only thing we do with the structure doc is store it and # Right now the only thing we do with the structure doc is store it and
...@@ -26,6 +32,9 @@ class CourseStructure(TimeStampedModel): ...@@ -26,6 +32,9 @@ class CourseStructure(TimeStampedModel):
@property @property
def structure(self): def structure(self):
"""
Deserializes a course structure JSON object
"""
if self.structure_json: if self.structure_json:
return json.loads(self.structure_json) return json.loads(self.structure_json)
return None return None
......
"""
Django Signals classes and functions for the Course Structure application
"""
from django.dispatch.dispatcher import receiver from django.dispatch.dispatcher import receiver
from xmodule.modulestore.django import SignalHandler from xmodule.modulestore.django import SignalHandler
...@@ -7,6 +10,9 @@ from .models import CourseStructure ...@@ -7,6 +10,9 @@ from .models import CourseStructure
@receiver(SignalHandler.course_published) @receiver(SignalHandler.course_published)
def listen_for_course_publish(sender, course_key, **kwargs): # pylint: disable=unused-argument def listen_for_course_publish(sender, course_key, **kwargs): # pylint: disable=unused-argument
"""
Course Structure application receiver for the course_published signal
"""
# Import tasks here to avoid a circular import. # Import tasks here to avoid a circular import.
from .tasks import update_course_structure from .tasks import update_course_structure
......
"""
Asynchronous tasks related to the Course Structure sub-application
"""
import json import json
import logging import logging
......
"""
Course Structure Content sub-application test cases
"""
import json import json
from xmodule_django.models import UsageKey from xmodule_django.models import UsageKey
...@@ -20,6 +23,9 @@ class SignalDisconnectTestMixin(object): ...@@ -20,6 +23,9 @@ class SignalDisconnectTestMixin(object):
class CourseStructureTaskTests(ModuleStoreTestCase): class CourseStructureTaskTests(ModuleStoreTestCase):
"""
Test cases covering Course Structure task-related workflows
"""
def setUp(self, **kwargs): def setUp(self, **kwargs):
super(CourseStructureTaskTests, self).setUp() super(CourseStructureTaskTests, self).setUp()
self.course = CourseFactory.create(org='TestX', course='TS101', run='T1') self.course = CourseFactory.create(org='TestX', course='TS101', run='T1')
...@@ -40,6 +46,9 @@ class CourseStructureTaskTests(ModuleStoreTestCase): ...@@ -40,6 +46,9 @@ class CourseStructureTaskTests(ModuleStoreTestCase):
blocks = {} blocks = {}
def add_block(block): def add_block(block):
"""
Inserts new child XBlocks into the existing course tree
"""
children = block.get_children() if block.has_children else [] children = block.get_children() if block.has_children else []
blocks[unicode(block.location)] = { blocks[unicode(block.location)] = {
......
...@@ -38,6 +38,9 @@ def _cohort_added(sender, **kwargs): ...@@ -38,6 +38,9 @@ def _cohort_added(sender, **kwargs):
def _cohort_membership_changed(sender, **kwargs): def _cohort_membership_changed(sender, **kwargs):
"""Emits a tracking log event each time cohort membership is modified""" """Emits a tracking log event each time cohort membership is modified"""
def get_event_iter(user_id_iter, cohort_iter): def get_event_iter(user_id_iter, cohort_iter):
"""
Returns a dictionary containing a mashup of cohort and user information for the given lists
"""
return ( return (
{"cohort_id": cohort.id, "cohort_name": cohort.name, "user_id": user_id} {"cohort_id": cohort.id, "cohort_name": cohort.name, "user_id": user_id}
for user_id in user_id_iter for user_id in user_id_iter
......
...@@ -101,6 +101,9 @@ def config_course_cohorts_legacy( ...@@ -101,6 +101,9 @@ def config_course_cohorts_legacy(
Nothing -- modifies course in place. Nothing -- modifies course in place.
""" """
def to_id(name): def to_id(name):
"""
Helper method to convert a discussion topic name to a database identifier
"""
return topic_name_to_id(course, name) return topic_name_to_id(course, name)
topics = dict((name, {"sort_key": "A", topics = dict((name, {"sort_key": "A",
......
...@@ -26,6 +26,10 @@ from ..tests.helpers import ( ...@@ -26,6 +26,10 @@ from ..tests.helpers import (
@patch("openedx.core.djangoapps.course_groups.cohorts.tracker") @patch("openedx.core.djangoapps.course_groups.cohorts.tracker")
class TestCohortSignals(TestCase): class TestCohortSignals(TestCase):
"""
Test cases to validate event emissions for various cohort-related workflows
"""
def setUp(self): def setUp(self):
super(TestCohortSignals, self).setUp() super(TestCohortSignals, self).setUp()
self.course_key = SlashSeparatedCourseKey("dummy", "dummy", "dummy") self.course_key = SlashSeparatedCourseKey("dummy", "dummy", "dummy")
...@@ -67,6 +71,9 @@ class TestCohortSignals(TestCase): ...@@ -67,6 +71,9 @@ class TestCohortSignals(TestCase):
mock_tracker.reset_mock() mock_tracker.reset_mock()
def assert_events(event_name_suffix, user_list, cohort_list): def assert_events(event_name_suffix, user_list, cohort_list):
"""
Confirms the presence of the specifed event for each user in the specified list of cohorts
"""
mock_tracker.emit.assert_has_calls([ mock_tracker.emit.assert_has_calls([
call( call(
"edx.cohort.user_" + event_name_suffix, "edx.cohort.user_" + event_name_suffix,
......
...@@ -100,6 +100,9 @@ class CohortViewsTestCase(ModuleStoreTestCase): ...@@ -100,6 +100,9 @@ class CohortViewsTestCase(ModuleStoreTestCase):
self.assertRaises(Http404, view, *view_args) self.assertRaises(Http404, view, *view_args)
def create_cohorted_discussions(self): def create_cohorted_discussions(self):
"""
Set up a cohorted discussion in the system, complete with all the fixings
"""
cohorted_inline_discussions = ['Topic A'] cohorted_inline_discussions = ['Topic A']
cohorted_course_wide_discussions = ["Topic B"] cohorted_course_wide_discussions = ["Topic B"]
cohorted_discussions = cohorted_inline_discussions + cohorted_course_wide_discussions cohorted_discussions = cohorted_inline_discussions + cohorted_course_wide_discussions
......
...@@ -196,7 +196,10 @@ class ProfileImageViewPostTestCase(ProfileImageEndpointMixin, APITestCase): ...@@ -196,7 +196,10 @@ class ProfileImageViewPostTestCase(ProfileImageEndpointMixin, APITestCase):
self.check_anonymous_request_rejected('post') self.check_anonymous_request_rejected('post')
self.assertFalse(mock_log.info.called) self.assertFalse(mock_log.info.called)
@patch('openedx.core.djangoapps.profile_images.views._make_upload_dt', side_effect=[TEST_UPLOAD_DT, TEST_UPLOAD_DT2]) @patch(
'openedx.core.djangoapps.profile_images.views._make_upload_dt',
side_effect=[TEST_UPLOAD_DT, TEST_UPLOAD_DT2]
)
def test_upload_self(self, mock_make_image_version, mock_log): # pylint: disable=unused-argument def test_upload_self(self, mock_make_image_version, mock_log): # pylint: disable=unused-argument
""" """
Test that an authenticated user can POST to their own upload endpoint. Test that an authenticated user can POST to their own upload endpoint.
......
"""
Programmatic integration point for User API Accounts sub-application
"""
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from django.db import transaction, IntegrityError from django.db import transaction, IntegrityError
import datetime import datetime
......
"""
Django REST Framework serializers for the User API Accounts sub-application
"""
from rest_framework import serializers from rest_framework import serializers
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.conf import settings from django.conf import settings
......
...@@ -212,6 +212,9 @@ class TestAccountApi(UserSettingsEventTestMixin, TestCase): ...@@ -212,6 +212,9 @@ class TestAccountApi(UserSettingsEventTestMixin, TestCase):
Test that eventing of language proficiencies, which happens update_account_settings method, behaves correctly. Test that eventing of language proficiencies, which happens update_account_settings method, behaves correctly.
""" """
def verify_event_emitted(new_value, old_value): def verify_event_emitted(new_value, old_value):
"""
Confirm that the user setting event was properly emitted
"""
update_account_settings(self.user, {"language_proficiencies": new_value}) update_account_settings(self.user, {"language_proficiencies": new_value})
self.assert_user_setting_event_emitted(setting='language_proficiencies', old=old_value, new=new_value) self.assert_user_setting_event_emitted(setting='language_proficiencies', old=old_value, new=new_value)
self.reset_tracker() self.reset_tracker()
...@@ -226,7 +229,9 @@ class TestAccountApi(UserSettingsEventTestMixin, TestCase): ...@@ -226,7 +229,9 @@ class TestAccountApi(UserSettingsEventTestMixin, TestCase):
@patch('openedx.core.djangoapps.user_api.accounts.image_helpers._PROFILE_IMAGE_SIZES', [50, 10]) @patch('openedx.core.djangoapps.user_api.accounts.image_helpers._PROFILE_IMAGE_SIZES', [50, 10])
@patch.dict( @patch.dict(
'openedx.core.djangoapps.user_api.accounts.image_helpers.PROFILE_IMAGE_SIZES_MAP', {'full': 50, 'small': 10}, clear=True 'openedx.core.djangoapps.user_api.accounts.image_helpers.PROFILE_IMAGE_SIZES_MAP',
{'full': 50, 'small': 10},
clear=True
) )
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Account APIs are only supported in LMS') @unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Account APIs are only supported in LMS')
class AccountSettingsOnCreationTest(TestCase): class AccountSettingsOnCreationTest(TestCase):
...@@ -275,7 +280,9 @@ class AccountSettingsOnCreationTest(TestCase): ...@@ -275,7 +280,9 @@ class AccountSettingsOnCreationTest(TestCase):
@ddt.ddt @ddt.ddt
class AccountCreationActivationAndPasswordChangeTest(TestCase): class AccountCreationActivationAndPasswordChangeTest(TestCase):
"""
Test cases to cover the account initialization workflow
"""
USERNAME = u'frank-underwood' USERNAME = u'frank-underwood'
PASSWORD = u'ṕáśśẃőŕd' PASSWORD = u'ṕáśśẃőŕd'
EMAIL = u'frank+underwood@example.com' EMAIL = u'frank+underwood@example.com'
...@@ -415,6 +422,9 @@ class AccountCreationActivationAndPasswordChangeTest(TestCase): ...@@ -415,6 +422,9 @@ class AccountCreationActivationAndPasswordChangeTest(TestCase):
self.assertEqual(len(mail.outbox), 1) self.assertEqual(len(mail.outbox), 1)
def _assert_is_datetime(self, timestamp): def _assert_is_datetime(self, timestamp):
"""
Internal helper to validate the type of the provided timestamp
"""
if not timestamp: if not timestamp:
return False return False
try: try:
......
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
"""
Test cases to cover Accounts-related behaviors of the User API application
"""
import datetime import datetime
from copy import deepcopy from copy import deepcopy
import ddt import ddt
...@@ -109,7 +112,9 @@ class UserAPITestCase(APITestCase): ...@@ -109,7 +112,9 @@ class UserAPITestCase(APITestCase):
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Account APIs are only supported in LMS') @unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Account APIs are only supported in LMS')
@patch('openedx.core.djangoapps.user_api.accounts.image_helpers._PROFILE_IMAGE_SIZES', [50, 10]) @patch('openedx.core.djangoapps.user_api.accounts.image_helpers._PROFILE_IMAGE_SIZES', [50, 10])
@patch.dict( @patch.dict(
'openedx.core.djangoapps.user_api.accounts.image_helpers.PROFILE_IMAGE_SIZES_MAP', {'full': 50, 'small': 10}, clear=True 'openedx.core.djangoapps.user_api.accounts.image_helpers.PROFILE_IMAGE_SIZES_MAP',
{'full': 50, 'small': 10},
clear=True
) )
class TestAccountAPI(UserAPITestCase): class TestAccountAPI(UserAPITestCase):
""" """
...@@ -261,6 +266,9 @@ class TestAccountAPI(UserAPITestCase): ...@@ -261,6 +266,9 @@ class TestAccountAPI(UserAPITestCase):
Test the return from GET based on user visibility setting. Test the return from GET based on user visibility setting.
""" """
def verify_fields_visible_to_all_users(response): def verify_fields_visible_to_all_users(response):
"""
Confirms that private fields are private, and public/shareable fields are public/shareable
"""
if preference_visibility == PRIVATE_VISIBILITY: if preference_visibility == PRIVATE_VISIBILITY:
self._verify_private_account_response(response) self._verify_private_account_response(response)
else: else:
...@@ -288,6 +296,9 @@ class TestAccountAPI(UserAPITestCase): ...@@ -288,6 +296,9 @@ class TestAccountAPI(UserAPITestCase):
as created by the test UserFactory). as created by the test UserFactory).
""" """
def verify_get_own_information(): def verify_get_own_information():
"""
Internal helper to perform the actual assertions
"""
response = self.send_get(self.client) response = self.send_get(self.client)
data = response.data data = response.data
self.assertEqual(15, len(data)) self.assertEqual(15, len(data))
...@@ -417,6 +428,9 @@ class TestAccountAPI(UserAPITestCase): ...@@ -417,6 +428,9 @@ class TestAccountAPI(UserAPITestCase):
client = self.login_client("client", "user") client = self.login_client("client", "user")
def verify_error_response(field_name, data): def verify_error_response(field_name, data):
"""
Internal helper to check the error messages returned
"""
self.assertEqual( self.assertEqual(
"This field is not editable via this API", data["field_errors"][field_name]["developer_message"] "This field is not editable via this API", data["field_errors"][field_name]["developer_message"]
) )
...@@ -467,12 +481,18 @@ class TestAccountAPI(UserAPITestCase): ...@@ -467,12 +481,18 @@ class TestAccountAPI(UserAPITestCase):
Test the metadata stored when changing the name field. Test the metadata stored when changing the name field.
""" """
def get_name_change_info(expected_entries): def get_name_change_info(expected_entries):
"""
Internal method to encapsulate the retrieval of old names used
"""
legacy_profile = UserProfile.objects.get(id=self.user.id) legacy_profile = UserProfile.objects.get(id=self.user.id)
name_change_info = legacy_profile.get_meta()["old_names"] name_change_info = legacy_profile.get_meta()["old_names"]
self.assertEqual(expected_entries, len(name_change_info)) self.assertEqual(expected_entries, len(name_change_info))
return name_change_info return name_change_info
def verify_change_info(change_info, old_name, requester, new_name): def verify_change_info(change_info, old_name, requester, new_name):
"""
Internal method to validate name changes
"""
self.assertEqual(3, len(change_info)) self.assertEqual(3, len(change_info))
self.assertEqual(old_name, change_info[0]) self.assertEqual(old_name, change_info[0])
self.assertEqual("Name change requested through account API by {}".format(requester), change_info[1]) self.assertEqual("Name change requested through account API by {}".format(requester), change_info[1])
...@@ -566,7 +586,10 @@ class TestAccountAPI(UserAPITestCase): ...@@ -566,7 +586,10 @@ class TestAccountAPI(UserAPITestCase):
([u"not_a_JSON_object"], [{u'non_field_errors': [u'Invalid data. Expected a dictionary, but got unicode.']}]), ([u"not_a_JSON_object"], [{u'non_field_errors': [u'Invalid data. Expected a dictionary, but got unicode.']}]),
([{}], [{"code": [u"This field is required."]}]), ([{}], [{"code": [u"This field is required."]}]),
([{u"code": u"invalid_language_code"}], [{'code': [u'"invalid_language_code" is not a valid choice.']}]), ([{u"code": u"invalid_language_code"}], [{'code': [u'"invalid_language_code" is not a valid choice.']}]),
([{u"code": u"kw"}, {u"code": u"el"}, {u"code": u"kw"}], [u'The language_proficiencies field must consist of unique languages']), (
[{u"code": u"kw"}, {u"code": u"el"}, {u"code": u"kw"}],
[u'The language_proficiencies field must consist of unique languages']
),
) )
@ddt.unpack @ddt.unpack
def test_patch_invalid_language_proficiencies(self, patch_value, expected_error_message): def test_patch_invalid_language_proficiencies(self, patch_value, expected_error_message):
...@@ -578,7 +601,10 @@ class TestAccountAPI(UserAPITestCase): ...@@ -578,7 +601,10 @@ class TestAccountAPI(UserAPITestCase):
response = self.send_patch(client, {"language_proficiencies": patch_value}, expected_status=400) response = self.send_patch(client, {"language_proficiencies": patch_value}, expected_status=400)
self.assertEqual( self.assertEqual(
response.data["field_errors"]["language_proficiencies"]["developer_message"], response.data["field_errors"]["language_proficiencies"]["developer_message"],
u"Value '{patch_value}' is not valid for field 'language_proficiencies': {error_message}".format(patch_value=patch_value, error_message=expected_error_message) u"Value '{patch_value}' is not valid for field 'language_proficiencies': {error_message}".format(
patch_value=patch_value,
error_message=expected_error_message
)
) )
@patch('openedx.core.djangoapps.user_api.accounts.serializers.AccountUserSerializer.save') @patch('openedx.core.djangoapps.user_api.accounts.serializers.AccountUserSerializer.save')
......
# pylint: disable=unused-import, missing-docstring # pylint: disable=unused-import
# TODO: eventually move this implementation into the user_api # TODO: eventually move this implementation into the user_api
"""
Django Administration forms module
"""
from student.forms import PasswordResetFormNoActive from student.forms import PasswordResetFormNoActive
"""
Django ORM model specifications for the User API application
"""
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.core.validators import RegexValidator from django.core.validators import RegexValidator
from django.db import models from django.db import models
......
...@@ -293,7 +293,9 @@ class TestPreferenceAPI(TestCase): ...@@ -293,7 +293,9 @@ class TestPreferenceAPI(TestCase):
@ddt.ddt @ddt.ddt
class UpdateEmailOptInTests(ModuleStoreTestCase): class UpdateEmailOptInTests(ModuleStoreTestCase):
"""
Test cases to cover API-driven email list opt-in update workflows
"""
USERNAME = u'frank-underwood' USERNAME = u'frank-underwood'
PASSWORD = u'ṕáśśẃőŕd' PASSWORD = u'ṕáśśẃőŕd'
EMAIL = u'frank+underwood@example.com' EMAIL = u'frank+underwood@example.com'
...@@ -385,6 +387,9 @@ class UpdateEmailOptInTests(ModuleStoreTestCase): ...@@ -385,6 +387,9 @@ class UpdateEmailOptInTests(ModuleStoreTestCase):
self.assertEqual(result_obj.value, expected_result) self.assertEqual(result_obj.value, expected_result)
def _assert_is_datetime(self, timestamp): def _assert_is_datetime(self, timestamp):
"""
Internal helper to assert the type of the provided timestamp value
"""
if not timestamp: if not timestamp:
return False return False
try: try:
......
...@@ -133,6 +133,9 @@ class TestPreferencesAPI(UserAPITestCase): ...@@ -133,6 +133,9 @@ class TestPreferencesAPI(UserAPITestCase):
self._do_create_preferences_test(False) self._do_create_preferences_test(False)
def _do_create_preferences_test(self, is_active): def _do_create_preferences_test(self, is_active):
"""
Internal helper to generalize the creation of a set of preferences
"""
self.client.login(username=self.user.username, password=self.test_password) self.client.login(username=self.user.username, password=self.test_password)
if not is_active: if not is_active:
self.user.is_active = False self.user.is_active = False
...@@ -361,6 +364,9 @@ class TestPreferencesDetailAPI(UserAPITestCase): ...@@ -361,6 +364,9 @@ class TestPreferencesDetailAPI(UserAPITestCase):
self._set_url(self.test_pref_key) self._set_url(self.test_pref_key)
def _set_url(self, preference_key): def _set_url(self, preference_key):
"""
Sets the url attribute including the username and provided preference key
"""
self.url = reverse( self.url = reverse(
self.url_endpoint_name, self.url_endpoint_name,
kwargs={'username': self.user.username, 'preference_key': preference_key} kwargs={'username': self.user.username, 'preference_key': preference_key}
...@@ -448,6 +454,9 @@ class TestPreferencesDetailAPI(UserAPITestCase): ...@@ -448,6 +454,9 @@ class TestPreferencesDetailAPI(UserAPITestCase):
self._do_create_preference_test(False) self._do_create_preference_test(False)
def _do_create_preference_test(self, is_active): def _do_create_preference_test(self, is_active):
"""
Generalization of the actual test workflow
"""
self.client.login(username=self.user.username, password=self.test_password) self.client.login(username=self.user.username, password=self.test_password)
if not is_active: if not is_active:
self.user.is_active = False self.user.is_active = False
......
"""
Django REST Framework serializers for the User API application
"""
from django.contrib.auth.models import User from django.contrib.auth.models import User
from rest_framework import serializers from rest_framework import serializers
from student.models import UserProfile from student.models import UserProfile
...@@ -6,14 +9,23 @@ from .models import UserPreference ...@@ -6,14 +9,23 @@ from .models import UserPreference
class UserSerializer(serializers.HyperlinkedModelSerializer): class UserSerializer(serializers.HyperlinkedModelSerializer):
"""
Serializer that generates a representation of a User entity containing a subset of fields
"""
name = serializers.SerializerMethodField() name = serializers.SerializerMethodField()
preferences = serializers.SerializerMethodField() preferences = serializers.SerializerMethodField()
def get_name(self, user): def get_name(self, user):
"""
Return the name attribute from the user profile object
"""
profile = UserProfile.objects.get(user=user) profile = UserProfile.objects.get(user=user)
return profile.name return profile.name
def get_preferences(self, user): def get_preferences(self, user):
"""
Returns the set of preferences as a dict for the specified user
"""
return dict([(pref.key, pref.value) for pref in user.preferences.all()]) return dict([(pref.key, pref.value) for pref in user.preferences.all()])
class Meta(object): # pylint: disable=missing-docstring class Meta(object): # pylint: disable=missing-docstring
...@@ -24,6 +36,9 @@ class UserSerializer(serializers.HyperlinkedModelSerializer): ...@@ -24,6 +36,9 @@ class UserSerializer(serializers.HyperlinkedModelSerializer):
class UserPreferenceSerializer(serializers.HyperlinkedModelSerializer): class UserPreferenceSerializer(serializers.HyperlinkedModelSerializer):
"""
Serializer that generates a represenation of a UserPreference entity
"""
user = UserSerializer() user = UserSerializer()
class Meta(object): # pylint: disable=missing-docstring class Meta(object): # pylint: disable=missing-docstring
......
...@@ -51,11 +51,10 @@ class InterceptErrorsTest(TestCase): ...@@ -51,11 +51,10 @@ class InterceptErrorsTest(TestCase):
@mock.patch('openedx.core.djangoapps.user_api.helpers.LOGGER') @mock.patch('openedx.core.djangoapps.user_api.helpers.LOGGER')
def test_logs_errors(self, mock_logger): def test_logs_errors(self, mock_logger):
exception = 'openedx.core.djangoapps.user_api.tests.test_helpers.FakeInputException'
expected_log_msg = ( expected_log_msg = (
u"An unexpected error occurred when calling 'intercepted_function' " u"An unexpected error occurred when calling 'intercepted_function' with arguments '()' and "
u"with arguments '()' and " u"keyword arguments '{'raise_error': <class '" + exception + u"'>}': FakeInputException()"
u"keyword arguments '{'raise_error': <class 'openedx.core.djangoapps.user_api.tests.test_helpers.FakeInputException'>}': "
u"FakeInputException()"
) )
# Verify that the raised exception has the error message # Verify that the raised exception has the error message
......
...@@ -15,6 +15,9 @@ from ..preferences.api import set_user_preference ...@@ -15,6 +15,9 @@ from ..preferences.api import set_user_preference
class UserPreferenceModelTest(ModuleStoreTestCase): class UserPreferenceModelTest(ModuleStoreTestCase):
"""
Test case covering User Preference ORM model attributes and custom operations
"""
def test_duplicate_user_key(self): def test_duplicate_user_key(self):
user = UserFactory.create() user = UserFactory.create()
UserPreferenceFactory.create(user=user, key="testkey", value="first") UserPreferenceFactory.create(user=user, key="testkey", value="first")
......
...@@ -53,7 +53,11 @@ class TestRandomUserPartitionScheme(PartitionTestCase): ...@@ -53,7 +53,11 @@ class TestRandomUserPartitionScheme(PartitionTestCase):
# make sure we get the same group back out every time # make sure we get the same group back out every time
for __ in range(10): for __ in range(10):
group2_id = RandomUserPartitionScheme.get_group_for_user(self.MOCK_COURSE_ID, self.user, self.user_partition) group2_id = RandomUserPartitionScheme.get_group_for_user(
self.MOCK_COURSE_ID,
self.user,
self.user_partition
)
self.assertEqual(group1_id, group2_id) self.assertEqual(group1_id, group2_id)
def test_get_group_for_user_with_assign(self): def test_get_group_for_user_with_assign(self):
......
...@@ -50,10 +50,16 @@ ROLE_LIST_URI = "/user_api/v1/forum_roles/Moderator/users/" ...@@ -50,10 +50,16 @@ ROLE_LIST_URI = "/user_api/v1/forum_roles/Moderator/users/"
@override_settings(EDX_API_KEY=TEST_API_KEY) @override_settings(EDX_API_KEY=TEST_API_KEY)
class ApiTestCase(TestCase): class ApiTestCase(TestCase):
"""
Parent test case for API workflow coverage
"""
LIST_URI = USER_LIST_URI LIST_URI = USER_LIST_URI
def basic_auth(self, username, password): def basic_auth(self, username, password):
"""
Returns a dictionary containing the http auth header with encoded username+password
"""
return {'HTTP_AUTHORIZATION': 'Basic ' + base64.b64encode('%s:%s' % (username, password))} return {'HTTP_AUTHORIZATION': 'Basic ' + base64.b64encode('%s:%s' % (username, password))}
def request_with_auth(self, method, *args, **kwargs): def request_with_auth(self, method, *args, **kwargs):
...@@ -107,6 +113,9 @@ class ApiTestCase(TestCase): ...@@ -107,6 +113,9 @@ class ApiTestCase(TestCase):
self.assertSelfReferential(user) self.assertSelfReferential(user)
def assertPrefIsValid(self, pref): def assertPrefIsValid(self, pref):
"""
Assert that the given preference is acknowledged by the system
"""
self.assertItemsEqual(pref.keys(), ["user", "key", "value", "url"]) self.assertItemsEqual(pref.keys(), ["user", "key", "value", "url"])
self.assertSelfReferential(pref) self.assertSelfReferential(pref)
self.assertUserIsValid(pref["user"]) self.assertUserIsValid(pref["user"])
...@@ -142,6 +151,9 @@ class ApiTestCase(TestCase): ...@@ -142,6 +151,9 @@ class ApiTestCase(TestCase):
class EmptyUserTestCase(ApiTestCase): class EmptyUserTestCase(ApiTestCase):
"""
Test that the endpoint supports empty user result sets
"""
def test_get_list_empty(self): def test_get_list_empty(self):
result = self.get_json(self.LIST_URI) result = self.get_json(self.LIST_URI)
self.assertEqual(result["count"], 0) self.assertEqual(result["count"], 0)
...@@ -165,6 +177,9 @@ class EmptyRoleTestCase(ApiTestCase): ...@@ -165,6 +177,9 @@ class EmptyRoleTestCase(ApiTestCase):
class UserApiTestCase(ApiTestCase): class UserApiTestCase(ApiTestCase):
"""
Generalized test case class for specific implementations below
"""
def setUp(self): def setUp(self):
super(UserApiTestCase, self).setUp() super(UserApiTestCase, self).setUp()
self.users = [ self.users = [
...@@ -182,6 +197,9 @@ class UserApiTestCase(ApiTestCase): ...@@ -182,6 +197,9 @@ class UserApiTestCase(ApiTestCase):
class RoleTestCase(UserApiTestCase): class RoleTestCase(UserApiTestCase):
"""
Test cases covering Role-related views and their behaviors
"""
course_id = SlashSeparatedCourseKey.from_deprecated_string("org/course/run") course_id = SlashSeparatedCourseKey.from_deprecated_string("org/course/run")
LIST_URI = ROLE_LIST_URI + "?course_id=" + course_id.to_deprecated_string() LIST_URI = ROLE_LIST_URI + "?course_id=" + course_id.to_deprecated_string()
...@@ -268,6 +286,9 @@ class RoleTestCase(UserApiTestCase): ...@@ -268,6 +286,9 @@ class RoleTestCase(UserApiTestCase):
class UserViewSetTest(UserApiTestCase): class UserViewSetTest(UserApiTestCase):
"""
Test cases covering the User DRF view set class and its various behaviors
"""
LIST_URI = USER_LIST_URI LIST_URI = USER_LIST_URI
def setUp(self): def setUp(self):
...@@ -382,6 +403,9 @@ class UserViewSetTest(UserApiTestCase): ...@@ -382,6 +403,9 @@ class UserViewSetTest(UserApiTestCase):
class UserPreferenceViewSetTest(UserApiTestCase): class UserPreferenceViewSetTest(UserApiTestCase):
"""
Test cases covering the User Preference DRF view class and its various behaviors
"""
LIST_URI = USER_PREFERENCE_LIST_URI LIST_URI = USER_PREFERENCE_LIST_URI
def setUp(self): def setUp(self):
...@@ -522,6 +546,9 @@ class UserPreferenceViewSetTest(UserApiTestCase): ...@@ -522,6 +546,9 @@ class UserPreferenceViewSetTest(UserApiTestCase):
class PreferenceUsersListViewTest(UserApiTestCase): class PreferenceUsersListViewTest(UserApiTestCase):
"""
Test cases covering the list viewing behavior for user preferences
"""
LIST_URI = "/user_api/v1/preferences/key0/users/" LIST_URI = "/user_api/v1/preferences/key0/users/"
def test_options(self): def test_options(self):
...@@ -880,7 +907,7 @@ class RegistrationViewTest(ThirdPartyAuthTestMixin, ApiTestCase): ...@@ -880,7 +907,7 @@ class RegistrationViewTest(ThirdPartyAuthTestMixin, ApiTestCase):
u"required": True, u"required": True,
u"label": u"Public username", u"label": u"Public username",
u"placeholder": u"JaneDoe", u"placeholder": u"JaneDoe",
u"instructions": u"The name that will identify you in your courses - <strong>(cannot be changed later)</strong>", u"instructions": u"The name that will identify you in your courses - <strong>(cannot be changed later)</strong>", # pylint: disable=line-too-long
u"restrictions": { u"restrictions": {
"min_length": USERNAME_MIN_LENGTH, "min_length": USERNAME_MIN_LENGTH,
"max_length": USERNAME_MAX_LENGTH "max_length": USERNAME_MAX_LENGTH
...@@ -968,7 +995,7 @@ class RegistrationViewTest(ThirdPartyAuthTestMixin, ApiTestCase): ...@@ -968,7 +995,7 @@ class RegistrationViewTest(ThirdPartyAuthTestMixin, ApiTestCase):
u"required": True, u"required": True,
u"label": u"Public username", u"label": u"Public username",
u"placeholder": u"JaneDoe", u"placeholder": u"JaneDoe",
u"instructions": u"The name that will identify you in your courses - <strong>(cannot be changed later)</strong>", u"instructions": u"The name that will identify you in your courses - <strong>(cannot be changed later)</strong>", # pylint: disable=line-too-long
u"restrictions": { u"restrictions": {
"min_length": USERNAME_MIN_LENGTH, "min_length": USERNAME_MIN_LENGTH,
"max_length": USERNAME_MAX_LENGTH "max_length": USERNAME_MAX_LENGTH
...@@ -1097,19 +1124,22 @@ class RegistrationViewTest(ThirdPartyAuthTestMixin, ApiTestCase): ...@@ -1097,19 +1124,22 @@ class RegistrationViewTest(ThirdPartyAuthTestMixin, ApiTestCase):
) )
@mock.patch.dict(settings.FEATURES, {"ENABLE_MKTG_SITE": True}) @mock.patch.dict(settings.FEATURES, {"ENABLE_MKTG_SITE": True})
def test_registration_honor_code_mktg_site_enabled(self): def test_registration_honor_code_mktg_site_enabled(self):
link_html = '<a href=\"https://www.test.com/honor\">Terms of Service and Honor Code</a>'
self._assert_reg_field( self._assert_reg_field(
{"honor_code": "required"}, {"honor_code": "required"},
{ {
"label": "I agree to the {platform_name} <a href=\"https://www.test.com/honor\">Terms of Service and Honor Code</a>.".format( "label": "I agree to the {platform_name} {link_html}.".format(
platform_name=settings.PLATFORM_NAME platform_name=settings.PLATFORM_NAME,
link_html=link_html
), ),
"name": "honor_code", "name": "honor_code",
"defaultValue": False, "defaultValue": False,
"type": "checkbox", "type": "checkbox",
"required": True, "required": True,
"errorMessages": { "errorMessages": {
"required": "You must agree to the {platform_name} <a href=\"https://www.test.com/honor\">Terms of Service and Honor Code</a>.".format( "required": "You must agree to the {platform_name} {link_html}.".format(
platform_name=settings.PLATFORM_NAME platform_name=settings.PLATFORM_NAME,
link_html=link_html
) )
} }
} }
...@@ -1118,19 +1148,22 @@ class RegistrationViewTest(ThirdPartyAuthTestMixin, ApiTestCase): ...@@ -1118,19 +1148,22 @@ class RegistrationViewTest(ThirdPartyAuthTestMixin, ApiTestCase):
@override_settings(MKTG_URLS_LINK_MAP={"HONOR": "honor"}) @override_settings(MKTG_URLS_LINK_MAP={"HONOR": "honor"})
@mock.patch.dict(settings.FEATURES, {"ENABLE_MKTG_SITE": False}) @mock.patch.dict(settings.FEATURES, {"ENABLE_MKTG_SITE": False})
def test_registration_honor_code_mktg_site_disabled(self): def test_registration_honor_code_mktg_site_disabled(self):
link_html = '<a href=\"/honor\">Terms of Service and Honor Code</a>'
self._assert_reg_field( self._assert_reg_field(
{"honor_code": "required"}, {"honor_code": "required"},
{ {
"label": "I agree to the {platform_name} <a href=\"/honor\">Terms of Service and Honor Code</a>.".format( "label": "I agree to the {platform_name} {link_html}.".format(
platform_name=settings.PLATFORM_NAME platform_name=settings.PLATFORM_NAME,
link_html=link_html
), ),
"name": "honor_code", "name": "honor_code",
"defaultValue": False, "defaultValue": False,
"type": "checkbox", "type": "checkbox",
"required": True, "required": True,
"errorMessages": { "errorMessages": {
"required": "You must agree to the {platform_name} <a href=\"/honor\">Terms of Service and Honor Code</a>.".format( "required": "You must agree to the {platform_name} {link_html}.".format(
platform_name=settings.PLATFORM_NAME platform_name=settings.PLATFORM_NAME,
link_html=link_html
) )
} }
} }
...@@ -1145,38 +1178,44 @@ class RegistrationViewTest(ThirdPartyAuthTestMixin, ApiTestCase): ...@@ -1145,38 +1178,44 @@ class RegistrationViewTest(ThirdPartyAuthTestMixin, ApiTestCase):
def test_registration_separate_terms_of_service_mktg_site_enabled(self): def test_registration_separate_terms_of_service_mktg_site_enabled(self):
# Honor code field should say ONLY honor code, # Honor code field should say ONLY honor code,
# not "terms of service and honor code" # not "terms of service and honor code"
link_html = '<a href=\"https://www.test.com/honor\">Honor Code</a>'
self._assert_reg_field( self._assert_reg_field(
{"honor_code": "required", "terms_of_service": "required"}, {"honor_code": "required", "terms_of_service": "required"},
{ {
"label": "I agree to the {platform_name} <a href=\"https://www.test.com/honor\">Honor Code</a>.".format( "label": "I agree to the {platform_name} {link_html}.".format(
platform_name=settings.PLATFORM_NAME platform_name=settings.PLATFORM_NAME,
link_html=link_html
), ),
"name": "honor_code", "name": "honor_code",
"defaultValue": False, "defaultValue": False,
"type": "checkbox", "type": "checkbox",
"required": True, "required": True,
"errorMessages": { "errorMessages": {
"required": "You must agree to the {platform_name} <a href=\"https://www.test.com/honor\">Honor Code</a>.".format( "required": "You must agree to the {platform_name} {link_html}.".format(
platform_name=settings.PLATFORM_NAME platform_name=settings.PLATFORM_NAME,
link_html=link_html
) )
} }
} }
) )
# Terms of service field should also be present # Terms of service field should also be present
link_html = '<a href=\"https://www.test.com/tos\">Terms of Service</a>'
self._assert_reg_field( self._assert_reg_field(
{"honor_code": "required", "terms_of_service": "required"}, {"honor_code": "required", "terms_of_service": "required"},
{ {
"label": "I agree to the {platform_name} <a href=\"https://www.test.com/tos\">Terms of Service</a>.".format( "label": "I agree to the {platform_name} {link_html}.".format(
platform_name=settings.PLATFORM_NAME platform_name=settings.PLATFORM_NAME,
link_html=link_html
), ),
"name": "terms_of_service", "name": "terms_of_service",
"defaultValue": False, "defaultValue": False,
"type": "checkbox", "type": "checkbox",
"required": True, "required": True,
"errorMessages": { "errorMessages": {
"required": "You must agree to the {platform_name} <a href=\"https://www.test.com/tos\">Terms of Service</a>.".format( "required": "You must agree to the {platform_name} {link_html}.".format(
platform_name=settings.PLATFORM_NAME platform_name=settings.PLATFORM_NAME,
link_html=link_html
) )
} }
} }
......
...@@ -270,7 +270,7 @@ class RegistrationView(APIView): ...@@ -270,7 +270,7 @@ class RegistrationView(APIView):
# Translators: This message is shown to users who attempt to create a new # Translators: This message is shown to users who attempt to create a new
# account using an email address associated with an existing account. # account using an email address associated with an existing account.
"email": _( "email": _(
u"It looks like {email_address} belongs to an existing account. Try again with a different email address." u"It looks like {email_address} belongs to an existing account. Try again with a different email address." # pylint: disable=line-too-long
).format(email_address=email), ).format(email_address=email),
# Translators: This message is shown to users who attempt to create a new # Translators: This message is shown to users who attempt to create a new
# account using a username associated with an existing account. # account using a username associated with an existing account.
...@@ -800,6 +800,9 @@ class PasswordResetView(APIView): ...@@ -800,6 +800,9 @@ class PasswordResetView(APIView):
class UserViewSet(viewsets.ReadOnlyModelViewSet): class UserViewSet(viewsets.ReadOnlyModelViewSet):
"""
DRF class for interacting with the User ORM object
"""
authentication_classes = (authentication.SessionAuthentication,) authentication_classes = (authentication.SessionAuthentication,)
permission_classes = (ApiKeyHeaderPermission,) permission_classes = (ApiKeyHeaderPermission,)
queryset = User.objects.all().prefetch_related("preferences") queryset = User.objects.all().prefetch_related("preferences")
...@@ -833,6 +836,9 @@ class ForumRoleUsersListView(generics.ListAPIView): ...@@ -833,6 +836,9 @@ class ForumRoleUsersListView(generics.ListAPIView):
class UserPreferenceViewSet(viewsets.ReadOnlyModelViewSet): class UserPreferenceViewSet(viewsets.ReadOnlyModelViewSet):
"""
DRF class for interacting with the UserPreference ORM
"""
authentication_classes = (authentication.SessionAuthentication,) authentication_classes = (authentication.SessionAuthentication,)
permission_classes = (ApiKeyHeaderPermission,) permission_classes = (ApiKeyHeaderPermission,)
queryset = UserPreference.objects.all() queryset = UserPreference.objects.all()
...@@ -844,6 +850,9 @@ class UserPreferenceViewSet(viewsets.ReadOnlyModelViewSet): ...@@ -844,6 +850,9 @@ class UserPreferenceViewSet(viewsets.ReadOnlyModelViewSet):
class PreferenceUsersListView(generics.ListAPIView): class PreferenceUsersListView(generics.ListAPIView):
"""
DRF class for listing a user's preferences
"""
authentication_classes = (authentication.SessionAuthentication,) authentication_classes = (authentication.SessionAuthentication,)
permission_classes = (ApiKeyHeaderPermission,) permission_classes = (ApiKeyHeaderPermission,)
serializer_class = UserSerializer serializer_class = UserSerializer
......
"""
Custom Django REST Framework request/response pipeline parsers
"""
from rest_framework import parsers from rest_framework import parsers
......
"""
API library for Django REST Framework permissions-oriented workflows
"""
from django.conf import settings from django.conf import settings
from rest_framework import permissions from rest_framework import permissions
from django.http import Http404 from django.http import Http404
...@@ -6,6 +10,9 @@ from student.roles import CourseStaffRole ...@@ -6,6 +10,9 @@ from student.roles import CourseStaffRole
class ApiKeyHeaderPermission(permissions.BasePermission): class ApiKeyHeaderPermission(permissions.BasePermission):
"""
Django REST Framework permissions class used to manage API Key integrations
"""
def has_permission(self, request, view): def has_permission(self, request, view):
""" """
Check for permissions by matching the configured API key and header Check for permissions by matching the configured API key and header
......
...@@ -24,7 +24,6 @@ from openedx.core.lib.api.authentication import ( ...@@ -24,7 +24,6 @@ from openedx.core.lib.api.authentication import (
OAuth2AuthenticationAllowInactiveUser, OAuth2AuthenticationAllowInactiveUser,
) )
from openedx.core.lib.api.permissions import IsUserInUrl from openedx.core.lib.api.permissions import IsUserInUrl
from util.milestones_helpers import any_unfulfilled_milestones
class DeveloperErrorViewMixin(object): class DeveloperErrorViewMixin(object):
...@@ -63,6 +62,10 @@ class DeveloperErrorViewMixin(object): ...@@ -63,6 +62,10 @@ class DeveloperErrorViewMixin(object):
return self.make_error_response(400, validation_error.messages[0]) return self.make_error_response(400, validation_error.messages[0])
def handle_exception(self, exc): def handle_exception(self, exc):
"""
Generalized helper method for managing specific API exception workflows
"""
if isinstance(exc, APIException): if isinstance(exc, APIException):
return self.make_error_response(exc.status_code, exc.detail) return self.make_error_response(exc.status_code, exc.detail)
elif isinstance(exc, Http404): elif isinstance(exc, Http404):
......
"""
Workflows useful for reporting on runtime characteristics of the system
"""
import os import os
import signal import signal
import tempfile import tempfile
......
...@@ -46,13 +46,13 @@ def test_acceptance(options): ...@@ -46,13 +46,13 @@ def test_acceptance(options):
'red', 'red',
'No system specified, running tests for both cms and lms.' 'No system specified, running tests for both cms and lms.'
) )
print(msg) print msg
if opts['default_store'] not in ['draft', 'split']: if opts['default_store'] not in ['draft', 'split']:
msg = colorize( msg = colorize(
'red', 'red',
'No modulestore specified, running tests for both draft and split.' 'No modulestore specified, running tests for both draft and split.'
) )
print(msg) print msg
suite = AcceptanceTestSuite('{} acceptance'.format(opts['system']), **opts) suite = AcceptanceTestSuite('{} acceptance'.format(opts['system']), **opts)
suite.run() suite.run()
...@@ -107,7 +107,7 @@ def run_bokchoy(**opts): ...@@ -107,7 +107,7 @@ def run_bokchoy(**opts):
default_store=test_suite.default_store, default_store=test_suite.default_store,
) )
) )
print(msg) print msg
test_suite.run() test_suite.run()
...@@ -120,12 +120,12 @@ def bokchoy_coverage(): ...@@ -120,12 +120,12 @@ def bokchoy_coverage():
coveragerc = Env.BOK_CHOY_COVERAGERC coveragerc = Env.BOK_CHOY_COVERAGERC
msg = colorize('green', "Combining coverage reports") msg = colorize('green', "Combining coverage reports")
print(msg) print msg
sh("coverage combine --rcfile={}".format(coveragerc)) sh("coverage combine --rcfile={}".format(coveragerc))
msg = colorize('green', "Generating coverage reports") msg = colorize('green', "Generating coverage reports")
print(msg) print msg
sh("coverage html --rcfile={}".format(coveragerc)) sh("coverage html --rcfile={}".format(coveragerc))
sh("coverage xml --rcfile={}".format(coveragerc)) sh("coverage xml --rcfile={}".format(coveragerc))
......
"""
Open edX Documentation Builder
Ties into Sphinx to generate files at the specified location(s)
"""
from __future__ import print_function from __future__ import print_function
import sys import sys
from paver.easy import * from paver.easy import *
......
...@@ -150,13 +150,13 @@ def i18n_rtl(): ...@@ -150,13 +150,13 @@ def i18n_rtl():
cmd = "i18n_tool transifex" cmd = "i18n_tool transifex"
sh(cmd + " rtl") sh(cmd + " rtl")
print("Now generating langugage files...") print "Now generating langugage files..."
cmd = "i18n_tool generate" cmd = "i18n_tool generate"
sh(cmd + " --rtl") sh(cmd + " --rtl")
print("Committing translations...") print "Committing translations..."
sh('git clean -fdX conf/locale') sh('git clean -fdX conf/locale')
sh('git add conf/locale') sh('git add conf/locale')
sh('git commit --amend') sh('git commit --amend')
...@@ -170,13 +170,13 @@ def i18n_ltr(): ...@@ -170,13 +170,13 @@ def i18n_ltr():
cmd = "i18n_tool transifex" cmd = "i18n_tool transifex"
sh(cmd + " ltr") sh(cmd + " ltr")
print("Now generating langugage files...") print "Now generating langugage files..."
cmd = "i18n_tool generate" cmd = "i18n_tool generate"
sh(cmd + " --ltr") sh(cmd + " --ltr")
print("Committing translations...") print "Committing translations..."
sh('git clean -fdX conf/locale') sh('git clean -fdX conf/locale')
sh('git add conf/locale') sh('git add conf/locale')
sh('git commit --amend') sh('git commit --amend')
...@@ -194,12 +194,13 @@ def i18n_robot_pull(): ...@@ -194,12 +194,13 @@ def i18n_robot_pull():
""" """
Pull source strings, generate po and mo files, and validate Pull source strings, generate po and mo files, and validate
""" """
# sh('paver test_i18n') # TODO tests were removed from repo, but there should still be tests that cover the translations... # sh('paver test_i18n')
# Validate the recently pulled translations, and give a bail option # Tests were removed from repo, but there should still be tests covering the translations
# TODO: Validate the recently pulled translations, and give a bail option
sh('git clean -fdX conf/locale/rtl') sh('git clean -fdX conf/locale/rtl')
sh('git clean -fdX conf/locale/eo') sh('git clean -fdX conf/locale/eo')
cmd = "i18n_tool validate" cmd = "i18n_tool validate"
print("\n\nValidating translations with `i18n_tool validate`...") print "\n\nValidating translations with `i18n_tool validate`..."
sh("{cmd}".format(cmd=cmd)) sh("{cmd}".format(cmd=cmd))
con = raw_input("Continue with committing these translations (y/n)? ") con = raw_input("Continue with committing these translations (y/n)? ")
......
...@@ -10,6 +10,9 @@ REPO_DIR = os.getcwd() ...@@ -10,6 +10,9 @@ REPO_DIR = os.getcwd()
class TestPaverBokChoyCmd(unittest.TestCase): class TestPaverBokChoyCmd(unittest.TestCase):
"""
Paver Bok Choy Command test cases
"""
def _expected_command(self, name, store=None): def _expected_command(self, name, store=None):
""" """
......
"""
Tests covering the Open edX Paver prequisites installation workflow
"""
import os import os
import unittest import unittest
from pavelib.prereqs import no_prereq_install from pavelib.prereqs import no_prereq_install
...@@ -29,25 +33,25 @@ class TestPaverPrereqInstall(unittest.TestCase): ...@@ -29,25 +33,25 @@ class TestPaverPrereqInstall(unittest.TestCase):
os.environ.clear() os.environ.clear()
os.environ.update(_orig_environ) os.environ.update(_orig_environ)
def test_no_prereq_install_true(self): def test_no_prereq_install_true_lowercase(self):
""" """
Ensure that 'true' will be True. Ensure that 'true' will be True.
""" """
self.check_val('true', True) self.check_val('true', True)
def test_no_prereq_install_false(self): def test_no_prereq_install_false_lowercase(self):
""" """
Ensure that 'false' will be False. Ensure that 'false' will be False.
""" """
self.check_val('false', False) self.check_val('false', False)
def test_no_prereq_install_True(self): def test_no_prereq_install_true(self):
""" """
Ensure that 'True' will be True. Ensure that 'True' will be True.
""" """
self.check_val('True', True) self.check_val('True', True)
def test_no_prereq_install_False(self): def test_no_prereq_install_false(self):
""" """
Ensure that 'False' will be False. Ensure that 'False' will be False.
""" """
......
...@@ -57,19 +57,19 @@ def compute_fingerprint(path_list): ...@@ -57,19 +57,19 @@ def compute_fingerprint(path_list):
hasher = hashlib.sha1() hasher = hashlib.sha1()
for path in path_list: for path_item in path_list:
# For directories, create a hash based on the modification times # For directories, create a hash based on the modification times
# of first-level subdirectories # of first-level subdirectories
if os.path.isdir(path): if os.path.isdir(path_item):
for dirname in sorted(os.listdir(path)): for dirname in sorted(os.listdir(path_item)):
p = os.path.join(path, dirname) path_name = os.path.join(path_item, dirname)
if os.path.isdir(p): if os.path.isdir(path_name):
hasher.update(str(os.stat(p).st_mtime)) hasher.update(str(os.stat(path_name).st_mtime))
# For files, hash the contents of the file # For files, hash the contents of the file
if os.path.isfile(path): if os.path.isfile(path_item):
with open(path, "rb") as file_handle: with open(path_item, "rb") as file_handle:
hasher.update(file_handle.read()) hasher.update(file_handle.read())
return hasher.hexdigest() return hasher.hexdigest()
...@@ -113,7 +113,7 @@ def prereq_cache(cache_name, paths, install_func): ...@@ -113,7 +113,7 @@ def prereq_cache(cache_name, paths, install_func):
post_install_hash = compute_fingerprint(paths) post_install_hash = compute_fingerprint(paths)
cache_file.write(post_install_hash) cache_file.write(post_install_hash)
else: else:
print('{cache} unchanged, skipping...'.format(cache=cache_name)) print '{cache} unchanged, skipping...'.format(cache=cache_name)
def ruby_prereqs_installation(): def ruby_prereqs_installation():
......
...@@ -263,5 +263,5 @@ def check_settings(args): ...@@ -263,5 +263,5 @@ def check_settings(args):
django_shell_cmd = django_cmd(system, settings, 'shell', '--plain', '--pythonpath=.') django_shell_cmd = django_cmd(system, settings, 'shell', '--plain', '--pythonpath=.')
sh("{import_cmd} | {shell_cmd}".format(import_cmd=import_cmd, shell_cmd=django_shell_cmd)) sh("{import_cmd} | {shell_cmd}".format(import_cmd=import_cmd, shell_cmd=django_shell_cmd))
except: except: # pylint: disable=bare-except
print("Failed to import settings", file=sys.stderr) print("Failed to import settings", file=sys.stderr)
...@@ -194,8 +194,6 @@ def coverage(options): ...@@ -194,8 +194,6 @@ def coverage(options):
""" """
Build the html, xml, and diff coverage reports Build the html, xml, and diff coverage reports
""" """
compare_branch = getattr(options, 'compare_branch', 'origin/master')
report_dir = Env.REPORT_DIR report_dir = Env.REPORT_DIR
rcfile = Env.PYTHON_COVERAGERC rcfile = Env.PYTHON_COVERAGERC
...@@ -262,4 +260,4 @@ def diff_coverage(options): ...@@ -262,4 +260,4 @@ def diff_coverage(options):
) )
) )
print("\n") print "\n"
...@@ -18,6 +18,7 @@ def kill_process(proc): ...@@ -18,6 +18,7 @@ def kill_process(proc):
""" """
p1_group = psutil.Process(proc.pid) p1_group = psutil.Process(proc.pid)
# pylint: disable=unexpected-keyword-arg
child_pids = p1_group.get_children(recursive=True) child_pids = p1_group.get_children(recursive=True)
for child_pid in child_pids: for child_pid in child_pids:
...@@ -55,6 +56,7 @@ def run_multi_processes(cmd_list, out_log=None, err_log=None): ...@@ -55,6 +56,7 @@ def run_multi_processes(cmd_list, out_log=None, err_log=None):
for cmd in cmd_list: for cmd in cmd_list:
pids.extend([subprocess.Popen(cmd, **kwargs)]) pids.extend([subprocess.Popen(cmd, **kwargs)])
# pylint: disable=unused-argument
def _signal_handler(*args): def _signal_handler(*args):
""" """
What to do when process is ended What to do when process is ended
...@@ -66,6 +68,7 @@ def run_multi_processes(cmd_list, out_log=None, err_log=None): ...@@ -66,6 +68,7 @@ def run_multi_processes(cmd_list, out_log=None, err_log=None):
signal.pause() signal.pause()
print("Processes ending") print("Processes ending")
# pylint: disable=broad-except
except Exception as err: except Exception as err:
print("Error running process {}".format(err), file=sys.stderr) print("Error running process {}".format(err), file=sys.stderr)
...@@ -109,6 +112,7 @@ def run_background_process(cmd, out_log=None, err_log=None, cwd=None): ...@@ -109,6 +112,7 @@ def run_background_process(cmd, out_log=None, err_log=None, cwd=None):
""" """
p1_group = psutil.Process(proc.pid) p1_group = psutil.Process(proc.pid)
# pylint: disable=unexpected-keyword-arg
child_pids = p1_group.get_children(recursive=True) child_pids = p1_group.get_children(recursive=True)
for child_pid in child_pids: for child_pid in child_pids:
......
...@@ -101,7 +101,7 @@ def wait_for_test_servers(): ...@@ -101,7 +101,7 @@ def wait_for_test_servers():
"red", "red",
"Could not contact {} test server".format(service) "Could not contact {} test server".format(service)
) )
print(msg) print msg
sys.exit(1) sys.exit(1)
...@@ -112,7 +112,7 @@ def is_mongo_running(): ...@@ -112,7 +112,7 @@ def is_mongo_running():
# The mongo command will connect to the service, # The mongo command will connect to the service,
# failing with a non-zero exit code if it cannot connect. # failing with a non-zero exit code if it cannot connect.
output = os.popen('mongo --eval "print(\'running\')"').read() output = os.popen('mongo --eval "print(\'running\')"').read()
return (output and "running" in output) return output and "running" in output
def is_memcache_running(): def is_memcache_running():
...@@ -130,9 +130,9 @@ def is_mysql_running(): ...@@ -130,9 +130,9 @@ def is_mysql_running():
""" """
# We need to check whether or not mysql is running as a process # We need to check whether or not mysql is running as a process
# even if it is not daemonized. # even if it is not daemonized.
with open(os.devnull, 'w') as DEVNULL: with open(os.devnull, 'w') as os_devnull:
#pgrep returns the PID, which we send to /dev/null #pgrep returns the PID, which we send to /dev/null
returncode = subprocess.call("pgrep mysqld", stdout=DEVNULL, shell=True) returncode = subprocess.call("pgrep mysqld", stdout=os_devnull, shell=True)
return returncode == 0 return returncode == 0
...@@ -153,7 +153,7 @@ def check_mongo(): ...@@ -153,7 +153,7 @@ def check_mongo():
""" """
if not is_mongo_running(): if not is_mongo_running():
msg = colorize('red', "Mongo is not running locally.") msg = colorize('red', "Mongo is not running locally.")
print(msg) print msg
sys.exit(1) sys.exit(1)
...@@ -163,7 +163,7 @@ def check_memcache(): ...@@ -163,7 +163,7 @@ def check_memcache():
""" """
if not is_memcache_running(): if not is_memcache_running():
msg = colorize('red', "Memcache is not running locally.") msg = colorize('red', "Memcache is not running locally.")
print(msg) print msg
sys.exit(1) sys.exit(1)
...@@ -173,7 +173,7 @@ def check_mysql(): ...@@ -173,7 +173,7 @@ def check_mysql():
""" """
if not is_mysql_running(): if not is_mysql_running():
msg = colorize('red', "MySQL is not running locally.") msg = colorize('red', "MySQL is not running locally.")
print(msg) print msg
sys.exit(1) sys.exit(1)
......
...@@ -51,6 +51,9 @@ class AcceptanceTest(TestSuite): ...@@ -51,6 +51,9 @@ class AcceptanceTest(TestSuite):
return cmd return cmd
def _update_assets(self): def _update_assets(self):
"""
Internal helper method to manage asset compilation
"""
args = [self.system, '--settings=acceptance'] args = [self.system, '--settings=acceptance']
if self.fasttest: if self.fasttest:
......
...@@ -37,7 +37,7 @@ class TestSuite(object): ...@@ -37,7 +37,7 @@ class TestSuite(object):
i.e. Checking for and defining required directories. i.e. Checking for and defining required directories.
""" """
print("\nSetting up for {suite_name}".format(suite_name=self.root)) print "\nSetting up for {suite_name}".format(suite_name=self.root)
self.failed_suites = [] self.failed_suites = []
def __exit__(self, exc_type, exc_value, traceback): def __exit__(self, exc_type, exc_value, traceback):
...@@ -50,7 +50,7 @@ class TestSuite(object): ...@@ -50,7 +50,7 @@ class TestSuite(object):
i.e. Cleaning mongo after the lms tests run. i.e. Cleaning mongo after the lms tests run.
""" """
print("\nCleaning up after {suite_name}".format(suite_name=self.root)) print "\nCleaning up after {suite_name}".format(suite_name=self.root)
@property @property
def cmd(self): def cmd(self):
...@@ -94,7 +94,7 @@ class TestSuite(object): ...@@ -94,7 +94,7 @@ class TestSuite(object):
kill_process(process) kill_process(process)
sys.exit(1) sys.exit(1)
else: else:
return (process.returncode == 0) return process.returncode == 0
def run_suite_tests(self): def run_suite_tests(self):
""" """
...@@ -123,7 +123,7 @@ class TestSuite(object): ...@@ -123,7 +123,7 @@ class TestSuite(object):
else: else:
msg = colorize('green', "\n\n{bar}\nNo test failures ".format(bar="=" * 48)) msg = colorize('green', "\n\n{bar}\nNo test failures ".format(bar="=" * 48))
print(msg) print msg
def run(self): def run(self):
""" """
......
...@@ -44,7 +44,7 @@ def clean_reports_dir(options): ...@@ -44,7 +44,7 @@ def clean_reports_dir(options):
Clean coverage files, to ensure that we don't use stale data to generate reports. Clean coverage files, to ensure that we don't use stale data to generate reports.
""" """
if getattr(options, 'skip_clean', False): if getattr(options, 'skip_clean', False):
print('--skip_clean is set, skipping...') print '--skip_clean is set, skipping...'
return return
# We delete the files but preserve the directory structure # We delete the files but preserve the directory structure
...@@ -71,16 +71,23 @@ def check_firefox_version(): ...@@ -71,16 +71,23 @@ def check_firefox_version():
""" """
expected_firefox_ver = "Mozilla Firefox 28.0" expected_firefox_ver = "Mozilla Firefox 28.0"
firefox_ver = subprocess.check_output("firefox --version", shell=True).strip() firefox_ver = subprocess.check_output("firefox --version", shell=True).strip()
debian_location = 'https://s3.amazonaws.com/vagrant.testeng.edx.org/'
debian_package = 'firefox_28.0%2Bbuild2-0ubuntu0.12.04.1_amd64.deb'
debian_path = '{location}{package}'.format(location=debian_location, package=debian_package)
if firefox_ver != expected_firefox_ver: if firefox_ver != expected_firefox_ver:
raise Exception( raise Exception(
'Required firefox version not found.\n' 'Required firefox version not found.\n'
'Expected: {expected_version}; Actual: {actual_version}.\n\n' 'Expected: {expected_version}; Actual: {actual_version}.\n\n'
'As the vagrant user in devstack, run the following:\n\n' 'As the vagrant user in devstack, run the following:\n\n'
'\t$ sudo wget -O /tmp/firefox_28.deb https://s3.amazonaws.com/vagrant.testeng.edx.org/firefox_28.0%2Bbuild2-0ubuntu0.12.04.1_amd64.deb\n' '\t$ sudo wget -O /tmp/firefox_28.deb {debian_path}\n'
'\t$ sudo apt-get remove firefox\n\n' '\t$ sudo apt-get remove firefox\n\n'
'\t$ sudo gdebi -nq /tmp/firefox_28.deb\n\n' '\t$ sudo gdebi -nq /tmp/firefox_28.deb\n\n'
'Confirm the new version:\n' 'Confirm the new version:\n'
'\t$ firefox --version\n' '\t$ firefox --version\n'
'\t{expected_version}'.format(actual_version=firefox_ver, expected_version=expected_firefox_ver) '\t{expected_version}'.format(
actual_version=firefox_ver,
expected_version=expected_firefox_ver,
debian_path=debian_path
)
) )
...@@ -50,7 +50,7 @@ git+https://github.com/edx/edx-oauth2-provider.git@0.5.7#egg=oauth2-provider==0. ...@@ -50,7 +50,7 @@ git+https://github.com/edx/edx-oauth2-provider.git@0.5.7#egg=oauth2-provider==0.
-e git+https://github.com/pmitros/RecommenderXBlock.git@518234bc354edbfc2651b9e534ddb54f96080779#egg=recommender-xblock -e git+https://github.com/pmitros/RecommenderXBlock.git@518234bc354edbfc2651b9e534ddb54f96080779#egg=recommender-xblock
-e git+https://github.com/edx/edx-search.git@release-2015-09-11a#egg=edx-search -e git+https://github.com/edx/edx-search.git@release-2015-09-11a#egg=edx-search
-e git+https://github.com/edx/edx-milestones.git@9b44a37edc3d63a23823c21a63cdd53ef47a7aa4#egg=edx-milestones -e git+https://github.com/edx/edx-milestones.git@9b44a37edc3d63a23823c21a63cdd53ef47a7aa4#egg=edx-milestones
git+https://github.com/edx/edx-lint.git@c5745631d2eee4e2efe8c31fa7b42fe2c12a0755#egg=edx_lint==0.2.7 git+https://github.com/edx/edx-lint.git@v0.2.9#egg=edx_lint==0.2.9
-e git+https://github.com/edx/xblock-utils.git@213a97a50276d6a2504d8133650b2930ead357a0#egg=xblock-utils -e git+https://github.com/edx/xblock-utils.git@213a97a50276d6a2504d8133650b2930ead357a0#egg=xblock-utils
-e git+https://github.com/edx-solutions/xblock-google-drive.git@138e6fa0bf3a2013e904a085b9fed77dab7f3f21#egg=xblock-google-drive -e git+https://github.com/edx-solutions/xblock-google-drive.git@138e6fa0bf3a2013e904a085b9fed77dab7f3f21#egg=xblock-google-drive
-e git+https://github.com/edx/edx-reverification-block.git@5162ca169ca1486479e02f6ad2f538e6b8ae6f00#egg=edx-reverification-block -e git+https://github.com/edx/edx-reverification-block.git@5162ca169ca1486479e02f6ad2f538e6b8ae6f00#egg=edx-reverification-block
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment