CAT-SOOP is a flexible, programmable learning management system based on the Python programming language. https://catsoop.mit.edu
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

677 lines
23 KiB

  1. # This file is part of CAT-SOOP
  2. # Copyright (c) 2011-2020 by The CAT-SOOP Developers <catsoop-dev@mit.edu>
  3. #
  4. # This program is free software: you can redistribute it and/or modify it under
  5. # the terms of the GNU Affero General Public License as published by the Free
  6. # Software Foundation, either version 3 of the License, or (at your option) any
  7. # later version.
  8. #
  9. # This program is distributed in the hope that it will be useful, but WITHOUT
  10. # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
  11. # FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
  12. # details.
  13. #
  14. # You should have received a copy of the GNU Affero General Public License
  15. # along with this program. If not, see <http://www.gnu.org/licenses/>.
  16. import os
  17. import ast
  18. import json
  19. import logging
  20. import traceback
  21. import collections.abc
  22. from base64 import b64encode
  23. from urllib.parse import urlencode
  24. LOGGER = logging.getLogger("cs")
  25. def _execfile(*args):
  26. fn = args[0]
  27. with open(fn) as f:
  28. c = compile(f.read(), fn, "exec")
  29. exec(c, *args[1:])
  30. def get_sandbox(context):
  31. base = os.path.join(
  32. context["cs_fs_root"], "__QTYPES__", "pythoncode", "__SANDBOXES__", "base.py"
  33. )
  34. _execfile(base, context)
  35. def js_files(info):
  36. if info["csq_interface"] == "ace":
  37. return ["BASE/scripts/ace/ace.js"]
  38. else:
  39. return []
  40. def html_format(string):
  41. s = (
  42. string.replace("&", "&amp;")
  43. .replace("<", "&lt;")
  44. .replace(">", "&gt;")
  45. .replace("\t", " ")
  46. .splitlines(False)
  47. )
  48. jx = 0
  49. for ix, line in enumerate(s):
  50. for jx, char in enumerate(line):
  51. if char != " ":
  52. break
  53. s[ix] = "&nbsp;" * jx + line[jx:]
  54. return "<br/>".join(s)
  55. defaults = {
  56. "csq_input_check": lambda x: None,
  57. "csq_code_pre": "",
  58. "csq_code_post": "",
  59. "csq_initial": "pass # Your code here",
  60. "csq_soln": 'print("Hello, World!")',
  61. "csq_tests": [],
  62. "csq_hint": lambda score, code, info: "", # post-test hint generator
  63. "csq_log_keypresses": True,
  64. "csq_variable_blacklist": [],
  65. "csq_import_blacklist": [],
  66. "csq_cpu_limit": 2,
  67. "csq_nproc_limit": 0,
  68. "csq_memory_limit": 32e6,
  69. "csq_interface": "ace",
  70. "csq_rows": 14,
  71. "csq_font_size": 16,
  72. "csq_always_show_tests": False,
  73. "csq_test_defaults": {},
  74. "csq_use_simple_checker": False,
  75. "csq_result_as_string": False,
  76. }
  77. class NoResult:
  78. pass
  79. def _default_check_function(sub, soln):
  80. sub = sub.get("result", NoResult)
  81. soln = soln.get("result", NoResult)
  82. return sub == soln and sub is not NoResult
  83. def _default_simple_check_function(sub, soln):
  84. return sub == soln
  85. def _default_string_check_function(sub, soln):
  86. return ast.literal_eval(sub) == ast.literal_eval(soln)
  87. test_defaults = {
  88. "npoints": 1,
  89. "code": "",
  90. "code_pre": "",
  91. "variable": "ans",
  92. "description": "",
  93. "include": False,
  94. "include_soln": False,
  95. "include_description": False,
  96. "grade": True,
  97. "show_description": True,
  98. "show_code": True,
  99. "show_stderr": True,
  100. "transform_output": lambda x: '<tt style="white-space: pre-wrap">%s</tt>'
  101. % (html_format(repr(x)),),
  102. "sandbox_options": {},
  103. "count_opcodes": False,
  104. "opcode_limit": None,
  105. "show_timing": False,
  106. "show_opcode_count": False,
  107. }
  108. def init(info):
  109. if info["csq_interface"] == "upload":
  110. info["csq_rerender"] = True
  111. def total_points(**info):
  112. if "csq_npoints" in info:
  113. return info["csq_npoints"]
  114. return total_test_points(**info)
  115. def total_test_points(**info):
  116. bak = info["csq_tests"]
  117. info["csq_tests"] = []
  118. for i in bak:
  119. info["csq_tests"].append(dict(test_defaults))
  120. info["csq_tests"][-1].update(info["csq_test_defaults"])
  121. info["csq_tests"][-1].update(i)
  122. return sum(i["npoints"] for i in info["csq_tests"])
  123. checktext = "Run Code"
  124. def handle_check(submissions, **info):
  125. try:
  126. code = info["csm_loader"].get_file_data(info, submissions, info["csq_name"])
  127. code = code.decode().replace("\r\n", "\n")
  128. except:
  129. return {
  130. "score": 0,
  131. "msg": '<div class="bs-callout bs-callout-danger"><span class="text-danger"><b>Error:</b> Unable to decode the specified file. Is this the file you intended to upload?</span></div>',
  132. }
  133. code = "\n\n".join(["import os\nos.unlink(__file__)", info["csq_code_pre"], code])
  134. get_sandbox(info)
  135. results = info["sandbox_run_code"](info, code, info.get("csq_sandbox_options", {}))
  136. err = info["fix_error_msg"](
  137. results["fname"], results["err"], info["csq_code_pre"].count("\n") + 2, code
  138. )
  139. complete = results.get("info", {}).get("complete", False)
  140. trunc = False
  141. outlines = results["out"].split("\n")
  142. if len(outlines) > 10:
  143. trunc = True
  144. outlines = outlines[:10]
  145. out = "\n".join(outlines)
  146. if len(out) >= 5000:
  147. trunc = True
  148. out = out[:5000]
  149. if trunc:
  150. out += "\n\n...OUTPUT TRUNCATED..."
  151. timeout = False
  152. if (not complete) and ("SIGTERM" in err):
  153. timeout = True
  154. err = (
  155. "Your code did not run to completion, "
  156. "but no error message was returned."
  157. "\nThis normally means that your code contains an "
  158. "infinite loop or otherwise took too long to run."
  159. )
  160. msg = '<div class="response">'
  161. if not timeout:
  162. msg += "<p><b>"
  163. if complete:
  164. msg += '<font color="darkgreen">' "Your code ran to completion." "</font>"
  165. else:
  166. msg += '<font color="red">' "Your code did not run to completion." "</font>"
  167. msg += "</b></p>"
  168. if out != "":
  169. msg += "\n<p><b>Your code produced the following output:</b>"
  170. msg += "<br/><pre>%s</pre></p>" % html_format(out)
  171. if err != "":
  172. if not timeout:
  173. msg += "\n<p><b>Your code produced an error:</b>"
  174. msg += "\n<br/><font color='red'><tt>%s</tt></font></p>" % html_format(err)
  175. msg += "</div>"
  176. return msg
  177. def handle_submission(submissions, **info):
  178. try:
  179. code = info["csm_loader"].get_file_data(info, submissions, info["csq_name"])
  180. code = code.decode().replace("\r\n", "\n")
  181. except Exception as err:
  182. LOGGER.warn(
  183. "[pythoncode] handle_submission error '%s', traceback=%s"
  184. % (err, traceback.format_exc())
  185. )
  186. return {
  187. "score": 0,
  188. "msg": '<div class="bs-callout bs-callout-danger"><span class="text-danger"><b>Error:</b> Unable to decode the specified file. Is this the file you intended to upload?</span></div>',
  189. }
  190. if info["csq_use_simple_checker"]:
  191. if info["csq_result_as_string"]:
  192. default_checker = _default_string_check_function
  193. else:
  194. default_checker = _default_simple_check_function
  195. else:
  196. default_checker = _default_check_function
  197. tests = [dict(test_defaults) for i in info["csq_tests"]]
  198. for (i, j) in zip(tests, info["csq_tests"]):
  199. i.update(info["csq_test_defaults"])
  200. i.update(j)
  201. show_tests = [i for i in tests if i["include"]]
  202. if len(show_tests) > 0:
  203. code = code.rsplit("### Test Cases")[0]
  204. inp = info["csq_input_check"](code)
  205. if inp is not None:
  206. msg = ('<div class="response">' '<font color="red">%s</font>' "</div>") % inp
  207. return {"score": 0, "msg": msg}
  208. bak = info["csq_tests"]
  209. info["csq_tests"] = []
  210. for i in bak:
  211. new = dict(test_defaults)
  212. i.update(info["csq_test_defaults"])
  213. new.update(i)
  214. if new["grade"]:
  215. info["csq_tests"].append(new)
  216. get_sandbox(info)
  217. score = 0
  218. if info["csq_always_show_tests"]:
  219. msg = ""
  220. else:
  221. msg = (
  222. """\n<br/><button onclick="if(this.nextSibling.style.display === 'none'){this.nextSibling.style.display = 'block';}else{this.nextSibling.style.display = 'none';}" class="btn btn-catsoop">"""
  223. "Show/Hide Detailed Results</button>"
  224. )
  225. msg += (
  226. '<div class="response" id="%s_result_showhide" %s>' "<h2>Test Results:</h2>"
  227. ) % (
  228. info["csq_name"],
  229. 'style="display:none"' if not info["csq_always_show_tests"] else "",
  230. )
  231. test_results = []
  232. count = 1
  233. for test in info["csq_tests"]:
  234. test["result_as_string"] = test.get(
  235. "result_as_string", info.get("csq_result_as_string", False)
  236. )
  237. out, err, log = info["sandbox_run_test"](info, code, test)
  238. if "cached_result" in test:
  239. log_s = {
  240. "result": test["cached_result"],
  241. "complete": True,
  242. "duration": 0.0,
  243. "opcode_count": 0,
  244. "opcode_limit_reached": False,
  245. }
  246. err_s = ""
  247. out_s = ""
  248. else:
  249. out_s, err_s, log_s = info["sandbox_run_test"](info, info["csq_soln"], test)
  250. if count != 1:
  251. msg += "\n<p></p><hr/><p></p>"
  252. msg += "\n<center><h3>Test %02d</h3>" % count
  253. if test["show_description"]:
  254. msg += "\n<i>%s</i>" % test["description"]
  255. msg += "</center><p></p>"
  256. if test["show_code"]:
  257. html_code_pieces = [
  258. i for i in map(lambda x: html_format(test[x]), ["code_pre", "code"])
  259. ]
  260. html_code_pieces.insert(1, "#Your Code Here")
  261. html_code = "<br/>".join(i for i in html_code_pieces if i)
  262. msg += "\nThe test case was:<br/>\n<p><tt>%s</tt></p>" % html_code
  263. msg += "<p>&nbsp;</p>"
  264. result = {"details": log, "out": out, "err": err}
  265. result_s = {"details": log_s, "out": out_s, "err": err_s}
  266. if test["variable"] is not None:
  267. if "result" in log:
  268. result["result"] = log["result"]
  269. del log["result"]
  270. if "result" in log_s:
  271. result_s["result"] = log_s["result"]
  272. del log_s["result"]
  273. checker = test.get("check_function", default_checker)
  274. try:
  275. if info["csq_use_simple_checker"]:
  276. # legacy checker
  277. check_result = checker(result["result"], result_s["result"])
  278. else:
  279. check_result = checker(result, result_s)
  280. except:
  281. check_result = 0.0
  282. if isinstance(check_result, collections.abc.Mapping):
  283. percentage = check_result["score"]
  284. extra_msg = check_result["msg"]
  285. elif isinstance(check_result, collections.abc.Sequence):
  286. percentage, extra_msg = check_result
  287. else:
  288. percentage = check_result
  289. extra_msg = ""
  290. test_results.append(percentage)
  291. imfile = None
  292. if percentage == 1.0:
  293. imfile = info["cs_check_image"]
  294. elif percentage == 0.0:
  295. imfile = info["cs_cross_image"]
  296. score += percentage * test["npoints"]
  297. expected_variable = test["variable"] is not None
  298. solution_ran = result_s != {}
  299. submission_ran = result != {}
  300. show_code = test["show_code"]
  301. error_in_solution = result_s["err"] != ""
  302. error_in_submission = result["err"] != ""
  303. solution_produced_output = result_s["out"] != ""
  304. submission_produced_output = result["out"] != ""
  305. got_submission_result = "result" in result
  306. got_solution_result = "result" in result_s
  307. if imfile is None:
  308. image = ""
  309. else:
  310. image = "<img src='%s' />" % imfile
  311. # report timing and/or opcode count
  312. if test["show_timing"] == True:
  313. test["show_timing"] = "%.06f"
  314. do_timing = test["show_timing"] and "duration" in result_s["details"]
  315. do_opcount = test["show_opcode_count"] and "opcode_count" in result_s["details"]
  316. if do_timing or do_opcount:
  317. msg += "\n<p>"
  318. if do_timing:
  319. _timing = result_s["details"]["duration"]
  320. msg += (
  321. "\nOur solution ran for %s seconds." % test["show_timing"]
  322. ) % _timing
  323. if do_timing and do_opcount:
  324. msg += "\n<br/>"
  325. if do_opcount:
  326. _opcount = result_s["details"]["opcode_count"]
  327. msg += "\nOur solution executed %s Python opcodes.<br/>" % _opcount
  328. if do_timing or do_opcount:
  329. msg += "\n</p>"
  330. if expected_variable and show_code:
  331. if got_solution_result:
  332. msg += (
  333. "\n<p>Our solution produced the following value for <tt>%s</tt>:"
  334. ) % test["variable"]
  335. m = test["transform_output"](result_s["result"])
  336. msg += "\n<br/><font color='blue'>%s</font></p>" % m
  337. else:
  338. msg += (
  339. "\n<p>Our solution did not produce a value for <tt>%s</tt>.</p>"
  340. ) % test["variable"]
  341. if solution_produced_output and show_code:
  342. msg += "\n<p>Our code produced the following output:"
  343. msg += "<br/><pre>%s</pre></p>" % html_format(result_s["out"])
  344. if error_in_solution and test["show_stderr"]:
  345. msg += "\n<p><b>OOPS!</b> Our code produced an error:"
  346. e = html_format(result_s["err"])
  347. msg += "\n<br/><font color='red'><tt>%s</tt></font></p>" % e
  348. if show_code:
  349. msg += "<p>&nbsp;</p>"
  350. # report timing and/or opcode count
  351. do_timing = test["show_timing"] and "duration" in result["details"]
  352. do_opcount = test["show_opcode_count"] and "opcode_count" in result["details"]
  353. if do_timing or do_opcount:
  354. msg += "\n<p>"
  355. if do_timing:
  356. _timing = result["details"]["duration"]
  357. msg += (
  358. "\nYour solution ran for %s seconds." % test["show_timing"]
  359. ) % _timing
  360. if do_timing and do_opcount:
  361. msg += "\n<br/>"
  362. if do_opcount:
  363. _opcount = result["details"]["opcode_count"]
  364. msg += "\nYour code executed %d Python opcodes.<br/>" % _opcount
  365. if do_timing or do_opcount:
  366. msg += "\n</p>"
  367. if expected_variable and show_code:
  368. if got_submission_result:
  369. msg += (
  370. "\n<p>Your submission produced the following value for <tt>%s</tt>:"
  371. ) % test["variable"]
  372. m = test["transform_output"](result["result"])
  373. msg += "\n<br/><font color='blue'>%s</font>%s</p>" % (m, image)
  374. else:
  375. msg += (
  376. "\n<p>Your submission did not produce a value for <tt>%s</tt>.</p>"
  377. ) % test["variable"]
  378. else:
  379. msg += "\n<center>%s</center>" % (image)
  380. if submission_produced_output and show_code:
  381. msg += "\n<p>Your code produced the following output:"
  382. msg += "<br/><pre>%s</pre></p>" % html_format(result["out"])
  383. if error_in_submission and test["show_stderr"]:
  384. msg += "\n<p>Your submission produced an error:"
  385. e = html_format(result["err"])
  386. msg += "\n<br/><font color='red'><tt>%s</tt></font></p>" % e
  387. msg += "\n<br/><center>%s</center>" % (image)
  388. if extra_msg:
  389. msg += "\n<p>%s</p>" % extra_msg
  390. count += 1
  391. msg += "\n</div>"
  392. tp = total_test_points(**info)
  393. overall = float(score) / tp if tp != 0 else 0
  394. hint_func = info.get("csq_hint")
  395. if hint_func:
  396. try:
  397. hint = hint_func(test_results, code, info)
  398. msg += hint or ""
  399. LOGGER.debug("[pythoncode] hint=%s" % hint)
  400. except Exception as err:
  401. LOGGER.warn(
  402. "[pythoncode] hint function %s produced error=%s at %s"
  403. % (hint_func, err, traceback.format_exc())
  404. )
  405. msg = (
  406. ("\n<br/>&nbsp;Your score on your most recent " "submission was: %01.02f%%")
  407. % (overall * 100)
  408. ) + msg
  409. out = {"score": overall, "msg": msg}
  410. return out
  411. def make_initial_display(info):
  412. init = info["csq_initial"]
  413. tests = [dict(test_defaults) for i in info["csq_tests"]]
  414. for (i, j) in zip(tests, info["csq_tests"]):
  415. i.update(j)
  416. show_tests = [i for i in tests if i["include"]]
  417. l = len(show_tests) - 1
  418. if l > -1:
  419. init += "\n\n\n### Test Cases:\n"
  420. get_sandbox(info)
  421. for ix, i in enumerate(show_tests):
  422. i["result_as_string"] = i.get(
  423. "result_as_string", info.get("csq_result_as_string", False)
  424. )
  425. init += "\n# Test Case %d" % (ix + 1)
  426. if i["include_soln"]:
  427. if "cached_result" in i:
  428. log_s = i["cached_result"]
  429. else:
  430. out_s, err_s, log_s = info["sandbox_run_test"](
  431. info, info["csq_soln"], i
  432. )
  433. init += " (Should print: %s)" % log_s
  434. init += "\n"
  435. if i["include_description"]:
  436. init += "# %s\n" % i["description"]
  437. init += i["code"]
  438. if info.get("csq_python3", True):
  439. init += '\nprint("Test Case %d:", %s)' % (ix + 1, i["variable"])
  440. if i["include_soln"]:
  441. init += '\nprint("Expected:", %s)' % (log_s,)
  442. else:
  443. init += '\nprint "Test Case %d:", %s' % (ix + 1, i["variable"])
  444. if i["include_soln"]:
  445. init += '\nprint "Expected:", %s' % (log_s,)
  446. if ix != l:
  447. init += "\n"
  448. return init
  449. def render_html_textarea(last_log, **info):
  450. return tutor.question("bigbox")[0]["render_html"](last_log, **info)
  451. def render_html_upload(last_log, **info):
  452. name = info["csq_name"]
  453. init = last_log.get(name, (None, info["csq_initial"]))
  454. if isinstance(init, str):
  455. fname = ""
  456. else:
  457. fname, init = init
  458. params = {
  459. "name": name,
  460. "init": str(init),
  461. "safeinit": (init or "").replace("<", "&lt;"),
  462. "b64init": b64encode(make_initial_display(info).encode()).decode(),
  463. "dl": (' download="%s"' % info["csq_skeleton_name"])
  464. if "csq_skeleton_name" in info
  465. else "download",
  466. }
  467. out = ""
  468. if info.get("csq_show_skeleton", True):
  469. out += (
  470. """\n<a href="data:text/plain;base64,%(b64init)s" """
  471. """target="_blank"%(dl)s>Code Skeleton</a><br />"""
  472. ) % params
  473. if last_log.get(name, None) is not None:
  474. try:
  475. fname, loc = last_log[name]
  476. loc = os.path.basename(loc)
  477. _path = info["cs_path_info"]
  478. if info["csm_cslog"].ENCRYPT_KEY is not None:
  479. seed = (
  480. info["cs_path_info"][0]
  481. if info["cs_path_info"]
  482. else info["cs_path_info"]
  483. )
  484. _path = [
  485. info["csm_cslog"]._e(i, repr(seed)) for i in info["cs_path_info"]
  486. ]
  487. else:
  488. _path = info["cs_path_info"]
  489. qstring = urlencode({"path": json.dumps(_path), "fname": loc})
  490. out += "<br/>"
  491. safe_fname = (
  492. fname.replace("<", "")
  493. .replace(">", "")
  494. .replace('"', "")
  495. .replace("'", "")
  496. )
  497. out += (
  498. '<a href="%s/_util/get_upload?%s" '
  499. 'download="%s">Download Most '
  500. "Recent Submission</a><br/>"
  501. ) % (info["cs_url_root"], qstring, safe_fname)
  502. except:
  503. pass
  504. out += (
  505. """\n<input type="file" style="display: none" id=%(name)s name="%(name)s" />"""
  506. % params
  507. )
  508. out += (
  509. """\n<button class="btn btn-catsoop" id="%s_select_button">Select File</button>&nbsp;"""
  510. """\n<tt><span id="%s_selected_file">No file selected</span></tt>"""
  511. ) % (name, name)
  512. out += (
  513. """\n<script type="text/javascript">"""
  514. "\n// @license magnet:?xt=urn:btih:0b31508aeb0634b347b8270c7bee4d411b5d4109&dn=agpl-3.0.txt AGPL-v3"
  515. """\ndocument.getElementById('%s').value = '';"""
  516. """\ndocument.getElementById('%s_select_button').addEventListener('click', function (){"""
  517. """\n document.getElementById("%s").click();"""
  518. """\n});"""
  519. """\ndocument.getElementById('%s').addEventListener('change', function (){"""
  520. """\n document.getElementById('%s_selected_file').innerText = document.getElementById('%s').value;"""
  521. """\n});"""
  522. "\n// @license-end"
  523. """\n</script>"""
  524. ) % (name, name, name, name, name, name)
  525. return out
  526. def render_html_ace(last_log, **info):
  527. name = info["csq_name"]
  528. init = last_log.get(name, None)
  529. if init is None:
  530. init = make_initial_display(info)
  531. init = str(init)
  532. fontsize = info["csq_font_size"]
  533. params = {
  534. "name": name,
  535. "init": init,
  536. "safeinit": init.replace("<", "&lt;"),
  537. "height": info["csq_rows"] * (fontsize + 4),
  538. "fontsize": fontsize,
  539. }
  540. return (
  541. """
  542. <div class="ace_editor_wrapper" id="container%(name)s">
  543. <div id="editor%(name)s" name="editor%(name)s" class="embedded_ace_code">%(safeinit)s</div></div>
  544. <input type="hidden" name="%(name)s" id="%(name)s" />
  545. <input type="hidden" name="%(name)s_log" id="%(name)s_log" />
  546. <script type="text/javascript">
  547. // @license magnet:?xt=urn:btih:0b31508aeb0634b347b8270c7bee4d411b5d4109&dn=agpl-3.0.txt AGPL-v3
  548. var log%(name)s = new Array();
  549. var editor%(name)s = ace.edit("editor%(name)s");
  550. editor%(name)s.setTheme("ace/theme/textmate");
  551. editor%(name)s.getSession().setMode("ace/mode/python");
  552. editor%(name)s.setShowFoldWidgets(false);
  553. editor%(name)s.setValue(%(init)r)
  554. document.getElementById("%(name)s").value = editor%(name)s.getValue();
  555. editor%(name)s.on("change",function(e){
  556. editor%(name)s.getSession().setUseSoftTabs(true);
  557. document.getElementById("%(name)s").value = editor%(name)s.getValue();
  558. });
  559. editor%(name)s.clearSelection()
  560. editor%(name)s.getSession().setUseSoftTabs(true);
  561. editor%(name)s.on("paste",function(txt){editor%(name)s.getSession().setUseSoftTabs(false);});
  562. editor%(name)s.getSession().setTabSize(4);
  563. editor%(name)s.setFontSize("%(fontsize)spx");
  564. document.getElementById("container%(name)s").style.height = "%(height)spx";
  565. document.getElementById("editor%(name)s").style.height = "%(height)spx";
  566. editor%(name)s.resize(true);
  567. // @license-end
  568. </script>"""
  569. % params
  570. )
  571. RENDERERS = {
  572. "textarea": render_html_textarea,
  573. "ace": render_html_ace,
  574. "upload": render_html_upload,
  575. }
  576. def render_html(last_log, **info):
  577. renderer = info["csq_interface"]
  578. if renderer in RENDERERS:
  579. return RENDERERS[renderer](last_log or {}, **info)
  580. return (
  581. "<font color='red'>" "Invalid <tt>pythoncode</tt> interface: %s" "</font>"
  582. ) % renderer
  583. def answer_display(**info):
  584. out = (
  585. "Here is the solution we wrote:<br/>"
  586. '\n<pre><code id="%s_soln_highlight" class="lang-python">%s</code></pre>'
  587. '\n<script type="text/javascript">'
  588. "\n// @license magnet:?xt=urn:btih:0b31508aeb0634b347b8270c7bee4d411b5d4109&dn=agpl-3.0.txt AGPL-v3"
  589. '\nhljs.highlightBlock(document.getElementById("%s_soln_highlight"));'
  590. "\n// @license-end"
  591. "\n</script>"
  592. ) % (info["csq_name"], info["csq_soln"].replace("<", "&lt;"), info["csq_name"])
  593. return out