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"),