From f555ffd585dc1eeacb6cbc3e00014a72a5b7c35c Mon Sep 17 00:00:00 2001
From: Robert Raposa <rraposa@edx.org>
Date: Sun, 17 Jan 2016 00:51:13 -0500
Subject: [PATCH] Refactor and improve js_utils helpers

- Rename escape_json_dumps to dump_js_escaped_json
- Rename escape_js_string to js_escaped_string
- Update js_escaped_string to output empty string for None
- Introduce dump_html_escaped_json
- Move dump_js_escaped_json after the pipe as new best practice
- Introduce additional uses of helpers
- Introduce new djangolib directory and move js_utils
---
 cms/djangoapps/contentstore/views/course.py   |   6 +-
 .../contentstore/views/entrance_exam.py       |   4 +-
 cms/djangoapps/contentstore/views/error.py    |   4 +-
 .../contentstore/views/organization.py        |   4 +-
 .../views/tests/test_certificates.py          |  19 +-
 cms/templates/base.html                       |  12 +-
 cms/templates/certificates.html               |  18 +-
 cms/templates/container.html                  |  12 +-
 cms/templates/course_info.html                |  15 +-
 cms/templates/course_outline.html             |   7 +-
 cms/templates/export.html                     |  14 +-
 cms/templates/group_configurations.html       |  12 +-
 cms/templates/import.html                     |   9 +-
 cms/templates/library.html                    |   6 +-
 cms/templates/manage_users.html               |  15 +-
 cms/templates/manage_users_lib.html           |  15 +-
 cms/templates/settings.html                   |   9 +-
 cms/templates/settings_advanced.html          |   9 +-
 cms/templates/settings_graders.html           |  10 +-
 cms/templates/studio_xblock_wrapper.html      |  12 +-
 cms/templates/textbooks.html                  |   4 +-
 .../student_account/test/test_views.py        |   4 +-
 .../teams/templates/teams/teams.html          |  36 +--
 lms/envs/test.py                              |   3 +-
 lms/templates/courseware/courses.html         |   4 +-
 lms/templates/financial-assistance/apply.html |  20 +-
 .../student_account/login_and_register.html   |   4 +-
 .../student_profile/learner_profile.html      |   4 +-
 lms/templates/support/enrollment.html         |   9 +-
 openedx/README.rst                            |   1 +
 openedx/core/djangolib/__init__.py            |   0
 openedx/core/djangolib/js_utils.py            | 149 +++++++++++
 openedx/core/djangolib/tests/__init__.py      |   0
 openedx/core/djangolib/tests/test_js_utils.py | 242 ++++++++++++++++++
 openedx/core/lib/js_utils.py                  |  83 ------
 openedx/core/lib/tests/test_js_utils.py       |  74 ------
 pavelib/utils/test/suites/nose_suite.py       |   1 +
 37 files changed, 582 insertions(+), 268 deletions(-)
 create mode 100644 openedx/core/djangolib/__init__.py
 create mode 100644 openedx/core/djangolib/js_utils.py
 create mode 100644 openedx/core/djangolib/tests/__init__.py
 create mode 100644 openedx/core/djangolib/tests/test_js_utils.py
 delete mode 100644 openedx/core/lib/js_utils.py
 delete mode 100644 openedx/core/lib/tests/test_js_utils.py

diff --git a/cms/djangoapps/contentstore/views/course.py b/cms/djangoapps/contentstore/views/course.py
index e37cf799399..ed2d9e36709 100644
--- a/cms/djangoapps/contentstore/views/course.py
+++ b/cms/djangoapps/contentstore/views/course.py
@@ -70,7 +70,7 @@ from openedx.core.djangoapps.programs.utils import get_programs
 from openedx.core.djangoapps.self_paced.models import SelfPacedConfiguration
 from openedx.core.lib.course_tabs import CourseTabPluginManager
 from openedx.core.lib.courses import course_image_url
-from openedx.core.lib.js_utils import escape_json_dumps
+from openedx.core.djangolib.js_utils import dump_js_escaped_json
 from student import auth
 from student.auth import has_course_author_access, has_studio_write_access, has_studio_read_access
 from student.roles import (
@@ -324,10 +324,10 @@ def course_search_index_handler(request, course_key_string):
         try:
             reindex_course_and_check_access(course_key, request.user)
         except SearchIndexingError as search_err:
-            return HttpResponse(escape_json_dumps({
+            return HttpResponse(dump_js_escaped_json({
                 "user_message": search_err.error_list
             }), content_type=content_type, status=500)
-        return HttpResponse(escape_json_dumps({
+        return HttpResponse(dump_js_escaped_json({
             "user_message": _("Course has been successfully reindexed.")
         }), content_type=content_type, status=200)
 
diff --git a/cms/djangoapps/contentstore/views/entrance_exam.py b/cms/djangoapps/contentstore/views/entrance_exam.py
index 21be96119e6..e770fe0d18e 100644
--- a/cms/djangoapps/contentstore/views/entrance_exam.py
+++ b/cms/djangoapps/contentstore/views/entrance_exam.py
@@ -10,7 +10,7 @@ from django.contrib.auth.decorators import login_required
 from django.views.decorators.csrf import ensure_csrf_cookie
 from django.http import HttpResponse, HttpResponseBadRequest
 
-from openedx.core.lib.js_utils import escape_json_dumps
+from openedx.core.djangolib.js_utils import dump_js_escaped_json
 from contentstore.views.helpers import create_xblock, remove_entrance_exam_graders
 from contentstore.views.item import delete_item
 from models.settings.course_metadata import CourseMetadata
@@ -186,7 +186,7 @@ def _get_entrance_exam(request, course_key):  # pylint: disable=W0613
     try:
         exam_descriptor = modulestore().get_item(exam_key)
         return HttpResponse(
-            escape_json_dumps({'locator': unicode(exam_descriptor.location)}),
+            dump_js_escaped_json({'locator': unicode(exam_descriptor.location)}),
             status=200, content_type='application/json')
     except ItemNotFoundError:
         return HttpResponse(status=404)
diff --git a/cms/djangoapps/contentstore/views/error.py b/cms/djangoapps/contentstore/views/error.py
index af4e6c1e27d..2eea0bc2a77 100644
--- a/cms/djangoapps/contentstore/views/error.py
+++ b/cms/djangoapps/contentstore/views/error.py
@@ -4,7 +4,7 @@ from django.http import (HttpResponse, HttpResponseServerError,
                          HttpResponseNotFound)
 from edxmako.shortcuts import render_to_string, render_to_response
 import functools
-from openedx.core.lib.js_utils import escape_json_dumps
+from openedx.core.djangolib.js_utils import dump_js_escaped_json
 
 __all__ = ['not_found', 'server_error', 'render_404', 'render_500']
 
@@ -18,7 +18,7 @@ def jsonable_error(status=500, message="The Studio servers encountered an error"
         @functools.wraps(func)
         def inner(request, *args, **kwargs):
             if request.is_ajax():
-                content = escape_json_dumps({"error": message})
+                content = dump_js_escaped_json({"error": message})
                 return HttpResponse(content, content_type="application/json",
                                     status=status)
             else:
diff --git a/cms/djangoapps/contentstore/views/organization.py b/cms/djangoapps/contentstore/views/organization.py
index 13ac777ee90..6c530dfe4fd 100644
--- a/cms/djangoapps/contentstore/views/organization.py
+++ b/cms/djangoapps/contentstore/views/organization.py
@@ -4,7 +4,7 @@ from django.utils.decorators import method_decorator
 from django.views.generic import View
 from django.http import HttpResponse
 
-from openedx.core.lib.js_utils import escape_json_dumps
+from openedx.core.djangolib.js_utils import dump_js_escaped_json
 from util.organizations_helpers import get_organizations
 
 
@@ -20,4 +20,4 @@ class OrganizationListView(View):
         """Returns organization list as json."""
         organizations = get_organizations()
         org_names_list = [(org["short_name"]) for org in organizations]
-        return HttpResponse(escape_json_dumps(org_names_list), content_type='application/json; charset=utf-8')
+        return HttpResponse(dump_js_escaped_json(org_names_list), content_type='application/json; charset=utf-8')
diff --git a/cms/djangoapps/contentstore/views/tests/test_certificates.py b/cms/djangoapps/contentstore/views/tests/test_certificates.py
index 4c60792d2ac..8f72f7ba1dc 100644
--- a/cms/djangoapps/contentstore/views/tests/test_certificates.py
+++ b/cms/djangoapps/contentstore/views/tests/test_certificates.py
@@ -276,8 +276,9 @@ class CertificatesListHandlerTestCase(EventTestMixin, CourseTestCase, Certificat
     @mock.patch.dict('django.conf.settings.FEATURES', {'CERTIFICATES_HTML_VIEW': True})
     def test_certificate_info_in_response(self):
         """
-        Test that certificate has been created and rendered properly.
+        Test that certificate has been created and rendered properly with non-audit course mode.
         """
+        CourseModeFactory.create(course_id=self.course.id, mode_slug='verified')
         response = self.client.ajax_post(
             self._url(),
             data=CERTIFICATE_JSON_WITH_SIGNATORIES
@@ -298,6 +299,22 @@ class CertificatesListHandlerTestCase(EventTestMixin, CourseTestCase, Certificat
         self.assertEqual(data[0]['description'], 'Test description')
         self.assertEqual(data[0]['version'], CERTIFICATE_SCHEMA_VERSION)
 
+    @mock.patch.dict('django.conf.settings.FEATURES', {'CERTIFICATES_HTML_VIEW': True})
+    def test_certificate_info_not_in_response(self):
+        """
+        Test that certificate has not been rendered audit only course mode.
+        """
+        response = self.client.ajax_post(
+            self._url(),
+            data=CERTIFICATE_JSON_WITH_SIGNATORIES
+        )
+
+        self.assertEqual(response.status_code, 201)
+
+        # in html response
+        result = self.client.get_html(self._url())
+        self.assertNotIn('Test certificate', result.content)
+
     def test_unsupported_http_accept_header(self):
         """
         Test if not allowed header present in request.
diff --git a/cms/templates/base.html b/cms/templates/base.html
index 32ce03dcdd7..512669c9b2a 100644
--- a/cms/templates/base.html
+++ b/cms/templates/base.html
@@ -2,8 +2,8 @@
 <%namespace name='static' file='static_content.html'/>
 <%!
 from django.utils.translation import ugettext as _
-from openedx.core.lib.js_utils import (
-    escape_json_dumps, escape_js_string
+from openedx.core.djangolib.js_utils import (
+    dump_js_escaped_json, js_escaped_string
 )
 %>
 <!doctype html>
@@ -47,7 +47,7 @@ from openedx.core.lib.js_utils import (
     <a class="nav-skip" href="#content">${_("Skip to main content")}</a>
 
     <script type="text/javascript">
-      window.baseUrl = "${escape_js_string(settings.STATIC_URL) | n}";
+      window.baseUrl = "${settings.STATIC_URL | n, js_escaped_string}";
       var require = {baseUrl: window.baseUrl};
     </script>
     <script type="text/javascript" src="${static.url("js/vendor/require.js")}"></script>
@@ -85,14 +85,14 @@ from openedx.core.lib.js_utils import (
             % if context_course:
               require(['js/factories/course'], function(CourseFactory) {
                   CourseFactory({
-                      id: "${escape_js_string(context_course.id) | n}",
+                      id: "${context_course.id | n, js_escaped_string}",
                       name: "${context_course.display_name_with_default_escaped | h}",
                       url_name: "${context_course.location.name | h}",
                       org: "${context_course.location.org | h}",
                       num: "${context_course.location.course | h}",
-                      display_course_number: "${_(context_course.display_coursenumber) if context_course.display_coursenumber else ''}",
+                      display_course_number: "${context_course.display_coursenumber | n, js_escaped_string}",
                       revision: "${context_course.location.revision | h}",
-                      self_paced: ${escape_json_dumps(context_course.self_paced) | n}
+                      self_paced: ${context_course.self_paced | n, dump_js_escaped_json}
                   });
               });
             % endif
diff --git a/cms/templates/certificates.html b/cms/templates/certificates.html
index d038cfe37e4..99ec02ab516 100644
--- a/cms/templates/certificates.html
+++ b/cms/templates/certificates.html
@@ -4,7 +4,9 @@
 <%!
 from contentstore import utils
 from django.utils.translation import ugettext as _
-from openedx.core.lib.js_utils import escape_json_dumps
+from openedx.core.djangolib.js_utils import (
+    dump_js_escaped_json, js_escaped_string
+)
 %>
 
 <%block name="title">${_("Course Certificates")}</%block>
@@ -29,11 +31,19 @@ CMS.User.isGlobalStaff = '${is_global_staff}'=='True' ? true : false;
 </%block>
 
 <%block name="requirejs">
+% if has_certificate_modes:
   require(["js/certificates/factories/certificates_page_factory"], function(CertificatesPageFactory) {
-      if(${escape_json_dumps(has_certificate_modes)}) {
-          CertificatesPageFactory(${escape_json_dumps(certificates) | n}, "${certificate_url}", "${course_outline_url}", ${escape_json_dumps(course_modes) | n}, ${escape_json_dumps(certificate_web_view_url) | n}, ${escape_json_dumps(is_active) | n}, ${escape_json_dumps(certificate_activation_handler_url) | n} );
-      }
+      CertificatesPageFactory(
+          ${certificates | n, dump_js_escaped_json},
+          "${certificate_url | n, js_escaped_string}",
+          "${course_outline_url | n, js_escaped_string}",
+          ${course_modes | n, dump_js_escaped_json},
+          ${certificate_web_view_url | n, dump_js_escaped_json},
+          ${is_active | n, dump_js_escaped_json},
+          ${certificate_activation_handler_url | n, dump_js_escaped_json}
+      );
   });
+% endif
 </%block>
 
 <%block name="content">
diff --git a/cms/templates/container.html b/cms/templates/container.html
index 16bed1f9702..2d5f40f21d9 100644
--- a/cms/templates/container.html
+++ b/cms/templates/container.html
@@ -9,7 +9,9 @@ else:
 </%def>
 <%!
 from contentstore.views.helpers import xblock_studio_url, xblock_type_display_name
-from openedx.core.lib.js_utils import escape_json_dumps
+from openedx.core.djangolib.js_utils import (
+    dump_js_escaped_json, js_escaped_string
+)
 from util.markup import HTML, ugettext as _
 %>
 <%block name="title">${xblock.display_name_with_default_escaped} ${xblock_type_display_name(xblock) | h}</%block>
@@ -32,11 +34,11 @@ from util.markup import HTML, ugettext as _
 <%block name="requirejs">
     require(["js/factories/container"], function(ContainerFactory) {
         ContainerFactory(
-            ${ escape_json_dumps(component_templates) | n },
-            ${ escape_json_dumps(xblock_info) | n },
-            "${action | h}",
+            ${component_templates | n, dump_js_escaped_json},
+            ${xblock_info | n, dump_js_escaped_json},
+            "${action | n, js_escaped_string}",
             {
-                isUnitPage: ${ escape_json_dumps(is_unit_page) | n },
+                isUnitPage: ${is_unit_page | n, dump_js_escaped_json},
                 canEdit: true
             }
         );
diff --git a/cms/templates/course_info.html b/cms/templates/course_info.html
index dc2d4640ce9..549a17dba2b 100644
--- a/cms/templates/course_info.html
+++ b/cms/templates/course_info.html
@@ -3,8 +3,9 @@
 <%namespace name='static' file='static_content.html'/>
 <%!
 from django.utils.translation import ugettext as _
-from django.template.defaultfilters import escapejs
-from openedx.core.lib.js_utils import escape_json_dumps
+from openedx.core.djangolib.js_utils import (
+    dump_js_escaped_json, js_escaped_string
+)
 %>
 
 ## TODO decode course # from context_course into title.
@@ -23,11 +24,11 @@ from openedx.core.lib.js_utils import escape_json_dumps
 <%block name="requirejs">
   require(["js/factories/course_info"], function(CourseInfoFactory) {
       CourseInfoFactory(
-        "${updates_url}",
-        "${handouts_locator | escapejs}",
-        "${base_asset_url}",
-        ${escape_json_dumps(push_notification_enabled) | n}
-      );
+        "${updates_url | n, js_escaped_string}",
+        "${handouts_locator | n, js_escaped_string}",
+        "${base_asset_url | n, js_escaped_string}",
+        ${push_notification_enabled | n, dump_js_escaped_json}
+       );
   });
 </%block>
 
diff --git a/cms/templates/course_outline.html b/cms/templates/course_outline.html
index c88499567b0..6c1f97d90ef 100644
--- a/cms/templates/course_outline.html
+++ b/cms/templates/course_outline.html
@@ -4,7 +4,7 @@
 import logging
 from util.date_utils import get_default_time_display
 from django.utils.translation import ugettext as _
-from openedx.core.lib.js_utils import escape_json_dumps
+from openedx.core.djangolib.js_utils import dump_js_escaped_json
 from contentstore.utils import reverse_usage_url
 from openedx.core.djangoapps.self_paced.models import SelfPacedConfiguration
 %>
@@ -15,7 +15,10 @@ from openedx.core.djangoapps.self_paced.models import SelfPacedConfiguration
 
 <%block name="requirejs">
     require(["js/factories/outline"], function (OutlineFactory) {
-        OutlineFactory(${escape_json_dumps(course_structure) | n}, ${escape_json_dumps(initial_state) | n});
+        OutlineFactory(
+            ${course_structure | n, dump_js_escaped_json},
+            ${initial_state | n, dump_js_escaped_json}
+        );
     });
 </%block>
 
diff --git a/cms/templates/export.html b/cms/templates/export.html
index 6a8168b4e4f..c6829d44696 100644
--- a/cms/templates/export.html
+++ b/cms/templates/export.html
@@ -11,7 +11,9 @@ else:
 
 <%!
   from django.utils.translation import ugettext as _
-  from openedx.core.lib.js_utils import escape_json_dumps
+  from openedx.core.djangolib.js_utils import (
+      dump_js_escaped_json, js_escaped_string
+  )
 %>
 <%block name="title">
 %if library:
@@ -24,11 +26,11 @@ else:
 
 <%block name="requirejs">
 % if in_err:
-  var hasUnit = ${escape_json_dumps(bool(unit)) | n},
-      editUnitUrl = "${edit_unit_url or ""}",
-      courselikeHomeUrl = "${courselike_home_url or ""}",
-      is_library = ${escape_json_dumps(library) | n}
-      errMsg = ${escape_json_dumps(raw_err_msg or "") | n};
+  var hasUnit = ${bool(unit) | n, dump_js_escaped_json},
+      editUnitUrl = "${edit_unit_url | n, js_escaped_string}",
+      courselikeHomeUrl = "${courselike_home_url | n, js_escaped_string}",
+      is_library = ${library | n, dump_js_escaped_json}
+      errMsg = "${raw_err_msg | n, js_escaped_string}";
 
   require(["js/factories/export"], function(ExportFactory) {
       ExportFactory(hasUnit, editUnitUrl, courselikeHomeUrl, is_library, errMsg);
diff --git a/cms/templates/group_configurations.html b/cms/templates/group_configurations.html
index 1fa5cc61319..9bc00afbc9a 100644
--- a/cms/templates/group_configurations.html
+++ b/cms/templates/group_configurations.html
@@ -5,7 +5,9 @@
 <%!
 from contentstore import utils
 from django.utils.translation import ugettext as _
-from openedx.core.lib.js_utils import escape_json_dumps
+from openedx.core.djangolib.js_utils import (
+    dump_js_escaped_json, js_escaped_string
+)
 %>
 
 <%block name="title">${_("Group Configurations")}</%block>
@@ -21,7 +23,13 @@ from openedx.core.lib.js_utils import escape_json_dumps
 
 <%block name="requirejs">
   require(["js/factories/group_configurations"], function(GroupConfigurationsFactory) {
-      GroupConfigurationsFactory(${escape_json_dumps(should_show_experiment_groups) | n}, ${escape_json_dumps(experiment_group_configurations) | n}, ${escape_json_dumps(content_group_configuration) | n}, "${group_configuration_url}", "${course_outline_url}");
+      GroupConfigurationsFactory(
+          ${should_show_experiment_groups | n, dump_js_escaped_json},
+          ${experiment_group_configurations | n, dump_js_escaped_json},
+          ${content_group_configuration | n, dump_js_escaped_json},
+          "${group_configuration_url | n, js_escaped_string}",
+          "${course_outline_url | n, js_escaped_string}"
+      );
   });
 </%block>
 
diff --git a/cms/templates/import.html b/cms/templates/import.html
index 989c2cbc128..b00db006d2f 100644
--- a/cms/templates/import.html
+++ b/cms/templates/import.html
@@ -10,7 +10,9 @@ else:
 <%namespace name='static' file='static_content.html'/>
 <%!
   from django.utils.translation import ugettext as _
-  from openedx.core.lib.js_utils import escape_json_dumps
+  from openedx.core.djangolib.js_utils import (
+      dump_js_escaped_json, js_escaped_string
+  )
 %>
 <%block name="title">
 %if library:
@@ -239,6 +241,9 @@ else:
 
 <%block name="requirejs">
   require(["js/factories/import"], function(ImportFactory) {
-      ImportFactory("${import_status_url}", ${escape_json_dumps(library) | n});
+      ImportFactory(
+          "${import_status_url | n, js_escaped_string}",
+          ${library | n, dump_js_escaped_json}
+      );
   });
 </%block>
diff --git a/cms/templates/library.html b/cms/templates/library.html
index cf8eebcbd71..50ffd7e3fbf 100644
--- a/cms/templates/library.html
+++ b/cms/templates/library.html
@@ -3,7 +3,7 @@
 <%!
 from contentstore.views.helpers import xblock_studio_url, xblock_type_display_name
 from django.utils.translation import ugettext as _
-from openedx.core.lib.js_utils import escape_json_dumps
+from openedx.core.djangolib.js_utils import dump_js_escaped_json
 %>
 <%block name="title">${context_library.display_name_with_default_escaped} ${xblock_type_display_name(context_library)}</%block>
 <%block name="bodyclass">is-signedin course container view-container view-library</%block>
@@ -24,8 +24,8 @@ from openedx.core.lib.js_utils import escape_json_dumps
 <%block name="requirejs">
     require(["js/factories/library"], function(LibraryFactory) {
         LibraryFactory(
-            ${escape_json_dumps(component_templates) | n},
-            ${escape_json_dumps(xblock_info) | n},
+            ${component_templates | n, dump_js_escaped_json},
+            ${xblock_info | n, dump_js_escaped_json},
             {
                 isUnitPage: false,
                 page_size: 10,
diff --git a/cms/templates/manage_users.html b/cms/templates/manage_users.html
index 687247c5903..94212ee0a28 100644
--- a/cms/templates/manage_users.html
+++ b/cms/templates/manage_users.html
@@ -2,7 +2,10 @@
 <%!
 from django.utils.translation import ugettext as _
 from django.core.urlresolvers import reverse
-from openedx.core.lib.js_utils import escape_json_dumps
+
+from openedx.core.djangolib.js_utils import (
+    dump_js_escaped_json, js_escaped_string
+)
 %>
 <%def name="online_help_token()"><% return "team_course" %></%def>
 <%block name="title">${_("Course Team Settings")}</%block>
@@ -114,11 +117,11 @@ from openedx.core.lib.js_utils import escape_json_dumps
 <%block name="requirejs">
   require(["js/factories/manage_users"], function(ManageCourseUsersFactory) {
       ManageCourseUsersFactory(
-        "${context_course.display_name | h}",
-        ${escape_json_dumps(users) | n},
-        "${reverse('contentstore.views.course_team_handler', kwargs={'course_key_string': unicode(context_course.id), 'email': '@@EMAIL@@'})}",
-        ${ request.user.id },
-        ${str(allow_actions).lower()}
+        "${context_course.display_name_with_default | h}",
+        ${users | n, dump_js_escaped_json},
+        "${reverse('contentstore.views.course_team_handler', kwargs={'course_key_string': unicode(context_course.id), 'email': '@@EMAIL@@'}) | n, js_escaped_string}",
+        ${request.user.id | n, dump_js_escaped_json},
+        ${allow_actions | n, dump_js_escaped_json}
       );
   });
 </%block>
diff --git a/cms/templates/manage_users_lib.html b/cms/templates/manage_users_lib.html
index f0b5f3e1d24..e8f4b0f51bd 100644
--- a/cms/templates/manage_users_lib.html
+++ b/cms/templates/manage_users_lib.html
@@ -2,7 +2,10 @@
 <%!
 from django.utils.translation import ugettext as _
 from django.core.urlresolvers import reverse
-from openedx.core.lib.js_utils import escape_json_dumps
+
+from openedx.core.djangolib.js_utils import (
+    dump_js_escaped_json, js_escaped_string
+)
 %>
 <%def name="online_help_token()"><% return "team_library" %></%def>
 <%block name="title">${_("Library User Access")}</%block>
@@ -107,11 +110,11 @@ from openedx.core.lib.js_utils import escape_json_dumps
 <%block name="requirejs">
   require(["js/factories/manage_users_lib"], function(ManageLibraryUsersFactory) {
       ManageLibraryUsersFactory(
-        "${context_library.display_name_with_default_escaped | h}",
-        ${escape_json_dumps(users) | n},
-        "${reverse('contentstore.views.course_team_handler', kwargs={'course_key_string': library_key, 'email': '@@EMAIL@@'})}",
-        ${ request.user.id },
-        ${str(allow_actions).lower()}
+        "${context_library.display_name_with_default | h}",
+        ${users | n, dump_js_escaped_json},
+        "${reverse('contentstore.views.course_team_handler', kwargs={'course_key_string': library_key, 'email': '@@EMAIL@@'}) | n, js_escaped_string}",
+        ${request.user.id | n, dump_js_escaped_json},
+        ${allow_actions | n, dump_js_escaped_json}
       );
   });
 </%block>
diff --git a/cms/templates/settings.html b/cms/templates/settings.html
index 265d2da82b5..c10e9ebc8b1 100644
--- a/cms/templates/settings.html
+++ b/cms/templates/settings.html
@@ -8,7 +8,9 @@
   import urllib
   from django.utils.translation import ugettext as _
   from contentstore import utils
-  from openedx.core.lib.js_utils import escape_json_dumps
+  from openedx.core.djangolib.js_utils import (
+      dump_js_escaped_json, js_escaped_string
+  )
 %>
 
 <%block name="header_extras">
@@ -31,7 +33,10 @@ CMS.URL.UPLOAD_ASSET = '${upload_asset_url}';
 
 <%block name="requirejs">
     require(["js/factories/settings"], function(SettingsFactory) {
-        SettingsFactory("${details_url}", ${escape_json_dumps(show_min_grade_warning) | n});
+        SettingsFactory(
+            "${details_url | n, js_escaped_string}",
+            ${show_min_grade_warning | n, dump_js_escaped_json}
+        );
     });
 </%block>
 
diff --git a/cms/templates/settings_advanced.html b/cms/templates/settings_advanced.html
index 5bfb09679e3..6c26c5eae7d 100644
--- a/cms/templates/settings_advanced.html
+++ b/cms/templates/settings_advanced.html
@@ -4,7 +4,9 @@
 <%!
   from django.utils.translation import ugettext as _
   from contentstore import utils
-  from openedx.core.lib.js_utils import escape_json_dumps
+  from openedx.core.djangolib.js_utils import (
+      dump_js_escaped_json, js_escaped_string
+  )
 %>
 <%block name="title">${_("Advanced Settings")}</%block>
 <%block name="bodyclass">is-signedin course advanced view-settings</%block>
@@ -19,7 +21,10 @@
 
 <%block name="requirejs">
     require(["js/factories/settings_advanced"], function(SettingsAdvancedFactory) {
-        SettingsAdvancedFactory(${escape_json_dumps(advanced_dict) | n}, "${advanced_settings_url}");
+        SettingsAdvancedFactory(
+            ${advanced_dict | n, dump_js_escaped_json},
+            "${advanced_settings_url | n, js_escaped_string}"
+        );
     });
 </%block>
 
diff --git a/cms/templates/settings_graders.html b/cms/templates/settings_graders.html
index acb5f35e7f0..44989d201d6 100644
--- a/cms/templates/settings_graders.html
+++ b/cms/templates/settings_graders.html
@@ -9,7 +9,9 @@
   from contentstore import utils
   from django.utils.translation import ugettext as _
   from models.settings.encoder import CourseSettingsEncoder
-  from openedx.core.lib.js_utils import escape_json_dumps
+  from openedx.core.djangolib.js_utils import (
+      dump_js_escaped_json, js_escaped_string
+  )
 %>
 
 <%block name="header_extras">
@@ -25,7 +27,11 @@
 </%block>
 <%block name="requirejs">
     require(["js/factories/settings_graders"], function(SettingsGradersFactory) {
-        SettingsGradersFactory(_.extend(${escape_json_dumps(course_details, cls=CourseSettingsEncoder) | n}, {is_credit_course: ${escape_json_dumps(is_credit_course) | n}}), "${grading_url}");
+        SettingsGradersFactory(
+            _.extend(${dump_js_escaped_json(course_details, cls=CourseSettingsEncoder) | n},
+                {is_credit_course: ${is_credit_course | n, dump_js_escaped_json}}),
+            "${grading_url | n, js_escaped_string}"
+        );
     });
 </%block>
 
diff --git a/cms/templates/studio_xblock_wrapper.html b/cms/templates/studio_xblock_wrapper.html
index 854639d1988..9cee2fda1f2 100644
--- a/cms/templates/studio_xblock_wrapper.html
+++ b/cms/templates/studio_xblock_wrapper.html
@@ -2,7 +2,9 @@
 from django.utils.translation import ugettext as _
 from contentstore.views.helpers import xblock_studio_url
 from contentstore.utils import is_visible_to_specific_content_groups
-from openedx.core.lib.js_utils import escape_json_dumps
+from openedx.core.djangolib.js_utils import (
+    dump_js_escaped_json, js_escaped_string
+)
 %>
 <%
 xblock_url = xblock_studio_url(xblock)
@@ -24,10 +26,10 @@ messages = xblock.validate().to_json()
 <script>
     require(["jquery", "js/factories/xblock_validation"], function($, XBlockValidationFactory) {
         XBlockValidationFactory(
-            ${escape_json_dumps(messages) | n},
-            $.parseJSON("${bool(xblock_url)}".toLowerCase()),  // xblock_url will be None or a string
-            $.parseJSON("${bool(is_root)}".toLowerCase()),  // is_root will be None or a boolean
-            $('div.xblock-validation-messages[data-locator="${xblock.location | h}"]')
+            ${messages | n, dump_js_escaped_json},
+            ${bool(xblock_url) | n, dump_js_escaped_json},  // xblock_url will be None or a string
+            ${bool(is_root) | n, dump_js_escaped_json},  // is_root will be None or a boolean
+            $('div.xblock-validation-messages[data-locator="${xblock.location | n, js_escaped_string}"]')
         );
     });
 </script>
diff --git a/cms/templates/textbooks.html b/cms/templates/textbooks.html
index 4fc8ae552b8..c550fe43d20 100644
--- a/cms/templates/textbooks.html
+++ b/cms/templates/textbooks.html
@@ -3,7 +3,7 @@
 <%namespace name='static' file='static_content.html'/>
 <%!
 from django.utils.translation import ugettext as _
-from openedx.core.lib.js_utils import escape_json_dumps
+from openedx.core.djangolib.js_utils import dump_js_escaped_json
 %>
 
 <%block name="title">${_("Textbooks")}</%block>
@@ -28,7 +28,7 @@ CMS.URL.LMS_BASE = "${settings.LMS_BASE}"
 </%block>
 <%block name="requirejs">
     require(["js/factories/textbooks"], function(TextbooksFactory) {
-        TextbooksFactory(${escape_json_dumps(textbooks) | n});
+        TextbooksFactory(${textbooks | n, dump_js_escaped_json});
     });
 </%block>
 
diff --git a/lms/djangoapps/student_account/test/test_views.py b/lms/djangoapps/student_account/test/test_views.py
index 145d6a5c3d1..43229700d68 100644
--- a/lms/djangoapps/student_account/test/test_views.py
+++ b/lms/djangoapps/student_account/test/test_views.py
@@ -20,7 +20,7 @@ from django.http import HttpRequest
 from course_modes.models import CourseMode
 from openedx.core.djangoapps.user_api.accounts.api import activate_account, create_account
 from openedx.core.djangoapps.user_api.accounts import EMAIL_MAX_LENGTH
-from openedx.core.lib.js_utils import escape_json_dumps
+from openedx.core.djangolib.js_utils import dump_js_escaped_json
 from student.tests.factories import UserFactory
 from student_account.views import account_settings_context
 from third_party_auth.tests.testutil import simulate_running_pipeline, ThirdPartyAuthTestMixin
@@ -387,7 +387,7 @@ class StudentAccountLoginAndRegistrationTest(ThirdPartyAuthTestMixin, UrlResetMi
             "finishAuthUrl": finish_auth_url,
             "errorMessage": None,
         }
-        auth_info = escape_json_dumps(auth_info)
+        auth_info = dump_js_escaped_json(auth_info)
 
         expected_data = '"third_party_auth": {auth_info}'.format(
             auth_info=auth_info
diff --git a/lms/djangoapps/teams/templates/teams/teams.html b/lms/djangoapps/teams/templates/teams/teams.html
index bc577ec938f..fdcc7e707f9 100644
--- a/lms/djangoapps/teams/templates/teams/teams.html
+++ b/lms/djangoapps/teams/templates/teams/teams.html
@@ -1,7 +1,11 @@
 ## mako
 <%! import json %>
-<%! from django.utils.translation import ugettext as _ %>
-<%! from openedx.core.lib.js_utils import escape_json_dumps %>
+<%!
+from django.utils.translation import ugettext as _
+from openedx.core.djangolib.js_utils import (
+    dump_js_escaped_json, js_escaped_string
+)
+%>
 <%namespace name='static' file='/static_content.html'/>
 <%inherit file="/main.html" />
 
@@ -33,20 +37,20 @@
 
 <%static:require_module module_name="teams/js/teams_tab_factory" class_name="TeamsTabFactory">
     TeamsTabFactory({
-        courseID: '${ unicode(course.id) }',
-        topics: ${ escape_json_dumps(topics) | n },
-        userInfo: ${ escape_json_dumps(user_info) | n },
-        topicUrl: '${ topic_url }',
-        topicsUrl: '${ topics_url }',
-        teamsUrl: '${ teams_url }',
-        teamsDetailUrl: '${ teams_detail_url }',
-        teamMembershipsUrl: '${ team_memberships_url }',
-        teamMembershipDetailUrl: '${ team_membership_detail_url }',
-        myTeamsUrl: '${ my_teams_url }',
-        maxTeamSize: ${ course.teams_max_size },
-        languages: ${ escape_json_dumps(languages) | n },
-        countries: ${ escape_json_dumps(countries) | n },
-        teamsBaseUrl: '${ teams_base_url }'
+        courseID: '${unicode(course.id) | n, js_escaped_string}',
+        topics: ${topics | n, dump_js_escaped_json},
+        userInfo: ${user_info | n, dump_js_escaped_json},
+        topicUrl: '${topic_url | n, js_escaped_string}',
+        topicsUrl: '${topics_url | n, js_escaped_string}',
+        teamsUrl: '${teams_url | n, js_escaped_string}',
+        teamsDetailUrl: '${teams_detail_url | n, js_escaped_string}',
+        teamMembershipsUrl: '${team_memberships_url | n, js_escaped_string}',
+        teamMembershipDetailUrl: '${team_membership_detail_url | n, js_escaped_string}',
+        myTeamsUrl: '${my_teams_url | n, js_escaped_string}',
+        maxTeamSize: ${course.teams_max_size | n, dump_js_escaped_json},
+        languages: ${languages | n, dump_js_escaped_json},
+        countries: ${countries | n, dump_js_escaped_json},
+        teamsBaseUrl: '${teams_base_url | n, js_escaped_string}'
     });
 </%static:require_module>
 </%block>
diff --git a/lms/envs/test.py b/lms/envs/test.py
index 429d5c05077..ab51774b5e5 100644
--- a/lms/envs/test.py
+++ b/lms/envs/test.py
@@ -495,7 +495,8 @@ MICROSITE_LOGISTRATION_HOSTNAME = 'logistration.testserver'
 # add extra template directory for test-only templates
 MAKO_TEMPLATES['main'].extend([
     COMMON_ROOT / 'test' / 'templates',
-    COMMON_ROOT / 'test' / 'test_microsites'
+    COMMON_ROOT / 'test' / 'test_microsites',
+    REPO_ROOT / 'openedx' / 'core' / 'djangolib' / 'tests' / 'templates',
 ])
 
 
diff --git a/lms/templates/courseware/courses.html b/lms/templates/courseware/courses.html
index 0fdcc6836a8..f9914bfe8f3 100644
--- a/lms/templates/courseware/courses.html
+++ b/lms/templates/courseware/courses.html
@@ -1,7 +1,7 @@
 <%!
   import json
   from django.utils.translation import ugettext as _
-  from openedx.core.lib.js_utils import escape_json_dumps
+  from openedx.core.djangolib.js_utils import dump_js_escaped_json
 %>
 <%inherit file="../main.html" />
 <%
@@ -19,7 +19,7 @@
   % endfor
   <%static:require_module module_name="js/discovery/discovery_factory" class_name="DiscoveryFactory">
     DiscoveryFactory(
-      ${ escape_json_dumps(course_discovery_meanings) | n },
+      ${course_discovery_meanings | n, dump_js_escaped_json},
       getParameterByName('search_query')
     );
   </%static:require_module>
diff --git a/lms/templates/financial-assistance/apply.html b/lms/templates/financial-assistance/apply.html
index 03b77352753..ca588d264a3 100644
--- a/lms/templates/financial-assistance/apply.html
+++ b/lms/templates/financial-assistance/apply.html
@@ -2,21 +2,23 @@
 <%!
 import json
 
-from openedx.core.lib.js_utils import escape_json_dumps
+from openedx.core.djangolib.js_utils import (
+    dump_js_escaped_json, js_escaped_string
+)
 %>
 <%namespace name='static' file='/static_content.html'/>
 
 <%block name="js_extra">
 <%static:require_module module_name="js/financial-assistance/financial_assistance_form_factory" class_name="FinancialAssistanceFactory">
 FinancialAssistanceFactory({
-    fields: ${escape_json_dumps(fields)},
-    user_details: ${escape_json_dumps(user_details)},
-    header_text: ${escape_json_dumps(header_text)},
-    student_faq_url: ${json.dumps(student_faq_url)},
-    dashboard_url: ${json.dumps(dashboard_url)},
-    account_settings_url: ${json.dumps(account_settings_url)},
-    platform_name: ${escape_json_dumps(platform_name)},
-    submit_url: ${json.dumps(submit_url)}
+    fields: ${fields | n, dump_js_escaped_json},
+    user_details: ${user_details | n, dump_js_escaped_json},
+    header_text: ${header_text | n, dump_js_escaped_json},
+    student_faq_url: '${student_faq_url | n, js_escaped_string}',
+    dashboard_url: '${dashboard_url | n, js_escaped_string}',
+    account_settings_url: '${account_settings_url | n, js_escaped_string}',
+    platform_name: '${platform_name | n, js_escaped_string}',
+    submit_url: '${submit_url | n, js_escaped_string}'
 });
 </%static:require_module>
 </%block>
diff --git a/lms/templates/student_account/login_and_register.html b/lms/templates/student_account/login_and_register.html
index 962936bb938..e83d1cbeec8 100644
--- a/lms/templates/student_account/login_and_register.html
+++ b/lms/templates/student_account/login_and_register.html
@@ -1,7 +1,7 @@
 <%!
     import json
     from django.utils.translation import ugettext as _
-    from openedx.core.lib.js_utils import escape_json_dumps
+    from openedx.core.djangolib.js_utils import dump_js_escaped_json
 %>
 <%namespace name='static' file='/static_content.html'/>
 
@@ -11,7 +11,7 @@
 
 <%block name="js_extra">
     <%static:require_module module_name="js/student_account/logistration_factory" class_name="LogistrationFactory">
-        var options = ${ escape_json_dumps(data) | n };
+        var options = ${data | n, dump_js_escaped_json};
         LogistrationFactory(options);
     </%static:require_module>
 </%block>
diff --git a/lms/templates/student_profile/learner_profile.html b/lms/templates/student_profile/learner_profile.html
index 17ab3043d87..e975796ddaa 100644
--- a/lms/templates/student_profile/learner_profile.html
+++ b/lms/templates/student_profile/learner_profile.html
@@ -4,7 +4,7 @@
 import json
 from django.core.urlresolvers import reverse
 from django.utils.translation import ugettext as _
-from openedx.core.lib.js_utils import escape_json_dumps
+from openedx.core.djangolib.js_utils import dump_js_escaped_json
 %>
 
 <%block name="pagetitle">${_("Learner Profile")}</%block>
@@ -24,7 +24,7 @@ from openedx.core.lib.js_utils import escape_json_dumps
 
 <%block name="js_extra">
 <%static:require_module module_name="js/student_profile/views/learner_profile_factory" class_name="LearnerProfileFactory">
-    var options = ${ escape_json_dumps(data) | n };
+    var options = ${data | n, dump_js_escaped_json};
     LearnerProfileFactory(options);
 </%static:require_module>
 </%block>
diff --git a/lms/templates/support/enrollment.html b/lms/templates/support/enrollment.html
index 84f34394728..d5dc8e2d7ad 100644
--- a/lms/templates/support/enrollment.html
+++ b/lms/templates/support/enrollment.html
@@ -1,7 +1,6 @@
 <%!
 from django.utils.translation import ugettext as _
-
-from openedx.core.lib.js_utils import escape_json_dumps
+from openedx.core.djangolib.js_utils import js_escaped_string
 %>
 
 <%namespace name='static' file='../static_content.html'/>
@@ -11,9 +10,9 @@ from openedx.core.lib.js_utils import escape_json_dumps
 <%block name="js_extra">
 <%static:require_module module_name="support/js/enrollment_factory" class_name="EnrollmentFactory">
     new EnrollmentFactory({
-        user: ${escape_json_dumps(username)},
-        enrollmentsUrl: ${escape_json_dumps(enrollmentsUrl)},
-        enrollmentSupportUrl: ${escape_json_dumps(enrollmentSupportUrl)},
+        user: '${username | n, js_escaped_string}',
+        enrollmentsUrl: '${enrollmentsUrl | n, js_escaped_string}',
+        enrollmentSupportUrl: '${enrollmentSupportUrl | n, js_escaped_string}',
     });
 </%static:require_module>
 </%block>
diff --git a/openedx/README.rst b/openedx/README.rst
index aece6f16f64..ced9d71fc8f 100644
--- a/openedx/README.rst
+++ b/openedx/README.rst
@@ -6,6 +6,7 @@ from Open edX will eventually live here, including the code in the lms, cms,
 and common directories.
 
 If you're adding a new Django app, place it in core/djangoapps. If you're adding
+utilities that require Django, place them in core/djangolib.  If you're adding
 code that defines no Django models or views of its own but is widely useful, put it
 in core/lib.
 
diff --git a/openedx/core/djangolib/__init__.py b/openedx/core/djangolib/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/openedx/core/djangolib/js_utils.py b/openedx/core/djangolib/js_utils.py
new file mode 100644
index 00000000000..35a20a7504d
--- /dev/null
+++ b/openedx/core/djangolib/js_utils.py
@@ -0,0 +1,149 @@
+"""
+Utilities for dealing with Javascript and JSON.
+"""
+import json
+
+from django.utils.html import escapejs
+from mako.filters import decode
+from markupsafe import escape
+
+from xmodule.modulestore import EdxJSONEncoder
+
+
+def _escape_json_for_js(json_dumps_string):
+    """
+    Escape output of JSON dumps that is safe to be embedded in a <SCRIPT> tag.
+
+    This implementation is based on escaping performed in
+    simplejson.JSONEncoderForHTML.
+
+    Arguments:
+        json_dumps_string (string): A JSON string to be escaped.
+
+            This must be the output of json.dumps to ensure:
+            1. The string contains valid JSON, and
+            2. That non-ascii characters are properly escaped
+
+    Returns:
+        (string) Escaped JSON that is safe to be embedded in HTML.
+
+    """
+    json_dumps_string = json_dumps_string.replace("&", "\\u0026")
+    json_dumps_string = json_dumps_string.replace(">", "\\u003e")
+    json_dumps_string = json_dumps_string.replace("<", "\\u003c")
+    return json_dumps_string
+
+
+def dump_js_escaped_json(obj, cls=EdxJSONEncoder):
+    """
+    JSON dumps and escapes objects that are safe to be embedded in JavaScript.
+
+    Use this for anything but strings (e.g. dicts, tuples, lists, bools, and
+    numbers).  For strings, use js_escaped_string.
+
+    The output of this method is also usable as plain-old JSON.
+
+    Usage:
+        Used as follows in a Mako template inside a <SCRIPT> tag::
+
+            var json_obj = ${obj | n, dump_js_escaped_json}
+
+        If you must use the cls argument, then use as follows::
+
+            var json_obj = ${dump_js_escaped_json(obj, cls) | n}
+
+        Use the "n" Mako filter above.  It is possible that the default filter
+        may include html escaping in the future, and this ensures proper
+        escaping.
+
+        Ensure ascii in json.dumps (ensure_ascii=True) allows safe skipping of
+        Mako's default filter decode.utf8.
+
+    Arguments:
+        obj: The object soon to become a JavaScript escaped JSON string.  The
+            object can be anything but strings (e.g. dicts, tuples, lists, bools, and
+            numbers).
+        cls (class): The JSON encoder class (defaults to EdxJSONEncoder).
+
+    Returns:
+        (string) Escaped encoded JSON.
+
+    """
+    json_string = json.dumps(obj, ensure_ascii=True, cls=cls)
+    json_string = _escape_json_for_js(json_string)
+    return json_string
+
+
+def dump_html_escaped_json(obj, cls=EdxJSONEncoder):
+    """
+    JSON dumps and escapes objects that are safe to be embedded in HTML.
+
+    Use this for anything but strings (e.g. dicts, tuples, lists, bools, and
+    numbers).  For strings, just used the default html filter.
+
+    Usage:
+        Used as follows in a Mako template inside a HTML, like in
+        a data attribute::
+
+            data-obj='${obj | n, dump_html_escaped_json}'
+
+        If you must use the cls argument, then use as follows::
+
+            data-obj='${dump_html_escaped_json(obj, cls) | n}'
+
+        Use the "n" Mako filter above.  The default filter will include
+        html escaping in the future, and this ensures proper ordering of
+        these calls.
+
+        Ensure ascii in json.dumps (ensure_ascii=True) allows safe skipping of
+        Mako's default filter decode.utf8.
+
+    Arguments:
+        obj: The object soon to become an HTML escaped JSON string.  The object
+            can be anything but strings (e.g. dicts, tuples, lists, bools, and
+            numbers).
+        cls (class): The JSON encoder class (defaults to EdxJSONEncoder).
+
+    Returns:
+        (string) Escaped encoded JSON.
+
+    """
+    json_string = json.dumps(obj, ensure_ascii=True, cls=cls)
+    json_string = escape(json_string)
+    return json_string
+
+
+def js_escaped_string(string_for_js):
+    """
+    Mako filter that escapes text for use in a JavaScript string.
+
+    If None is provided, returns an empty string.
+
+    Usage:
+        Used as follows in a Mako template inside a <SCRIPT> tag::
+
+            var my_string_for_js = "${my_string_for_js | n, js_escaped_string}"
+
+        The surrounding quotes for the string must be included.
+
+        Use the "n" Mako filter above.  It is possible that the default filter
+        may include html escaping in the future, and this ensures proper
+        escaping.
+
+        Mako's default filter decode.utf8 is applied here since this default
+        filter is skipped in the Mako template with "n".
+
+    Arguments:
+        string_for_js (string): Text to be properly escaped for use in a
+            JavaScript string.
+
+    Returns:
+        (string) Text properly escaped for use in a JavaScript string as
+        unicode.  Returns empty string if argument is None.
+
+    """
+    if string_for_js is None:
+        string_for_js = ""
+    string_for_js = decode.utf8(string_for_js)
+    string_for_js = escapejs(string_for_js)
+    return string_for_js
diff --git a/openedx/core/djangolib/tests/__init__.py b/openedx/core/djangolib/tests/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/openedx/core/djangolib/tests/test_js_utils.py b/openedx/core/djangolib/tests/test_js_utils.py
new file mode 100644
index 00000000000..6bfb51476c8
--- /dev/null
+++ b/openedx/core/djangolib/tests/test_js_utils.py
@@ -0,0 +1,242 @@
+# -*- coding: utf-8 -*-
+"""
+Tests for js_utils.py
+"""
+import json
+from unittest import TestCase
+import HTMLParser
+
+from mako.template import Template
+
+from openedx.core.djangolib.js_utils import (
+    dump_js_escaped_json, dump_html_escaped_json, js_escaped_string
+)
+
+
+class TestJSUtils(TestCase):
+    """
+    Test JS utils
+    """
+
+    class NoDefaultEncoding(object):
+        """
+        Helper class that has no default JSON encoding
+        """
+        def __init__(self, value):
+            self.value = value
+
+    class SampleJSONEncoder(json.JSONEncoder):
+        """
+        A test encoder that is used to prove that the encoder does its job before the escaping.
+        """
+        # pylint: disable=method-hidden
+        def default(self, noDefaultEncodingObj):
+            return noDefaultEncodingObj.value.replace("<script>", "sample-encoder-was-here")
+
+    def test_dump_js_escaped_json_escapes_unsafe_html(self):
+        """
+        Test dump_js_escaped_json properly escapes &, <, and >.
+        """
+        malicious_dict = {"</script><script>alert('hello, ');</script>": "</script><script>alert('&world!');</script>"}
+        expected_escaped_json = (
+            r'''{"\u003c/script\u003e\u003cscript\u003ealert('hello, ');\u003c/script\u003e": '''
+            r'''"\u003c/script\u003e\u003cscript\u003ealert('\u0026world!');\u003c/script\u003e"}'''
+        )
+
+        escaped_json = dump_js_escaped_json(malicious_dict)
+        self.assertEquals(expected_escaped_json, escaped_json)
+
+    def test_dump_js_escaped_json_with_custom_encoder_escapes_unsafe_html(self):
+        """
+        Test dump_js_escaped_json first encodes with custom JSNOEncoder before escaping &, <, and >
+
+        The test encoder class should first perform the replacement of "<script>" with
+        "sample-encoder-was-here", and then should escape the remaining &, <, and >.
+
+        """
+        malicious_dict = {
+            "</script><script>alert('hello, ');</script>":
+            self.NoDefaultEncoding("</script><script>alert('&world!');</script>")
+        }
+        expected_custom_escaped_json = (
+            r'''{"\u003c/script\u003e\u003cscript\u003ealert('hello, ');\u003c/script\u003e": '''
+            r'''"\u003c/script\u003esample-encoder-was-herealert('\u0026world!');\u003c/script\u003e"}'''
+        )
+
+        escaped_json = dump_js_escaped_json(malicious_dict, cls=self.SampleJSONEncoder)
+        self.assertEquals(expected_custom_escaped_json, escaped_json)
+
+    def test_dump_html_escaped_json_escapes_unsafe_html(self):
+        """
+        Test dump_html_escaped_json properly escapes &, <, and >.
+        """
+        malicious_dict = {"</script><script>alert('hello, ');</script>": "</script><script>alert('&world!');</script>"}
+        expected_escaped_json = (
+            "{&#34;&lt;/script&gt;&lt;script&gt;alert(&#39;hello, &#39;);&lt;/script&gt;&#34;: "
+            "&#34;&lt;/script&gt;&lt;script&gt;alert(&#39;&amp;world!&#39;);&lt;/script&gt;&#34;}"
+        )
+
+        escaped_json = dump_html_escaped_json(malicious_dict)
+        self.assertEquals(expected_escaped_json, escaped_json)
+
+    def test_dump_html_escaped_json_with_custom_encoder_escapes_unsafe_html(self):
+        """
+        Test dump_html_escaped_json first encodes with custom JSNOEncoder before escaping &, <, and >
+
+        The test encoder class should first perform the replacement of "<script>" with
+        "sample-encoder-was-here", and then should escape the remaining &, <, and >.
+
+        """
+        malicious_dict = {
+            "</script><script>alert('hello, ');</script>":
+            self.NoDefaultEncoding("</script><script>alert('&world!');</script>")
+        }
+        expected_custom_escaped_json = (
+            "{&#34;&lt;/script&gt;&lt;script&gt;alert(&#39;hello, &#39;);&lt;/script&gt;&#34;: "
+            "&#34;&lt;/script&gt;sample-encoder-was-herealert(&#39;&amp;world!&#39;);&lt;/script&gt;&#34;}"
+        )
+        escaped_json = dump_html_escaped_json(malicious_dict, cls=self.SampleJSONEncoder)
+        self.assertEquals(expected_custom_escaped_json, escaped_json)
+
+    def test_js_escaped_string_escapes_unsafe_html(self):
+        """
+        Test js_escaped_string escapes &, <, and >, as well as returns a unicode type
+        """
+        malicious_js_string = "</script><script>alert('hello, ');</script>"
+
+        expected_escaped_string_for_js = unicode(
+            r"\u003C/script\u003E\u003Cscript\u003Ealert(\u0027hello, \u0027)\u003B\u003C/script\u003E"
+        )
+        escaped_string_for_js = js_escaped_string(malicious_js_string)
+        self.assertEquals(expected_escaped_string_for_js, escaped_string_for_js)
+
+    def test_js_escaped_string_with_none(self):
+        """
+        Test js_escaped_string returns empty string for None
+        """
+        escaped_string_for_js = js_escaped_string(None)
+        self.assertEquals(u"", escaped_string_for_js)
+
+    def test_mako(self):
+        """
+        Tests the full suite of Mako best practices by running all of the
+        combinations of types of data and types of escaping through a Mako
+        template.
+
+        Additionally, validates the best practices themselves by validating
+        the expectations to ensure they can properly be unescaped and/or
+        parsed from json where applicable.
+        """
+        test_dict = {
+            'test_string': u'test-=&\\;\'"<>☃'.encode(encoding='utf-8'),
+            'test_tuple': (1, 2, 3),
+            'test_number': 3.5,
+            'test_bool': False,
+        }
+
+        template = Template(
+            """
+                <%!
+                from openedx.core.djangolib.js_utils import (
+                    dump_js_escaped_json, dump_html_escaped_json, js_escaped_string
+                )
+                %>
+                <body>
+                    <div
+                        data-test-dict='${test_dict | n, dump_html_escaped_json}'
+                        data-test-string='${test_dict["test_string"]}'
+                        data-test-tuple='${test_dict["test_tuple"] | n, dump_html_escaped_json}'
+                        data-test-number='${test_dict["test_number"] | n, dump_html_escaped_json}'
+                        data-test-bool='${test_dict["test_bool"] | n, dump_html_escaped_json}'
+                    ></div>
+
+                    <script>
+                        var test_dict = ${test_dict | n, dump_js_escaped_json}
+                        var test_string = '${test_dict["test_string"] | n, js_escaped_string}'
+                        var test_none_string = '${None | n, js_escaped_string}'
+                        var test_tuple = ${test_dict["test_tuple"] | n, dump_js_escaped_json}
+                        var test_number = ${test_dict["test_number"] | n, dump_js_escaped_json}
+                        var test_bool = ${test_dict["test_bool"] | n, dump_js_escaped_json}
+                        var test_none_json = ${None | n, dump_js_escaped_json}
+                    </script>
+                </body>
+            """,
+            default_filters=['decode.utf8', 'h'],
+        )
+        out = template.render(test_dict=test_dict)
+
+        expected_json_for_html = (
+            r"{&#34;test_bool&#34;: false, &#34;test_number&#34;: 3.5, "
+            r"&#34;test_tuple&#34;: [1, 2, 3], &#34;test_string&#34;: "
+            r"&#34;test-=&amp;\\;&#39;\&#34;&lt;&gt;\u2603&#34;}"
+        )
+        expected_attr_json_for_html = "data-test-dict='" + expected_json_for_html + "'"
+        self._validate_expectation_of_json_for_html(test_dict, expected_json_for_html)
+        self.assertIn(expected_attr_json_for_html, out)
+        self.assertIn(u"data-test-string='test-=&amp;\\;&#39;&#34;&lt;&gt;☃'", out)
+        self.assertIn("data-test-tuple='[1, 2, 3]'", out)
+        self.assertIn("data-test-number='3.5'", out)
+        self.assertIn("data-test-bool='false'", out)
+        expected_string_for_js_in_dict = r'''test-=\u0026\\;'\"\u003c\u003e\u2603'''
+        self._validate_expectation_of_string_for_js(test_dict['test_string'], expected_string_for_js_in_dict)
+        self.assertIn(
+            (
+                'var test_dict = {"test_bool": false, "test_number": 3.5, '
+                '"test_tuple": [1, 2, 3], "test_string": "' + expected_string_for_js_in_dict + '"}'
+            ), out)
+        expected_string_for_js = r"test\u002D\u003D\u0026\u005C\u003B\u0027\u0022\u003C\u003E☃"
+        self._validate_expectation_of_string_for_js(test_dict['test_string'], expected_string_for_js)
+        self.assertIn(
+            "var test_string = '" + expected_string_for_js.decode(encoding='utf-8') + "'",
+            out)
+        self.assertIn("var test_none_string = ''", out)
+        self.assertIn("var test_tuple = [1, 2, 3]", out)
+        self.assertIn("var test_number = 3.5", out)
+        self.assertIn("var test_bool = false", out)
+        self.assertIn("var test_none_json = null", out)
+
+    def _validate_expectation_of_json_for_html(self, test_dict, expected_json_for_html_string):
+        """
+        Proves that the expectation string is a reasonable one, since it is
+        not very human readable with all of the escaping.
+
+        Ensures that after unescaping (html) the string can be parsed to a
+        (nearly) equivalent dict.
+
+        Assertions will fail if the expectation is invalid.
+
+        Arguments:
+            test_dict: The original dict to be tested in the Mako template.
+            expected_json_for_html_string: An html escaped json string that
+                should be parseable into a near equivalent to test_dict.
+
+        """
+        html_parser = HTMLParser.HTMLParser()
+
+        expected_json = html_parser.unescape(expected_json_for_html_string)
+        parsed_expected_dict = json.loads(expected_json)
+        # tuples become arrays in json, so it is parsed to a list that is
+        # switched back to a tuple before comparing
+        parsed_expected_dict['test_tuple'] = tuple(parsed_expected_dict['test_tuple'])
+        self.assertEqual(test_dict['test_string'].decode(encoding='utf-8'), parsed_expected_dict['test_string'])
+        self.assertEqual(test_dict['test_tuple'], parsed_expected_dict['test_tuple'])
+        self.assertEqual(test_dict['test_number'], parsed_expected_dict['test_number'])
+        self.assertEqual(test_dict['test_bool'], parsed_expected_dict['test_bool'])
+
+    def _validate_expectation_of_string_for_js(self, test_string, expected_string_for_js):
+        """
+        Proves that the expectation string is a reasonable one, since it is
+        not very human readable with all of the escaping.
+
+        Ensures that after parsing the string is equal to the original.
+
+        Assertions will fail if the expectation is invalid.
+
+        Arguments:
+            test_string: The original string to be tested in the Mako template.
+            expected_string_for_js: An escaped for js string that should be
+                parseable into the same string as test_string.
+
+        """
+        parsed_expected_string = json.loads('"' + expected_string_for_js + '"')
+        self.assertEqual(test_string.decode(encoding='utf-8'), parsed_expected_string)
diff --git a/openedx/core/lib/js_utils.py b/openedx/core/lib/js_utils.py
deleted file mode 100644
index 020ad6066be..00000000000
--- a/openedx/core/lib/js_utils.py
+++ /dev/null
@@ -1,83 +0,0 @@
-"""
-Utilities for dealing with Javascript and JSON.
-"""
-import json
-from django.template.defaultfilters import escapejs
-from mako.filters import decode
-from xmodule.modulestore import EdxJSONEncoder
-
-
-def _escape_json_for_html(json_string):
-    """
-    Escape JSON that is safe to be embedded in HTML.
-
-    This implementation is based on escaping performed in simplejson.JSONEncoderForHTML.
-
-    Arguments:
-        json_string (string): The JSON string to be escaped
-
-    Returns:
-        (string) Escaped JSON that is safe to be embedded in HTML.
-
-    """
-    json_string = json_string.replace("&", "\\u0026")
-    json_string = json_string.replace(">", "\\u003e")
-    json_string = json_string.replace("<", "\\u003c")
-    return json_string
-
-
-def escape_json_dumps(obj, cls=EdxJSONEncoder):
-    """
-    JSON dumps and escapes JSON that is safe to be embedded in HTML.
-
-    Usage:
-        Can be used inside a Mako template inside a <SCRIPT> as follows:
-            var my_json = ${escape_json_dumps(my_object) | n}
-
-        Use the "n" Mako filter above.  It is possible that the
-            default filter may include html escaping in the future, and
-            we must make sure to get the proper escaping.
-
-        Ensure ascii in json.dumps (ensure_ascii=True) allows safe skipping of Mako's
-            default filter decode.utf8.
-
-    Arguments:
-        obj: The json object to be encoded and dumped to a string
-        cls (class): The JSON encoder class (defaults to EdxJSONEncoder)
-
-    Returns:
-        (string) Escaped encoded JSON
-
-    """
-    encoded_json = json.dumps(obj, ensure_ascii=True, cls=cls)
-    encoded_json = _escape_json_for_html(encoded_json)
-    return encoded_json
-
-
-def escape_js_string(js_string):
-    """
-    Escape a javascript string that is safe to be embedded in HTML.
-
-    Usage:
-        Can be used inside a Mako template inside a <SCRIPT> as follows:
-            var my_js_string = "${escape_js_string(my_js_string) | n}"
-
-        Must include the surrounding quotes for the string.
-
-        Use the "n" Mako filter above.  It is possible that the
-            default filter may include html escaping in the future, and
-            we must make sure to get the proper escaping.
-
-        Mako's default filter decode.utf8 is applied here since this default
-            filter is skipped in the Mako template with "n".
-
-    Arguments:
-        js_string (string): The javascript string to be escaped
-
-    Returns:
-        (string) Escaped javascript as unicode
-
-    """
-    js_string = decode.utf8(js_string)
-    js_string = escapejs(js_string)
-    return js_string
diff --git a/openedx/core/lib/tests/test_js_utils.py b/openedx/core/lib/tests/test_js_utils.py
deleted file mode 100644
index 767917bb0b1..00000000000
--- a/openedx/core/lib/tests/test_js_utils.py
+++ /dev/null
@@ -1,74 +0,0 @@
-"""
-Tests for js_utils.py
-"""
-import json
-from unittest import TestCase
-from openedx.core.lib.js_utils import (
-    escape_json_dumps, escape_js_string
-)
-
-
-class TestJSUtils(TestCase):
-    """
-    Test JS utils
-    """
-
-    class NoDefaultEncoding(object):
-        """
-        Helper class that has no default JSON encoding
-        """
-        def __init__(self, value):
-            self.value = value
-
-    class SampleJSONEncoder(json.JSONEncoder):
-        """
-        A test encoder that is used to prove that the encoder does its job before the escaping.
-        """
-        # pylint: disable=method-hidden
-        def default(self, noDefaultEncodingObj):
-            return noDefaultEncodingObj.value.replace("<script>", "sample-encoder-was-here")
-
-    def test_escape_json_dumps_escapes_unsafe_html(self):
-        """
-        Test escape_json_dumps properly escapes &, <, and >.
-        """
-        malicious_json = {"</script><script>alert('hello, ');</script>": "</script><script>alert('&world!');</script>"}
-        expected_encoded_json = (
-            r'''{"\u003c/script\u003e\u003cscript\u003ealert('hello, ');\u003c/script\u003e": '''
-            r'''"\u003c/script\u003e\u003cscript\u003ealert('\u0026world!');\u003c/script\u003e"}'''
-        )
-
-        encoded_json = escape_json_dumps(malicious_json)
-        self.assertEquals(expected_encoded_json, encoded_json)
-
-    def test_escape_json_dumps_with_custom_encoder_escapes_unsafe_html(self):
-        """
-        Test escape_json_dumps first encodes with custom JSNOEncoder before escaping &, <, and >
-
-        The test encoder class should first perform the replacement of "<script>" with
-        "sample-encoder-was-here", and then should escape the remaining &, <, and >.
-
-        """
-        malicious_json = {
-            "</script><script>alert('hello, ');</script>":
-            self.NoDefaultEncoding("</script><script>alert('&world!');</script>")
-        }
-        expected_custom_encoded_json = (
-            r'''{"\u003c/script\u003e\u003cscript\u003ealert('hello, ');\u003c/script\u003e": '''
-            r'''"\u003c/script\u003esample-encoder-was-herealert('\u0026world!');\u003c/script\u003e"}'''
-        )
-
-        encoded_json = escape_json_dumps(malicious_json, cls=self.SampleJSONEncoder)
-        self.assertEquals(expected_custom_encoded_json, encoded_json)
-
-    def test_escape_js_string_escapes_unsafe_html(self):
-        """
-        Test escape_js_string escapes &, <, and >, as well as returns a unicode type
-        """
-        malicious_js_string = "</script><script>alert('hello, ');</script>"
-
-        expected_escaped_js_string = unicode(
-            r"\u003C/script\u003E\u003Cscript\u003Ealert(\u0027hello, \u0027)\u003B\u003C/script\u003E"
-        )
-        escaped_js_string = escape_js_string(malicious_js_string)
-        self.assertEquals(expected_escaped_js_string, escaped_js_string)
diff --git a/pavelib/utils/test/suites/nose_suite.py b/pavelib/utils/test/suites/nose_suite.py
index 16699d13ef8..e7f93defd81 100644
--- a/pavelib/utils/test/suites/nose_suite.py
+++ b/pavelib/utils/test/suites/nose_suite.py
@@ -158,6 +158,7 @@ class SystemTestSuite(NoseTestSuite):
 
         if self.root == 'lms':
             default_test_id += " {system}/tests.py"
+            default_test_id += " openedx/core/djangolib"
 
         if self.root == 'cms':
             default_test_id += " {system}/tests/*"
-- 
GitLab