markdown-xblock 2.4.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. markdown_xblock/__init__.py +2 -0
  2. markdown_xblock/html.py +335 -0
  3. markdown_xblock/public/plugins/codemirror/LICENSE +21 -0
  4. markdown_xblock/public/plugins/codemirror/codemirror-4.8/addon/dialog/dialog.css +32 -0
  5. markdown_xblock/public/plugins/codemirror/codemirror-4.8/addon/dialog/dialog.js +155 -0
  6. markdown_xblock/public/plugins/codemirror/codemirror-4.8/addon/display/fullscreen.css +6 -0
  7. markdown_xblock/public/plugins/codemirror/codemirror-4.8/addon/display/fullscreen.js +41 -0
  8. markdown_xblock/public/plugins/codemirror/codemirror-4.8/addon/edit/matchbrackets.js +120 -0
  9. markdown_xblock/public/plugins/codemirror/codemirror-4.8/addon/search/search.js +159 -0
  10. markdown_xblock/public/plugins/codemirror/codemirror-4.8/addon/search/searchcursor.js +189 -0
  11. markdown_xblock/public/plugins/codemirror/codemirror-4.8/addon/selection/active-line.js +71 -0
  12. markdown_xblock/public/plugins/codemirror/codemirror-4.8/lib/codemirror.css +319 -0
  13. markdown_xblock/public/plugins/codemirror/codemirror-4.8/lib/codemirror.js +7923 -0
  14. markdown_xblock/public/plugins/codemirror/codemirror-4.8/mode/css/css.js +717 -0
  15. markdown_xblock/public/plugins/codemirror/codemirror-4.8/mode/htmlmixed/htmlmixed.js +121 -0
  16. markdown_xblock/public/plugins/codemirror/codemirror-4.8/mode/javascript/javascript.js +684 -0
  17. markdown_xblock/public/plugins/codemirror/codemirror-4.8/mode/markdown/markdown.js +883 -0
  18. markdown_xblock/public/plugins/codemirror/codemirror-4.8/mode/xml/xml.js +384 -0
  19. markdown_xblock/public/plugins/codemirror/langs/en.js +8 -0
  20. markdown_xblock/public/plugins/codemirror/plugin.min.js +1 -0
  21. markdown_xblock/public/plugins/codemirror/source.html +270 -0
  22. markdown_xblock/public/plugins/codesample/css/prism.css +141 -0
  23. markdown_xblock/public/plugins/codesample/js/prism.js +15 -0
  24. markdown_xblock/public/plugins/codesample/plugin.min.js +1 -0
  25. markdown_xblock/public/plugins/image/plugin.min.js +1 -0
  26. markdown_xblock/public/plugins/link/plugin.min.js +1 -0
  27. markdown_xblock/public/plugins/lists/plugin.min.js +1 -0
  28. markdown_xblock/public/plugins/textcolor/plugin.min.js +1 -0
  29. markdown_xblock/public/skin/content.inline.min.css +1 -0
  30. markdown_xblock/public/skin/content.min.css +2 -0
  31. markdown_xblock/public/skin/fonts/icomoon-small.eot +0 -0
  32. markdown_xblock/public/skin/fonts/icomoon-small.svg +175 -0
  33. markdown_xblock/public/skin/fonts/icomoon-small.ttf +0 -0
  34. markdown_xblock/public/skin/fonts/icomoon-small.woff +0 -0
  35. markdown_xblock/public/skin/fonts/icomoon.eot +0 -0
  36. markdown_xblock/public/skin/fonts/icomoon.svg +153 -0
  37. markdown_xblock/public/skin/fonts/icomoon.ttf +0 -0
  38. markdown_xblock/public/skin/fonts/icomoon.woff +0 -0
  39. markdown_xblock/public/skin/fonts/readme.md +1 -0
  40. markdown_xblock/public/skin/img/anchor.gif +0 -0
  41. markdown_xblock/public/skin/img/loader.gif +0 -0
  42. markdown_xblock/public/skin/img/object.gif +0 -0
  43. markdown_xblock/public/skin/img/trans.gif +0 -0
  44. markdown_xblock/public/skin/img/wline.gif +0 -0
  45. markdown_xblock/public/skin/skin.ie7.min.css +1 -0
  46. markdown_xblock/public/skin/skin.json +51 -0
  47. markdown_xblock/public/skin/skin.min.css +1 -0
  48. markdown_xblock/static/css/html.css +15 -0
  49. markdown_xblock/static/css/pygments.css +61 -0
  50. markdown_xblock/static/html/lms.html +1 -0
  51. markdown_xblock/static/html/studio.html +29 -0
  52. markdown_xblock/static/js/html.js +233 -0
  53. markdown_xblock/static/js/tinymce/themes/modern/theme.min.js +1 -0
  54. markdown_xblock/static/js/tinymce/tinymce.js +26801 -0
  55. markdown_xblock/static/js/tinymce/tinymce.min.js +2 -0
  56. markdown_xblock/translations/README.txt +4 -0
  57. markdown_xblock/utils.py +11 -0
  58. markdown_xblock-2.4.0.dist-info/METADATA +158 -0
  59. markdown_xblock-2.4.0.dist-info/RECORD +83 -0
  60. markdown_xblock-2.4.0.dist-info/WHEEL +5 -0
  61. markdown_xblock-2.4.0.dist-info/entry_points.txt +2 -0
  62. markdown_xblock-2.4.0.dist-info/licenses/LICENSE +671 -0
  63. markdown_xblock-2.4.0.dist-info/top_level.txt +5 -0
  64. releasenotes/notes/add-default-css-for-tables-3a53e02e8a350253.yaml +5 -0
  65. releasenotes/notes/add_public_view-d24763b7941dc2b0.yaml +4 -0
  66. releasenotes/notes/custom-css-classes-7984a9c95147d39c.yaml +5 -0
  67. releasenotes/notes/export-md-to-file-ab1528b4c28cb2f0.yaml +5 -0
  68. releasenotes/notes/fix-course-export-b35c78a22be0c6b5.yaml +6 -0
  69. releasenotes/notes/missing-codemirror-files-d64ccb15d0faf640.yaml +6 -0
  70. releasenotes/notes/pypi-release-3e4d5ceb7b6e252a.yaml +7 -0
  71. releasenotes/notes/python-3.12-support-dac839f8d01ac6b9.yaml +4 -0
  72. releasenotes/notes/refactor-ci-36abda8c15250d06.yaml +6 -0
  73. releasenotes/notes/refactor-export-import-49bbf0e8a364f139.yaml +8 -0
  74. releasenotes/notes/remove_py35-0b265861449b1753.yaml +9 -0
  75. releasenotes/notes/safe_mode_replace_escape-40fdb78ec8aa5eff.yaml +7 -0
  76. releasenotes/notes/show_in_read_only_mode-3af39459d5d56b33.yaml +7 -0
  77. requirements/base.txt +9 -0
  78. requirements/dev.txt +2 -0
  79. requirements/reno.txt +1 -0
  80. requirements/test.txt +9 -0
  81. tests/__init__.py +1 -0
  82. tests/test_basics.py +280 -0
  83. var/.placeholder +2 -0
@@ -0,0 +1,5 @@
1
+ ---
2
+ other:
3
+ - |
4
+ Add default CSS for tables to match the current raw-HTML
5
+ XBlock table styling for better user experience.
@@ -0,0 +1,4 @@
1
+ ---
2
+ features:
3
+ - |
4
+ The XBlock can now also be used in a public course.
@@ -0,0 +1,5 @@
1
+ ---
2
+ features:
3
+ - |
4
+ The XBlock now has a new editable field 'classes' that allows course authors
5
+ to add custom CSS classes to the component.
@@ -0,0 +1,5 @@
1
+ ---
2
+ other:
3
+ - |
4
+ Markdown content is now written to a separate .md file during
5
+ course export and read from that file during course import.
@@ -0,0 +1,6 @@
1
+ ---
2
+ other:
3
+ - |
4
+ Markdown content is now exported between opening and closing
5
+ <markdown></markdown> tags instead of the markdown element's "data"
6
+ attribute.
@@ -0,0 +1,6 @@
1
+ ---
2
+ fixes:
3
+ - |
4
+ Course authors using Open edX Studio would not be able to use
5
+ syntax highlighting, due to missing JavaScript and CSS files for
6
+ CodeMirror. These files are now included.
@@ -0,0 +1,7 @@
1
+ ---
2
+ prelude: >
3
+ This is the first PyPI release of this XBlock.
4
+ other:
5
+ - |
6
+ This project now uses reno (https://pypi.org/project/reno/) to
7
+ manage its changelog and release notes.
@@ -0,0 +1,4 @@
1
+ ---
2
+ features:
3
+ - |
4
+ Add support for Python 3.12
@@ -0,0 +1,6 @@
1
+ ---
2
+ other:
3
+ - |
4
+ The unit testing / linting / CI setup now uses tox and flake8, and
5
+ includes a .gitlab-ci.yml configuration for those who mirror the
6
+ repository to a GitLab instance.
@@ -0,0 +1,8 @@
1
+ ---
2
+
3
+ other:
4
+ - |
5
+ Each markdown file written during the course export
6
+ is no longer accompanied by an xml file with XBlock attributes.
7
+ The XBlock attributes are now included in the markdown node,
8
+ defined in a vertical block.
@@ -0,0 +1,9 @@
1
+ ---
2
+ upgrade:
3
+ - |
4
+ This release removes support for Python 3.5. Do not upgrade to
5
+ this release on platforms still using Python 3.5.
6
+ deprecations:
7
+ - |
8
+ With this release, all automated testing on Python 3.5 has been
9
+ removed, and Python 3.8 is the recommended Python release.
@@ -0,0 +1,7 @@
1
+ ---
2
+ features:
3
+ - |
4
+ The XBlock settings now support the string values "replace" and "escape"
5
+ for safe_mode, just like the upstream Markdown2 library does. A Boolean
6
+ value of True is considered to be equivalent to "replace". A value of
7
+ False, None, or the empty string disables safe mode.
@@ -0,0 +1,7 @@
1
+ ---
2
+ fixes:
3
+ - |
4
+ The XBlock now renders normally in "read-only" ("masquerading")
5
+ mode, which applies when a course instructor (a user with either
6
+ per-course or platform-wide Staff permissions) selects the "View
7
+ this course as" → "Specific learner" option in the Open edX LMS.
requirements/base.txt ADDED
@@ -0,0 +1,9 @@
1
+ # Requirements for app run
2
+
3
+ xblock-sdk
4
+ django-statici18n<=2.7.1
5
+ edx-i18n-tools<=2.0
6
+ Mako<=1.3.12
7
+ bleach<=6.3.0
8
+ django<=5.2
9
+ markdown2>=2.5.4
requirements/dev.txt ADDED
@@ -0,0 +1,2 @@
1
+ # Requirements for development
2
+ reno
requirements/reno.txt ADDED
@@ -0,0 +1 @@
1
+ reno
requirements/test.txt ADDED
@@ -0,0 +1,9 @@
1
+ # Requirements for test runs
2
+
3
+ lazy<=1.6
4
+ pytest-cov<=7.0.0
5
+ mock<6
6
+
7
+ # Github requirements
8
+ openedx-django-pyfs>=3.8.0
9
+ XBlock>=5.2,<7
tests/__init__.py ADDED
@@ -0,0 +1 @@
1
+ """Tests module."""
tests/test_basics.py ADDED
@@ -0,0 +1,280 @@
1
+ """
2
+ Markdown XBlock tests
3
+ """
4
+ from __future__ import print_function
5
+
6
+ import unittest
7
+
8
+ from mock import Mock, patch
9
+ from workbench.runtime import WorkbenchRuntime
10
+ from xblock.field_data import DictFieldData
11
+ from xblock.fields import ScopeIds
12
+
13
+ from markdown2 import MarkdownError
14
+
15
+ import markdown_xblock
16
+ from markdown_xblock.html import DEFAULT_EXTRAS
17
+
18
+
19
+ class TestMarkdownXBlock(unittest.TestCase):
20
+ """
21
+ Unit tests for `markdown_xblock`
22
+ """
23
+ def setUp(self):
24
+ block_type = 'markdown'
25
+ self.runtime = WorkbenchRuntime()
26
+ def_id = self.runtime.id_generator.create_definition(block_type)
27
+ usage_id = self.runtime.id_generator.create_usage(def_id)
28
+
29
+ self.scope_ids = ScopeIds('user', block_type, def_id, usage_id)
30
+
31
+ def test_render(self):
32
+ """
33
+ Test a basic rendering with default settings.
34
+ """
35
+ field_data = DictFieldData({'data': '# This is h1'})
36
+ block = markdown_xblock.MarkdownXBlock(self.runtime, field_data, scope_ids=self.scope_ids)
37
+ fragment = block.student_view()
38
+ self.assertIn('<div class="markdown_xblock"><h1>This is h1</h1>\n</div>\n', fragment.content)
39
+
40
+ def test_public_view(self):
41
+ """
42
+ Test public view rendering.
43
+ """
44
+ field_data = DictFieldData({'data': '# This is h1'})
45
+ block = markdown_xblock.MarkdownXBlock(self.runtime, field_data, scope_ids=self.scope_ids)
46
+ student_view_fragment = block.student_view()
47
+ self.assertIn('<div class="markdown_xblock"><h1>This is h1</h1>\n</div>\n',
48
+ student_view_fragment.content)
49
+ public_view_fragment = block.public_view()
50
+ self.assertIn('<div class="markdown_xblock"><h1>This is h1</h1>\n</div>\n',
51
+ public_view_fragment.content)
52
+
53
+ def test_render_default_settings(self):
54
+ """
55
+ Test a basic rendering with default settings.
56
+
57
+ Expects the content to be sanitized by injecting
58
+ [HTML_REMOVED] in place of the HTML tags, and the sentence
59
+ being wrapped in a <p> block (since the <h1> tags are no
60
+ longer interpreted as HTML).
61
+ """
62
+ field_data = DictFieldData({'data': '<h1>This is h1</h1>'})
63
+ block = markdown_xblock.MarkdownXBlock(self.runtime, field_data, scope_ids=self.scope_ids)
64
+ fragment = block.student_view()
65
+ self.assertIn(
66
+ '<div class="markdown_xblock"><p>[HTML_REMOVED]This is h1[HTML_REMOVED]</p>\n</div>\n',
67
+ fragment.content
68
+ )
69
+
70
+ def test_render_invalid_safe_mode(self):
71
+ """
72
+ Test a basic rendering with default settings.
73
+
74
+ Expects the rendering to fail since safe_mode is not set to
75
+ one of 'replace', 'escape', True or False.
76
+ """
77
+ field_data = DictFieldData({'data': '<h1>This is h1</h1>'})
78
+ block = markdown_xblock.MarkdownXBlock(self.runtime, field_data, scope_ids=self.scope_ids)
79
+ settings = {
80
+ "extras": DEFAULT_EXTRAS,
81
+ "safe_mode": 'this is an invalid safe mode'
82
+ }
83
+ with patch('markdown_xblock.html.get_xblock_settings') as get_settings_mock:
84
+ get_settings_mock.return_value = settings
85
+ with self.assertRaises(MarkdownError):
86
+ _ = block.student_view()
87
+
88
+ def test_render_replace_inline_html(self):
89
+ """
90
+ Test a basic rendering with safe_mode explicitly set to 'replace'.
91
+
92
+ Expects the content to be sanitized by injecting
93
+ [HTML_REMOVED] in place of the HTML tags, and the sentence
94
+ being wrapped in a <p> block (since the <h1> tags are no
95
+ longer interpreted as HTML).
96
+ """
97
+ field_data = DictFieldData({'data': '<h1>This is h1</h1>'})
98
+ block = markdown_xblock.MarkdownXBlock(self.runtime, field_data, scope_ids=self.scope_ids)
99
+ settings = {
100
+ "extras": DEFAULT_EXTRAS,
101
+ "safe_mode": 'replace'
102
+ }
103
+ with patch('markdown_xblock.html.get_xblock_settings') as get_settings_mock:
104
+ get_settings_mock.return_value = settings
105
+ fragment = block.student_view()
106
+ self.assertIn(
107
+ '<div class="markdown_xblock"><p>[HTML_REMOVED]This is h1[HTML_REMOVED]</p>\n</div>\n',
108
+ fragment.content
109
+ )
110
+
111
+ def test_render_replace_inline_html_boolean_setting(self):
112
+ """
113
+ Test a basic rendering with safe_mode set to True (instead of 'replace').
114
+
115
+ Expects the content to be sanitized by injecting
116
+ [HTML_REMOVED] in place of the HTML tags, and the sentence
117
+ being wrapped in a <p> block (since the <h1> tags are no
118
+ longer interpreted as HTML).
119
+ """
120
+ field_data = DictFieldData({'data': '<h1>This is h1</h1>'})
121
+ block = markdown_xblock.MarkdownXBlock(self.runtime, field_data, scope_ids=self.scope_ids)
122
+ settings = {
123
+ "extras": DEFAULT_EXTRAS,
124
+ "safe_mode": True
125
+ }
126
+ with patch('markdown_xblock.html.get_xblock_settings') as get_settings_mock:
127
+ get_settings_mock.return_value = settings
128
+ fragment = block.student_view()
129
+ self.assertIn(
130
+ '<div class="markdown_xblock"><p>[HTML_REMOVED]This is h1[HTML_REMOVED]</p>\n</div>\n',
131
+ fragment.content
132
+ )
133
+
134
+ def test_render_escape_inline_html(self):
135
+ """
136
+ Test a basic rendering with safe_mode set to 'escape'
137
+
138
+ Expects the content to be sanitized by escaping < and > with &lt; and &gt;
139
+ and the sentence being wrapped in a <p> block (since the <h1> tags are
140
+ no longer interpreted as HTML).
141
+ """
142
+ field_data = DictFieldData({'data': '<h1>This is h1</h1>'})
143
+ block = markdown_xblock.MarkdownXBlock(self.runtime, field_data, scope_ids=self.scope_ids)
144
+ settings = {
145
+ "extras": DEFAULT_EXTRAS,
146
+ "safe_mode": 'escape'
147
+ }
148
+ with patch('markdown_xblock.html.get_xblock_settings') as get_settings_mock:
149
+ get_settings_mock.return_value = settings
150
+ fragment = block.student_view()
151
+ self.assertIn(
152
+ '<div class="markdown_xblock"><p>&lt;h1&gt;This is h1&lt;/h1&gt;</p>\n</div>\n',
153
+ fragment.content
154
+ )
155
+
156
+ def test_render_allow_inline_html(self):
157
+ """
158
+ Test a basic rendering with javascript enabled.
159
+ Expects the content *not* to be sanitized.
160
+ """
161
+ field_data = DictFieldData({'data': '<h1>This is h1</h1>'})
162
+ block = markdown_xblock.MarkdownXBlock(self.runtime, field_data, scope_ids=self.scope_ids)
163
+ settings = {
164
+ "extras": DEFAULT_EXTRAS,
165
+ "safe_mode": False
166
+ }
167
+ with patch('markdown_xblock.html.get_xblock_settings') as get_settings_mock:
168
+ get_settings_mock.return_value = settings
169
+ fragment = block.student_view()
170
+ self.assertIn('<div class="markdown_xblock"><h1>This is h1</h1>\n</div>\n',
171
+ fragment.content)
172
+
173
+ def test_render_url_safe_mode_replace(self):
174
+ """
175
+ Test rendering a URL with safe_mode set to 'replace'.
176
+ """
177
+ field_data = DictFieldData(
178
+ {'data': '[test1](https://test.com/courses/course-v1:Org+Class+Version/about) [test2](https://test.com/courses/course-v1%3AOrg%2BClass%2BVersion/about)'} # noqa: E501
179
+ )
180
+ block = markdown_xblock.MarkdownXBlock(self.runtime, field_data, scope_ids=self.scope_ids)
181
+ settings = {
182
+ "extras": DEFAULT_EXTRAS,
183
+ "safe_mode": 'replace'
184
+ }
185
+ with patch('markdown_xblock.html.get_xblock_settings') as get_settings_mock:
186
+ get_settings_mock.return_value = settings
187
+ fragment = block.student_view()
188
+ self.assertIn(
189
+ '<a href="https://test.com/courses/course-v1:Org+Class+Version/about">test1</a>',
190
+ fragment.content
191
+ )
192
+ self.assertIn(
193
+ '<a href="https://test.com/courses/course-v1%3AOrg%2BClass%2BVersion/about">test2</a>',
194
+ fragment.content
195
+ )
196
+
197
+ def test_render_url_safe_mode_escape(self):
198
+ """
199
+ Test rendering a URL with safe_mode set to 'replace'.
200
+ """
201
+ field_data = DictFieldData(
202
+ {'data': '[test1](https://test.com/courses/course-v1:Org+Class+Version/about) [test2](https://test.com/courses/course-v1%3AOrg%2BClass%2BVersion/about)'} # noqa: E501
203
+ )
204
+ block = markdown_xblock.MarkdownXBlock(self.runtime, field_data, scope_ids=self.scope_ids)
205
+ settings = {
206
+ "extras": DEFAULT_EXTRAS,
207
+ "safe_mode": 'escape'
208
+ }
209
+ with patch('markdown_xblock.html.get_xblock_settings') as get_settings_mock:
210
+ get_settings_mock.return_value = settings
211
+ fragment = block.student_view()
212
+ self.assertIn(
213
+ '<a href="https://test.com/courses/course-v1:Org+Class+Version/about">test1</a>',
214
+ fragment.content
215
+ )
216
+ self.assertIn(
217
+ '<a href="https://test.com/courses/course-v1%3AOrg%2BClass%2BVersion/about">test2</a>',
218
+ fragment.content
219
+ )
220
+
221
+ def test_render_url_no_safe_mode(self):
222
+ """
223
+ Test rendering a URL with safe_mode disabled.
224
+ """
225
+ field_data = DictFieldData(
226
+ {'data': '[test](https://test.com/courses/course-v1:Org+Class+Version/about)'}
227
+ )
228
+ block = markdown_xblock.MarkdownXBlock(self.runtime, field_data, scope_ids=self.scope_ids)
229
+ settings = {
230
+ "extras": DEFAULT_EXTRAS,
231
+ "safe_mode": False
232
+ }
233
+ with patch('markdown_xblock.html.get_xblock_settings') as get_settings_mock:
234
+ get_settings_mock.return_value = settings
235
+ fragment = block.student_view()
236
+ self.assertIn(
237
+ '<a href="https://test.com/courses/course-v1:Org+Class+Version/about">test</a>',
238
+ fragment.content
239
+ )
240
+
241
+ def test_substitution_no_system(self):
242
+ """
243
+ Test that the substitution is not performed when `system` is not present inside XBlock.
244
+ """
245
+ field_data = DictFieldData({'data': '%%USER_ID%% %%COURSE_ID%%'})
246
+ block = markdown_xblock.MarkdownXBlock(self.runtime, field_data, scope_ids=self.scope_ids)
247
+ fragment = block.student_view()
248
+ self.assertIn('<div class="markdown_xblock"><p>%%USER_ID%% %%COURSE_ID%%</p>\n</div>\n', fragment.content)
249
+
250
+ def test_substitution_not_found(self):
251
+ """
252
+ Test that the keywords are not replaced when they're not found.
253
+ """
254
+ field_data = DictFieldData({'data': 'USER_ID%% %%COURSE_ID%%'})
255
+ block = markdown_xblock.MarkdownXBlock(self.runtime, field_data, scope_ids=self.scope_ids)
256
+ block.system = Mock(anonymous_student_id=None)
257
+ fragment = block.student_view()
258
+ self.assertIn('<div class="markdown_xblock"><p>USER_ID%% %%COURSE_ID%%</p>\n</div>\n', fragment.content)
259
+
260
+ def test_user_id_substitution(self):
261
+ """
262
+ Test replacing %%USER_ID%% with anonymous user ID.
263
+ """
264
+ field_data = DictFieldData({'data': '%%USER_ID%%'})
265
+ block = markdown_xblock.MarkdownXBlock(self.runtime, field_data, scope_ids=self.scope_ids)
266
+ block.system = Mock(anonymous_student_id='test_user')
267
+ fragment = block.student_view()
268
+ self.assertIn('<div class="markdown_xblock"><p>test_user</p>\n</div>\n', fragment.content)
269
+
270
+ def test_course_id_substitution(self):
271
+ """
272
+ Test replacing %%COURSE_ID%% with HTML representation of course key.
273
+ """
274
+ field_data = DictFieldData({'data': '%%COURSE_ID%%'})
275
+ block = markdown_xblock.MarkdownXBlock(self.runtime, field_data, scope_ids=self.scope_ids)
276
+ course_locator_mock = Mock()
277
+ course_locator_mock.html_id = Mock(return_value='test_course')
278
+ block.system = Mock(course_id=course_locator_mock)
279
+ fragment = block.student_view()
280
+ self.assertIn('<div class="markdown_xblock"><p>test_course</p>\n</div>\n', fragment.content)
var/.placeholder ADDED
@@ -0,0 +1,2 @@
1
+ This file exists solely to preserve its parent directory,
2
+ which the unit tests use to store Workbench logs.