Skip to content

Commit f491f5b

Browse files
fix: refactor MakoService to allow specifying template more explicitly (Take 2) (openedx#33077)
* fix: refactor MakoService to allow specifying namespace per template (openedx#33061) * fix: instr. dashboard broken by bulk email reusing HtmlBlock studio_view * fix: lint issue from unused import
1 parent d83d769 commit f491f5b

26 files changed

+134
-83
lines changed

cms/djangoapps/contentstore/views/tests/test_preview.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -186,7 +186,7 @@ def student_view(self, context):
186186
Renders the output that a student will see.
187187
"""
188188
fragment = Fragment()
189-
fragment.add_content(self.runtime.service(self, 'mako').render_template('edxmako.html', context))
189+
fragment.add_content(self.runtime.service(self, 'mako').render_lms_template('edxmako.html', context))
190190
return fragment
191191

192192

cms/lib/xblock/authoring_mixin.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ def visibility_view(self, _context=None):
3737
"""
3838
fragment = Fragment()
3939
from cms.djangoapps.contentstore.utils import reverse_course_url
40-
fragment.add_content(self.runtime.service(self, 'mako').render_template('visibility_editor.html', {
40+
fragment.add_content(self.runtime.service(self, 'mako').render_cms_template('visibility_editor.html', {
4141
'xblock': self,
4242
'manage_groups_url': reverse_course_url('group_configurations_list_handler', self.location.course_key),
4343
}))

common/djangoapps/edxmako/services.py

+46-2
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,23 @@
11
"""
22
Supports rendering an XBlock to HTML using mako templates.
33
"""
4-
4+
from django.template import engines
5+
from django.template.utils import InvalidTemplateEngineError
56
from xblock.reference.plugins import Service
67

78
from common.djangoapps.edxmako.shortcuts import render_to_string
9+
from common.djangoapps.edxmako import Engines
10+
11+
try:
12+
engines[Engines.PREVIEW]
13+
except InvalidTemplateEngineError:
14+
# We are running in the CMS:
15+
lms_mako_namespace = "main"
16+
cms_mako_namespace = None
17+
else:
18+
# We are running in the LMS:
19+
lms_mako_namespace = "lms.main"
20+
cms_mako_namespace = "main"
821

922

1023
class MakoService(Service):
@@ -21,10 +34,41 @@ def __init__(
2134
**kwargs
2235
):
2336
super().__init__(**kwargs)
37+
# Set the "default" namespace prefix, in case it's not specified when render_template() is called.
2438
self.namespace_prefix = namespace_prefix
2539

2640
def render_template(self, template_file, dictionary, namespace='main'):
2741
"""
28-
Takes (template_file, dictionary) and returns rendered HTML.
42+
DEPRECATED. Takes (template_file, dictionary) and returns rendered HTML.
43+
44+
Use render_lms_template or render_cms_template instead. Or better yet,
45+
don't use mako templates at all. React or django templates are much
46+
safer.
2947
"""
3048
return render_to_string(template_file, dictionary, namespace=self.namespace_prefix + namespace)
49+
50+
def render_lms_template(self, template_file, dictionary):
51+
"""
52+
Render a template which is found in one of the LMS edx-platform template
53+
dirs. (lms.envs.common.MAKO_TEMPLATE_DIRS_BASE)
54+
55+
Templates which are in these dirs will only work with this function:
56+
edx-platform/lms/templates/
57+
edx-platform/xmodule/capa/templates/
58+
openedx/features/course_experience/templates
59+
"""
60+
return render_to_string(template_file, dictionary, namespace=lms_mako_namespace)
61+
62+
def render_cms_template(self, template_file, dictionary):
63+
"""
64+
Render a template which is found in one of the CMS edx-platform template
65+
dirs. (cms.envs.common.MAKO_TEMPLATE_DIRS_BASE)
66+
67+
Templates which are in these dirs will only work with this function:
68+
edx-platform/cms/templates/
69+
common/static/
70+
openedx/features/course_experience/templates
71+
"""
72+
if cms_mako_namespace is None:
73+
raise RuntimeError("Cannot access CMS templates from the LMS")
74+
return render_to_string(template_file, dictionary, namespace=cms_mako_namespace)

lms/djangoapps/courseware/tests/test_video_mongo.py

+11-11
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,7 @@ def test_video_constructor(self):
148148

149149
mako_service = self.block.runtime.service(self.block, 'mako')
150150
assert get_context_dict_from_string(context) ==\
151-
get_context_dict_from_string(mako_service.render_template('video.html', expected_context))
151+
get_context_dict_from_string(mako_service.render_lms_template('video.html', expected_context))
152152

153153

154154
class TestVideoNonYouTube(TestVideo): # pylint: disable=test-inherits-tests
@@ -233,7 +233,7 @@ def test_video_constructor(self):
233233

234234
mako_service = self.block.runtime.service(self.block, 'mako')
235235
expected_result = get_context_dict_from_string(
236-
mako_service.render_template('video.html', expected_context)
236+
mako_service.render_lms_template('video.html', expected_context)
237237
)
238238
assert get_context_dict_from_string(context) == expected_result
239239
assert expected_result['download_video_link'] == 'example.mp4'
@@ -506,7 +506,7 @@ def test_get_html_track(self):
506506

507507
mako_service = self.block.runtime.service(self.block, 'mako')
508508
assert get_context_dict_from_string(context) ==\
509-
get_context_dict_from_string(mako_service.render_template('video.html', expected_context))
509+
get_context_dict_from_string(mako_service.render_lms_template('video.html', expected_context))
510510

511511
def test_get_html_source(self):
512512
# lint-amnesty, pylint: disable=invalid-name, redefined-outer-name
@@ -620,7 +620,7 @@ def test_get_html_source(self):
620620

621621
mako_service = self.block.runtime.service(self.block, 'mako')
622622
assert get_context_dict_from_string(context) ==\
623-
get_context_dict_from_string(mako_service.render_template('video.html', expected_context))
623+
get_context_dict_from_string(mako_service.render_lms_template('video.html', expected_context))
624624

625625
def test_get_html_with_non_existent_edx_video_id(self):
626626
"""
@@ -766,7 +766,7 @@ def test_get_html_with_mocked_edx_video_id(self):
766766

767767
mako_service = self.block.runtime.service(self.block, 'mako')
768768
assert get_context_dict_from_string(context) ==\
769-
get_context_dict_from_string(mako_service.render_template('video.html', expected_context))
769+
get_context_dict_from_string(mako_service.render_lms_template('video.html', expected_context))
770770

771771
def test_get_html_with_existing_edx_video_id(self):
772772
"""
@@ -794,7 +794,7 @@ def test_get_html_with_existing_edx_video_id(self):
794794
context, expected_context = self.helper_get_html_with_edx_video_id(data)
795795
mako_service = self.block.runtime.service(self.block, 'mako')
796796
assert get_context_dict_from_string(context) ==\
797-
get_context_dict_from_string(mako_service.render_template('video.html', expected_context))
797+
get_context_dict_from_string(mako_service.render_lms_template('video.html', expected_context))
798798

799799
def test_get_html_with_existing_unstripped_edx_video_id(self):
800800
"""
@@ -825,7 +825,7 @@ def test_get_html_with_existing_unstripped_edx_video_id(self):
825825

826826
mako_service = self.block.runtime.service(self.block, 'mako')
827827
assert get_context_dict_from_string(context) ==\
828-
get_context_dict_from_string(mako_service.render_template('video.html', expected_context))
828+
get_context_dict_from_string(mako_service.render_lms_template('video.html', expected_context))
829829

830830
def encode_and_create_video(self, edx_video_id):
831831
"""
@@ -1050,7 +1050,7 @@ def side_effect(*args, **kwargs): # lint-amnesty, pylint: disable=unused-argume
10501050

10511051
mako_service = self.block.runtime.service(self.block, 'mako')
10521052
assert get_context_dict_from_string(context) ==\
1053-
get_context_dict_from_string(mako_service.render_template('video.html', expected_context))
1053+
get_context_dict_from_string(mako_service.render_lms_template('video.html', expected_context))
10541054

10551055
# pylint: disable=invalid-name
10561056
def test_get_html_cdn_source_external_video(self):
@@ -1156,7 +1156,7 @@ def test_get_html_cdn_source_external_video(self):
11561156

11571157
mako_service = self.block.runtime.service(self.block, 'mako')
11581158
assert get_context_dict_from_string(context) ==\
1159-
get_context_dict_from_string(mako_service.render_template('video.html', expected_context))
1159+
get_context_dict_from_string(mako_service.render_lms_template('video.html', expected_context))
11601160

11611161
@ddt.data(
11621162
(True, ['youtube', 'desktop_webm', 'desktop_mp4', 'hls']),
@@ -2409,7 +2409,7 @@ def test_bumper_metadata(self, get_url_for_profiles, get_bumper_settings, is_bum
24092409
}
24102410

24112411
mako_service = self.block.runtime.service(self.block, 'mako')
2412-
expected_content = mako_service.render_template('video.html', expected_context)
2412+
expected_content = mako_service.render_lms_template('video.html', expected_context)
24132413
assert get_context_dict_from_string(content) == get_context_dict_from_string(expected_content)
24142414

24152415

@@ -2506,7 +2506,7 @@ def assert_content_matches_expectations(self, autoadvanceenabled_must_be, autoad
25062506

25072507
mako_service = self.block.runtime.service(self.block, 'mako')
25082508
with override_settings(FEATURES=self.FEATURES):
2509-
expected_content = mako_service.render_template('video.html', expected_context)
2509+
expected_content = mako_service.render_lms_template('video.html', expected_context)
25102510

25112511
assert get_context_dict_from_string(content) == get_context_dict_from_string(expected_content)
25122512

lms/djangoapps/instructor/views/instructor_dashboard.py

+21-28
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,11 @@
11
"""
22
Instructor Dashboard Views
33
"""
4-
5-
64
import datetime
75
import logging
8-
import uuid
96
from functools import reduce
10-
from unittest.mock import patch
117

8+
import markupsafe
129
import pytz
1310
from django.conf import settings
1411
from django.contrib.auth.decorators import login_required
@@ -25,11 +22,9 @@
2522
from edx_django_utils.plugins import get_plugins_view_context
2623
from opaque_keys import InvalidKeyError
2724
from opaque_keys.edx.keys import CourseKey
28-
from xblock.field_data import DictFieldData
29-
from xblock.fields import ScopeIds
3025

3126
from common.djangoapps.course_modes.models import CourseMode, CourseModesArchive
32-
from common.djangoapps.edxmako.shortcuts import render_to_response
27+
from common.djangoapps.edxmako.shortcuts import render_to_response, render_to_string
3328
from common.djangoapps.student.models import CourseEnrollment
3429
from common.djangoapps.student.roles import (
3530
CourseFinanceAdminRole,
@@ -62,9 +57,6 @@
6257
from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers
6358
from openedx.core.djangolib.markup import HTML, Text
6459
from openedx.core.lib.courses import get_course_by_id
65-
from openedx.core.lib.url_utils import quote_slashes
66-
from openedx.core.lib.xblock_utils import wrap_xblock
67-
from xmodule.html_block import HtmlBlock # lint-amnesty, pylint: disable=wrong-import-order
6860
from xmodule.modulestore.django import modulestore # lint-amnesty, pylint: disable=wrong-import-order
6961
from xmodule.tabs import CourseTab # lint-amnesty, pylint: disable=wrong-import-order
7062

@@ -662,28 +654,29 @@ def _section_send_email(course, access):
662654
""" Provide data for the corresponding bulk email section """
663655
course_key = course.id
664656

665-
# Monkey-patch applicable_aside_types to return no asides for the duration of this render
666-
with patch.object(course.runtime, 'applicable_aside_types', null_applicable_aside_types):
667-
# This HtmlBlock is only being used to generate a nice text editor.
668-
html_block = HtmlBlock(
669-
course.runtime,
670-
DictFieldData({'data': ''}),
671-
ScopeIds(None, None, None, course_key.make_usage_key('html', 'fake'))
672-
)
673-
fragment = course.runtime.render(html_block, 'studio_view')
674-
fragment = wrap_xblock(
675-
'LmsRuntime', html_block, 'studio_view', fragment, None,
676-
extra_data={"course-id": str(course_key)},
677-
usage_id_serializer=lambda usage_id: quote_slashes(str(usage_id)),
678-
# Generate a new request_token here at random, because this module isn't connected to any other
679-
# xblock rendering.
680-
request_token=uuid.uuid1().hex
681-
)
657+
# Render an HTML editor, using the same template as the HTML XBlock's visual
658+
# editor. This basically pretends to be an HTML XBlock so that the XBlock
659+
# initialization JS will manage the editor for us. This is a hack, and
660+
# should be replace by a proper HTML Editor React component.
661+
fake_block_data = {
662+
"init": "XBlockToXModuleShim",
663+
"usage-id": course_key.make_usage_key('html', 'fake'),
664+
"runtime-version": "1",
665+
"runtime-class": "LmsRuntime",
666+
}
667+
email_editor = render_to_string("xblock_wrapper.html", {
668+
# This minimal version of the wrapper context is extracted from wrap_xblock():
669+
"classes": ["xblock", "xblock-studio_view", "xmodule_edit", "xmodule_HtmlBlock"],
670+
"data_attributes": ' '.join(f'data-{markupsafe.escape(key)}="{markupsafe.escape(value)}"'
671+
for key, value in fake_block_data.items()),
672+
"js_init_parameters": {"xmodule-type": "HTMLEditingDescriptor"},
673+
"content": render_to_string("widgets/html-edit.html", {"editor": "visual", "data": ""}),
674+
})
675+
682676
cohorts = []
683677
if is_course_cohorted(course_key):
684678
cohorts = get_course_cohorts(course)
685679
course_modes = CourseMode.modes_for_course(course_key, include_expired=True, only_selectable=False)
686-
email_editor = fragment.content
687680
section_data = {
688681
'section_key': 'send_email',
689682
'section_display_name': _('Email'),

xmodule/annotatable_block.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,7 @@ def get_html(self):
172172
'content_html': self._render_content()
173173
}
174174

175-
return self.runtime.service(self, 'mako').render_template('annotatable.html', context)
175+
return self.runtime.service(self, 'mako').render_lms_template('annotatable.html', context)
176176

177177
def student_view(self, context): # lint-amnesty, pylint: disable=unused-argument
178178
"""
@@ -191,7 +191,7 @@ def studio_view(self, _context):
191191
Return the studio view.
192192
"""
193193
fragment = Fragment(
194-
self.runtime.service(self, 'mako').render_template(self.mako_template, self.get_context())
194+
self.runtime.service(self, 'mako').render_cms_template(self.mako_template, self.get_context())
195195
)
196196
add_sass_to_fragment(fragment, 'AnnotatableBlockEditor.scss')
197197
add_webpack_js_to_fragment(fragment, 'AnnotatableBlockEditor')

xmodule/capa_block.py

+4-4
Original file line numberDiff line numberDiff line change
@@ -356,7 +356,7 @@ def studio_view(self, _context):
356356
Return the studio view.
357357
"""
358358
fragment = Fragment(
359-
self.runtime.service(self, 'mako').render_template(self.mako_template, self.get_context())
359+
self.runtime.service(self, 'mako').render_cms_template(self.mako_template, self.get_context())
360360
)
361361
add_sass_to_fragment(fragment, 'ProblemBlockEditor.scss')
362362
add_webpack_js_to_fragment(fragment, 'ProblemBlockEditor')
@@ -898,7 +898,7 @@ def get_html(self):
898898
"""
899899
curr_score, total_possible = self.get_display_progress()
900900

901-
return self.runtime.service(self, 'mako').render_template('problem_ajax.html', {
901+
return self.runtime.service(self, 'mako').render_lms_template('problem_ajax.html', {
902902
'element_id': self.location.html_id(),
903903
'id': str(self.location),
904904
'ajax_url': self.ajax_url,
@@ -1247,7 +1247,7 @@ def get_problem_html(self, encapsulate=True, submit_notification=False):
12471247
'submit_disabled_cta': submit_disabled_ctas[0] if submit_disabled_ctas else None,
12481248
}
12491249

1250-
html = self.runtime.service(self, 'mako').render_template('problem.html', context)
1250+
html = self.runtime.service(self, 'mako').render_lms_template('problem.html', context)
12511251

12521252
if encapsulate:
12531253
html = HTML('<div id="problem_{id}" class="problem" data-url="{ajax_url}">{html}</div>').format(
@@ -1548,7 +1548,7 @@ def get_answer(self, _data):
15481548

15491549
return {
15501550
'answers': new_answers,
1551-
'correct_status_html': self.runtime.service(self, 'mako').render_template(
1551+
'correct_status_html': self.runtime.service(self, 'mako').render_lms_template(
15521552
'status_span.html',
15531553
{'status': Status('correct', self.runtime.service(self, "i18n").gettext)}
15541554
)

xmodule/conditional_block.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -223,7 +223,7 @@ def student_view(self, _context):
223223

224224
def get_html(self):
225225
required_html_ids = [block.location.html_id() for block in self.get_required_blocks]
226-
return self.runtime.service(self, 'mako').render_template('conditional_ajax.html', {
226+
return self.runtime.service(self, 'mako').render_lms_template('conditional_ajax.html', {
227227
'element_id': self.location.html_id(),
228228
'ajax_url': self.ajax_url,
229229
'depends': ';'.join(required_html_ids)
@@ -249,7 +249,7 @@ def studio_view(self, _context):
249249
Return the studio view.
250250
"""
251251
fragment = Fragment(
252-
self.runtime.service(self, 'mako').render_template(self.mako_template, self.get_context())
252+
self.runtime.service(self, 'mako').render_cms_template(self.mako_template, self.get_context())
253253
)
254254
add_webpack_js_to_fragment(fragment, 'ConditionalBlockEditor')
255255
shim_xmodule_js(fragment, self.studio_js_module_name)
@@ -262,7 +262,7 @@ def handle_ajax(self, _dispatch, _data):
262262
if not self.is_condition_satisfied():
263263
context = {'module': self,
264264
'message': self.conditional_message}
265-
html = self.runtime.service(self, 'mako').render_template('conditional_block.html', context)
265+
html = self.runtime.service(self, 'mako').render_lms_template('conditional_block.html', context)
266266
return json.dumps({'fragments': [{'content': html}], 'message': bool(self.conditional_message)})
267267

268268
fragments = [child.render(STUDENT_VIEW).to_dict() for child in self.get_children()]

xmodule/discussion_block.py

+3-2
Original file line numberDiff line numberDiff line change
@@ -204,7 +204,7 @@ def student_view(self, context=None):
204204
'login_msg': login_msg,
205205
}
206206
fragment.add_content(
207-
self.runtime.service(self, 'mako').render_template('discussion/_discussion_inline.html', context)
207+
self.runtime.service(self, 'mako').render_lms_template('discussion/_discussion_inline.html', context)
208208
)
209209

210210
fragment.initialize_js('DiscussionInlineBlock')
@@ -216,7 +216,8 @@ def author_view(self, context=None): # pylint: disable=unused-argument
216216
Renders author view for Studio.
217217
"""
218218
fragment = Fragment()
219-
fragment.add_content(self.runtime.service(self, 'mako').render_template(
219+
# For historic reasons, this template is in the LMS templates folder:
220+
fragment.add_content(self.runtime.service(self, 'mako').render_lms_template(
220221
'discussion/_discussion_inline_studio.html',
221222
{
222223
'discussion_id': self.discussion_id,

xmodule/error_block.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ def student_view(self, _context):
6060
"""
6161
Return a fragment that contains the html for the student view.
6262
"""
63-
fragment = Fragment(self.runtime.service(self, 'mako').render_template('module-error.html', {
63+
fragment = Fragment(self.runtime.service(self, 'mako').render_lms_template('module-error.html', {
6464
'staff_access': True,
6565
'data': self.contents,
6666
'error': self.error_msg,

xmodule/html_block.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,7 @@ def studio_view(self, _context):
134134
Return the studio view.
135135
"""
136136
fragment = Fragment(
137-
self.runtime.service(self, 'mako').render_template(self.mako_template, self.get_context())
137+
self.runtime.service(self, 'mako').render_cms_template(self.mako_template, self.get_context())
138138
)
139139
add_sass_to_fragment(fragment, 'HtmlBlockEditor.scss')
140140
add_webpack_js_to_fragment(fragment, 'HtmlBlockEditor')
@@ -463,7 +463,7 @@ def get_html(self):
463463
'visible_updates': course_updates[:3],
464464
'hidden_updates': course_updates[3:],
465465
}
466-
return self.runtime.service(self, 'mako').render_template(
466+
return self.runtime.service(self, 'mako').render_lms_template(
467467
f"{self.TEMPLATE_DIR}/course_updates.html",
468468
context,
469469
)

0 commit comments

Comments
 (0)