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.
 
 
 

419 lines
14 KiB

  1. # This file is part of CAT-SOOP
  2. # Copyright (c) 2011-2017 Adam Hartz <hartz@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 re
  18. from base64 import b64encode
  19. import json
  20. def _execfile(*args):
  21. fn = args[0]
  22. with open(fn) as f:
  23. c = compile(f.read(), fn, 'exec')
  24. exec(c, *args[1:])
  25. def get_sandbox(context):
  26. base = os.path.join(context['cs_fs_root'], '__QTYPES__', 'pythoncode',
  27. '__SANDBOXES__', 'base.py')
  28. _execfile(base, context)
  29. def html_format(string):
  30. s = string.replace('&', '&amp;').replace('<', '&lt;').replace(
  31. '>', '&gt;').replace('\t', ' ').splitlines(False)
  32. jx = 0
  33. for ix, line in enumerate(s):
  34. for jx, char in enumerate(line):
  35. if char != ' ':
  36. break
  37. s[ix] = '&nbsp;' * jx + line[jx:]
  38. return '<br/>'.join(s)
  39. defaults = {
  40. 'csq_input_check': lambda x: None,
  41. 'csq_code_pre': '',
  42. 'csq_code_post': '',
  43. 'csq_initial': 'pass # Your code here',
  44. 'csq_soln': 'print("Hello, World!")',
  45. 'csq_tests': [],
  46. 'csq_log_keypresses': True,
  47. 'csq_variable_blacklist': [],
  48. 'csq_import_blacklist': [],
  49. 'csq_cpu_limit': 2,
  50. 'csq_nproc_limit': 0,
  51. 'csq_memory_limit': 32e6,
  52. 'csq_interface': 'ace',
  53. 'csq_rows': 14,
  54. 'csq_font_size': 16,
  55. 'csq_always_show_tests': False,
  56. }
  57. test_defaults = {
  58. 'npoints': 1,
  59. 'code': "ans = 3",
  60. 'variable': 'ans',
  61. 'description': '',
  62. 'include': False,
  63. 'include_soln': False,
  64. 'include_description': False,
  65. 'grade': True,
  66. 'show_description': True,
  67. 'show_code': True,
  68. 'check_function': lambda sub, soln: (sub == soln != '') * 1.0,
  69. 'transform_output': lambda x: '<tt>%s</tt>' % (html_format(x), ),
  70. }
  71. def total_points(**info):
  72. bak = info['csq_tests']
  73. info['csq_tests'] = []
  74. for i in bak:
  75. info['csq_tests'].append(dict(test_defaults))
  76. info['csq_tests'][-1].update(i)
  77. return sum(i['npoints'] for i in info['csq_tests'])
  78. checktext = 'Run Code'
  79. def handle_check(submissions, **info):
  80. py3k = info.get('csq_python3', True)
  81. code = submissions[info['csq_name']]
  82. if info['csq_interface'] == 'upload':
  83. code = csm_tools.data_uri.DataURI(code[1]).data.decode()
  84. code = code.replace('\r\n', '\n')
  85. if py3k:
  86. _printer = "print('_catsoop_code_done_running')"
  87. else:
  88. _printer = "print '_catsoop_code_done_running'"
  89. code = '\n\n'.join([info['csq_code_pre'], code, _printer])
  90. get_sandbox(info)
  91. fname, out, err = info['sandbox_run_code'](
  92. info, code, info.get('csq_sandbox_options', {}))
  93. err = info['fix_error_msg'](fname, err,
  94. info['csq_code_pre'].count('\n') + 2, code)
  95. complete = False
  96. if '_catsoop_code_done_running' in out:
  97. complete = True
  98. out = out.rsplit('_catsoop_code_done_running', 1)[0]
  99. trunc = False
  100. outlines = out.split('\n')
  101. if len(outlines) > 10:
  102. trunc = True
  103. outlines = outlines[:10]
  104. out = '\n'.join(outlines)
  105. if len(out) >= 5000:
  106. trunc = True
  107. out = out[:5000]
  108. if trunc:
  109. out += "\n\n...OUTPUT TRUNCATED..."
  110. timeout = False
  111. if (not complete) and ('SIGTERM' in err):
  112. timeout = True
  113. err = ("Your code did not run to completion, "
  114. "but no error message was returned."
  115. "\nThis normally means that your code contains an "
  116. "infinite loop or otherwise took too long to run.")
  117. msg = '<div class="response">'
  118. if not timeout:
  119. msg += '<p><b>'
  120. if complete:
  121. msg += ('<font color="darkgreen">'
  122. 'Your code ran to completion.'
  123. '</font>')
  124. else:
  125. msg += ('<font color="red">'
  126. 'Your code did not run to completion.'
  127. '</font>')
  128. msg += '</b></p>'
  129. if out != '':
  130. msg += "\n<p><b>Your code produced the following output:</b>"
  131. msg += "<br/><pre>%s</pre></p>" % html_format(out)
  132. if err != '':
  133. if not timeout:
  134. msg += "\n<p><b>Your code produced an error:</b>"
  135. msg += "\n<br/><font color='red'><tt>%s</tt></font></p>" % html_format(err)
  136. msg += '</div>'
  137. return msg
  138. def handle_submission(submissions, **info):
  139. code = submissions[info['csq_name']]
  140. if info['csq_interface'] == 'upload':
  141. code = csm_tools.data_uri.DataURI(code[1]).data.decode()
  142. code = code.replace('\r\n', '\n')
  143. tests = [dict(test_defaults) for i in info['csq_tests']]
  144. for (i, j) in zip(tests, info['csq_tests']):
  145. i.update(j)
  146. show_tests = [i for i in tests if i['include']]
  147. if len(show_tests) > 0:
  148. code = code.rsplit('### Test Cases')[0]
  149. inp = info['csq_input_check'](code)
  150. if inp is not None:
  151. msg = ('<div class="response">'
  152. '<font color="red">%s</font>'
  153. '</div>') % inp
  154. return {'score': 0, 'msg': msg}
  155. bak = info['csq_tests']
  156. info['csq_tests'] = []
  157. for i in bak:
  158. new = dict(test_defaults)
  159. new.update(i)
  160. if new['grade']:
  161. info['csq_tests'].append(new)
  162. get_sandbox(info)
  163. score = 0
  164. if info['csq_always_show_tests']:
  165. msg = ''
  166. else:
  167. msg = ('\n<br/><button onclick="$(\'#%s_result_showhide\').toggle()">'
  168. 'Show/Hide Detailed Results</button>') % info['csq_name']
  169. msg += ('<div class="response" id="%s_result_showhide" %s>'
  170. '<h2>Test Results:</h2>') % (info['csq_name'], 'style="display:none">'
  171. if not info['csq_always_show_tests']
  172. else '')
  173. count = 1
  174. for test in info['csq_tests']:
  175. out, err, log = info['sandbox_run_test'](info, code, test)
  176. if 'cached_result' in test:
  177. log_s = repr(test['cached_result'])
  178. err_s = 'Loaded cached result'
  179. else:
  180. out_s, err_s, log_s = info['sandbox_run_test'](
  181. info, info['csq_soln'], test)
  182. if count != 1:
  183. msg += "\n\n<p></p><hr/><p></p>\n\n"
  184. msg += "\n<center><h3>Test %02d</h3>" % count
  185. if test['show_description']:
  186. msg += "\n<i>%s</i>" % test['description']
  187. msg += "</center><p></p>"
  188. if test['show_code']:
  189. html_code = html_format(test['code'])
  190. msg += "\nThe test case was:<br/>\n<tt>%s</tt>" % html_code
  191. try:
  192. percentage = test['check_function'](log, log_s)
  193. except:
  194. percentage = 0.0
  195. imfile = None
  196. if percentage == 1.0:
  197. imfile = "check.png"
  198. elif percentage == 0.0:
  199. imfile = "cross.png"
  200. score += percentage * test['npoints']
  201. if imfile is None:
  202. image = ''
  203. else:
  204. image = "<img src='BASE/images/%s' />" % imfile
  205. if log_s != '' and test['show_code']: # Our solution ran successfully
  206. msg += ("\n<p>Our solution produced the following "
  207. "value for <tt>%s</tt>:") % test['variable']
  208. m = test['transform_output'](log_s)
  209. msg += "\n<br/><font color='blue'>%s</font></p>" % m
  210. elif log_s == '':
  211. msg += "\n<p><b>OOPS!</b> Our code produced an error:"
  212. e = html_format(err_s)
  213. msg += "\n<br/><font color='red'><tt>%s</tt></font></p>" % e
  214. if log != '' and test['show_code']:
  215. msg += ("\n<p>Your submission produced the following "
  216. "value for <tt>%s</tt>:") % test['variable']
  217. m = test['transform_output'](log)
  218. msg += "\n<br/><font color='blue'>%s</font>%s</p>" % (m, image)
  219. elif log != '':
  220. msg += "\n<br/><center>%s</center></p>" % (image)
  221. if out != '' and test['show_code']:
  222. msg += "\n<p>Your code produced the following output:"
  223. msg += "<br/><pre>%s</pre></p>" % html_format(out)
  224. msg += '<p>'
  225. if err != '':
  226. msg += "\nYour submission produced an error:"
  227. e = html_format(err)
  228. msg += "\n<br/><font color='red'><tt>%s</tt></font>" % e
  229. msg += "\n<br/><center>%s</center></p>" % (image)
  230. count += 1
  231. msg += "\n</div>"
  232. tp = total_points(**info)
  233. overall = float(score) / tp if tp != 0 else 0
  234. msg = (('\n<br/>&nbsp;Your score on your most recent '
  235. 'submission was: %01.02f%%') % (overall * 100)) + msg
  236. return {'score': overall, 'msg': msg}
  237. def make_initial_display(info):
  238. init = info['csq_initial']
  239. tests = [dict(test_defaults) for i in info['csq_tests']]
  240. for (i, j) in zip(tests, info['csq_tests']):
  241. i.update(j)
  242. show_tests = [i for i in tests if i['include']]
  243. l = len(show_tests) - 1
  244. if l > -1:
  245. init += '\n\n\n### Test Cases:\n'
  246. get_sandbox(info)
  247. for ix, i in enumerate(show_tests):
  248. init += '\n# Test Case %d' % (ix + 1)
  249. if i['include_soln']:
  250. if 'cached_result' in i:
  251. log_s = cached_result
  252. else:
  253. out_s, err_s, log_s = info['sandbox_run_test'](
  254. info, info['csq_soln'], i)
  255. init += ' (Should print: %s)' % log_s
  256. init += '\n'
  257. if i['include_description']:
  258. init += '# %s\n' % i['description']
  259. init += i['code']
  260. if info.get('csq_python3', True):
  261. init += '\nprint("Test Case %d:", %s)' % (ix + 1, i['variable'])
  262. if i['include_soln']:
  263. init += '\nprint("Expected:", %s)' % (log_s, )
  264. else:
  265. init += '\nprint "Test Case %d:", %s' % (ix + 1, i['variable'])
  266. if i['include_soln']:
  267. init += '\nprint "Expected:", %s' % (log_s, )
  268. if ix != l:
  269. init += '\n'
  270. return init
  271. def render_html_textarea(last_log, **info):
  272. return tutor.question('bigbox')[0]['render_html'](last_log, **info)
  273. def render_html_upload(last_log, **info):
  274. name = info['csq_name']
  275. init = last_log.get(name, (None, info['csq_initial']))
  276. if isinstance(init, str):
  277. fname = ''
  278. else:
  279. fname, init = init
  280. params = {
  281. 'name': name,
  282. 'init': str(init),
  283. 'safeinit': (init or '').replace('<', '&lt;'),
  284. 'b64init': b64encode(make_initial_display(info).encode()).decode(),
  285. 'dl': (' download="%s"' % info['csq_skeleton_name'])
  286. if 'csq_skeleton_name' in info else 'download',
  287. 'dl2': (' download="%s"' % fname)
  288. if 'csq_skeleton_name' in info else 'download',
  289. }
  290. out = ''
  291. if info.get('csq_show_skeleton', True):
  292. out += ('''<a href="data:text/plain;base64,%(b64init)s" '''
  293. '''target="_blank"%(dl)s>Code Skeleton</a><br />''') % params
  294. if last_log.get(name, None) is not None:
  295. code = last_log[name]
  296. if isinstance(code, str):
  297. code = (None, "data:text/plain;base64,%s" % b64encode(code.encode()))
  298. out += ('''<a href="%s" '''
  299. '''target="_blank" id="%s_lastfile">'''
  300. '''Your Last Submission</a><br />''') % (code[1], name)
  301. out += '''<input type="file" id=%(name)s name="%(name)s" />''' % params
  302. return out
  303. def render_html_ace(last_log, **info):
  304. name = info['csq_name']
  305. init = last_log.get(name, None)
  306. if init is None:
  307. init = make_initial_display(info)
  308. init = str(init.encode('utf-8', 'replace').decode('ascii', 'ignore'))
  309. fontsize = info['csq_font_size']
  310. params = {
  311. 'name': name,
  312. 'init': init,
  313. 'safeinit': init.replace('<', '&lt;'),
  314. 'height': info['csq_rows'] * (fontsize + 4),
  315. 'fontsize': fontsize,
  316. }
  317. return '''
  318. <div class="ace_editor_wrapper" id="container%(name)s">
  319. <div id="editor%(name)s" name="editor%(name)s" class="embedded_ace_code">%(safeinit)s</div></div>
  320. <input type="hidden" name="%(name)s" id="%(name)s" />
  321. <input type="hidden" name="%(name)s_log" id="%(name)s_log" />
  322. <script type="text/javascript" src="https://cdn.jsdelivr.net/ace/1.2.4/noconflict/ace.js"></script>
  323. <script type="text/javascript">
  324. var log%(name)s = new Array();
  325. var editor%(name)s = ace.edit("editor%(name)s");
  326. editor%(name)s.setTheme("ace/theme/textmate");
  327. editor%(name)s.getSession().setMode("ace/mode/python");
  328. editor%(name)s.setShowFoldWidgets(false);
  329. editor%(name)s.setValue(%(init)r)
  330. $("#%(name)s").val(editor%(name)s.getValue());
  331. editor%(name)s.on("change",function(e){
  332. editor%(name)s.getSession().setUseSoftTabs(true);
  333. $("#%(name)s").val(editor%(name)s.getValue());
  334. });
  335. editor%(name)s.clearSelection()
  336. editor%(name)s.getSession().setUseSoftTabs(true);
  337. editor%(name)s.on("paste",function(txt){editor%(name)s.getSession().setUseSoftTabs(false);});
  338. editor%(name)s.getSession().setTabSize(4);
  339. editor%(name)s.setFontSize("%(fontsize)spx");
  340. $("#container%(name)s").height(%(height)s);
  341. $("#editor%(name)s").height(%(height)s);
  342. editor%(name)s.resize(true);
  343. </script>''' % params
  344. RENDERERS = {
  345. 'textarea': render_html_textarea,
  346. 'ace': render_html_ace,
  347. 'upload': render_html_upload
  348. }
  349. def render_html(last_log, **info):
  350. renderer = info['csq_interface']
  351. if renderer in RENDERERS:
  352. return RENDERERS[renderer](last_log or {}, **info)
  353. return ("<font color='red'>"
  354. "Invalid <tt>pythoncode</tt> interface: %s"
  355. "</font>") % renderer
  356. def answer_display(**info):
  357. out = ('Here is the solution we wrote:<br/>'
  358. '<pre><code id="%s_soln_highlight" class="lang-python">%s</code></pre>'
  359. '<script type="text/javascript">hljs.highlightBlock($("#%s_soln_highlight")[0]);</script>') % (info['csq_name'], info['csq_soln'].replace('<','&lt;'), info['csq_name'])
  360. return out