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.
 
 
 

1938 lines
71 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. import json
  19. import random
  20. import string
  21. import traceback
  22. import collections
  23. _prefix = 'cs_defaulthandler_'
  24. def _n(n):
  25. return "%s%s" % (_prefix, n)
  26. def _unknown_handler(action):
  27. return lambda x: 'Unknown Action: %s' % action
  28. def _get(context, key, default, cast=lambda x: x):
  29. v = context.get(key, default)
  30. return cast(v(context) if isinstance(v, collections.Callable) else v)
  31. def handle(context):
  32. # set some variables in context
  33. pre_handle(context)
  34. mode_handlers = {'view': handle_view,
  35. 'submit': handle_submit,
  36. 'check': handle_check,
  37. 'save': handle_save,
  38. 'viewanswer': handle_viewanswer,
  39. 'clearanswer': handle_clearanswer,
  40. 'viewexplanation': handle_viewexplanation,
  41. 'content_only': handle_content_only,
  42. 'raw_html': handle_raw_html,
  43. 'copy': handle_copy,
  44. 'copy_seed': handle_copy_seed,
  45. 'activate': handle_activate,
  46. 'lock': handle_lock,
  47. 'unlock': handle_unlock,
  48. 'grade': handle_grade,
  49. 'passthrough': lambda c: '',
  50. 'new_seed': handle_new_seed,
  51. 'list_questions': handle_list_questions,
  52. 'get_state': handle_get_state,
  53. 'manage_groups': manage_groups,
  54. 'render_single_question': handle_single_question,
  55. 'stats': handle_stats,
  56. 'whdw': handle_whdw,
  57. }
  58. action = context[_n('action')]
  59. return mode_handlers.get(action, _unknown_handler(action))(context)
  60. def handle_list_questions(context):
  61. types = {k: v[0]['qtype'] for k,v in context[_n('name_map')].items()}
  62. order = list(context[_n('name_map')])
  63. return make_return_json(context, {'order': order, 'types': types}, [])
  64. def handle_get_state(context):
  65. ll = context[_n('last_log')]
  66. for i in ll:
  67. if isinstance(ll[i], set):
  68. ll[i] = list(ll[i])
  69. return make_return_json(context, ll, [])
  70. def handle_single_question(context):
  71. lastlog = context[_n('last_log')]
  72. lastsubmit = lastlog.get('last_submit', {})
  73. qname = context['cs_form'].get('name', None)
  74. elt = context[_n('name_map')][qname]
  75. o = render_question(elt, context, lastsubmit, wrap=False)
  76. return ('200', 'OK'), {'Content-type': 'text/html'}, o
  77. def handle_copy_seed(context):
  78. if context[_n('impersonating')]:
  79. impersonated = context[_n('uname')]
  80. uname = context[_n('real_uname')]
  81. course = context['cs_course']
  82. logname = '.'.join(['random.seed'] + context['cs_path_info'])
  83. stored = context['csm_cslog'].most_recent(course, impersonated,
  84. logname, None)
  85. context['csm_cslog'].update_log(course, uname, logname, stored)
  86. return handle_save(context)
  87. def _new_random_seed(n=100):
  88. try:
  89. return os.urandom(n)
  90. except:
  91. return ''.join(random.choice(string.ascii_letters) for i in range(n))
  92. def handle_new_seed(context):
  93. uname = context[_n('uname')]
  94. course = context['cs_course']
  95. logname = '.'.join(['random.seed'] + context['cs_path_info'])
  96. context['csm_cslog'].update_log(course, uname, logname, _new_random_seed())
  97. # Rerender the questions
  98. names = context[_n('question_names')]
  99. outdict = {}
  100. for name in names:
  101. outdict[name] = {'rerender': 'Please refresh the page'}
  102. return make_return_json(context, outdict)
  103. def handle_activate(context):
  104. submitted_pass = context[_n('form')].get('activation_password', '')
  105. if submitted_pass == context[_n('activation_password')]:
  106. newstate = dict(context[_n('last_log')])
  107. newstate['activated'] = True
  108. course = context['cs_course']
  109. uname = context[_n('uname')]
  110. logname = context[_n('logname_state')]
  111. context['csm_cslog'].overwrite_log(course, uname, logname, newstate)
  112. context[_n('last_log')] = newstate
  113. return handle_view(context)
  114. def handle_copy(context):
  115. if context[_n('impersonating')]:
  116. context[_n('uname')] = context[_n('real_uname')]
  117. ll = context['csm_cslog'].most_recent(context['cs_course'],
  118. context[_n('uname')],
  119. context[_n('logname_state')], {})
  120. context[_n('last_log')] = ll
  121. return handle_save(context)
  122. def handle_activation_form(context):
  123. context['cs_content_header'] = 'Problem Activation'
  124. out = '<form method="POST">'
  125. out += ('\nActivation Password: '
  126. '<input type="text" '
  127. 'name="activation_password" '
  128. 'value="" />'
  129. '\n&nbsp;'
  130. '\n<input type="submit" '
  131. 'name="action" '
  132. 'value="Activate" />')
  133. if 'admin' in context[_n('perms')]:
  134. pwd = context[_n('activation_password')]
  135. out += ('\n<p><u>Staff:</u> password is '
  136. '<tt><font color="blue">%s</font></tt>') % pwd
  137. out += '</form>'
  138. p = context[_n('perms')]
  139. if 'submit' in p or 'submit_all' in p:
  140. log_action(context, {'action': 'show_activation_form'})
  141. return out
  142. def handle_raw_html(context):
  143. # base function: display the problem
  144. uname = context[_n('uname')]
  145. perms = context[_n('perms')]
  146. lastlog = context[_n('last_log')]
  147. lastsubmit = lastlog.get('last_submit', {})
  148. if (_get(context, 'cs_auth_required', True, bool) and
  149. 'view' not in perms and 'view_all' not in perms):
  150. return 'You are not allowed to view this page.'
  151. if (_get(context, 'cs_require_activation', False, bool) and
  152. not lastlog.get('activated', False)):
  153. return 'You must activate this page first.'
  154. due = context[_n('due')]
  155. timing = context[_n('timing')]
  156. if timing == -1 and ('view_all' not in perms):
  157. reltime = context['csm_time'].long_timestamp(context[_n('rel')])
  158. reltime = reltime.replace(';', ' at')
  159. return ('This page is not yet available. '
  160. 'It will become available on %s.') % reltime
  161. if 'submit' in perms or 'submit_all' in perms:
  162. # only log an entry for users who can submit
  163. log_action(context, {'action': 'view',
  164. 'score': lastlog.get('score', 0.0)})
  165. page = ''
  166. num_questions = len(context[_n('name_map')])
  167. if (num_questions > 0 and _get(context, 'cs_show_due', True, bool) and
  168. context.get('cs_due_date', 'NEVER') != 'NEVER'):
  169. duetime = context['csm_time'].long_timestamp(due)
  170. page += ('<tutoronly><center>'
  171. 'The questions below are due on %s.'
  172. '<br/><hr><br/></center></tutoronly>') % duetime
  173. for elt in context['cs_problem_spec']:
  174. if isinstance(elt, str):
  175. page += elt
  176. else:
  177. # this is a question
  178. page += render_question(elt, context, lastsubmit)
  179. page += default_javascript(context)
  180. page += default_timer(context)
  181. context['cs_template'] = 'BASE/templates/empty.template'
  182. return page
  183. def handle_content_only(context):
  184. # base function: display the problem
  185. uname = context[_n('uname')]
  186. perms = context[_n('perms')]
  187. lastlog = context[_n('last_log')]
  188. lastsubmit = lastlog.get('last_submit', {})
  189. if (_get(context, 'cs_auth_required', True, bool) and
  190. 'view' not in perms and 'view_all' not in perms):
  191. return 'You are not allowed to view this page.'
  192. if (_get(context, 'cs_require_activation', False, bool) and
  193. not lastlog.get('activated', False)):
  194. return 'You must activate this page first.'
  195. due = context[_n('due')]
  196. timing = context[_n('timing')]
  197. if timing == -1 and ('view_all' not in perms):
  198. reltime = context['csm_time'].long_timestamp(context[_n('rel')])
  199. reltime = reltime.replace(';', ' at')
  200. return ('This page is not yet available. '
  201. 'It will become available on %s.') % reltime
  202. if 'submit' in perms or 'submit_all' in perms:
  203. # only log an entry for users who can submit
  204. log_action(context, {'action': 'view',
  205. 'score': lastlog.get('score', 0.0)})
  206. page = ''
  207. num_questions = len(context[_n('name_map')])
  208. if (num_questions > 0 and _get(context, 'cs_show_due', True, bool) and
  209. context.get('cs_due_date', 'NEVER') != 'NEVER'):
  210. duetime = context['csm_time'].long_timestamp(due)
  211. page += ('<tutoronly><center>'
  212. 'The questions below are due on %s.'
  213. '<br/><hr><br/></center></tutoronly>') % duetime
  214. for elt in context['cs_problem_spec']:
  215. if isinstance(elt, str):
  216. page += elt
  217. else:
  218. # this is a question
  219. page += render_question(elt, context, lastsubmit)
  220. page += default_javascript(context)
  221. page += default_timer(context)
  222. context['cs_template'] = 'BASE/templates/noborder.template'
  223. return page
  224. def handle_view(context):
  225. # base function: display the problem
  226. uname = context[_n('uname')]
  227. perms = context[_n('perms')]
  228. lastlog = context[_n('last_log')]
  229. lastsubmit = lastlog.get('last_submit', {})
  230. if (_get(context, 'cs_auth_required', True, bool) and
  231. 'view' not in perms and 'view_all' not in perms):
  232. return 'You are not allowed to view this page.'
  233. if (_get(context, 'cs_require_activation', False, bool) and
  234. not lastlog.get('activated', False)):
  235. return handle_activation_form(context)
  236. due = context[_n('due')]
  237. timing = context[_n('timing')]
  238. if timing == -1 and ('view_all' not in perms):
  239. reltime = context['csm_time'].long_timestamp(context[_n('rel')])
  240. reltime = reltime.replace(';', ' at')
  241. return ('This page is not yet available. '
  242. 'It will become available on %s.') % reltime
  243. if 'submit' in perms or 'submit_all' in perms:
  244. # only log an entry for users who can submit
  245. log_action(context, {'action': 'view',
  246. 'score': lastlog.get('score', 0.0)})
  247. page = ''
  248. num_questions = len(context[_n('name_map')])
  249. if (num_questions > 0 and _get(context, 'cs_show_due', True, bool) and
  250. context.get('cs_due_date', 'NEVER') != 'NEVER'):
  251. duetime = context['csm_time'].long_timestamp(due)
  252. page += ('<tutoronly><center>'
  253. 'The questions below are due on %s.'
  254. '<br/><hr><br/></center></tutoronly>') % duetime
  255. for elt in context['cs_problem_spec']:
  256. if isinstance(elt, str):
  257. page += elt
  258. else:
  259. # this is a question
  260. page += render_question(elt, context, lastsubmit)
  261. page += default_javascript(context)
  262. page += default_timer(context)
  263. return page
  264. def get_manual_grading_entry(context, name):
  265. pg_name = context[_n('logname_grades')]
  266. uname = context['cs_user_info'].get('username', 'None')
  267. log = context['csm_cslog'].read_log(context['cs_course'], uname, pg_name)
  268. out = None
  269. for i in log:
  270. if i['qname'] == name:
  271. out = i
  272. return out
  273. def make_score_display(context, name, score, assume_submit=False):
  274. _, args = context[_n('name_map')][name]
  275. if not _get(args, 'csq_show_score', True, bool):
  276. if name in context[_n('last_log')].get('scores', {}) or assume_submit:
  277. return 'Submission received.'
  278. else:
  279. return ''
  280. gmode = _get(args, 'csq_grading_mode', 'auto', str)
  281. if gmode == 'manual':
  282. log = get_manual_grading_entry(context, name)
  283. if log is not None:
  284. score = log['score']
  285. if score is None:
  286. if name in context[_n('last_log')].get('scores', {}) or assume_submit:
  287. return 'Grade not available.'
  288. else:
  289. return ''
  290. c = context.get('cs_score_message', None)
  291. try:
  292. return c(score)
  293. except:
  294. colorthing = 255 * score
  295. r = max(0, 200 - colorthing)
  296. g = min(200, colorthing)
  297. s = score * 100
  298. return ('<span style="color:rgb(%d,%d,0);font-weight:bolder;">'
  299. '%.02f%%</span>') % (r, g, s)
  300. def handle_clearanswer(context):
  301. names = context[_n('question_names')]
  302. timing = context[_n('timing')]
  303. due = context[_n('due')]
  304. lastlog = context[_n('last_log')]
  305. answerviewed = context[_n('answer_viewed')]
  306. explanationviewed = context[_n('explanation_viewed')]
  307. newstate = dict(lastlog)
  308. newstate['timestamp'] = context['cs_timestamp']
  309. if 'last_submit' not in newstate:
  310. newstate['last_submit'] = {}
  311. outdict = {} # dictionary containing the responses for each question
  312. for name in names:
  313. out = {}
  314. error = clearanswer_msg(context, context[_n('perms')], name)
  315. if error is not None:
  316. out['error_msg'] = error
  317. outdict[name] = out
  318. continue
  319. q, args = context[_n('name_map')][name]
  320. out['clear'] = True
  321. outdict[name] = out
  322. answerviewed.discard(name)
  323. explanationviewed.discard(name)
  324. newstate['answer_viewed'] = answerviewed
  325. newstate['explanation_viewed'] = explanationviewed
  326. # update problemstate log
  327. course = context['cs_course']
  328. uname = context[_n('uname')]
  329. logname = context[_n('logname_state')]
  330. context['csm_cslog'].overwrite_log(course, uname, logname, newstate)
  331. # log submission in problemactions
  332. duetime = context['csm_time'].detailed_timestamp(due)
  333. log_action(context, {'action': 'viewanswer',
  334. 'names': names,
  335. 'score': newstate.get('score', 0.0),
  336. 'response': outdict,
  337. 'due_date': duetime})
  338. return make_return_json(context, outdict)
  339. def explanation_display(x):
  340. return '<hr /><p><b>Explanation:</b></p>%s' % x
  341. def handle_viewexplanation(context):
  342. names = context[_n('question_names')]
  343. timing = context[_n('timing')]
  344. due = context[_n('due')]
  345. lastlog = context[_n('last_log')]
  346. explanationviewed = context[_n('explanation_viewed')]
  347. loader = context['csm_loader']
  348. language = context['csm_language']
  349. newstate = dict(lastlog)
  350. newstate['timestamp'] = context['cs_timestamp']
  351. if 'last_submit' not in newstate:
  352. newstate['last_submit'] = {}
  353. outdict = {} # dictionary containing the responses for each question
  354. for name in names:
  355. out = {}
  356. error = viewexp_msg(context, context[_n('perms')], name)
  357. if error is not None:
  358. out['error_msg'] = error
  359. outdict[name] = out
  360. continue
  361. q, args = context[_n('name_map')][name]
  362. exp = explanation_display(args['csq_explanation'])
  363. out['explanation'] = language.source_transform_string(context, exp)
  364. outdict[name] = out
  365. explanationviewed.add(name)
  366. newstate['explanation_viewed'] = explanationviewed
  367. # update problemstate log
  368. course = context['cs_course']
  369. uname = context[_n('uname')]
  370. logname = context[_n('logname_state')]
  371. context['csm_cslog'].overwrite_log(course, uname, logname, newstate)
  372. # log submission in problemactions
  373. duetime = context['csm_time'].detailed_timestamp(due)
  374. log_action(context, {'action': 'viewanswer',
  375. 'names': names,
  376. 'score': newstate.get('score', 0.0),
  377. 'response': outdict,
  378. 'due_date': duetime})
  379. return make_return_json(context, outdict)
  380. def handle_viewanswer(context):
  381. names = context[_n('question_names')]
  382. timing = context[_n('timing')]
  383. due = context[_n('due')]
  384. lastlog = context[_n('last_log')]
  385. answerviewed = context[_n('answer_viewed')]
  386. loader = context['csm_loader']
  387. language = context['csm_language']
  388. newstate = dict(lastlog)
  389. newstate['timestamp'] = context['cs_timestamp']
  390. if 'last_submit' not in newstate:
  391. newstate['last_submit'] = {}
  392. outdict = {} # dictionary containing the responses for each question
  393. for name in names:
  394. out = {}
  395. error = viewanswer_msg(context, context[_n('perms')], name)
  396. if error is not None:
  397. out['error_msg'] = error
  398. outdict[name] = out
  399. continue
  400. q, args = context[_n('name_map')][name]
  401. # if we are here, no errors occurred. go ahead with checking.
  402. ans = q['answer_display'](**args)
  403. out['answer'] = language.source_transform_string(context, ans)
  404. outdict[name] = out
  405. answerviewed.add(name)
  406. newstate['answer_viewed'] = answerviewed
  407. # update problemstate log
  408. course = context['cs_course']
  409. uname = context[_n('uname')]
  410. logname = context[_n('logname_state')]
  411. context['csm_cslog'].overwrite_log(course, uname, logname, newstate)
  412. # log submission in problemactions
  413. duetime = context['csm_time'].detailed_timestamp(due)
  414. log_action(context, {'action': 'viewanswer',
  415. 'names': names,
  416. 'score': newstate.get('score', 0.0),
  417. 'response': outdict,
  418. 'due_date': duetime})
  419. return make_return_json(context, outdict)
  420. def handle_lock(context):
  421. names = context[_n('question_names')]
  422. timing = context[_n('timing')]
  423. due = context[_n('due')]
  424. lastlog = context[_n('last_log')]
  425. locked = context[_n('locked')]
  426. newstate = dict(lastlog)
  427. newstate['timestamp'] = context['cs_timestamp']
  428. if 'last_submit' not in newstate:
  429. newstate['last_submit'] = {}
  430. outdict = {} # dictionary containing the responses for each question
  431. for name in names:
  432. q, args = context[_n('name_map')][name]
  433. outdict[name] = {}
  434. locked.add(name)
  435. # automatically view the answer if the option is set
  436. if 'lock' in _get_auto_view(args) and q.get('allow_viewanswer', True) and _get(args, 'csq_allow_viewanswer', True, bool):
  437. if name not in newstate.get('answer_viewed', set()):
  438. c = dict(context)
  439. c[_n('question_names')] = [name]
  440. o = json.loads(handle_viewanswer(c)[2])
  441. ll = context['csm_cslog'].most_recent(
  442. context['cs_course'], context.get('cs_username', 'None'),
  443. context[_n('logname_state')], {})
  444. newstate['answer_viewed'] = ll.get('answer_viewed', set())
  445. newstate['explanation_viewed'] = ll.get('explanation_viewed',
  446. set())
  447. outdict[name].update(o[name])
  448. newstate['locked'] = locked
  449. # update problemstate log
  450. course = context['cs_course']
  451. uname = context[_n('uname')]
  452. logname = context[_n('logname_state')]
  453. context['csm_cslog'].overwrite_log(course, uname, logname, newstate)
  454. # log submission in problemactions
  455. duetime = context['csm_time'].detailed_timestamp(due)
  456. log_action(context, {'action': 'lock',
  457. 'names': names,
  458. 'score': newstate.get('score', 0.0),
  459. 'response': outdict,
  460. 'due_date': duetime})
  461. return make_return_json(context, outdict)
  462. def handle_grade(context):
  463. names = context[_n('question_names')]
  464. perms = context[_n('perms')]
  465. newentries = []
  466. outdict = {}
  467. for name in names:
  468. if name.endswith('_grading_score') or name.endswith(
  469. '_grading_comments'):
  470. continue
  471. error = grade_msg(context, perms, name)
  472. if error is not None:
  473. outdict[name] = {'error_msg': error}
  474. continue
  475. q, args = context[_n('name_map')][name]
  476. npoints = float(q['total_points'](**args))
  477. try:
  478. f = context[_n('form')]
  479. rawscore = f.get('%s_grading_score' % name, '')
  480. comments = f.get('%s_grading_comments' % name, '')
  481. score = float(rawscore)
  482. except:
  483. outdict[name] = {'error_msg': 'Invalid score: %s' % rawscore}
  484. continue
  485. newentries.append({'qname': name,
  486. 'grader': context[_n('real_uname')],
  487. 'score': score / npoints,
  488. 'comments': comments,
  489. 'timestamp': context['cs_timestamp']})
  490. outdict[name] = {
  491. 'score_display': make_score_display(context, name, score),
  492. 'message': "<b>Grader's Comments:</b><br/><br/>%s" % context['csm_language']._md_format_string(context, comments),
  493. 'score': score,
  494. }
  495. # update problemstate log
  496. course = context['cs_course']
  497. uname = context[_n('uname')]
  498. logname = context[_n('logname_grades')]
  499. for i in newentries:
  500. context['csm_cslog'].update_log(course, uname, logname, i)
  501. # log submission in problemactions
  502. log_action(context, {'action': 'grade',
  503. 'names': names,
  504. 'scores': newentries,
  505. 'grader': context[_n('real_uname')]})
  506. return make_return_json(context, outdict, names=list(outdict.keys()))
  507. def handle_unlock(context):
  508. names = context[_n('question_names')]
  509. timing = context[_n('timing')]
  510. due = context[_n('due')]
  511. lastlog = context[_n('last_log')]
  512. locked = context[_n('locked')]
  513. newstate = dict(lastlog)
  514. newstate['timestamp'] = context['cs_timestamp']
  515. if 'last_submit' not in newstate:
  516. newstate['last_submit'] = {}
  517. outdict = {} # dictionary containing the responses for each question
  518. for name in names:
  519. q, args = context[_n('name_map')][name]
  520. outdict[name] = {}
  521. locked.remove(name)
  522. newstate['locked'] = locked
  523. # update problemstate log
  524. course = context['cs_course']
  525. uname = context[_n('uname')]
  526. logname = context[_n('logname_state')]
  527. context['csm_cslog'].overwrite_log(course, uname, logname, newstate)
  528. # log submission in problemactions
  529. duetime = context['csm_time'].detailed_timestamp(due)
  530. log_action(context, {'action': 'unlock',
  531. 'names': names,
  532. 'score': newstate.get('score', 0.0),
  533. 'response': outdict,
  534. 'due_date': duetime})
  535. return make_return_json(context, outdict)
  536. def handle_save(context):
  537. names = context[_n('question_names')]
  538. timing = context[_n('timing')]
  539. due = context[_n('due')]
  540. lastlog = context[_n('last_log')]
  541. newstate = dict(lastlog)
  542. newstate['timestamp'] = context['cs_timestamp']
  543. if 'last_submit' not in newstate:
  544. newstate['last_submit'] = {}
  545. outdict = {} # dictionary containing the responses for each question
  546. saved_names = []
  547. for name in names:
  548. out = {}
  549. error = save_msg(context, context[_n('perms')], name)
  550. if error is not None:
  551. out['error_msg'] = error
  552. outdict[name] = out
  553. continue
  554. question, args = context[_n('name_map')].get(name)
  555. sub = context[_n('form')].get(name, '')
  556. if sub == "": # don't overwrite things with blank strings
  557. outdict[name] = {}
  558. continue
  559. saved_names.append(name)
  560. # if we are here, no errors occurred. go ahead with checking.
  561. newstate['last_submit'][name] = sub
  562. rerender = question.get('always_rerender', False)
  563. if rerender is True:
  564. out['rerender'] = context['csm_language'].source_transform_string(context, args.get(
  565. 'csq_prompt', ''))
  566. out['rerender'] += question['render_html'](newstate['last_submit'],
  567. **args)
  568. elif rerender:
  569. out['rerender'] = rerender
  570. out['score_display'] = ''
  571. out['message'] = ''
  572. outdict[name] = out
  573. # cache responses
  574. newstate['%s_score_display' % name] = out['score_display']
  575. newstate['%s_message' % name] = out['message']
  576. # update problemstate log
  577. if len(saved_names) > 0:
  578. course = context['cs_course']
  579. uname = context[_n('uname')]
  580. logname = context[_n('logname_state')]
  581. context['csm_cslog'].overwrite_log(course, uname, logname, newstate)
  582. # log submission in problemactions
  583. duetime = context['csm_time'].detailed_timestamp(due)
  584. subbed = {n: context[_n('form')].get(n, '') for n in saved_names}
  585. log_action(context, {'action': 'save',
  586. 'names': saved_names,
  587. 'submitted': subbed,
  588. 'score': newstate.get('score', 0.0),
  589. 'response': outdict,
  590. 'due_date': duetime})
  591. return make_return_json(context, outdict)
  592. def handle_check(context):
  593. names = context[_n('question_names')]
  594. timing = context[_n('timing')]
  595. due = context[_n('due')]
  596. lastlog = context[_n('last_log')]
  597. namemap = context[_n('name_map')]
  598. newstate = dict(lastlog)
  599. newstate['timestamp'] = context['cs_timestamp']
  600. if 'last_submit' not in newstate:
  601. newstate['last_submit'] = {}
  602. outdict = {} # dictionary containing the responses for each question
  603. for name in names:
  604. out = {}
  605. sub = context[_n('form')].get(name, '')
  606. # if we are here, no errors occurred. go ahead with checking.
  607. newstate['last_submit'][name] = sub
  608. question, args = namemap[name]
  609. try:
  610. response = question['handle_check'](context[_n('form')], **args)
  611. except:
  612. response = exc_message(context)
  613. out['score_display'] = ''
  614. out['message'] = context['csm_language'].handle_custom_tags(context,
  615. response)
  616. rerender = question.get('always_rerender', False)
  617. if rerender is True:
  618. out['rerender'] = question['render_html'](newstate['last_submit'],
  619. **args)
  620. elif rerender:
  621. out['rerender'] = rerender
  622. outdict[name] = out
  623. # cache responses
  624. newstate['%s_score_display' % name] = out['score_display']
  625. newstate['%s_message' % name] = out['message']
  626. # update problemstate log
  627. course = context['cs_course']
  628. uname = context[_n('uname')]
  629. logname = context[_n('logname_state')]
  630. context['csm_cslog'].overwrite_log(course, uname, logname, newstate)
  631. # log submission in problemactions
  632. duetime = context['csm_time'].detailed_timestamp(due)
  633. subbed = {n: context[_n('form')].get(n, '') for n in names}
  634. log_action(context, {'action': 'check',
  635. 'names': names,
  636. 'submitted': subbed,
  637. 'score': newstate.get('score', 0.0),
  638. 'response': outdict,
  639. 'due_date': duetime})
  640. return make_return_json(context, outdict)
  641. def handle_submit(context):
  642. names = context[_n('question_names')]
  643. due = context[_n('due')]
  644. lastlog = context[_n('last_log')]
  645. nsubmits_used = context[_n('nsubmits_used')]
  646. answer_viewed = context[_n('answer_viewed')]
  647. scores = lastlog.get('scores', {})
  648. namemap = context[_n('name_map')]
  649. timing = context[_n('timing')]
  650. newstate = dict(lastlog)
  651. newstate['last_submit_time'] = context['cs_timestamp']
  652. newstate['last_submit_times'] = newstate.get('last_submit_times', {})
  653. newstate['timestamp'] = context['cs_timestamp']
  654. if 'last_submit' not in newstate:
  655. newstate['last_submit'] = {}
  656. names_done = set()
  657. outdict = {} # dictionary containing the responses for each question
  658. for name in names:
  659. if name.startswith('__'):
  660. name = name[2:].rsplit('_', 1)[0]
  661. if name in names_done:
  662. continue
  663. names_done.add(name)
  664. newstate['last_submit_times'][name] = context['cs_timestamp']
  665. out = {}
  666. sub = context[_n('form')].get(name, '')
  667. error = submit_msg(context, context[_n('perms')], name)
  668. if error is not None:
  669. out['error_msg'] = error
  670. outdict[name] = out
  671. continue
  672. # if we are here, no errors occurred. go ahead with checking.
  673. nsubmits_used[name] = nsubmits_used.get(name, 0) + 1
  674. newstate['last_submit'][name] = sub
  675. question, args = namemap[name]
  676. grading_mode = _get(args, 'csq_grading_mode', 'auto', str)
  677. if grading_mode == 'auto':
  678. try:
  679. resp = question['handle_submission'](context[_n('form')], **
  680. args)
  681. scores[name] = resp['score']
  682. msg = resp['msg']
  683. except:
  684. resp = {}
  685. scores[name] = 0.0
  686. msg = exc_message(context)
  687. elif grading_mode == 'manual':
  688. resp = {}
  689. msg = 'Submission received for manual grading.'
  690. scores[name] = None
  691. else:
  692. resp = {}
  693. scores[name] = 0.0
  694. msg = '<font color="red">Unknown grading mode: %s. Please contact staff.</font>' % grading_mode
  695. out['score_display'] = make_score_display(
  696. context, name, scores[name],
  697. assume_submit=True)
  698. out['message'] = context['csm_language'].handle_custom_tags(context,
  699. msg)
  700. out['score'] = scores[name]
  701. rerender = resp.get('rerender', False) or question.get(
  702. 'always_rerender', False)
  703. if rerender is True:
  704. out['rerender'] = question['render_html'](newstate['last_submit'],
  705. **args)
  706. elif rerender:
  707. out['rerender'] = rerender
  708. outdict[name] = out
  709. if resp.get('lock', False):
  710. c = dict(context)
  711. c[_n('question_names')] = [name]
  712. o = json.loads(handle_lock(c)[2])
  713. ll = context['csm_cslog'].most_recent(
  714. context['cs_course'], context.get('cs_username', 'None'),
  715. context[_n('logname_state')], {})
  716. newstate['locked'] = ll.get('locked', set())
  717. outdict[name].update(o[name])
  718. # auto view answer if the option is set
  719. if 'submit_all' not in context[_n('orig_perms')]:
  720. x = nsubmits_left(context, name)
  721. if (question.get('allow_viewanswer', True) and (((out['score'] == 1 and 'perfect' in _get_auto_view(args)) or
  722. (x[0] == 0 and 'nosubmits' in _get_auto_view(args))) and
  723. _get(args, 'csq_allow_viewanswer', True, bool))):
  724. # this is a hack...
  725. c = dict(context)
  726. c[_n('question_names')] = [name]
  727. o = json.loads(handle_viewanswer(c)[2])
  728. ll = context['csm_cslog'].most_recent(
  729. context['cs_course'], context.get('cs_username', 'None'),
  730. context[_n('logname_state')], {})
  731. newstate['answer_viewed'] = ll.get('answer_viewed', set())
  732. newstate['explanation_viewed'] = ll.get('explanation_viewed',
  733. set())
  734. outdict[name].update(o[name])
  735. # cache responses
  736. newstate['%s_score_display' % name] = out['score_display']
  737. newstate['%s_message' % name] = out['message']
  738. # update score
  739. if any(scores[i] is None for i in scores):
  740. newstate['score'] = None
  741. else:
  742. num = 0.0
  743. denom = 0.0
  744. for n in namemap:
  745. q, args = namemap[n]
  746. d = q['total_points'](**args)
  747. denom += d
  748. num += scores.get(n, 0.0) * d
  749. newstate['score'] = 0.0 if num == denom == 0.0 else num / denom
  750. context[_n('nsubmits_used')] = newstate['nsubmits_used'] = nsubmits_used
  751. newstate['scores'] = scores
  752. # update problemstate log
  753. course = context['cs_course']
  754. uname = context[_n('uname')]
  755. logname = context[_n('logname_state')]
  756. context['csm_cslog'].overwrite_log(course, uname, logname, newstate)
  757. # log submission in problemactions
  758. duetime = context['csm_time'].detailed_timestamp(due)
  759. subbed = {n: context[_n('form')].get(n, '') for n in names}
  760. log_action(context, {'action': 'submit',
  761. 'names': names,
  762. 'submitted': subbed,
  763. 'score': newstate['score'],
  764. 'scores': newstate['scores'],
  765. 'response': outdict,
  766. 'due_date': duetime})
  767. context['csm_loader'].run_plugins(context, context['cs_course'], 'post_submit', context)
  768. return make_return_json(context, outdict)
  769. def manage_groups(context):
  770. # displays the screen to admins who are adjusting groups
  771. perms = context['cs_user_info'].get('permissions', [])
  772. if 'groups' not in perms and 'admin' not in perms:
  773. return 'You are not allowed to view this page.'
  774. form = context['cs_form']
  775. # show the main partnering page
  776. section = context['cs_user_info'].get('section', None)
  777. default_section = context.get('cs_default_section', 'default')
  778. all_sections = context.get('cs_sections', [])
  779. if len(all_sections) == 0:
  780. all_sections = {default_section: 'Default Section'}
  781. if section is None:
  782. section = default_section
  783. hdr = ('Group Assignments for %s, Section '
  784. '<span id="cs_groups_section">%s</span>')
  785. hdr %= (context['cs_original_path'], section)
  786. context['cs_content_header'] = hdr
  787. # menu for choosing section to display
  788. out = '\nShow Current Groups for Section:\n<select name="section" id="section">'
  789. for i in sorted(all_sections):
  790. s = ' selected' if str(i) == str(section) else ''
  791. out += '\n<option value="%s"%s>%s</option>' % (i, s, i)
  792. out += '\n</select>'
  793. # empty table that will eventually be populated with groups
  794. out += ('\n<p>\n<h2>Groups:</h2>'
  795. '\n<div id="cs_groups_table" border="1" align="left">'
  796. '\nLoading...'
  797. '\n</div>')
  798. # create partnership from two students
  799. out += ('\n<p>\n<h2>Make New Partnership:</h2>'
  800. '\nStudent 1: <select name="cs_groups_name1" id="cs_groups_name1">'
  801. '</select>&nbsp;'
  802. '\nStudent 2: <select name="cs_groups_name2" id="cs_groups_name2">'
  803. '</select>&nbsp;'
  804. '\n<button class="btn btn-catsoop" id="cs_groups_newpartners">Partner Students</button>'
  805. '</p>')
  806. # add a student to a group
  807. out += ('\n<p>\n<h2>Add Student to Group:</h2>'
  808. '\nStudent: <select name="cs_groups_nameadd" id="cs_groups_nameadd">'
  809. '</select>&nbsp;'
  810. '\nGroup: <select name="cs_groups_groupadd" id="cs_groups_groupadd">'
  811. '</select>&nbsp;'
  812. '\n<button class="btn btn-catsoop" id="cs_groups_addtogroup">Add to Group</button></p>')
  813. # randomly assign all groups. this needs to be sufficiently scary...
  814. out += ('\n<p><h2>Randomly assign groups</h2>'
  815. '\n<button class="btn btn-catsoop" id="cs_groups_reassign">Reassign Groups</button></p>')
  816. all_group_names = context.get('cs_group_names', None)
  817. if all_group_names is None:
  818. all_group_names = map(str, range(100))
  819. else:
  820. all_group_names = sorted(all_group_names)
  821. all_group_names = list(all_group_names)
  822. out += '\n<script type="text/javascript">catsoop.group_names = %s</script>' % all_group_names
  823. out += '\n<script type="text/javascript" src="__HANDLER__/default/cs_groups.js"></script>'
  824. return out + default_javascript(context)
  825. def clearanswer_msg(context, perms, name):
  826. namemap = context[_n('name_map')]
  827. timing = context[_n('timing')]
  828. ansviewed = context[_n('answer_viewed')]
  829. i = context[_n('impersonating')]
  830. _, qargs = namemap[name]
  831. error = None
  832. if ('submit' not in perms and 'submit_all' not in perms):
  833. error = ('You are not allowed undo your viewing of '
  834. 'the answer to this question.')
  835. elif name not in ansviewed:
  836. error = "You have not viewed the answer for this question."
  837. elif name not in namemap:
  838. error = ('No question with name %s. '
  839. 'Please refresh before submitting.') % name
  840. elif 'submit_all' not in perms:
  841. if timing == -1 and not i:
  842. error = 'This question is not yet available.'
  843. if not qargs.get('csq_allow_submit_after_answer_viewed', False):
  844. error = ('You are not allowed to undo your viewing of '
  845. 'the answer to this question.')
  846. return error
  847. def viewexp_msg(context, perms, name):
  848. namemap = context[_n('name_map')]
  849. timing = context[_n('timing')]
  850. ansviewed = context[_n('answer_viewed')]
  851. expviewed = context[_n('explanation_viewed')]
  852. _, qargs = namemap[name]
  853. error = None
  854. if ('submit' not in perms and 'submit_all' not in perms):
  855. error = 'You are not allowed to view the answer to this question.'
  856. elif name not in ansviewed:
  857. error = 'You have not yet viewed the answer for this question.'
  858. elif name in expviewed:
  859. error = 'You have already viewed the explanation for this question.'
  860. elif name not in namemap:
  861. error = ('No question with name %s. '
  862. 'Please refresh before submitting.') % name
  863. elif ('submit_all' not in perms) and timing == -1:
  864. error = 'This question is not yet available.'
  865. elif not _get(qargs, 'csq_allow_viewexplanation', True, bool):
  866. error = 'Viewing explanations is not allowed for this question.'
  867. else:
  868. q, args = namemap[name]
  869. if 'csq_explanation' not in args:
  870. error = 'No explanation supplied for this question.'
  871. return error
  872. def viewanswer_msg(context, perms, name):
  873. namemap = context[_n('name_map')]
  874. timing = context[_n('timing')]
  875. ansviewed = context[_n('answer_viewed')]
  876. i = context[_n('impersonating')]
  877. _, qargs = namemap[name]
  878. error = None
  879. if not _.get('allow_viewanswer', True):
  880. error = 'You cannot view the answer to this type of question.'
  881. elif ('submit' not in perms and 'submit_all' not in perms):
  882. error = 'You are not allowed to view the answer to this question.'
  883. elif name in ansviewed:
  884. error = 'You have already viewed the answer for this question.'
  885. elif name not in namemap:
  886. error = ('No question with name %s. '
  887. 'Please refresh before submitting.') % name
  888. elif 'submit_all' not in perms:
  889. if timing == -1 and not i:
  890. error = 'This question is not yet available.'
  891. elif not _get(qargs, 'csq_allow_viewanswer', True, bool):
  892. error = 'Viewing the answer is not allowed for this question.'
  893. return error
  894. def save_msg(context, perms, name):
  895. namemap = context[_n('name_map')]
  896. timing = context[_n('timing')]
  897. i = context[_n('impersonating')]
  898. _, qargs = namemap[name]
  899. error = None
  900. if not _.get('allow_save', True):
  901. error = 'You cannot save this type of question.'
  902. elif 'submit' not in perms and 'submit_all' not in perms:
  903. error = 'You are not allowed to check answers to this question.'
  904. elif name not in namemap:
  905. error = ('No question with name %s. '
  906. 'Please refresh before submitting.') % name
  907. elif 'submit_all' not in perms:
  908. if timing == -1 and not i:
  909. error = 'This question is not yet available.'
  910. elif name in context[_n('locked')]:
  911. error = 'You are not allowed to save for this question (it has been locked).'
  912. elif (not _get(qargs, 'csq_allow_submit_after_answer_viewed', False,
  913. bool) and name in context[_n('answer_viewed')]):
  914. error = 'You are not allowed to save to this question after viewing the answer.'
  915. elif timing == 1 and _get(context, 'cs_auto_lock', False, bool):
  916. error = ('You are not allowed to save after the '
  917. 'deadline for this question.')
  918. elif not _get(qargs, 'csq_allow_save', True, bool):
  919. error = 'Saving is not allowed for this question.'
  920. return error
  921. def check_msg(context, perms, name):
  922. namemap = context[_n('name_map')]
  923. timing = context[_n('timing')]
  924. i = context[_n('impersonating')]
  925. _, qargs = namemap[name]
  926. error = None
  927. if 'submit' not in perms and 'submit_all' not in perms:
  928. error = 'You are not allowed to check answers to this question.'
  929. elif name not in namemap:
  930. error = ('No question with name %s. '
  931. 'Please refresh before submitting.') % name
  932. elif namemap[name][0].get('handle_check', None) is None:
  933. error = 'This question type does not support checking.'
  934. elif 'submit_all' not in perms:
  935. if timing == -1 and not i:
  936. error = 'This question is not yet available.'
  937. elif name in context[_n('locked')]:
  938. error = 'You are not allowed to check answers to this question.'
  939. elif (not _get(qargs, 'csq_allow_submit_after_answer_viewed', False,
  940. bool) and name in context[_n('answer_viewed')]):
  941. error = 'You are not allowed to check answers to this question after viewing the answer.'
  942. elif timing == 1 and _get(context, 'cs_auto_lock', False, bool):
  943. error = ('You are not allowed to check after the '
  944. 'deadline for this problem.')
  945. elif not _get(qargs, 'csq_allow_check', True, bool):
  946. error = 'Checking is not allowed for this question.'
  947. return error
  948. def grade_msg(context, perms, name):
  949. namemap = context[_n('name_map')]
  950. _, qargs = namemap[name]
  951. if 'grade' not in perms:
  952. return 'You are not allowed to grade exercises.'
  953. def submit_msg(context, perms, name):
  954. if name.startswith('__'):
  955. name = name[2:].rsplit('_', 1)[0]
  956. namemap = context[_n('name_map')]
  957. timing = context[_n('timing')]
  958. i = context[_n('impersonating')]
  959. _, qargs = namemap[name]
  960. error = None
  961. if not _.get('allow_submit', True):
  962. error = 'You cannot submit this type of question.'
  963. if (not _.get('allow_self_submit', True)) and 'real_user' not in context['cs_user_info']:
  964. error = 'You cannot submit this type of question yourself.'
  965. elif 'submit' not in perms and 'submit_all' not in perms:
  966. error = 'You are not allowed to submit answers to this question.'
  967. elif name not in namemap:
  968. error = ('No question with name %s. '
  969. 'Please refresh before submitting.') % name
  970. elif 'submit_all' not in perms:
  971. # don't allow if...
  972. if timing == -1 and not i:
  973. # ...the problem has not yet been released
  974. error = 'This question is not yet open for submissions.'
  975. elif _get(context, 'cs_auto_lock', False, bool) and timing == 1:
  976. # ...the problem auto locks and it is after the due date
  977. error = ('Submissions are not allowed after the '
  978. 'deadline for this question')
  979. elif name in context[_n('locked')]:
  980. error = 'You are not allowed to submit to this question.'
  981. elif (not _get(qargs, 'csq_allow_submit_after_answer_viewed', False,
  982. bool) and name in context[_n('answer_viewed')]):
  983. # ...the answer has been viewed and submissions after
  984. # viewing the answer are not allowed
  985. error = ('You are not allowed to submit to this question '
  986. 'because you have already viewed the answer.')
  987. elif not _get(qargs, 'csq_allow_submit', True, bool):
  988. # ...submissions are not allowed
  989. error = 'Submissions are not allowed for this question.'
  990. elif (not _get(qargs, 'csq_grading_mode', 'auto', str) == 'manual' and
  991. get_manual_grading_entry(context, name) is not None):
  992. # ...prior submission has been graded
  993. error = 'You are not allowed to submit after a previous submission has been graded.'
  994. else:
  995. # ...the user does not have enough checks left
  996. nleft, _ = nsubmits_left(context, name)
  997. if nleft <= 0:
  998. error = ('You have used all of your allowed '
  999. 'submissions for this question.')
  1000. return error
  1001. def log_action(context, log_entry):
  1002. course = context['cs_course']
  1003. uname = context[_n('uname')]
  1004. logname = context[_n('logname_actions')]
  1005. entry = {'action': context[_n('action')],
  1006. 'timestamp': context['cs_timestamp'],
  1007. 'ip': context['cs_ip'],
  1008. 'user_info': context['cs_user_info'],
  1009. 'form': context['cs_form']}
  1010. entry.update(log_entry)
  1011. context['csm_cslog'].update_log(course, uname, logname, entry)
  1012. def simple_return_json(val):
  1013. content = json.dumps(val, separators=(',', ':'))
  1014. length = str(len(content))
  1015. retcode = ('200', 'OK')
  1016. headers = {'Content-type': 'application/json', 'Content-length': length}
  1017. return retcode, headers, content
  1018. def make_return_json(context, ret, names=None):
  1019. names = context[_n('question_names')] if names is None else names
  1020. names = set(i[2:].rsplit('_', 1)[0] if i.startswith('__') else i
  1021. for i in names)
  1022. for name in names:
  1023. ret[name]['nsubmits_left'] = nsubmits_left(context, name)[1],
  1024. ret[name]['buttons'] = make_buttons(context, name)
  1025. return simple_return_json(ret)
  1026. def render_question(elt, context, lastsubmit, wrap=True):
  1027. q, args = elt
  1028. name = args['csq_name']
  1029. lastlog = context[_n('last_log')]
  1030. answer_viewed = context[_n('answer_viewed')]
  1031. if wrap:
  1032. out = '\n<!--START question %s -->' % (name)
  1033. else:
  1034. out = ''
  1035. if wrap and q.get('indiv', True) and args.get('csq_indiv', True):
  1036. out += '\n<div class="question question-%s" id="cs_qdiv_%s" style="position: static">' % (q['qtype'], name)
  1037. out += '\n<div id="%s_rendered_question">\n' % name
  1038. out += context['csm_language'].source_transform_string(context, args.get(
  1039. 'csq_prompt', ''))
  1040. out += q['render_html'](lastsubmit, **args)
  1041. out += '\n</div>'
  1042. out += '<div>'
  1043. out += (('\n<span id="%s_buttons">' % name) + make_buttons(context, name) +
  1044. "</span>")
  1045. out += ('\n<div id="%s_loading" class="loader" style="display:none;">'
  1046. '</div>') % name
  1047. out += (('\n<span id="%s_score_display">' % args['csq_name']) +
  1048. make_score_display(context, name, lastlog.get('scores', {}).get(
  1049. name, None)) + '</span>')
  1050. out += (('\n<div id="%s_nsubmits_left" class="nsubmits_left">' % name) +
  1051. nsubmits_left(context, name)[1] + "</div>")
  1052. out += '</div>'
  1053. if name in answer_viewed:
  1054. answerclass = ' class="solution"'
  1055. showanswer = True
  1056. elif context[_n('impersonating')]:
  1057. answerclass = ' class="impsolution"'
  1058. showanswer = True
  1059. else:
  1060. answerclass = ''
  1061. showanswer = False
  1062. out += '\n<div id="%s_solution_container"%s>' % (args['csq_name'],
  1063. answerclass)
  1064. out += '\n<div id="%s_solution">' % (args['csq_name'])
  1065. if showanswer:
  1066. ans = q['answer_display'](**args)
  1067. out += '\n'
  1068. out += context['csm_language'].source_transform_string(context, ans)
  1069. out += '\n</div>'
  1070. out += '\n<div id="%s_solution_explanation">' % name
  1071. if (name in context[_n('explanation_viewed')] and
  1072. args.get('csq_explanation', '') != ''):
  1073. exp = explanation_display(args['csq_explanation'])
  1074. out += context['csm_language'].source_transform_string(context, exp)
  1075. out += '\n</div>'
  1076. out += '\n</div>'
  1077. out += '\n<div id="%s_message">' % args['csq_name']
  1078. gmode = _get(args, 'csq_grading_mode', 'auto', str)
  1079. ll = context[_n('last_log')].get('%s_message' % name, '')
  1080. if gmode == 'manual':
  1081. q, args = context[_n('name_map')][name]
  1082. lastlog = get_manual_grading_entry(context, name) or {}
  1083. lastscore = lastlog.get('score', '')
  1084. lastcomments = lastlog.get('comments', '')
  1085. tpoints = q['total_points'](**args)
  1086. comments = (get_manual_grading_entry(context, name) or {}).get('comments',None)
  1087. if comments is not None:
  1088. comments = context['csm_language']._md_format_string(context, comments)
  1089. try:
  1090. score_output = lastscore * tpoints
  1091. except:
  1092. score_output = ""
  1093. if comments is not None:
  1094. ll = '<b>Score:</b> %s (out of %s)<br><br><b>Grader\'s Comments:</b><br/>%s' % (
  1095. score_output, tpoints, comments)
  1096. out += ll + "</div>"
  1097. if wrap and q.get('indiv', True) and args.get('csq_indiv', True):
  1098. out += '\n</div>'
  1099. if wrap:
  1100. out += '\n<!--END question %s -->\n' % args['csq_name']
  1101. return out
  1102. def nsubmits_left(context, name):
  1103. nused = context[_n('nsubmits_used')].get(name, 0)
  1104. q, args = context[_n('name_map')][name]
  1105. if not q.get('allow_submit', True) or not q.get('allow_self_submit', True):
  1106. return 0, ''
  1107. info = q.get('defaults', {})
  1108. info.update(args)
  1109. # look up 'nsubmits' in the question's arguments
  1110. # (fall back on default in qtype)
  1111. nsubmits = info.get('csq_nsubmits', None)
  1112. if nsubmits is None:
  1113. nsubmits = context.get('cs_nsubmits_default', float('inf'))
  1114. perms = context[_n('orig_perms')]
  1115. if 'submit' not in perms and 'submit_all' not in perms:
  1116. return 0, ''
  1117. nleft = max(0, nsubmits - nused)
  1118. for (regex, nchecks) in context['cs_user_info'].get('nsubmits_extra', []):
  1119. if re.match(regex, '.'.join(context['cs_path_info'][1:] + [name])):
  1120. nleft += nchecks
  1121. nmsg = context.get('cs_nsubmits_message', None)
  1122. if nmsg is None:
  1123. if nleft < float('inf'):
  1124. msg = "<i>You have %d submission%s remaining.</i>" % (nleft, 's'
  1125. if nleft != 1
  1126. else '')
  1127. else:
  1128. msg = "<i>You have infinitely-many submissions remaining.</i>"
  1129. else:
  1130. msg = nmsg(nsubmits, nused, nleft)
  1131. if 'submit_all' in perms:
  1132. msg = (
  1133. "As staff, you are always allowed to submit. "
  1134. "If you were a student, you would see the following:<br/>%s") % msg
  1135. return max(0, nleft), msg
  1136. def button_text(x, msg):
  1137. if x is None:
  1138. return msg
  1139. else:
  1140. return None
  1141. _button_map = {
  1142. 'submit': (submit_msg, 'Submit'),
  1143. 'save': (save_msg, 'Save'),
  1144. 'viewanswer': (viewanswer_msg, 'View Answer'),
  1145. 'clearanswer': (clearanswer_msg, 'Clear Answer'),
  1146. 'viewexplanation': (viewexp_msg, 'View Explanation'),
  1147. 'check': (check_msg, True),
  1148. }
  1149. def make_buttons(context, name):
  1150. uname = context[_n('uname')]
  1151. rp = context[_n('perms')] # the real user's perms
  1152. p = context[_n('orig_perms')] # the impersonated user's perms, if any
  1153. i = context[_n('impersonating')]
  1154. q, args = context[_n('name_map')][name]
  1155. nsubmits, _ = nsubmits_left(context, name)
  1156. buttons = {'copy_seed': None, 'copy': None, 'new_seed': None}
  1157. buttons['new_seed'] = ("New Random Seed" if 'submit_all' in p and
  1158. context.get('cs_random_inited', False) else None)
  1159. abuttons = {
  1160. 'copy_seed': ('Copy Random Seed'
  1161. if context.get('cs_random_inited', False) else None),
  1162. 'copy': 'Copy to My Account',
  1163. 'lock': None,
  1164. 'unlock': None
  1165. }
  1166. for (b, (func, text)) in list(_button_map.items()):
  1167. buttons[b] = button_text(func(context, p, name), text)
  1168. abuttons[b] = button_text(func(context, rp, name), text)
  1169. for d in (buttons, abuttons):
  1170. if d['check']:
  1171. d['check'] = q.get('checktext', 'Check')
  1172. if name in context[_n('locked')]:
  1173. abuttons['unlock'] = 'Unlock'
  1174. else:
  1175. abuttons['lock'] = 'Lock'
  1176. aout = ''
  1177. if i:
  1178. for k in {'submit', 'check', 'save'}:
  1179. if buttons[k] is not None:
  1180. abuttons[k] = None
  1181. elif abuttons[k] is not None:
  1182. abuttons[k] += ' (as %s)' % uname
  1183. for k in ('viewanswer', 'clearanswer', 'viewexplanation'):
  1184. if buttons[k] is not None:
  1185. abuttons[k] = None
  1186. elif abuttons[k] is not None:
  1187. abuttons[k] += ' (for %s)' % uname
  1188. aout = '<div><b><font color="red">Admin Buttons:</font></b><br/>'
  1189. for k in ('copy', 'copy_seed', 'check', 'save', 'submit', 'viewanswer',
  1190. 'viewexplanation', 'clearanswer', 'lock', 'unlock'):
  1191. x = {'b': abuttons[k], 'k': k, 'n': name}
  1192. if abuttons[k] is not None:
  1193. aout += (
  1194. '\n<button id="%(n)s_%(k)s" '
  1195. 'class="%(k)s" '
  1196. 'style="background-color: #FFD9D9; border-color: red;" '
  1197. 'onclick="catsoop.%(k)s(\'%(n)s\');">'
  1198. '%(b)s</button>') % x
  1199. # in manual grading mode, add a box and button for grading
  1200. gmode = _get(args, 'csq_grading_mode', 'auto', str)
  1201. if gmode == 'manual':
  1202. lastlog = get_manual_grading_entry(context, name) or {}
  1203. lastscore = lastlog.get('score', '')
  1204. lastcomments = lastlog.get('comments', '')
  1205. tpoints = q['total_points'](**args)
  1206. try:
  1207. output = lastscore * tpoints
  1208. except:
  1209. output = ""
  1210. aout += ('<br/><b><font color="red">Grading:</font></b>'
  1211. '<table border="0" width="100%%">'
  1212. '<tr><td align="right" width="30%%">'
  1213. '<font color="red">Points Earned (out of %2.2f):</font>'
  1214. '</td><td><input type="text" value="%s" size="5" '
  1215. 'style="border-color: red;" '
  1216. 'id="%s_grading_score" '
  1217. 'name="%s_grading_score" /></td></tr>'
  1218. '<tr><td align="right">'
  1219. '<font color="red">Comments:</font></td>'
  1220. '<td><textarea rows="5" id="%s_grading_comments" '
  1221. 'name="%s_grading_comments" '
  1222. 'style="width: 100%%; border-color: red;">'
  1223. '%s'
  1224. '</textarea></td></tr><tr><td></td><td>'
  1225. '<button class="grade" '
  1226. 'style="background-color: #FFD9D9; '
  1227. 'border-color: red;" '
  1228. 'onclick="catsoop.grade(\'%s\');">'
  1229. 'Submit Grade'
  1230. '</button></td></tr></table>') % (tpoints, output, name,
  1231. name, name, name,
  1232. lastcomments, name)
  1233. aout += '</div>'
  1234. out = ''
  1235. for k in ('check', 'save', 'submit', 'viewanswer', 'viewexplanation',
  1236. 'clearanswer', 'new_seed'):
  1237. x = {'b': buttons[k], 'k': k, 'n': name}
  1238. if buttons[k] is not None:
  1239. out += ('\n<button id="%(n)s_%(k)s" '
  1240. 'class="%(k)s" '
  1241. 'onclick="catsoop.%(k)s(\'%(n)s\');">'
  1242. '%(b)s</button>') % x
  1243. return out + aout
  1244. def pre_handle(context):
  1245. # enumerate the questions in this problem
  1246. context[_n('name_map')] = collections.OrderedDict()
  1247. qcount = 0
  1248. for elt in context['cs_problem_spec']:
  1249. if isinstance(elt, tuple):
  1250. m = elt[1]
  1251. context[_n('name_map')][m['csq_name']] = elt
  1252. # who is the user (and, who is being impersonated?)
  1253. user_info = context.get('cs_user_info', {})
  1254. uname = user_info.get('username', 'None')
  1255. real = user_info.get('real_user', user_info)
  1256. context[_n('role')] = real.get('role', 'None')
  1257. context[_n('section')] = real.get('section', None)
  1258. context[_n('perms')] = real.get('permissions', [])
  1259. context[_n('orig_perms')] = user_info.get('permissions', [])
  1260. context[_n('uname')] = uname
  1261. context[_n('real_uname')] = real.get('username', uname)
  1262. context[_n('impersonating')] = (
  1263. context[_n('uname')] != context[_n('real_uname')])
  1264. # store release and due dates
  1265. r = context[_n('rel')] = context['csm_tutor'].get_release_date(context)
  1266. d = context[_n('due')] = context['csm_tutor'].get_due_date(context)
  1267. n = context['csm_time'].from_detailed_timestamp(context['cs_timestamp'])
  1268. context[_n('now')] = n
  1269. context[_n('timing')] = -1 if n <= r else 0 if n <= d else 1
  1270. if _get(context, 'cs_require_activation', False, bool):
  1271. pwd = _get(context, 'cs_activation_password', 'password', str)
  1272. context[_n('activation_password')] = pwd
  1273. # determine the right log name to look up, and grab the most recent entry
  1274. loghead = '.'.join(context['cs_path_info'][1:])
  1275. ps_name = '%s.problemstate' % loghead
  1276. pa_name = '%s.problemactions' % loghead
  1277. pg_name = '%s.problemgrades' % loghead
  1278. grp_name = '%s.group' % loghead
  1279. ll = context['csm_cslog'].most_recent(context['cs_course'],
  1280. uname,
  1281. ps_name,
  1282. {})
  1283. _cs_group_path = context.get('cs_groups_to_use', context['cs_path_info'])
  1284. context[_n('all_groups')] = context['csm_groups'].list_groups(context,
  1285. context['cs_course'],
  1286. _cs_group_path)
  1287. context[_n('group')] = context['csm_groups'].get_group(context,
  1288. context['cs_course'],
  1289. _cs_group_path,
  1290. uname,
  1291. context[_n('all_groups')])
  1292. _ag = context[_n('all_groups')]
  1293. _g = context[_n('group')]
  1294. context[_n('group_members')] = _gm = _ag.get(_g[0], {}).get(_g[1], [])
  1295. if uname not in _gm:
  1296. _gm.append(uname)
  1297. context[_n('last_log')] = ll
  1298. context[_n('logname_state')] = ps_name
  1299. context[_n('logname_actions')] = pa_name
  1300. context[_n('logname_grades')] = pg_name
  1301. context[_n('logname_group')] = grp_name
  1302. context[_n('logname_groups')] = '%ss' % grp_name
  1303. context[_n('locked')] = ll.get('locked', set())
  1304. context[_n('answer_viewed')] = ll.get('answer_viewed', set())
  1305. context[_n('explanation_viewed')] = ll.get('explanation_viewed', set())
  1306. context[_n('nsubmits_used')] = ll.get('nsubmits_used', {})
  1307. # what is the user trying to do?
  1308. context[_n('action')] = context['cs_form'].get('action', 'view').lower()
  1309. if context[_n('action')] in ('view', 'activate',
  1310. 'passthrough', 'list_questions',
  1311. 'get_state', 'manage_groups',
  1312. 'render_single_question'):
  1313. context[_n('form')] = context['cs_form']
  1314. else:
  1315. names = context['cs_form'].get('names', "[]")
  1316. context[_n('question_names')] = json.loads(names)
  1317. context[_n('form')] = json.loads(context['cs_form'].get('data', '{}'))
  1318. def _get_auto_view(context):
  1319. # when should we automatically view the answer?
  1320. ava = context.get('csq_auto_viewanswer', False)
  1321. if ava is True:
  1322. ava = set(['nosubmits', 'perfect', 'lock'])
  1323. elif isinstance(ava, str):
  1324. ava = set([ava])
  1325. elif not ava:
  1326. ava = set()
  1327. return ava
  1328. def default_javascript(context):
  1329. namemap = context[_n('name_map')]
  1330. if 'submit_all' in context[_n('perms')]:
  1331. skip_alert = list(namemap.keys())
  1332. else:
  1333. skipper = 'csq_allow_submit_after_answer_viewed'
  1334. skip_alert = [name for (name, (q, args)) in list(namemap.items())
  1335. if _get(args, skipper, False, bool)]
  1336. out = '''
  1337. <script type="text/javascript" src="__HANDLER__/default/cs_ajax.js"></script>
  1338. <script type="text/javascript">
  1339. catsoop.all_questions = %(allqs)r;
  1340. catsoop.api_token = %(secret)s;
  1341. catsoop.this_path = %(path)r;
  1342. catsoop.path_info = %(pathinfo)r;
  1343. catsoop.course = %(course)s;
  1344. catsoop.url_root = %(root)r;
  1345. '''
  1346. if len(namemap) > 0:
  1347. out += '''catsoop.imp = %(imp)r;
  1348. catsoop.skip_alert = %(skipalert)s;
  1349. catsoop.viewans_confirm = "Are you sure? Viewing the answer will prevent any further submissions to this question. Press 'OK' to view the answer, or press 'Cancel' if you have changed your mind.";
  1350. '''
  1351. out += '</script>'
  1352. api_tok = 'null'
  1353. given_tok = context.get('cs_user_info', {}).get('api_token', None)
  1354. if given_tok is not None:
  1355. api_tok = repr(given_tok)
  1356. return out % {
  1357. 'skipalert': json.dumps(skip_alert),
  1358. 'allqs': list(context[_n('name_map')].keys()),
  1359. 'user': context[_n('real_uname')],
  1360. 'path': '/'.join([context['cs_url_root']] + context['cs_path_info']),
  1361. 'imp': context[_n('uname')] if context[_n('impersonating')] else '',
  1362. 'secret': api_tok,
  1363. 'course': repr(context['cs_course']) if context['cs_course'] else 'null',
  1364. 'pathinfo': context['cs_path_info'],
  1365. 'root': context['cs_url_root'],
  1366. }
  1367. def default_timer(context):
  1368. out = ''
  1369. if not _get(context, 'cs_auto_lock', False, bool):
  1370. return out
  1371. if len(context[_n('locked')]) >= len(context[_n('name_map')]):
  1372. return out
  1373. if context[_n('now')] > context[_n('due')]:
  1374. # view answers immediately if viewed past the due date
  1375. out += '\n<script type="text/javascript">'
  1376. out += "\ncatsoop.ajaxrequest(catsoop.all_questions,'lock');"
  1377. out += '\n</script>'
  1378. return out
  1379. else:
  1380. out += '\n<script type="text/javascript">'
  1381. out += ("\ncatsoop.timer_now = %d;"
  1382. "\ncatsoop.timer_due = %d;"
  1383. "\ncatsoop.time_url = %r;") % (
  1384. context['csm_time'].unix(context[_n('now')]),
  1385. context['csm_time'].unix(context[_n('due')]),
  1386. context['cs_url_root'] + '/cs_util/time')
  1387. out += '\n</script>'
  1388. out += ('<script type="text/javascript" '
  1389. 'src="__HANDLER__/default/cs_timer.js"></script>')
  1390. return out
  1391. def exc_message(context):
  1392. exc = traceback.format_exc()
  1393. exc = context['csm_errors'].clear_info(context, exc)
  1394. return ('<p><font color="red">'
  1395. '<b>CAT-SOOP ERROR:</b>'
  1396. '<pre>%s</pre></font>') % exc
  1397. def _get_scores(context):
  1398. section = str(context.get('cs_form', {}).get('section',
  1399. context.get('cs_user_info').get('section', 'default')))
  1400. util = context['csm_util']
  1401. usernames = util.list_all_users(context, context['cs_course'])
  1402. users = [
  1403. util.read_user_file(context, context['cs_course'], username, {})
  1404. for username in usernames
  1405. ]
  1406. no_section = context.get('cs_whdw_no_section', False)
  1407. students = [
  1408. user
  1409. for user in users
  1410. if user.get('role', None) in ['Student', 'SLA'] and (no_section or str(user.get('section', 'default')) == section)
  1411. ]
  1412. questions = context[_n('name_map')]
  1413. scores = collections.OrderedDict()
  1414. for name, question in questions.items():
  1415. if not context.get('cs_whdw_filter', lambda q: True)(question): continue
  1416. counts = {}
  1417. for student in students:
  1418. username = student.get('username', 'None')
  1419. log = context['csm_cslog'].most_recent(
  1420. context['cs_course'],
  1421. username,
  1422. '.'.join(context['cs_path_info'][1:] + ['problemstate']),
  1423. {},
  1424. )
  1425. score = log.get('scores', {}).get(name, None)
  1426. counts[username] = score
  1427. scores[name] = counts
  1428. return scores
  1429. def handle_stats(context):
  1430. perms = context['cs_user_info'].get('permissions', [])
  1431. if 'whdw' not in perms:
  1432. return 'You are not allowed to view this page.'
  1433. section = str(context.get('cs_form', {}).get('section',
  1434. context.get('cs_user_info').get('section', 'default')))
  1435. questions = context[_n('name_map')]
  1436. stats = collections.OrderedDict()
  1437. groups = context['csm_groups'].list_groups(
  1438. context,
  1439. context['cs_course'],
  1440. context['cs_path_info'][1:],
  1441. ).get(section, None)
  1442. if groups:
  1443. total = len(groups)
  1444. for name, scores in _get_scores(context).items():
  1445. counts = {
  1446. 'completed': 0,
  1447. 'attempted': 0,
  1448. 'not tried': 0,
  1449. }
  1450. for members in groups.values():
  1451. score = min(
  1452. (scores.get(member, None) for member in members),
  1453. key=lambda x: -1 if x is None else x,
  1454. )
  1455. if score is None:
  1456. counts['not tried'] += 1
  1457. elif score == 1:
  1458. counts['completed'] += 1
  1459. else:
  1460. counts['attempted'] += 1
  1461. stats[name] = counts
  1462. else:
  1463. total = 0
  1464. for name, scores in _get_scores(context).items():
  1465. counts = {
  1466. 'completed': 0,
  1467. 'attempted': 0,
  1468. 'not tried': 0,
  1469. }
  1470. for score in scores.values():
  1471. if score is None:
  1472. counts['not tried'] += 1
  1473. elif score == 1:
  1474. counts['completed'] += 1
  1475. else:
  1476. counts['attempted'] += 1
  1477. stats[name] = counts
  1478. total = max(total, sum(counts.values()))
  1479. BeautifulSoup = context['csm_tools'].bs4.BeautifulSoup
  1480. soup = BeautifulSoup('')
  1481. table = soup.new_tag('table')
  1482. table['class'] = 'table table-bordered'
  1483. header = soup.new_tag('tr')
  1484. for heading in ['name', 'completed', 'attempted', 'not tried']:
  1485. th = soup.new_tag('th')
  1486. th.string = heading
  1487. header.append(th)
  1488. table.append(header)
  1489. for name, counts in stats.items():
  1490. tr = soup.new_tag('tr')
  1491. td = soup.new_tag('td')
  1492. a = soup.new_tag('a',
  1493. href = '?section={}&action=whdw&question={}'.format(section, name),
  1494. )
  1495. qargs = questions[name][1]
  1496. a.string = qargs.get('csq_display_name', name)
  1497. td.append(a)
  1498. td['class'] = 'text-left'
  1499. tr.append(td)
  1500. for key in ['completed', 'attempted', 'not tried']:
  1501. td = soup.new_tag('td')
  1502. td.string = '{count}/{total} ({percent:.2%})'.format(
  1503. count = counts[key],
  1504. total = total,
  1505. percent = (counts[key] / total) if total != 0 else 0,
  1506. )
  1507. td['class'] = 'text-right'
  1508. tr.append(td)
  1509. table.append(tr)
  1510. soup.append(table)
  1511. return str(soup)
  1512. def _real_name(context, username):
  1513. return (context['csm_cslog'].most_recent(None, 'extra_info', username, None) or {}).get('name', None)
  1514. def _whdw_name(context, username):
  1515. real_name = _real_name(context, username)
  1516. if real_name:
  1517. return '{} (<a href="?as={}" target="_blank">{}</a>)'.format(real_name, username, username)
  1518. else:
  1519. return username
  1520. def handle_whdw(context):
  1521. perms = context['cs_user_info'].get('permissions', [])
  1522. if 'whdw' not in perms:
  1523. return 'You are not allowed to view this page.'
  1524. section = str(context.get('cs_form', {}).get('section',
  1525. context.get('cs_user_info').get('section', 'default')))
  1526. usernames = util.list_all_users(context, context['cs_course'])
  1527. users = [
  1528. util.read_user_file(context, context['cs_course'], username, {})
  1529. for username in usernames
  1530. ]
  1531. question = context['cs_form']['question']
  1532. qtype, qargs = context[_n('name_map')][question]
  1533. display_name = qargs.get('csq_display_name', qargs['csq_name'])
  1534. context['cs_content_header'] += ' | {}'.format(display_name)
  1535. scores = _get_scores(context)[question]
  1536. groups = context['csm_groups'].list_groups(
  1537. context,
  1538. context['cs_course'],
  1539. context['cs_path_info'][1:],
  1540. ).get(section, None)
  1541. BeautifulSoup = context['csm_tools'].bs4.BeautifulSoup
  1542. soup = BeautifulSoup('')
  1543. if groups:
  1544. css = soup.new_tag('style')
  1545. css.string = '''\
  1546. .whdw-cell {
  1547. border: 1px white solid;
  1548. }
  1549. .whdw-not-tried {
  1550. background-color: #ff6961;
  1551. color: black;
  1552. }
  1553. .whdw-attempted {
  1554. background-color: #ffb347;
  1555. color: black;
  1556. }
  1557. .whdw-completed {
  1558. background-color: #77dd77;
  1559. color: black;
  1560. }
  1561. .whdw-cell ul {
  1562. padding-left: 5px;
  1563. }
  1564. '''
  1565. soup.append(css)
  1566. grid = soup.new_tag('div')
  1567. grid['class'] = 'row'
  1568. for group, members in sorted(groups.items()):
  1569. min_score = min(
  1570. (scores.get(member, None) for member in members),
  1571. key=lambda x: -1 if x is None else x,
  1572. )
  1573. cell = soup.new_tag('div')
  1574. cell['class'] = 'col-sm-3 whdw-cell {}'.format({
  1575. None: 'whdw-not-tried',
  1576. 1: 'whdw-completed',
  1577. }.get(min_score, 'whdw-attempted'))
  1578. grid.append(cell)
  1579. header = soup.new_tag('div')
  1580. header['class'] = 'text-center'
  1581. header.string = '{}'.format(group)
  1582. cell.append(header)
  1583. people = soup.new_tag('ul')
  1584. header['class'] = 'text-center'
  1585. for member in members:
  1586. m = soup.new_tag('li')
  1587. name = soup.new_tag('span')
  1588. name.insert(1, BeautifulSoup(_whdw_name(context, member)))
  1589. m.append(name)
  1590. score = soup.new_tag('span')
  1591. score['class'] = 'pull-right'
  1592. score.string = str(scores.get(member, None))
  1593. m.append(score)
  1594. people.append(m)
  1595. cell.append(people)
  1596. soup.append(grid)
  1597. return str(soup)
  1598. else:
  1599. states = {
  1600. 'completed': [],
  1601. 'attempted': [],
  1602. 'not tried': [],
  1603. }
  1604. for username, score in scores.items():
  1605. if score is None:
  1606. state = 'not tried'
  1607. elif score == 1:
  1608. state = 'completed'
  1609. else:
  1610. state = 'attempted'
  1611. states[state].append(username)
  1612. for state in ['not tried', 'attempted', 'completed']:
  1613. usernames = states[state]
  1614. h3 = soup.new_tag('h3')
  1615. h3.string = '{} ({})'.format(state, len(states[state]))
  1616. soup.append(h3)
  1617. grid = soup.new_tag('div')
  1618. grid['class'] = 'row'
  1619. for username in sorted(usernames):
  1620. cell = soup.new_tag('div')
  1621. cell.insert(1, BeautifulSoup(_whdw_name(context, username)))
  1622. cell['class'] = 'col-sm-2'
  1623. grid.append(cell)
  1624. soup.append(grid)
  1625. return str(soup)