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:
ha-D
2021-08-05 12:26:51 +00:00
committed by Braden MacDonald
parent 210f911395
commit d3bc4601ae
6 changed files with 102 additions and 1 deletions

View File

@@ -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=_(

View File

@@ -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]

View File

@@ -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(

View File

@@ -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,
}

View File

@@ -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>

View File

@@ -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>