diff --git a/openedx/core/lib/tests/test_xblock_utils.py b/openedx/core/lib/tests/test_xblock_utils.py new file mode 100644 index 0000000000..313dd8e047 --- /dev/null +++ b/openedx/core/lib/tests/test_xblock_utils.py @@ -0,0 +1,205 @@ +""" +Tests for xblock_utils.py +""" +from __future__ import unicode_literals, absolute_import + +import ddt +import uuid + +from django.test.client import RequestFactory + +from courseware.models import StudentModule # pylint: disable=import-error +from lms.djangoapps.lms_xblock.runtime import quote_slashes +from xblock.fragment import Fragment +from xmodule.modulestore import ModuleStoreEnum +from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase +from xmodule.modulestore.tests.factories import CourseFactory + +from openedx.core.lib.xblock_utils import ( + wrap_fragment, + request_token, + wrap_xblock, + replace_jump_to_id_urls, + replace_course_urls, + replace_static_urls, + grade_histogram, + sanitize_html_id +) + + +@ddt.ddt +class TestXblockUtils(SharedModuleStoreTestCase): + """ + Tests for xblock utility functions. + """ + + @classmethod + def setUpClass(cls): + super(TestXblockUtils, cls).setUpClass() + cls.course_mongo = CourseFactory.create( + default_store=ModuleStoreEnum.Type.mongo, + org='TestX', + number='TS01', + run='2015' + ) + cls.course_split = CourseFactory.create( + default_store=ModuleStoreEnum.Type.split, + org='TestX', + number='TS02', + run='2015' + ) + + def setUp(self): + super(TestXblockUtils, self).setUp() + + def create_fragment(self, content=None): + """ + Create a fragment. + """ + fragment = Fragment(content) + fragment.add_css('body {background-color:red;}') + fragment.add_javascript('alert("Hi!");') + return fragment + + def test_wrap_fragment(self): + """ + Verify that wrap_fragment adds new content. + """ + new_content = '

New Content

' + fragment = self.create_fragment() + wrapped_fragment = wrap_fragment(fragment, new_content) + self.assertEqual('

New Content

', wrapped_fragment.content) + self.assertEqual('body {background-color:red;}', wrapped_fragment.resources[0].data) + self.assertEqual('alert("Hi!");', wrapped_fragment.resources[1].data) + + def test_request_token(self): + """ + Verify that a proper token is returned. + """ + request_with_token = RequestFactory().get('/') + request_with_token._xblock_token = '123' # pylint: disable=protected-access + token = request_token(request_with_token) + self.assertEqual(token, '123') + + request_without_token = RequestFactory().get('/') + token = request_token(request_without_token) + # Test to see if the token is an uuid1 hex value + test_uuid = uuid.UUID(token, version=1) + self.assertEqual(token, test_uuid.hex) + + @ddt.data( + ('course_mongo', 'data-usage-id="i4x:;_;_TestX;_TS01;_course;_2015"'), + ('course_split', 'data-usage-id="block-v1:TestX+TS02+2015+type@course+block@course"') + ) + @ddt.unpack + def test_wrap_xblock(self, course_id, data_usage_id): + """ + Verify that new content is added and the resources are the same. + """ + fragment = self.create_fragment(u"

Test!

") + course = getattr(self, course_id) + test_wrap_output = wrap_xblock( + runtime_class='TestRuntime', + block=course, + view='baseview', + frag=fragment, + context=None, + usage_id_serializer=lambda usage_id: quote_slashes(unicode(usage_id)), + request_token=uuid.uuid1().get_hex() + ) + self.assertIsInstance(test_wrap_output, Fragment) + self.assertIn('xblock-baseview', test_wrap_output.content) + self.assertIn('data-runtime-class="TestRuntime"', test_wrap_output.content) + self.assertIn(data_usage_id, test_wrap_output.content) + self.assertIn('

Test!

', test_wrap_output.content) + self.assertEqual(test_wrap_output.resources[0].data, u'body {background-color:red;}') + self.assertEqual(test_wrap_output.resources[1].data, 'alert("Hi!");') + + @ddt.data('course_mongo', 'course_split') + def test_replace_jump_to_id_urls(self, course_id): + """ + Verify that the jump-to URL has been replaced. + """ + course = getattr(self, course_id) + test_replace = replace_jump_to_id_urls( + course_id=course.id, + jump_to_id_base_url='/base_url/', + block=course, + view='baseview', + frag=Fragment(''), + context=None + ) + self.assertIsInstance(test_replace, Fragment) + self.assertEqual(test_replace.content, '') + + @ddt.data( + ('course_mongo', ''), + ('course_split', '') + ) + @ddt.unpack + def test_replace_course_urls(self, course_id, anchor_tag): + """ + Verify that the course URL has been replaced. + """ + course = getattr(self, course_id) + test_replace = replace_course_urls( + course_id=course.id, + block=course, + view='baseview', + frag=Fragment(''), + context=None + ) + self.assertIsInstance(test_replace, Fragment) + self.assertEqual(test_replace.content, anchor_tag) + + @ddt.data( + ('course_mongo', ''), + ('course_split', '') + ) + @ddt.unpack + def test_replace_static_urls(self, course_id, anchor_tag): + """ + Verify that the static URL has been replaced. + """ + course = getattr(self, course_id) + test_replace = replace_static_urls( + data_dir=None, + course_id=course.id, + block=course, + view='baseview', + frag=Fragment(''), + context=None + ) + self.assertIsInstance(test_replace, Fragment) + self.assertEqual(test_replace.content, anchor_tag) + + @ddt.data('course_mongo', 'course_split') + def test_grade_histogram(self, course_id): + """ + Verify that a histogram has been created. + """ + course = getattr(self, course_id) + usage_key = course.id.make_usage_key('problem', 'first_problem') + StudentModule.objects.create( + student_id=1, + grade=100, + module_state_key=usage_key + ) + StudentModule.objects.create( + student_id=2, + grade=50, + module_state_key=usage_key + ) + + grades = grade_histogram(usage_key) + self.assertEqual(grades[0], (50.0, 1)) + self.assertEqual(grades[1], (100.0, 1)) + + def test_sanitize_html_id(self): + """ + Verify that colons and dashes are replaced. + """ + dirty_string = 'I:have-un:allowed_characters' + clean_string = sanitize_html_id(dirty_string) + + self.assertEqual(clean_string, 'I_have_un_allowed_characters') diff --git a/openedx/core/lib/xblock_utils.py b/openedx/core/lib/xblock_utils.py index a0aefb3ef6..b8024abfc5 100644 --- a/openedx/core/lib/xblock_utils.py +++ b/openedx/core/lib/xblock_utils.py @@ -5,9 +5,10 @@ Functions that can are used to modify XBlock fragments for use in the LMS and St import datetime import json import logging +import markupsafe +import re import static_replace import uuid -import markupsafe from lxml import html, etree from contracts import contract @@ -145,7 +146,7 @@ def wrap_xblock( def replace_jump_to_id_urls(course_id, jump_to_id_base_url, block, view, frag, context): # pylint: disable=unused-argument """ This will replace a link between courseware in the format - /jump_to/ with a URL for a page that will correctly redirect + /jump_to_id/ with a URL for a page that will correctly redirect This is similar to replace_course_urls, but much more flexible and durable for Studio authored courses. See more comments in static_replace.replace_jump_to_urls @@ -156,7 +157,7 @@ def replace_jump_to_id_urls(course_id, jump_to_id_base_url, block, view, frag, c the end of this URL at re-write time output: a new :class:`~xblock.fragment.Fragment` that modifies `frag` with - content that has been update with /jump_to links replaced + content that has been update with /jump_to_id links replaced """ return wrap_fragment(frag, static_replace.replace_jump_to_id_urls(frag.content, course_id, jump_to_id_base_url)) @@ -211,6 +212,14 @@ def grade_histogram(module_id): return grades +def sanitize_html_id(html_id): + """ + Template uses element_id in js function names, so can't allow dashes and colons. + """ + sanitized_html_id = re.sub(r'[:-]', '_', html_id) + return sanitized_html_id + + @contract(user=User, has_instructor_access=bool, block=XBlock, view=basestring, frag=Fragment, context="dict|None") def add_staff_markup(user, has_instructor_access, disable_staff_debug_info, block, view, frag, context): # pylint: disable=unused-argument """ @@ -299,8 +308,7 @@ def add_staff_markup(user, has_instructor_access, disable_staff_debug_info, bloc 'source_file': source_file, 'source_url': '%s/%s/tree/master/%s' % (giturl, data_dir, source_file), 'category': str(block.__class__.__name__), - # Template uses element_id in js function names, so can't allow dashes - 'element_id': block.location.html_id().replace('-', '_'), + 'element_id': sanitize_html_id(block.location.html_id()), 'edit_link': edit_link, 'user': user, 'xqa_server': settings.FEATURES.get('XQA_SERVER', "http://your_xqa_server.com"),