feat: Add course-wide custom scripts
Imlements OEP-15 by adding two fields to the course settings:
- Course-wide Custom JS
- Course-wide Custom CSS
The resources defined in these fields will be rendered in all course pages.
Rebase b6cb629849..0578e1c4c6 onto b6cb629849:
- Add course-wide resources to API for MFE use
- Revert "Add course-wide resources to API for MFE use" reverts commit 53648dcf0afe3cd171c9dc2eb5e56b871b2bcfb2
Signed-off-by: Gabor Boros <gabor.brs@gmail.com>
This commit is contained in:
@@ -998,6 +998,19 @@ class CourseFields: # lint-amnesty, pylint: disable=missing-class-docstring
|
||||
),
|
||||
scope=Scope.settings, default=False
|
||||
)
|
||||
|
||||
course_wide_js = List(
|
||||
display_name=_("Course-wide Custom JS"),
|
||||
help=_('Enter Javascript resource URLs you want to be loaded globally throughout the course pages.'),
|
||||
scope=Scope.settings,
|
||||
)
|
||||
|
||||
course_wide_css = List(
|
||||
display_name=_("Course-wide Custom CSS"),
|
||||
help=_('Enter CSS resource URLs you want to be loaded globally throughout the course pages.'),
|
||||
scope=Scope.settings,
|
||||
)
|
||||
|
||||
other_course_settings = Dict(
|
||||
display_name=_("Other Course Settings"),
|
||||
help=_(
|
||||
|
||||
@@ -6,6 +6,7 @@ Tests courseware views.py
|
||||
import html
|
||||
import itertools
|
||||
import json
|
||||
import re
|
||||
from datetime import datetime, timedelta
|
||||
from uuid import uuid4
|
||||
|
||||
@@ -65,6 +66,7 @@ from lms.djangoapps.courseware.toggles import (
|
||||
from lms.djangoapps.courseware.user_state_client import DjangoXBlockUserStateClient
|
||||
from lms.djangoapps.grades.config.waffle import ASSUME_ZERO_GRADE_IF_ABSENT
|
||||
from lms.djangoapps.grades.config.waffle import waffle_switch as grades_waffle_switch
|
||||
from lms.djangoapps.instructor.access import allow_access
|
||||
from lms.djangoapps.verify_student.models import VerificationDeadline
|
||||
from lms.djangoapps.verify_student.services import IDVerificationService
|
||||
from openedx.core.djangoapps.catalog.tests.factories import CourseFactory as CatalogCourseFactory
|
||||
@@ -3743,3 +3745,64 @@ class ContentOptimizationTestCase(ModuleStoreTestCase):
|
||||
response = self.client.get(url)
|
||||
assert response.status_code == 200
|
||||
assert b"MathJax.Hub.Config" in response.content
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
class TestCourseWideResources(ModuleStoreTestCase):
|
||||
"""
|
||||
Tests that custom course-wide resources are rendered in course pages
|
||||
"""
|
||||
|
||||
@ddt.data(
|
||||
('courseware', 'course_id', False, True),
|
||||
('dates', 'course_id', False, False),
|
||||
('progress', 'course_id', False, False),
|
||||
('instructor_dashboard', 'course_id', True, False),
|
||||
('forum_form_discussion', 'course_id', False, False),
|
||||
('render_xblock', 'usage_key_string', False, True),
|
||||
)
|
||||
@ddt.unpack
|
||||
def test_course_wide_resources(self, url_name, param, is_instructor, is_rendered):
|
||||
"""
|
||||
Tests that the <script> and <link> tags are created for course-wide custom resources.
|
||||
Also, test that the order which the resources are added match the given order.
|
||||
"""
|
||||
user = UserFactory()
|
||||
|
||||
js = ['/test.js', 'https://testcdn.com/js/lib.min.js', '//testcdn.com/js/lib2.js']
|
||||
css = ['https://testcdn.com/css/lib.min.css', '//testcdn.com/css/lib2.css', '/test.css']
|
||||
|
||||
course = CourseFactory.create(course_wide_js=js, course_wide_css=css)
|
||||
chapter = ItemFactory.create(parent=course, category='chapter')
|
||||
sequence = ItemFactory.create(parent=chapter, category='sequential', display_name='Sequence')
|
||||
|
||||
CourseOverview.load_from_module_store(course.id)
|
||||
CourseEnrollmentFactory(user=user, course_id=course.id)
|
||||
if is_instructor:
|
||||
allow_access(course, user, 'instructor')
|
||||
assert self.client.login(username=user.username, password='test')
|
||||
|
||||
kwargs = None
|
||||
if param == 'course_id':
|
||||
kwargs = {'course_id': str(course.id)}
|
||||
elif param == 'usage_key_string':
|
||||
kwargs = {'usage_key_string': str(sequence.location)}
|
||||
response = self.client.get(reverse(url_name, kwargs=kwargs))
|
||||
|
||||
content = response.content.decode('utf-8')
|
||||
js_match = [re.search(f'<script .*src=[\'"]{j}[\'"].*>', content) for j in js]
|
||||
css_match = [re.search(f'<link .*href=[\'"]{c}[\'"].*>', content) for c in css]
|
||||
|
||||
# custom resources are included
|
||||
if is_rendered:
|
||||
assert None not in js_match
|
||||
assert None not in css_match
|
||||
|
||||
# custom resources are added in order
|
||||
for i in range(len(js) - 1):
|
||||
assert js_match[i].start() < js_match[i + 1].start()
|
||||
for i in range(len(css) - 1):
|
||||
assert css_match[i].start() < css_match[i + 1].start()
|
||||
else:
|
||||
assert js_match == [None, None, None]
|
||||
assert css_match == [None, None, None]
|
||||
|
||||
@@ -444,7 +444,8 @@ class CoursewareIndex(View):
|
||||
'section_title': None,
|
||||
'sequence_title': None,
|
||||
'disable_accordion': not DISABLE_COURSE_OUTLINE_PAGE_FLAG.is_enabled(self.course.id),
|
||||
'show_search': show_search
|
||||
'show_search': show_search,
|
||||
'render_course_wide_assets': True,
|
||||
}
|
||||
courseware_context.update(
|
||||
get_experiment_user_metadata_context(
|
||||
|
||||
@@ -1806,6 +1806,7 @@ def render_xblock(request, usage_key_string, check_if_enrolled=True):
|
||||
'is_learning_mfe': is_learning_mfe,
|
||||
'is_mobile_app': is_request_from_mobile_app(request),
|
||||
'reset_deadlines_url': reverse(RESET_COURSE_DEADLINES_NAME),
|
||||
'render_course_wide_assets': True,
|
||||
|
||||
**optimization_flags,
|
||||
}
|
||||
|
||||
@@ -171,6 +171,11 @@ from common.djangoapps.pipeline_mako import render_require_js_path_overrides
|
||||
</script>
|
||||
% endif
|
||||
|
||||
% if course and course is not UNDEFINED and render_course_wide_assets:
|
||||
% for css in course.course_wide_css:
|
||||
<link rel="stylesheet" href="${css}" type="text/css">
|
||||
% endfor
|
||||
% endif
|
||||
</head>
|
||||
|
||||
<body class="${static.dir_rtl()} <%block name='bodyclass'/> lang_${LANGUAGE_CODE}">
|
||||
@@ -215,6 +220,12 @@ from common.djangoapps.pipeline_mako import render_require_js_path_overrides
|
||||
<%static:optional_include_mako file="body-extra.html" is_theming_enabled="True" />
|
||||
<script type="text/javascript" src="${static.url('js/src/jquery_extend_patch.js')}"></script>
|
||||
<div id="lean_overlay"></div>
|
||||
|
||||
% if course and course is not UNDEFINED and render_course_wide_assets:
|
||||
% for js in course.course_wide_js:
|
||||
<script type="text/javascript" src="${js}"></script>
|
||||
% endfor
|
||||
% endif
|
||||
</body>
|
||||
</html>
|
||||
|
||||
|
||||
@@ -20,6 +20,12 @@
|
||||
{% block headextra %}{% endblock %}
|
||||
{% render_block "css" %}
|
||||
|
||||
{% if request.course and request.render_course_wide_assets %}
|
||||
{% for css in request.course.course_wide_css %}
|
||||
<link rel="stylesheet" href="{{css}}" type="text/css">
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
{% optional_include "head-extra.html"|microsite_template_path %}
|
||||
|
||||
<meta name="path_prefix" content="{{EDX_ROOT_URL}}">
|
||||
@@ -46,6 +52,12 @@
|
||||
{% javascript 'base_application' %}
|
||||
|
||||
{% render_block "js" %}
|
||||
|
||||
{% if request.course and request.render_course_wide_assets %}
|
||||
{% for js in request.course.course_wide_js %}
|
||||
<script type="text/javascript" src="{{js}}"></script>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</body>
|
||||
</html>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user