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.
 
 
 

2486 lines
84 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 re
  18. import json
  19. import time
  20. import uuid
  21. import random
  22. import shutil
  23. import string
  24. import hashlib
  25. import binascii
  26. import traceback
  27. import collections
  28. from bs4 import BeautifulSoup
  29. _prefix = "cs_defaulthandler_"
  30. def new_entry(context, qname, action):
  31. """
  32. Enqueue an asynchronous request to be processed (by the checker), e.g.
  33. a problem submission for grading.
  34. context = dict
  35. qname = question name / ID
  36. action = "check" or "submit"
  37. Returns uuid for the new queue entry.
  38. """
  39. id_ = str(uuid.uuid4())
  40. obj = {
  41. "path": context["cs_path_info"],
  42. "username": context.get("cs_username", "None"),
  43. "names": [qname],
  44. "form": {k: v for k, v in context[_n("form")].items() if qname in k},
  45. "time": time.time(),
  46. "action": action,
  47. }
  48. # add LTI data, if present in current session (needed for sending grade back to tool consumer)
  49. session = context["cs_session_data"]
  50. if session.get("is_lti_user"):
  51. obj["lti_data"] = session.get("lti_data")
  52. # safely save queue entry in database file (stage then mv)
  53. loc = os.path.join(context["cs_data_root"], "_logs", "_checker", "staging", id_)
  54. os.makedirs(os.path.dirname(loc), exist_ok=True)
  55. with open(loc, "wb") as f:
  56. f.write(context["csm_cslog"].prep(obj))
  57. newloc = os.path.join(
  58. context["cs_data_root"],
  59. "_logs",
  60. "_checker",
  61. "queued",
  62. "%s_%s" % (time.time(), id_),
  63. )
  64. shutil.move(loc, newloc)
  65. return id_
  66. def _n(n):
  67. return "%s%s" % (_prefix, n)
  68. def _unknown_handler(action):
  69. return lambda x: "Unknown Action: %s" % action
  70. def _get(context, key, default, cast=lambda x: x):
  71. v = context.get(key, default)
  72. return cast(v(context) if isinstance(v, collections.Callable) else v)
  73. def handle(context):
  74. # set some variables in context
  75. pre_handle(context)
  76. mode_handlers = {
  77. "view": handle_view,
  78. "submit": handle_submit,
  79. "check": handle_check,
  80. "save": handle_save,
  81. "viewanswer": handle_viewanswer,
  82. "clearanswer": handle_clearanswer,
  83. "viewexplanation": handle_viewexplanation,
  84. "content_only": handle_content_only,
  85. "raw_html": handle_raw_html,
  86. "copy": handle_copy,
  87. "copy_seed": handle_copy_seed,
  88. "activate": handle_activate,
  89. "lock": handle_lock,
  90. "unlock": handle_unlock,
  91. "grade": handle_grade,
  92. "passthrough": lambda c: "",
  93. "new_seed": handle_new_seed,
  94. "list_questions": handle_list_questions,
  95. "get_state": handle_get_state,
  96. "manage_groups": manage_groups,
  97. "render_single_question": handle_single_question,
  98. "stats": handle_stats,
  99. "whdw": handle_whdw,
  100. }
  101. action = context[_n("action")]
  102. return mode_handlers.get(action, _unknown_handler(action))(context)
  103. def handle_list_questions(context):
  104. types = {k: v[0]["qtype"] for k, v in context[_n("name_map")].items()}
  105. order = list(context[_n("name_map")])
  106. return make_return_json(context, {"order": order, "types": types}, [])
  107. def handle_get_state(context):
  108. ll = context[_n("last_log")]
  109. for i in ll:
  110. if isinstance(ll[i], set):
  111. ll[i] = list(ll[i])
  112. ll["scores"] = {}
  113. for k, v in ll.get("last_submit_id", {}).items():
  114. try:
  115. with open(
  116. os.path.join(
  117. context["cs_data_root"],
  118. "_logs",
  119. "_checker",
  120. "results",
  121. v[0],
  122. v[1],
  123. v,
  124. ),
  125. "rb",
  126. ) as f:
  127. row = context["csm_cslog"].unprep(f.read())
  128. except:
  129. row = None
  130. if row is None:
  131. ll["scores"][k] = 0.0
  132. else:
  133. ll["scores"][k] = row["score"] or 0.0
  134. return make_return_json(context, ll, [])
  135. def handle_single_question(context):
  136. lastlog = context[_n("last_log")]
  137. lastsubmit = lastlog.get("last_submit", {})
  138. qname = context["cs_form"].get("name", None)
  139. elt = context[_n("name_map")][qname]
  140. o = render_question(elt, context, lastsubmit, wrap=False)
  141. return ("200", "OK"), {"Content-type": "text/html"}, o
  142. def handle_copy_seed(context):
  143. if context[_n("impersonating")]:
  144. impersonated = context[_n("uname")]
  145. uname = context[_n("real_uname")]
  146. path = context["cs_path_info"]
  147. logname = "random_seed"
  148. stored = context["csm_cslog"].most_recent(impersonated, path, logname, None)
  149. context["csm_cslog"].update_log(uname, path, logname, stored)
  150. return handle_save(context)
  151. def _new_random_seed(n=100):
  152. try:
  153. return os.urandom(n)
  154. except:
  155. return "".join(random.choice(string.ascii_letters) for i in range(n))
  156. def handle_new_seed(context):
  157. uname = context[_n("uname")]
  158. context["csm_cslog"].update_log(
  159. uname, context["cs_path_info"], "random_seed", _new_random_seed()
  160. )
  161. # Rerender the questions
  162. names = context[_n("question_names")]
  163. outdict = {}
  164. for name in names:
  165. outdict[name] = {"rerender": "Please refresh the page"}
  166. return make_return_json(context, outdict)
  167. def handle_activate(context):
  168. submitted_pass = context[_n("form")].get("activation_password", "")
  169. if submitted_pass == context[_n("activation_password")]:
  170. newstate = dict(context[_n("last_log")])
  171. newstate["activated"] = True
  172. uname = context[_n("uname")]
  173. context["csm_cslog"].overwrite_log(
  174. uname, context["cs_path_info"], "problemstate", newstate
  175. )
  176. context[_n("last_log")] = newstate
  177. return handle_view(context)
  178. def handle_copy(context):
  179. if context[_n("impersonating")]:
  180. context[_n("uname")] = context[_n("real_uname")]
  181. ll = context["csm_cslog"].most_recent(
  182. context[_n("uname")], context["cs_path_info"], "problemstate", {}
  183. )
  184. context[_n("last_log")] = ll
  185. return handle_save(context)
  186. def handle_activation_form(context):
  187. context["cs_content_header"] = "Problem Activation"
  188. out = '<form method="POST">'
  189. out += (
  190. "\nActivation Password: "
  191. '<input type="text" '
  192. 'name="activation_password" '
  193. 'value="" />'
  194. "\n&nbsp;"
  195. '\n<input type="submit" '
  196. 'name="action" '
  197. 'value="Activate" />'
  198. )
  199. if "admin" in context[_n("perms")]:
  200. pwd = context[_n("activation_password")]
  201. out += (
  202. "\n<p><u>Staff:</u> password is " '<tt><font color="blue">%s</font></tt>'
  203. ) % pwd
  204. out += "</form>"
  205. p = context[_n("perms")]
  206. if "submit" in p or "submit_all" in p:
  207. log_action(context, {"action": "show_activation_form"})
  208. return out
  209. def handle_raw_html(context):
  210. # base function: display the problem
  211. perms = context[_n("perms")]
  212. lastlog = context[_n("last_log")]
  213. lastsubmit = lastlog.get("last_submit", {})
  214. if (
  215. _get(context, "cs_auth_required", True, bool)
  216. and "view" not in perms
  217. and "view_all" not in perms
  218. ):
  219. return "You are not allowed to view this page."
  220. if _get(context, "cs_require_activation", False, bool) and not lastlog.get(
  221. "activated", False
  222. ):
  223. return "You must activate this page first."
  224. due = context[_n("due")]
  225. timing = context[_n("timing")]
  226. if timing == -1 and ("view_all" not in perms):
  227. reltime = context["csm_time"].long_timestamp(context[_n("rel")])
  228. reltime = reltime.replace(";", " at")
  229. return (
  230. "This page is not yet available. " "It will become available on %s."
  231. ) % reltime
  232. page = ""
  233. num_questions = len(context[_n("name_map")])
  234. if (
  235. num_questions > 0
  236. and _get(context, "cs_show_due", True, bool)
  237. and context.get("cs_due_date", "NEVER") != "NEVER"
  238. ):
  239. duetime = context["csm_time"].long_timestamp(due)
  240. page += (
  241. "<tutoronly><center>"
  242. "The questions below are due on %s."
  243. "<br/>&nbsp;<br/></center></tutoronly>"
  244. ) % duetime
  245. for elt in context["cs_problem_spec"]:
  246. if isinstance(elt, str):
  247. page += elt
  248. else:
  249. # this is a question
  250. page += render_question(elt, context, lastsubmit)
  251. page += default_javascript(context)
  252. page += default_timer(context)
  253. context["cs_template"] = "BASE/templates/empty.template"
  254. return page
  255. def handle_content_only(context):
  256. # base function: display the problem
  257. perms = context[_n("perms")]
  258. lastlog = context[_n("last_log")]
  259. lastsubmit = lastlog.get("last_submit", {})
  260. if (
  261. _get(context, "cs_auth_required", True, bool)
  262. and "view" not in perms
  263. and "view_all" not in perms
  264. ):
  265. return "You are not allowed to view this page."
  266. if _get(context, "cs_require_activation", False, bool) and not lastlog.get(
  267. "activated", False
  268. ):
  269. return "You must activate this page first."
  270. due = context[_n("due")]
  271. timing = context[_n("timing")]
  272. if timing == -1 and ("view_all" not in perms):
  273. reltime = context["csm_time"].long_timestamp(context[_n("rel")])
  274. reltime = reltime.replace(";", " at")
  275. return (
  276. "This page is not yet available. " "It will become available on %s."
  277. ) % reltime
  278. page = ""
  279. num_questions = len(context[_n("name_map")])
  280. if (
  281. num_questions > 0
  282. and _get(context, "cs_show_due", True, bool)
  283. and context.get("cs_due_date", "NEVER") != "NEVER"
  284. ):
  285. duetime = context["csm_time"].long_timestamp(due)
  286. page += (
  287. "<tutoronly><center>"
  288. "The questions below are due on %s."
  289. "<br/>&nbsp;<br/></center></tutoronly>"
  290. ) % duetime
  291. for elt in context["cs_problem_spec"]:
  292. if isinstance(elt, str):
  293. page += elt
  294. else:
  295. # this is a question
  296. page += render_question(elt, context, lastsubmit)
  297. page += default_javascript(context)
  298. page += default_timer(context)
  299. context["cs_template"] = "BASE/templates/noborder.template"
  300. return page
  301. def handle_view(context):
  302. # base function: display the problem
  303. perms = context[_n("perms")]
  304. lastlog = context[_n("last_log")]
  305. lastsubmit = lastlog.get("last_submit", {})
  306. if (
  307. _get(context, "cs_auth_required", True, bool)
  308. and "view" not in perms
  309. and "view_all" not in perms
  310. ):
  311. return "You are not allowed to view this page."
  312. if _get(context, "cs_require_activation", False, bool) and not lastlog.get(
  313. "activated", False
  314. ):
  315. return handle_activation_form(context)
  316. due = context[_n("due")]
  317. timing = context[_n("timing")]
  318. if timing == -1 and ("view_all" not in perms):
  319. reltime = context["csm_time"].long_timestamp(context[_n("rel")])
  320. reltime = reltime.replace(";", " at")
  321. return (
  322. "This page is not yet available. " "It will become available on %s."
  323. ) % reltime
  324. page = ""
  325. num_questions = len(context[_n("name_map")])
  326. if (
  327. num_questions > 0
  328. and _get(context, "cs_show_due", True, bool)
  329. and context.get("cs_due_date", "NEVER") != "NEVER"
  330. ):
  331. duetime = context["csm_time"].long_timestamp(due)
  332. page += (
  333. "<tutoronly><center>"
  334. "The questions below are due on %s."
  335. "<br/>&nbsp;<br/></center></tutoronly>"
  336. ) % duetime
  337. js_loads = []
  338. for elt in context["cs_problem_spec"]:
  339. if isinstance(elt, str):
  340. page += elt
  341. else:
  342. # this is a question
  343. page += render_question(elt, context, lastsubmit)
  344. # handle javascript if necessary
  345. if "js_files" in elt[0]:
  346. a = elt[0].get("defaults", {})
  347. a.update(elt[1])
  348. js_loads.extend(elt[0]["js_files"](a))
  349. if js_loads:
  350. context["cs_scripts"] += (
  351. "\n\n <!--JS for questions-->\n "
  352. + "\n ".join(
  353. '<script type="text/javascript" src="%s"></script>'
  354. % context["csm_dispatch"].get_real_url(context, i)
  355. for i in js_loads
  356. )
  357. )
  358. page += default_javascript(context)
  359. page += default_timer(context)
  360. return page
  361. def get_manual_grading_entry(context, name):
  362. uname = context["cs_user_info"].get("username", "None")
  363. log = context["csm_cslog"].read_log(uname, context["cs_path_info"], "problemgrades")
  364. out = None
  365. for i in log:
  366. if i["qname"] == name:
  367. out = i
  368. return out
  369. def handle_clearanswer(context):
  370. names = context[_n("question_names")]
  371. due = context[_n("due")]
  372. lastlog = context[_n("last_log")]
  373. answerviewed = context[_n("answer_viewed")]
  374. explanationviewed = context[_n("explanation_viewed")]
  375. newstate = dict(lastlog)
  376. newstate["timestamp"] = context["cs_timestamp"]
  377. if "last_submit" not in newstate:
  378. newstate["last_submit"] = {}
  379. outdict = {} # dictionary containing the responses for each question
  380. for name in names:
  381. if name.startswith("__"):
  382. continue
  383. out = {}
  384. error = clearanswer_msg(context, context[_n("perms")], name)
  385. if error is not None:
  386. out["error_msg"] = error
  387. outdict[name] = out
  388. continue
  389. q, args = context[_n("name_map")][name]
  390. out["clear"] = True
  391. outdict[name] = out
  392. answerviewed.discard(name)
  393. explanationviewed.discard(name)
  394. newstate["answer_viewed"] = answerviewed
  395. newstate["explanation_viewed"] = explanationviewed
  396. # update problemstate log
  397. uname = context[_n("uname")]
  398. context["csm_cslog"].overwrite_log(
  399. uname, context["cs_path_info"], "problemstate", newstate
  400. )
  401. # log submission in problemactions
  402. duetime = context["csm_time"].detailed_timestamp(due)
  403. log_action(
  404. context,
  405. {
  406. "action": "viewanswer",
  407. "names": names,
  408. "score": newstate.get("score", 0.0),
  409. "response": outdict,
  410. "due_date": duetime,
  411. },
  412. )
  413. return make_return_json(context, outdict)
  414. def explanation_display(x):
  415. return "<hr /><p><b>Explanation:</b></p>%s" % x
  416. def handle_viewexplanation(context, outdict=None, skip_empty=False):
  417. """
  418. context: (dict) catsoop context
  419. outdict: (dict) output for each question, defaults to {}
  420. """
  421. names = context[_n("question_names")]
  422. due = context[_n("due")]
  423. lastlog = context[_n("last_log")]
  424. explanationviewed = context[_n("explanation_viewed")]
  425. loader = context["csm_loader"]
  426. language = context["csm_language"]
  427. newstate = dict(lastlog)
  428. newstate["timestamp"] = context["cs_timestamp"]
  429. if "last_submit" not in newstate:
  430. newstate["last_submit"] = {}
  431. outdict = outdict or {} # dictionary containing the responses for each question
  432. for name in names:
  433. if name.startswith("__"):
  434. continue
  435. out = outdict.get(name, {})
  436. q, args = context[_n("name_map")][name]
  437. if "csq_explanation" not in args and skip_empty:
  438. continue
  439. error = viewexp_msg(context, context[_n("perms")], name)
  440. if error is not None:
  441. out["error_msg"] = error
  442. outdict[name] = out
  443. continue
  444. exp = explanation_display(args["csq_explanation"])
  445. out["explanation"] = language.source_transform_string(context, exp)
  446. outdict[name] = out
  447. explanationviewed.add(name)
  448. newstate["explanation_viewed"] = explanationviewed
  449. # update problemstate log
  450. uname = context[_n("uname")]
  451. context["csm_cslog"].overwrite_log(
  452. uname, context["cs_path_info"], "problemstate", newstate
  453. )
  454. # log submission in problemactions
  455. duetime = context["csm_time"].detailed_timestamp(due)
  456. log_action(
  457. context,
  458. {
  459. "action": "viewanswer",
  460. "names": names,
  461. "score": newstate.get("score", 0.0),
  462. "response": outdict,
  463. "due_date": duetime,
  464. },
  465. )
  466. return make_return_json(context, outdict)
  467. def handle_viewanswer(context):
  468. names = context[_n("question_names")]
  469. due = context[_n("due")]
  470. lastlog = context[_n("last_log")]
  471. answerviewed = context[_n("answer_viewed")]
  472. loader = context["csm_loader"]
  473. language = context["csm_language"]
  474. newstate = dict(lastlog)
  475. newstate["timestamp"] = context["cs_timestamp"]
  476. if "last_submit" not in newstate:
  477. newstate["last_submit"] = {}
  478. outdict = {} # dictionary containing the responses for each question
  479. for name in names:
  480. if name.startswith("__"):
  481. continue
  482. out = {}
  483. error = viewanswer_msg(context, context[_n("perms")], name)
  484. if error is not None:
  485. out["error_msg"] = error
  486. outdict[name] = out
  487. continue
  488. q, args = context[_n("name_map")][name]
  489. # if we are here, no errors occurred. go ahead with checking.
  490. ans = q["answer_display"](**args)
  491. out["answer"] = language.source_transform_string(context, ans)
  492. outdict[name] = out
  493. answerviewed.add(name)
  494. newstate["answer_viewed"] = answerviewed
  495. # update problemstate log
  496. uname = context[_n("uname")]
  497. context["csm_cslog"].overwrite_log(
  498. uname, context["cs_path_info"], "problemstate", newstate
  499. )
  500. # log submission in problemactions
  501. duetime = context["csm_time"].detailed_timestamp(due)
  502. log_action(
  503. context,
  504. {
  505. "action": "viewanswer",
  506. "names": names,
  507. "score": newstate.get("score", 0.0),
  508. "response": outdict,
  509. "due_date": duetime,
  510. },
  511. )
  512. if context.get("cs_ui_config_flags", {}).get("auto_show_explanation_with_answer"):
  513. context[_n("last_log")] = newstate
  514. return handle_viewexplanation(context, outdict, skip_empty=True)
  515. return make_return_json(context, outdict)
  516. def handle_lock(context):
  517. names = context[_n("question_names")]
  518. due = context[_n("due")]
  519. lastlog = context[_n("last_log")]
  520. locked = context[_n("locked")]
  521. newstate = dict(lastlog)
  522. newstate["timestamp"] = context["cs_timestamp"]
  523. if "last_submit" not in newstate:
  524. newstate["last_submit"] = {}
  525. outdict = {} # dictionary containing the responses for each question
  526. for name in names:
  527. if name.startswith("__"):
  528. continue
  529. q, args = context[_n("name_map")][name]
  530. outdict[name] = {}
  531. locked.add(name)
  532. # automatically view the answer if the option is set
  533. if (
  534. "lock" in _get_auto_view(args)
  535. and q.get("allow_viewanswer", True)
  536. and _get(args, "csq_allow_viewanswer", True, bool)
  537. ):
  538. if name not in newstate.get("answer_viewed", set()):
  539. c = dict(context)
  540. c[_n("question_names")] = [name]
  541. o = json.loads(handle_viewanswer(c)[2])
  542. ll = context[_n("last_log")]
  543. newstate["answer_viewed"] = ll.get("answer_viewed", set())
  544. newstate["explanation_viewed"] = ll.get("explanation_viewed", set())
  545. outdict[name].update(o[name])
  546. newstate["locked"] = locked
  547. # update problemstate log
  548. uname = context[_n("uname")]
  549. context["csm_cslog"].overwrite_log(
  550. uname, context["cs_path_info"], "problemstate", newstate
  551. )
  552. # log submission in problemactions
  553. duetime = context["csm_time"].detailed_timestamp(due)
  554. log_action(
  555. context,
  556. {
  557. "action": "lock",
  558. "names": names,
  559. "score": newstate.get("score", 0.0),
  560. "response": outdict,
  561. "due_date": duetime,
  562. },
  563. )
  564. return make_return_json(context, outdict)
  565. def handle_grade(context):
  566. names = context[_n("question_names")]
  567. perms = context[_n("perms")]
  568. newentries = []
  569. outdict = {}
  570. for name in names:
  571. if name.endswith("_grading_score") or name.endswith("_grading_comments"):
  572. continue
  573. error = grade_msg(context, perms, name)
  574. if error is not None:
  575. outdict[name] = {"error_msg": error}
  576. continue
  577. q, args = context[_n("name_map")][name]
  578. npoints = float(q["total_points"](**args))
  579. try:
  580. f = context[_n("form")]
  581. rawscore = f.get("%s_grading_score" % name, "")
  582. comments = f.get("%s_grading_comments" % name, "")
  583. score = float(rawscore)
  584. except:
  585. outdict[name] = {
  586. "error_msg": "Invalid score: %s\n%s" % (rawscore, comments)
  587. }
  588. continue
  589. newentries.append(
  590. {
  591. "qname": name,
  592. "grader": context[_n("real_uname")],
  593. "score": score / npoints,
  594. "comments": comments,
  595. "timestamp": context["cs_timestamp"],
  596. }
  597. )
  598. _, args = context[_n("name_map")][name]
  599. outdict[name] = {
  600. "score_display": context["csm_tutor"].make_score_display(
  601. context, args, name, score / npoints, last_log=context[_n("last_log")]
  602. ),
  603. "message": "<b>Grader's Comments:</b><br/><br/>%s"
  604. % context["csm_language"]._md_format_string(context, comments),
  605. "score": score / npoints,
  606. }
  607. # update problemstate log
  608. uname = context[_n("uname")]
  609. for i in newentries:
  610. context["csm_cslog"].update_log(
  611. uname, context["cs_path_info"], "problemgrades", i
  612. )
  613. # log submission in problemactions
  614. log_action(
  615. context,
  616. {
  617. "action": "grade",
  618. "names": names,
  619. "scores": newentries,
  620. "grader": context[_n("real_uname")],
  621. },
  622. )
  623. return make_return_json(context, outdict, names=list(outdict.keys()))
  624. def handle_unlock(context):
  625. names = context[_n("question_names")]
  626. due = context[_n("due")]
  627. lastlog = context[_n("last_log")]
  628. locked = context[_n("locked")]
  629. newstate = dict(lastlog)
  630. newstate["timestamp"] = context["cs_timestamp"]
  631. if "last_submit" not in newstate:
  632. newstate["last_submit"] = {}
  633. outdict = {} # dictionary containing the responses for each question
  634. for name in names:
  635. q, args = context[_n("name_map")][name]
  636. outdict[name] = {}
  637. locked.remove(name)
  638. newstate["locked"] = locked
  639. # update problemstate log
  640. uname = context[_n("uname")]
  641. context["csm_cslog"].overwrite_log(
  642. uname, context["cs_path_info"], "problemstate", newstate
  643. )
  644. # log submission in problemactions
  645. duetime = context["csm_time"].detailed_timestamp(due)
  646. log_action(
  647. context,
  648. {
  649. "action": "unlock",
  650. "names": names,
  651. "score": newstate.get("score", 0.0),
  652. "response": outdict,
  653. "due_date": duetime,
  654. },
  655. )
  656. return make_return_json(context, outdict)
  657. def handle_save(context):
  658. names = context[_n("question_names")]
  659. due = context[_n("due")]
  660. lastlog = context[_n("last_log")]
  661. newstate = dict(lastlog)
  662. newstate["timestamp"] = context["cs_timestamp"]
  663. if "last_submit" not in newstate:
  664. newstate["last_submit"] = {}
  665. outdict = {} # dictionary containing the responses for each question
  666. saved_names = []
  667. for name in names:
  668. sub = context[_n("form")].get(name, "")
  669. out = {}
  670. if name.startswith("__"):
  671. newstate["last_submit"][name] = sub
  672. continue
  673. error = save_msg(context, context[_n("perms")], name)
  674. if error is not None:
  675. out["error_msg"] = error
  676. outdict[name] = out
  677. continue
  678. question, args = context[_n("name_map")].get(name)
  679. saved_names.append(name)
  680. # if we are here, no errors occurred. go ahead with checking.
  681. newstate["last_submit"][name] = sub
  682. rerender = args.get("csq_rerender", question.get("always_rerender", False))
  683. if rerender is True:
  684. out["rerender"] = context["csm_language"].source_transform_string(
  685. context, args.get("csq_prompt", "")
  686. )
  687. out["rerender"] += question["render_html"](newstate["last_submit"], **args)
  688. elif rerender:
  689. out["rerender"] = rerender
  690. out["score_display"] = ""
  691. out["message"] = ""
  692. outdict[name] = out
  693. # cache responses
  694. if "score_displays" not in newstate:
  695. newstate["score_displays"] = {}
  696. if "cached_responses" not in newstate:
  697. newstate["cached_responses"] = {}
  698. newstate["score_displays"][name] = out["score_display"]
  699. newstate["cached_responses"][name] = out["message"]
  700. # update problemstate log
  701. if len(saved_names) > 0:
  702. uname = context[_n("uname")]
  703. context["csm_cslog"].overwrite_log(
  704. uname, context["cs_path_info"], "problemstate", newstate
  705. )
  706. # log submission in problemactions
  707. duetime = context["csm_time"].detailed_timestamp(due)
  708. subbed = {n: context[_n("form")].get(n, "") for n in saved_names}
  709. log_action(
  710. context,
  711. {
  712. "action": "save",
  713. "names": saved_names,
  714. "submitted": subbed,
  715. "score": newstate.get("score", 0.0),
  716. "response": outdict,
  717. "due_date": duetime,
  718. },
  719. )
  720. return make_return_json(context, outdict)
  721. def handle_check(context):
  722. names = context[_n("question_names")]
  723. due = context[_n("due")]
  724. lastlog = context[_n("last_log")]
  725. namemap = context[_n("name_map")]
  726. newstate = dict(lastlog)
  727. newstate["timestamp"] = context["cs_timestamp"]
  728. if "last_submit" not in newstate:
  729. newstate["last_submit"] = {}
  730. names_done = set()
  731. outdict = {} # dictionary containing the responses for each question
  732. entry_ids = {}
  733. if "checker_ids" not in newstate:
  734. newstate["checker_ids"] = {}
  735. if "last_submit" not in newstate:
  736. newstate["last_submit"] = {}
  737. if "last_submit_id" not in newstate:
  738. newstate["last_submit_id"] = {}
  739. if "cached_responses" not in newstate:
  740. newstate["cached_responses"] = {}
  741. if "extra_data" not in newstate:
  742. newstate["extra_data"] = {}
  743. if "score_displays" not in newstate:
  744. newstate["score_displays"] = {}
  745. for name in names:
  746. if name.startswith("__"):
  747. name = name[2:].rsplit("_", 1)[0]
  748. if name in names_done:
  749. continue
  750. out = {}
  751. sub = context[_n("form")].get(name, "")
  752. error = check_msg(context, context[_n("perms")], name)
  753. if error is not None:
  754. out["error_msg"] = error
  755. outdict[name] = out
  756. submit_succeeded = False
  757. continue
  758. # if we are here, no errors occurred. go ahead with checking.
  759. newstate["last_submit"][name] = sub
  760. question, args = namemap[name]
  761. grading_mode = _get(args, "csq_grading_mode", "auto", str)
  762. if grading_mode == "legacy":
  763. try:
  764. msg = question["handle_check"](context[_n("form")], **args)
  765. except:
  766. msg = exc_message(context)
  767. out["score_display"] = ""
  768. out["message"] = context["csm_language"].handle_custom_tags(context, msg)
  769. if name in newstate.get("checker_ids", {}):
  770. del newstate["checker_ids"][name]
  771. newstate["cached_responses"][name] = out["message"]
  772. newstate["score_displays"][name] = ""
  773. else:
  774. magic = new_entry(context, name, "check")
  775. entry_ids[name] = entry_id = magic
  776. rerender = args.get("csq_rerender", question.get("always_rerender", False))
  777. if rerender is True:
  778. out["rerender"] = question["render_html"](
  779. newstate["last_submit"], **args
  780. )
  781. elif rerender:
  782. out["rerender"] = rerender
  783. out["score_display"] = ""
  784. out["message"] = WEBSOCKET_RESPONSE % {
  785. "name": name,
  786. "magic": entry_id,
  787. "websocket": context["cs_checker_websocket"],
  788. "loading": context["cs_loading_image"],
  789. "id_css": (
  790. ' style="display:none;"'
  791. if context.get("cs_show_submission_id", True)
  792. else ""
  793. ),
  794. }
  795. out["magic"] = entry_id
  796. # cache responses
  797. newstate["checker_ids"][name] = entry_id
  798. newstate["score_displays"][name] = ""
  799. if name in newstate.get("cached_responses", {}):
  800. del newstate["cached_responses"][name]
  801. outdict[name] = out
  802. # update problemstate log
  803. uname = context[_n("uname")]
  804. context["csm_cslog"].overwrite_log(
  805. uname, context["cs_path_info"], "problemstate", newstate
  806. )
  807. # log submission in problemactions
  808. duetime = context["csm_time"].detailed_timestamp(due)
  809. subbed = {n: context[_n("form")].get(n, "") for n in names}
  810. log_action(
  811. context,
  812. {
  813. "action": "check",
  814. "names": names,
  815. "submitted": subbed,
  816. "checker_ids": entry_ids,
  817. "due_date": duetime,
  818. },
  819. )
  820. return make_return_json(context, outdict)
  821. def handle_submit(context):
  822. names = context[_n("question_names")]
  823. due = context[_n("due")]
  824. uname = context[_n("uname")]
  825. lastlog = context[_n("last_log")]
  826. nsubmits_used = context[_n("nsubmits_used")]
  827. namemap = context[_n("name_map")]
  828. newstate = dict(lastlog)
  829. newstate["last_submit_times"] = newstate.get("last_submit_times", {})
  830. newstate["timestamp"] = context["cs_timestamp"]
  831. if "last_submit" not in newstate:
  832. newstate["last_submit"] = {}
  833. if "last_submit_id" not in newstate:
  834. newstate["last_submit_id"] = {}
  835. if "cached_responses" not in newstate:
  836. newstate["cached_responses"] = {}
  837. if "checker_ids" not in newstate:
  838. newstate["checker_ids"] = {}
  839. if "extra_data" not in newstate:
  840. newstate["extra_data"] = {}
  841. if "score_displays" not in newstate:
  842. newstate["score_displays"] = {}
  843. names_done = set()
  844. outdict = {} # dictionary containing the responses for each question
  845. # here, we don't do a whole lot. we log a submission and add it to the
  846. # checker's queue.
  847. entry_ids = {}
  848. submit_succeeded = True
  849. scores = {}
  850. messages = {}
  851. for name in names:
  852. sub = context[_n("form")].get(name, "")
  853. if name.startswith("__"):
  854. newstate["last_submit"][name] = sub
  855. name = name[2:].rsplit("_", 1)[0]
  856. if name in names_done:
  857. continue
  858. names_done.add(name)
  859. out = {}
  860. error = submit_msg(context, context[_n("perms")], name)
  861. if error is not None:
  862. out["error_msg"] = error
  863. outdict[name] = out
  864. submit_succeeded = False
  865. continue
  866. newstate["last_submit"][name] = sub
  867. newstate["last_submit_times"][name] = context["cs_timestamp"]
  868. # if we are here, no errors occurred. go ahead with submitting.
  869. nsubmits_used[name] = nsubmits_used.get(name, 0) + 1
  870. question, args = namemap[name]
  871. grading_mode = _get(args, "csq_grading_mode", "auto", str)
  872. if grading_mode == "auto":
  873. # 'auto' grading mode is the default. sends things to the
  874. # asynchronous checker to be run.
  875. magic = new_entry(context, name, "submit")
  876. entry_ids[name] = entry_id = magic
  877. out["message"] = WEBSOCKET_RESPONSE % {
  878. "name": name,
  879. "magic": entry_id,
  880. "websocket": context["cs_checker_websocket"],
  881. "loading": context["cs_loading_image"],
  882. "id_css": (
  883. ' style="display:none;"'
  884. if context.get("cs_show_submission_id", True)
  885. else ""
  886. ),
  887. }
  888. out["magic"] = entry_id
  889. out["score_display"] = ""
  890. if name in newstate["cached_responses"]:
  891. del newstate["cached_responses"][name]
  892. newstate["checker_ids"][name] = entry_id
  893. newstate["last_submit_id"][name] = entry_id
  894. elif grading_mode == "legacy":
  895. # 'legacy' grading mode implements the old behavior: check the
  896. # submission and cache the result, all within this request.
  897. if name in newstate["checker_ids"]:
  898. del newstate["checker_ids"][name]
  899. try:
  900. resp = question["handle_submission"](context[_n("form")], **args)
  901. score = resp["score"]
  902. msg = context["csm_language"].handle_custom_tags(context, resp["msg"])
  903. extra = resp.get("extra_data", None)
  904. except:
  905. resp = {}
  906. score = 0.0
  907. msg = exc_message(context)
  908. extra = None
  909. out["score"] = scores[name] = newstate.setdefault("scores", {})[
  910. name
  911. ] = score
  912. out["message"] = messages[name] = newstate["cached_responses"][name] = msg
  913. out["score_display"] = context["csm_tutor"].make_score_display(
  914. context,
  915. args,
  916. name,
  917. score,
  918. assume_submit=True,
  919. last_log=context[_n("last_log")],
  920. )
  921. newstate["extra_data"][name] = out["extra_data"] = extra
  922. # auto lock if the option is set.
  923. if resp.get("lock", False):
  924. c = dict(context)
  925. c[_n("question_names")] = [name]
  926. o = json.loads(handle_lock(c)[2])
  927. ll = context[_n("last_log")]
  928. newstate["locked"] = ll.get("locked", set())
  929. outdict[name].update(o[name])
  930. # auto view answer if the option is set
  931. if "submit_all" not in context[_n("orig_perms")]:
  932. x = nsubmits_left(context, name)
  933. if question.get("allow_viewanswer", True) and (
  934. (
  935. (out["score"] == 1 and "perfect" in _get_auto_view(args))
  936. or (x[0] == 0 and "nosubmits" in _get_auto_view(args))
  937. )
  938. and _get(args, "csq_allow_viewanswer", True, bool)
  939. ):
  940. # this is a hack...
  941. c = dict(context)
  942. c[_n("question_names")] = [name]
  943. o = json.loads(handle_viewanswer(c)[2])
  944. ll = context[_n("last_log")]
  945. newstate["answer_viewed"] = ll.get("answer_viewed", set())
  946. newstate["explanation_viewed"] = ll.get("explanation_viewed", set())
  947. outdict[name].update(o[name])
  948. elif grading_mode == "manual":
  949. # submitted for manual grading.
  950. out["message"] = "Submission received for manual grading."
  951. out["score_display"] = context["csm_tutor"].make_score_display(
  952. context,
  953. args,
  954. name,
  955. None,
  956. assume_submit=True,
  957. last_log=context[_n("last_log")],
  958. )
  959. if name in newstate["checker_ids"]:
  960. del newstate["checker_ids"][name]
  961. newstate["cached_responses"][name] = out["message"]
  962. else:
  963. out["message"] = (
  964. '<font color="red">Unknown grading mode: %s. Please contact staff.</font>'
  965. % grading_mode
  966. )
  967. out["score_display"] = context["csm_tutor"].make_score_display(
  968. context,
  969. args,
  970. name,
  971. 0.0,
  972. assume_submit=True,
  973. last_log=context[_n("last_log")],
  974. )
  975. if name in newstate["checker_ids"]:
  976. del newstate["checker_ids"][name]
  977. newstate["cached_responses"][name] = out["message"]
  978. if submit_succeeded:
  979. newstate["last_submit_time"] = context["cs_timestamp"]
  980. rerender = args.get("csq_rerender", question.get("always_rerender", False))
  981. if rerender is True:
  982. out["rerender"] = question["render_html"](newstate["last_submit"], **args)
  983. elif rerender:
  984. out["rerender"] = rerender
  985. outdict[name] = out
  986. # cache responses
  987. newstate["score_displays"][name] = out["score_display"]
  988. context[_n("nsubmits_used")] = newstate["nsubmits_used"] = nsubmits_used
  989. # update problemstate log
  990. context["csm_cslog"].overwrite_log(
  991. uname, context["cs_path_info"], "problemstate", newstate
  992. )
  993. # log submission in problemactions
  994. duetime = context["csm_time"].detailed_timestamp(due)
  995. subbed = {n: context[_n("form")].get(n, "") for n in names}
  996. log_action(
  997. context,
  998. {
  999. "action": "submit",
  1000. "names": names,
  1001. "submitted": subbed,
  1002. "checker_ids": entry_ids or None,
  1003. "scores": scores or None,
  1004. "messages": messages or None,
  1005. "due_date": duetime,
  1006. },
  1007. )
  1008. context["csm_loader"].run_plugins(
  1009. context, context["cs_course"], "post_submit", context
  1010. )
  1011. return make_return_json(context, outdict)
  1012. def manage_groups(context):
  1013. # displays the screen to admins who are adjusting groups
  1014. if context["cs_light_color"] is None:
  1015. context["cs_light_color"] = compute_light_color(context["cs_base_color"])
  1016. perms = context["cs_user_info"].get("permissions", [])
  1017. if "groups" not in perms and "admin" not in perms:
  1018. return "You are not allowed to view this page."
  1019. # show the main partnering page
  1020. section = context["cs_user_info"].get("section", None)
  1021. default_section = context.get("cs_default_section", "default")
  1022. all_sections = context.get("cs_sections", [])
  1023. if len(all_sections) == 0:
  1024. all_sections = {default_section: "Default Section"}
  1025. if section is None:
  1026. section = default_section
  1027. hdr = "Group Assignments for %s, Section " '<span id="cs_groups_section">%s</span>'
  1028. hdr %= (context["cs_original_path"], section)
  1029. context["cs_content_header"] = hdr
  1030. # menu for choosing section to display
  1031. out = '\nShow Current Groups for Section:\n<select name="section" id="section">'
  1032. for i in sorted(all_sections):
  1033. s = " selected" if str(i) == str(section) else ""
  1034. out += '\n<option value="%s"%s>%s</option>' % (i, s, i)
  1035. out += "\n</select>"
  1036. # empty table that will eventually be populated with groups
  1037. out += (
  1038. "\n<p>\n<h2>Groups:</h2>"
  1039. '\n<div id="cs_groups_table" border="1" align="left">'
  1040. "\nLoading..."
  1041. "\n</div>"
  1042. )
  1043. # create partnership from two students
  1044. out += (
  1045. "\n<p>\n<h2>Make New Partnership:</h2>"
  1046. '\nStudent 1: <select name="cs_groups_name1" id="cs_groups_name1">'
  1047. "</select>&nbsp;"
  1048. '\nStudent 2: <select name="cs_groups_name2" id="cs_groups_name2">'
  1049. "</select>&nbsp;"
  1050. '\n<button class="btn btn-catsoop" id="cs_groups_newpartners">Partner Students</button>'
  1051. "</p>"
  1052. )
  1053. # add a student to a group
  1054. out += (
  1055. "\n<p>\n<h2>Add Student to Group:</h2>"
  1056. '\nStudent: <select name="cs_groups_nameadd" id="cs_groups_nameadd">'
  1057. "</select>&nbsp;"
  1058. '\nGroup: <select name="cs_groups_groupadd" id="cs_groups_groupadd">'
  1059. "</select>&nbsp;"
  1060. '\n<button class="btn btn-catsoop" id="cs_groups_addtogroup">Add to Group</button></p>'
  1061. )
  1062. # randomly assign all groups. this needs to be sufficiently scary...
  1063. out += (
  1064. "\n<p><h2>Randomly assign groups</h2>"
  1065. '\n<button class="btn btn-catsoop" id="cs_groups_reassign">Reassign Groups</button></p>'
  1066. )
  1067. all_group_names = context.get("cs_group_names", None)
  1068. if all_group_names is None:
  1069. all_group_names = map(str, range(100))
  1070. else:
  1071. all_group_names = sorted(all_group_names)
  1072. all_group_names = list(all_group_names)
  1073. out += (
  1074. '\n<script type="text/javascript">'
  1075. "\n// @license magnet:?xt=urn:btih:0b31508aeb0634b347b8270c7bee4d411b5d4109&dn=agpl-3.0.txt AGPL-v3"
  1076. "\ncatsoop.group_names = %s"
  1077. "\n// @license-end"
  1078. "\n</script>"
  1079. ) % all_group_names
  1080. out += (
  1081. '\n<script type="text/javascript" src="_handler/default/cs_groups.js"></script>'
  1082. )
  1083. return out + default_javascript(context)
  1084. def clearanswer_msg(context, perms, name):
  1085. namemap = context[_n("name_map")]
  1086. timing = context[_n("timing")]
  1087. ansviewed = context[_n("answer_viewed")]
  1088. i = context[_n("impersonating")]
  1089. _, qargs = namemap[name]
  1090. error = None
  1091. if "submit" not in perms and "submit_all" not in perms:
  1092. error = (
  1093. "You are not allowed undo your viewing of " "the answer to this question."
  1094. )
  1095. elif name not in ansviewed:
  1096. error = "You have not viewed the answer for this question."
  1097. elif name not in namemap:
  1098. error = (
  1099. "No question with name %s. " "Please refresh before submitting."
  1100. ) % name
  1101. elif "submit_all" not in perms:
  1102. if timing == -1 and not i:
  1103. error = "This question is not yet available."
  1104. if not qargs.get("csq_allow_submit_after_answer_viewed", False):
  1105. error = (
  1106. "You are not allowed to undo your viewing of "
  1107. "the answer to this question."
  1108. )
  1109. return error
  1110. def viewexp_msg(context, perms, name):
  1111. namemap = context[_n("name_map")]
  1112. timing = context[_n("timing")]
  1113. ansviewed = context[_n("answer_viewed")]
  1114. expviewed = context[_n("explanation_viewed")]
  1115. _, qargs = namemap[name]
  1116. error = None
  1117. if "submit" not in perms and "submit_all" not in perms:
  1118. error = "You are not allowed to view the answer to this question."
  1119. elif name not in ansviewed:
  1120. error = "You have not yet viewed the answer for this question."
  1121. elif name in expviewed:
  1122. error = "You have already viewed the explanation for this question."
  1123. elif name not in namemap:
  1124. error = (
  1125. "No question with name %s. " "Please refresh before submitting."
  1126. ) % name
  1127. elif ("submit_all" not in perms) and timing == -1:
  1128. error = "This question is not yet available."
  1129. elif not _get(qargs, "csq_allow_viewexplanation", True, bool):
  1130. error = "Viewing explanations is not allowed for this question."
  1131. else:
  1132. q, args = namemap[name]
  1133. if "csq_explanation" not in args:
  1134. error = "No explanation supplied for this question."
  1135. return error
  1136. def viewanswer_msg(context, perms, name):
  1137. namemap = context[_n("name_map")]
  1138. timing = context[_n("timing")]
  1139. ansviewed = context[_n("answer_viewed")]
  1140. i = context[_n("impersonating")]
  1141. _, qargs = namemap[name]
  1142. error = None
  1143. if not _.get("allow_viewanswer", True):
  1144. error = "You cannot view the answer to this type of question."
  1145. elif "submit" not in perms and "submit_all" not in perms:
  1146. error = "You are not allowed to view the answer to this question."
  1147. elif name in ansviewed:
  1148. error = "You have already viewed the answer for this question."
  1149. elif name not in namemap:
  1150. error = (
  1151. "No question with name %s. " "Please refresh before submitting."
  1152. ) % name
  1153. elif "submit_all" not in perms:
  1154. if timing == -1 and not i:
  1155. error = "This question is not yet available."
  1156. elif not _get(qargs, "csq_allow_viewanswer", True, bool):
  1157. error = "Viewing the answer is not allowed for this question."
  1158. return error
  1159. def save_msg(context, perms, name):
  1160. namemap = context[_n("name_map")]
  1161. timing = context[_n("timing")]
  1162. i = context[_n("impersonating")]
  1163. _, qargs = namemap[name]
  1164. error = None
  1165. if not _.get("allow_save", True):
  1166. error = "You cannot save this type of question."
  1167. elif "submit" not in perms and "submit_all" not in perms:
  1168. error = "You are not allowed to check answers to this question."
  1169. elif name not in namemap:
  1170. error = (
  1171. "No question with name %s. " "Please refresh before submitting."
  1172. ) % name
  1173. elif "submit_all" not in perms:
  1174. if timing == -1 and not i:
  1175. error = "This question is not yet available."
  1176. elif name in context[_n("locked")]:
  1177. error = (
  1178. "You are not allowed to save for this question (it has been locked)."
  1179. )
  1180. elif (
  1181. not _get(qargs, "csq_allow_submit_after_answer_viewed", False, bool)
  1182. and name in context[_n("answer_viewed")]
  1183. ):
  1184. error = (
  1185. "You are not allowed to save to this question after viewing the answer."
  1186. )
  1187. elif timing == 1 and _get(context, "cs_auto_lock", False, bool):
  1188. error = (
  1189. "You are not allowed to save after the " "deadline for this question."
  1190. )
  1191. elif not _get(qargs, "csq_allow_save", True, bool):
  1192. error = "Saving is not allowed for this question."
  1193. return error
  1194. def check_msg(context, perms, name):
  1195. namemap = context[_n("name_map")]
  1196. timing = context[_n("timing")]
  1197. i = context[_n("impersonating")]
  1198. _, qargs = namemap[name]
  1199. error = None
  1200. if "submit" not in perms and "submit_all" not in perms:
  1201. error = "You are not allowed to check answers to this question."
  1202. elif name not in namemap:
  1203. error = (
  1204. "No question with name %s. " "Please refresh before submitting."
  1205. ) % name
  1206. elif namemap[name][0].get("handle_check", None) is None:
  1207. error = "This question type does not support checking."
  1208. elif "submit_all" not in perms:
  1209. if timing == -1 and not i:
  1210. error = "This question is not yet available."
  1211. elif name in context[_n("locked")]:
  1212. error = "You are not allowed to check answers to this question."
  1213. elif (
  1214. not _get(qargs, "csq_allow_submit_after_answer_viewed", False, bool)
  1215. and name in context[_n("answer_viewed")]
  1216. ):
  1217. error = "You are not allowed to check answers to this question after viewing the answer."
  1218. elif timing == 1 and _get(context, "cs_auto_lock", False, bool):
  1219. error = (
  1220. "You are not allowed to check after the " "deadline for this problem."
  1221. )
  1222. elif not _get(qargs, "csq_allow_check", True, bool):
  1223. error = "Checking is not allowed for this question."
  1224. return error
  1225. def grade_msg(context, perms, name):
  1226. namemap = context[_n("name_map")]
  1227. _, qargs = namemap[name]
  1228. if "grade" not in perms:
  1229. return "You are not allowed to grade exercises."
  1230. def submit_msg(context, perms, name):
  1231. if name.startswith("__"):
  1232. name = name[2:].rsplit("_", 1)[0]
  1233. namemap = context[_n("name_map")]
  1234. timing = context[_n("timing")]
  1235. i = context[_n("impersonating")]
  1236. _, qargs = namemap[name]
  1237. error = None
  1238. if not _.get("allow_submit", True):
  1239. error = "You cannot submit this type of question."
  1240. if (not _.get("allow_self_submit", True)) and "real_user" not in context[
  1241. "cs_user_info"
  1242. ]:
  1243. error = "You cannot submit this type of question yourself."
  1244. elif "submit" not in perms and "submit_all" not in perms:
  1245. error = "You are not allowed to submit answers to this question."
  1246. elif name not in namemap:
  1247. error = (
  1248. "No question with name %s. " "Please refresh before submitting."
  1249. ) % name
  1250. elif "submit_all" not in perms:
  1251. # don't allow if...
  1252. if timing == -1 and not i:
  1253. # ...the problem has not yet been released
  1254. error = "This question is not yet open for submissions."
  1255. elif _get(context, "cs_auto_lock", False, bool) and timing == 1:
  1256. # ...the problem auto locks and it is after the due date
  1257. error = (
  1258. "Submissions are not allowed after the " "deadline for this question"
  1259. )
  1260. elif name in context[_n("locked")]:
  1261. error = "You are not allowed to submit to this question."
  1262. elif (
  1263. not _get(qargs, "csq_allow_submit_after_answer_viewed", False, bool)
  1264. and name in context[_n("answer_viewed")]
  1265. ):
  1266. # ...the answer has been viewed and submissions after
  1267. # viewing the answer are not allowed
  1268. error = (
  1269. "You are not allowed to submit to this question "
  1270. "because you have already viewed the answer."
  1271. )
  1272. elif not _get(qargs, "csq_allow_submit", True, bool):
  1273. # ...submissions are not allowed (custom message)
  1274. if "csq_nosubmit_message" in qargs:
  1275. if context[_n("action")] != "view":
  1276. error = qargs["csq_nosubmit_message"](qargs)
  1277. else:
  1278. error = "Submissions are not allowed for this question."
  1279. elif (
  1280. not _get(qargs, "csq_grading_mode", "auto", str) == "manual"
  1281. and context["csm_tutor"].get_manual_grading_entry(context, name) is not None
  1282. ):
  1283. # ...prior submission has been graded
  1284. error = "You are not allowed to submit after a previous submission has been graded."
  1285. else:
  1286. # ...the user does not have enough checks left
  1287. nleft, _ = nsubmits_left(context, name)
  1288. if nleft <= 0:
  1289. error = (
  1290. "You have used all of your allowed "
  1291. "submissions for this question."
  1292. )
  1293. return error
  1294. def log_action(context, log_entry):
  1295. uname = context[_n("uname")]
  1296. entry = {
  1297. "action": context[_n("action")],
  1298. "timestamp": context["cs_timestamp"],
  1299. "user_info": context["cs_user_info"],
  1300. }
  1301. entry.update(log_entry)
  1302. context["csm_cslog"].update_log(
  1303. uname, context["cs_path_info"], "problemactions", entry
  1304. )
  1305. def simple_return_json(val):
  1306. content = json.dumps(val, separators=(",", ":"))
  1307. length = str(len(content))
  1308. retcode = ("200", "OK")
  1309. headers = {"Content-type": "application/json", "Content-length": length}
  1310. return retcode, headers, content
  1311. def make_return_json(context, ret, names=None):
  1312. names = context[_n("question_names")] if names is None else names
  1313. names = set(i[2:].rsplit("_", 1)[0] if i.startswith("__") else i for i in names)
  1314. ctx2 = dict(context)
  1315. if ctx2[_n("action")] != "view":
  1316. ctx2[_n("action")] = "view"
  1317. for name in names:
  1318. ret[name]["nsubmits_left"] = (nsubmits_left(context, name)[1],)
  1319. ret[name]["buttons"] = make_buttons(ctx2, name)
  1320. return simple_return_json(ret)
  1321. def render_question(elt, context, lastsubmit, wrap=True):
  1322. q, args = elt
  1323. name = args["csq_name"]
  1324. lastlog = context[_n("last_log")]
  1325. answer_viewed = context[_n("answer_viewed")]
  1326. if wrap:
  1327. out = "\n<!--START question %s -->" % (name)
  1328. else:
  1329. out = ""
  1330. if wrap and q.get("indiv", True) and args.get("csq_indiv", True):
  1331. out += (
  1332. '\n<div class="question question-%s" id="cs_qdiv_%s" style="position: static">'
  1333. % (q["qtype"], name)
  1334. )
  1335. out += '\n<div id="%s_rendered_question">\n' % name
  1336. out += context["csm_language"].source_transform_string(
  1337. context, args.get("csq_prompt", "")
  1338. )
  1339. out += q["render_html"](lastsubmit, **args)
  1340. out += "\n</div>"
  1341. out += "<div>"
  1342. out += ('\n<span id="%s_buttons">' % name) + make_buttons(context, name) + "</span>"
  1343. out += (
  1344. '\n<span id="%s_loading_wrapper">'
  1345. '\n<span id="%s_loading" style="display:none;"><img src="%s"/>'
  1346. "</span>\n</span>"
  1347. ) % (name, name, context["cs_loading_image"])
  1348. out += (
  1349. ('\n<span id="%s_score_display">' % args["csq_name"])
  1350. + context["csm_tutor"].make_score_display(
  1351. context, args, name, None, last_log=context[_n("last_log")]
  1352. )
  1353. + "</span>"
  1354. )
  1355. out += (
  1356. ('\n<div id="%s_nsubmits_left" class="nsubmits_left">' % name)
  1357. + nsubmits_left(context, name)[1]
  1358. + "</div>"
  1359. )
  1360. out += "</div>"
  1361. if name in answer_viewed:
  1362. answerclass = ' class="solution"'
  1363. showanswer = True
  1364. elif context[_n("impersonating")]:
  1365. answerclass = ' class="impsolution"'
  1366. showanswer = True
  1367. else:
  1368. answerclass = ""
  1369. showanswer = False
  1370. out += '\n<div id="%s_solution_container"%s>' % (args["csq_name"], answerclass)
  1371. out += '\n<div id="%s_solution">' % (args["csq_name"])
  1372. if showanswer:
  1373. ans = q["answer_display"](**args)
  1374. out += "\n"
  1375. out += context["csm_language"].source_transform_string(context, ans)
  1376. out += "\n</div>"
  1377. out += '\n<div id="%s_solution_explanation">' % name
  1378. if (
  1379. name in context[_n("explanation_viewed")]
  1380. and args.get("csq_explanation", "") != ""
  1381. ):
  1382. exp = explanation_display(args["csq_explanation"])
  1383. out += context["csm_language"].source_transform_string(context, exp)
  1384. out += "\n</div>"
  1385. out += "\n</div>"
  1386. out += '\n<div id="%s_message">' % args["csq_name"]
  1387. gmode = _get(args, "csq_grading_mode", "auto", str)
  1388. message = context[_n("last_log")].get("cached_responses", {}).get(name, "")
  1389. magic = context[_n("last_log")].get("checker_ids", {}).get(name, None)
  1390. if magic is not None:
  1391. checker_loc = os.path.join(
  1392. context["cs_data_root"],
  1393. "_logs",
  1394. "_checker",
  1395. "results",
  1396. magic[0],
  1397. magic[1],
  1398. magic,
  1399. )
  1400. if os.path.isfile(checker_loc):
  1401. with open(checker_loc, "rb") as f:
  1402. result = context["csm_cslog"].unprep(f.read())
  1403. message = (
  1404. '\n<script type="text/javascript">'
  1405. "\n// @license magnet:?xt=urn:btih:0b31508aeb0634b347b8270c7bee4d411b5d4109&dn=agpl-3.0.txt AGPL-v3"
  1406. '\ndocument.getElementById("%s_score_display").innerHTML = %r;'
  1407. "\n// @license-end"
  1408. "\n</script>"
  1409. ) % (name, result["score_box"])
  1410. try:
  1411. result["response"] = result["response"].decode()
  1412. except:
  1413. pass
  1414. message += "\n" + result["response"]
  1415. else:
  1416. message = WEBSOCKET_RESPONSE % {
  1417. "name": name,
  1418. "magic": magic,
  1419. "websocket": context["cs_checker_websocket"],
  1420. "loading": context["cs_loading_image"],
  1421. "id_css": (
  1422. ' style="display:none;"'
  1423. if context.get("cs_show_submission_id", True)
  1424. else ""
  1425. ),
  1426. }
  1427. if gmode == "manual":
  1428. q, args = context[_n("name_map")][name]
  1429. lastlog = context["csm_tutor"].get_manual_grading_entry(context, name) or {}
  1430. lastscore = lastlog.get("score", "")
  1431. tpoints = q["total_points"](**args)
  1432. comments = (
  1433. context["csm_tutor"].get_manual_grading_entry(context, name) or {}
  1434. ).get("comments", None)
  1435. if comments is not None:
  1436. comments = context["csm_language"]._md_format_string(context, comments)
  1437. try:
  1438. score_output = lastscore * tpoints
  1439. except:
  1440. score_output = ""
  1441. if comments is not None:
  1442. message = (
  1443. "<b>Score:</b> %s (out of %s)<br><br><b>Grader's Comments:</b><br/>%s"
  1444. % (score_output, tpoints, comments)
  1445. )
  1446. out += message + "</div>"
  1447. if wrap and q.get("indiv", True) and args.get("csq_indiv", True):
  1448. out += "\n</div>"
  1449. if wrap:
  1450. out += "\n<!--END question %s -->\n" % args["csq_name"]
  1451. return out
  1452. def nsubmits_left(context, name):
  1453. nused = context[_n("nsubmits_used")].get(name, 0)
  1454. q, args = context[_n("name_map")][name]
  1455. if not q.get("allow_submit", True) or not q.get("allow_self_submit", True):
  1456. return 0, ""
  1457. info = q.get("defaults", {})
  1458. info.update(args)
  1459. # look up 'nsubmits' in the question's arguments
  1460. # (fall back on default in qtype)
  1461. nsubmits = info.get("csq_nsubmits", None)
  1462. if nsubmits is None:
  1463. nsubmits = context.get("cs_nsubmits_default", float("inf"))
  1464. perms = context[_n("orig_perms")]
  1465. if "submit" not in perms and "submit_all" not in perms:
  1466. return 0, ""
  1467. nleft = max(0, nsubmits - nused)
  1468. for (regex, nchecks) in context["cs_user_info"].get("nsubmits_extra", []):
  1469. if re.match(regex, ".".join(context["cs_path_info"][1:] + [name])):
  1470. nleft += nchecks
  1471. nmsg = info.get("csq_nsubmits_message", None)
  1472. if nmsg is None:
  1473. if nleft < float("inf"):
  1474. msg = "<i>You have %d submission%s remaining.</i>" % (
  1475. nleft,
  1476. "s" if nleft != 1 else "",
  1477. )
  1478. else:
  1479. msg = "<i>You have infinitely many submissions remaining.</i>"
  1480. else:
  1481. msg = nmsg(nsubmits, nused, nleft)
  1482. if "submit_all" in perms:
  1483. msg = (
  1484. "As staff, you are always allowed to submit. "
  1485. "If you were a student, you would see the following:<br/>%s"
  1486. ) % msg
  1487. return max(0, nleft), msg
  1488. def button_text(x, msg):
  1489. if x is None:
  1490. return msg
  1491. else:
  1492. return None
  1493. _button_map = {
  1494. "submit": (submit_msg, "Submit"),
  1495. "save": (save_msg, "Save"),
  1496. "viewanswer": (viewanswer_msg, "View Answer"),
  1497. "clearanswer": (clearanswer_msg, "Clear Answer"),
  1498. "viewexplanation": (viewexp_msg, "View Explanation"),
  1499. "check": (check_msg, True),
  1500. }
  1501. def make_buttons(context, name):
  1502. uname = context[_n("uname")]
  1503. rp = context[_n("perms")] # the real user's perms
  1504. p = context[_n("orig_perms")] # the impersonated user's perms, if any
  1505. i = context[_n("impersonating")]
  1506. q, args = context[_n("name_map")][name]
  1507. nsubmits, _ = nsubmits_left(context, name)
  1508. buttons = {"copy_seed": None, "copy": None, "new_seed": None}
  1509. buttons["new_seed"] = (
  1510. "New Random Seed"
  1511. if "submit_all" in p and context.get("cs_random_inited", False)
  1512. else None
  1513. )
  1514. abuttons = {
  1515. "copy_seed": (
  1516. "Copy Random Seed" if context.get("cs_random_inited", False) else None
  1517. ),
  1518. "copy": "Copy to My Account",
  1519. "lock": None,
  1520. "unlock": None,
  1521. }
  1522. for (b, (func, text)) in list(_button_map.items()):
  1523. buttons[b] = button_text(func(context, p, name), text)
  1524. abuttons[b] = button_text(func(context, rp, name), text)
  1525. for d in (buttons, abuttons):
  1526. if d["check"]:
  1527. d["check"] = q.get("checktext", "Check")
  1528. if name in context[_n("locked")]:
  1529. abuttons["unlock"] = "Unlock"
  1530. else:
  1531. abuttons["lock"] = "Lock"
  1532. aout = ""
  1533. if i:
  1534. for k in {"submit", "check", "save"}:
  1535. if buttons[k] is not None:
  1536. abuttons[k] = None
  1537. elif abuttons[k] is not None:
  1538. abuttons[k] += " (as %s)" % uname
  1539. for k in ("viewanswer", "clearanswer", "viewexplanation"):
  1540. if buttons[k] is not None:
  1541. abuttons[k] = None
  1542. elif abuttons[k] is not None:
  1543. abuttons[k] += " (for %s)" % uname
  1544. aout = '<div><b><font color="red">Admin Buttons:</font></b><br/>'
  1545. for k in (
  1546. "copy",
  1547. "copy_seed",
  1548. "check",
  1549. "save",
  1550. "submit",
  1551. "viewanswer",
  1552. "viewexplanation",
  1553. "clearanswer",
  1554. "lock",
  1555. "unlock",
  1556. ):
  1557. x = {"b": abuttons[k], "k": k, "n": name}
  1558. if abuttons[k] is not None:
  1559. aout += (
  1560. '\n<button id="%(n)s_%(k)s" '
  1561. 'class="%(k)s btn btn-danger" '
  1562. "onclick=\"catsoop.%(k)s('%(n)s');\">"
  1563. "%(b)s</button>"
  1564. ) % x
  1565. # in manual grading mode, add a box and button for grading
  1566. gmode = _get(args, "csq_grading_mode", "auto", str)
  1567. if gmode == "manual":
  1568. lastlog = context["csm_tutor"].get_manual_grading_entry(context, name) or {}
  1569. lastscore = lastlog.get("score", "")
  1570. lastcomments = lastlog.get("comments", "")
  1571. tpoints = q["total_points"](**args)
  1572. try:
  1573. output = lastscore * tpoints
  1574. except:
  1575. output = ""
  1576. aout += (
  1577. '<br/><b><font color="red">Grading:</font></b>'
  1578. '<table border="0" width="100%%">'
  1579. '<tr><td align="right" width="30%%">'
  1580. '<font color="red">Points Earned (out of %2.2f):</font>'
  1581. '</td><td><input type="text" value="%s" size="5" '
  1582. 'style="border-color: red;" '
  1583. 'id="%s_grading_score" '
  1584. 'name="%s_grading_score" /></td></tr>'
  1585. '<tr><td align="right">'
  1586. '<font color="red">Comments:</font></td>'
  1587. '<td><textarea rows="5" id="%s_grading_comments" '
  1588. 'name="%s_grading_comments" '
  1589. 'style="width: 100%%; border-color: red;">'
  1590. "%s"
  1591. "</textarea></td></tr><tr><td></td><td>"
  1592. '<button class="grade" '
  1593. 'style="background-color: #FFD9D9; '
  1594. 'border-color: red;" '
  1595. "onclick=\"catsoop.grade('%s');\">"
  1596. "Submit Grade"
  1597. "</button></td></tr></table>"
  1598. ) % (tpoints, output, name, name, name, name, lastcomments, name)
  1599. aout += "</div>"
  1600. out = ""
  1601. for k in (
  1602. "check",
  1603. "save",
  1604. "submit",
  1605. "viewanswer",
  1606. "viewexplanation",
  1607. "clearanswer",
  1608. "new_seed",
  1609. ):
  1610. x = {"b": buttons[k], "k": k, "n": name, "s": ""}
  1611. heb = context.get("cs_ui_config_flags", {}).get("highlight_explanation_button")
  1612. if k == "viewexplanation" and heb:
  1613. color = "blue"
  1614. if heb is not True:
  1615. color = heb
  1616. x["s"] = "background-color:%s;" % color
  1617. if buttons[k] is not None:
  1618. out += (
  1619. '\n<button id="%(n)s_%(k)s" '
  1620. 'class="%(k)s btn btn-catsoop" '
  1621. 'style="margin-top: 10px;%(s)s" '
  1622. "onclick=\"catsoop.%(k)s('%(n)s');\">"
  1623. "%(b)s</button>"
  1624. ) % x
  1625. return out + aout
  1626. def pre_handle(context):
  1627. # enumerate the questions in this problem
  1628. context[_n("name_map")] = collections.OrderedDict()
  1629. for elt in context["cs_problem_spec"]:
  1630. if isinstance(elt, tuple):
  1631. m = elt[1]
  1632. context[_n("name_map")][m["csq_name"]] = elt
  1633. if "init" in elt[0]:
  1634. a = elt[0].get("defaults", {})
  1635. a.update(elt[1])
  1636. elt[0]["init"](a)
  1637. # who is the user (and, who is being impersonated?)
  1638. user_info = context.get("cs_user_info", {})
  1639. uname = user_info.get("username", "None")
  1640. real = user_info.get("real_user", user_info)
  1641. context[_n("role")] = real.get("role", "None")
  1642. context[_n("section")] = real.get("section", None)
  1643. context[_n("perms")] = real.get("permissions", [])
  1644. context[_n("orig_perms")] = user_info.get("permissions", [])
  1645. context[_n("uname")] = uname
  1646. context[_n("real_uname")] = real.get("username", uname)
  1647. context[_n("impersonating")] = context[_n("uname")] != context[_n("real_uname")]
  1648. # store release and due dates
  1649. r = context[_n("rel")] = context["csm_tutor"].get_release_date(context)
  1650. d = context[_n("due")] = context["csm_tutor"].get_due_date(context)
  1651. n = context["csm_time"].from_detailed_timestamp(context["cs_timestamp"])
  1652. context[_n("now")] = n
  1653. context[_n("timing")] = -1 if n <= r else 0 if n <= d else 1
  1654. if _get(context, "cs_require_activation", False, bool):
  1655. pwd = _get(context, "cs_activation_password", "password", str)
  1656. context[_n("activation_password")] = pwd
  1657. # determine the right log name to look up, and grab the most recent entry
  1658. loghead = "___".join(context["cs_path_info"][1:])
  1659. ll = context["csm_cslog"].most_recent(
  1660. uname, context["cs_path_info"], "problemstate", {}
  1661. )
  1662. _cs_group_path = context.get("cs_groups_to_use", context["cs_path_info"])
  1663. context[_n("all_groups")] = context["csm_groups"].list_groups(
  1664. context, _cs_group_path
  1665. )
  1666. context[_n("group")] = context["csm_groups"].get_group(
  1667. context, _cs_group_path, uname, context[_n("all_groups")]
  1668. )
  1669. _ag = context[_n("all_groups")]
  1670. _g = context[_n("group")]
  1671. context[_n("group_members")] = _gm = _ag.get(_g[0], {}).get(_g[1], [])
  1672. if uname not in _gm:
  1673. _gm.append(uname)
  1674. context[_n("last_log")] = ll
  1675. context[_n("locked")] = set(ll.get("locked", set()))
  1676. context[_n("answer_viewed")] = set(ll.get("answer_viewed", set()))
  1677. context[_n("explanation_viewed")] = set(ll.get("explanation_viewed", set()))
  1678. context[_n("nsubmits_used")] = ll.get("nsubmits_used", {})
  1679. # what is the user trying to do?
  1680. context[_n("action")] = context["cs_form"].get("action", "view").lower()
  1681. if context[_n("action")] in (
  1682. "view",
  1683. "activate",
  1684. "passthrough",
  1685. "list_questions",
  1686. "get_state",
  1687. "manage_groups",
  1688. "render_single_question",
  1689. ):
  1690. context[_n("form")] = context["cs_form"]
  1691. else:
  1692. names = context["cs_form"].get("names", "[]")
  1693. context[_n("question_names")] = json.loads(names)
  1694. context[_n("form")] = json.loads(context["cs_form"].get("data", "{}"))
  1695. if context["cs_upload_management"] == "file":
  1696. for name, value in context[_n("form")].items():
  1697. if name == "__names__":
  1698. continue
  1699. if isinstance(value, list):
  1700. data = csm_thirdparty.data_uri.DataURI(value[1]).data
  1701. if context["csm_cslog"].ENCRYPT_KEY is not None:
  1702. seed = (
  1703. context["cs_path_info"][0]
  1704. if context["cs_path_info"]
  1705. else context["cs_path_info"]
  1706. )
  1707. _path = [
  1708. context["csm_cslog"]._e(i, repr(seed))
  1709. for i in context["cs_path_info"]
  1710. ]
  1711. else:
  1712. _path = context["cs_path_info"]
  1713. dir_ = os.path.join(
  1714. context["cs_data_root"], "_logs", "_uploads", *_path
  1715. )
  1716. os.makedirs(dir_, exist_ok=True)
  1717. value[0] = (
  1718. value[0]
  1719. .replace("<", "")
  1720. .replace(">", "")
  1721. .replace('"', "")
  1722. .replace('"', "")
  1723. )
  1724. hstring = hashlib.sha256(data).hexdigest()
  1725. info = {
  1726. "filename": value[0],
  1727. "username": context["cs_username"],
  1728. "time": context["csm_time"].detailed_timestamp(
  1729. context["cs_now"]
  1730. ),
  1731. "question": name,
  1732. "hash": hstring,
  1733. }
  1734. disk_fname = "_csfile.%s%s" % (uuid.uuid4().hex, hstring)
  1735. dirname = os.path.join(dir_, disk_fname)
  1736. os.makedirs(dirname, exist_ok=True)
  1737. with open(os.path.join(dirname, "content"), "wb") as f:
  1738. f.write(context["csm_cslog"].compress_encrypt(data))
  1739. with open(os.path.join(dirname, "info"), "wb") as f:
  1740. f.write(context["csm_cslog"].prep(info))
  1741. value[1] = dirname
  1742. elif context["cs_upload_management"] == "db":
  1743. pass
  1744. else:
  1745. raise Exception(
  1746. "unknown upload management style: %r" % context["cs_upload_management"]
  1747. )
  1748. def _get_auto_view(context):
  1749. # when should we automatically view the answer?
  1750. ava = context.get("csq_auto_viewanswer", False)
  1751. if ava is True:
  1752. ava = set(["nosubmits", "perfect", "lock"])
  1753. elif isinstance(ava, str):
  1754. ava = set([ava])
  1755. elif not ava:
  1756. ava = set()
  1757. return ava
  1758. def default_javascript(context):
  1759. namemap = context[_n("name_map")]
  1760. if "submit_all" in context[_n("perms")]:
  1761. skip_alert = list(namemap.keys())
  1762. else:
  1763. skipper = "csq_allow_submit_after_answer_viewed"
  1764. skip_alert = [
  1765. name
  1766. for (name, (q, args)) in list(namemap.items())
  1767. if _get(args, skipper, False, bool)
  1768. ]
  1769. out = """
  1770. <script type="text/javascript" src="_handler/default/cs_ajax.js"></script>
  1771. <script type="text/javascript">
  1772. // @license magnet:?xt=urn:btih:0b31508aeb0634b347b8270c7bee4d411b5d4109&dn=agpl-3.0.txt AGPL-v3
  1773. catsoop.all_questions = %(allqs)r;
  1774. catsoop.username = %(uname)s;
  1775. catsoop.api_token = %(secret)s;
  1776. catsoop.this_path = %(path)r;
  1777. catsoop.path_info = %(pathinfo)r;
  1778. catsoop.course = %(course)s;
  1779. catsoop.url_root = %(root)r;
  1780. """
  1781. if len(namemap) > 0:
  1782. out += """catsoop.imp = %(imp)r;
  1783. catsoop.skip_alert = %(skipalert)s;
  1784. 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.";
  1785. """
  1786. out += "\n// @license-end"
  1787. out += "</script>"
  1788. api_tok = "null"
  1789. uname = "null"
  1790. given_tok = context.get("cs_user_info", {}).get("api_token", None)
  1791. if given_tok is not None:
  1792. api_tok = repr(given_tok)
  1793. given_uname = context.get("cs_user_info", {}).get("username", None)
  1794. if given_uname is not None:
  1795. uname = repr(given_uname)
  1796. return out % {
  1797. "skipalert": json.dumps(skip_alert),
  1798. "allqs": list(context[_n("name_map")].keys()),
  1799. "user": context[_n("real_uname")],
  1800. "path": "/".join([context["cs_url_root"]] + context["cs_path_info"]),
  1801. "imp": context[_n("uname")] if context[_n("impersonating")] else "",
  1802. "secret": api_tok,
  1803. "course": repr(context["cs_course"]) if context["cs_course"] else "null",
  1804. "pathinfo": context["cs_path_info"],
  1805. "root": context["cs_url_root"],
  1806. "uname": uname,
  1807. }
  1808. def default_timer(context):
  1809. out = ""
  1810. if not _get(context, "cs_auto_lock", False, bool):
  1811. return out
  1812. if len(context[_n("locked")]) >= len(context[_n("name_map")]):
  1813. return out
  1814. if context[_n("now")] > context[_n("due")]:
  1815. # view answers immediately if viewed past the due date
  1816. out += '\n<script type="text/javascript">'
  1817. out += "\n// @license magnet:?xt=urn:btih:0b31508aeb0634b347b8270c7bee4d411b5d4109&dn=agpl-3.0.txt AGPL-v3"
  1818. out += "\ncatsoop.ajaxrequest(catsoop.all_questions,'lock');"
  1819. out += "\n// @license-end"
  1820. out += "\n</script>"
  1821. return out
  1822. else:
  1823. out += '\n<script type="text/javascript">'
  1824. out += "\n// @license magnet:?xt=urn:btih:0b31508aeb0634b347b8270c7bee4d411b5d4109&dn=agpl-3.0.txt AGPL-v3"
  1825. out += (
  1826. "\ncatsoop.timer_now = %d;"
  1827. "\ncatsoop.timer_due = %d;"
  1828. "\ncatsoop.time_url = %r;"
  1829. ) % (
  1830. context["csm_time"].unix(context[_n("now")]),
  1831. context["csm_time"].unix(context[_n("due")]),
  1832. context["cs_url_root"] + "/_util/time",
  1833. )
  1834. out += "\n// @license-end"
  1835. out += "\n</script>"
  1836. out += (
  1837. '<script type="text/javascript" '
  1838. 'src="_handler/default/cs_timer.js"></script>'
  1839. )
  1840. return out
  1841. def exc_message(context):
  1842. exc = traceback.format_exc()
  1843. exc = context["csm_errors"].clear_info(context, exc)
  1844. return (
  1845. '<p><font color="red">' "<b>CAT-SOOP ERROR:</b>" "<pre>%s</pre></font>"
  1846. ) % exc
  1847. def _get_scores(context):
  1848. section = str(
  1849. context.get("cs_form", {}).get(
  1850. "section", context.get("cs_user_info").get("section", "default")
  1851. )
  1852. )
  1853. user = context["csm_user"]
  1854. usernames = user.list_all_users(context, context["cs_course"])
  1855. users = [
  1856. user.read_user_file(context, context["cs_course"], username, {})
  1857. for username in usernames
  1858. ]
  1859. no_section = context.get("cs_whdw_no_section", False)
  1860. students = [
  1861. user
  1862. for user in users
  1863. if user.get("role", None) in ["Student", "SLA"]
  1864. and (no_section or str(user.get("section", "default")) == section)
  1865. ]
  1866. questions = context[_n("name_map")]
  1867. scores = collections.OrderedDict()
  1868. for name, question in questions.items():
  1869. if not context.get("cs_whdw_filter", lambda q: True)(question):
  1870. continue
  1871. counts = {}
  1872. for student in students:
  1873. username = student.get("username", "None")
  1874. log = context["csm_cslog"].most_recent(
  1875. username, context["cs_path_info"], "problemstate", {}
  1876. )
  1877. log = context["csm_tutor"].compute_page_stats(
  1878. context, username, context["cs_path_info"], ["state"]
  1879. )["state"]
  1880. score = log.get("scores", {}).get(name, None)
  1881. counts[username] = score
  1882. scores[name] = counts
  1883. return scores
  1884. def handle_stats(context):
  1885. perms = context["cs_user_info"].get("permissions", [])
  1886. if "whdw" not in perms:
  1887. return "You are not allowed to view this page."
  1888. section = str(
  1889. context.get("cs_form", {}).get(
  1890. "section", context.get("cs_user_info").get("section", "default")
  1891. )
  1892. )
  1893. questions = context[_n("name_map")]
  1894. stats = collections.OrderedDict()
  1895. groups = (
  1896. context["csm_groups"]
  1897. .list_groups(context, context["cs_path_info"])
  1898. .get(section, None)
  1899. )
  1900. if groups:
  1901. total = len(groups)
  1902. for name, scores in _get_scores(context).items():
  1903. counts = {"completed": 0, "attempted": 0, "not tried": 0}
  1904. for members in groups.values():
  1905. score = min(
  1906. (scores.get(member, None) for member in members),
  1907. key=lambda x: -1 if x is None else x,
  1908. )
  1909. if score is None:
  1910. counts["not tried"] += 1
  1911. elif score == 1:
  1912. counts["completed"] += 1
  1913. else:
  1914. counts["attempted"] += 1
  1915. stats[name] = counts
  1916. else:
  1917. total = 0
  1918. for name, scores in _get_scores(context).items():
  1919. counts = {"completed": 0, "attempted": 0, "not tried": 0}
  1920. for score in scores.values():
  1921. if score is None:
  1922. counts["not tried"] += 1
  1923. elif score == 1:
  1924. counts["completed"] += 1
  1925. else:
  1926. counts["attempted"] += 1
  1927. stats[name] = counts
  1928. total = max(total, sum(counts.values()))
  1929. soup = BeautifulSoup("", "html.parser")
  1930. table = soup.new_tag("table")
  1931. table["class"] = "table table-bordered"
  1932. header = soup.new_tag("tr")
  1933. for heading in ["name", "completed", "attempted", "not tried"]:
  1934. th = soup.new_tag("th")
  1935. th.string = heading
  1936. header.append(th)
  1937. table.append(header)
  1938. for name, counts in stats.items():
  1939. tr = soup.new_tag("tr")
  1940. td = soup.new_tag("td")
  1941. a = soup.new_tag(
  1942. "a", href="?section={}&action=whdw&question={}".format(section, name)
  1943. )
  1944. qargs = questions[name][1]
  1945. a.string = qargs.get("csq_display_name", name)
  1946. td.append(a)
  1947. td["class"] = "text-left"
  1948. tr.append(td)
  1949. for key in ["completed", "attempted", "not tried"]:
  1950. td = soup.new_tag("td")
  1951. td.string = "{count}/{total} ({percent:.2%})".format(
  1952. count=counts[key],
  1953. total=total,
  1954. percent=(counts[key] / total) if total != 0 else 0,
  1955. )
  1956. td["class"] = "text-right"
  1957. tr.append(td)
  1958. table.append(tr)
  1959. soup.append(table)
  1960. return str(soup)
  1961. def _real_name(context, username):
  1962. return (
  1963. context["csm_cslog"].most_recent("_extra_info", [], username, None) or {}
  1964. ).get("name", None)
  1965. def _whdw_name(context, username):
  1966. real_name = _real_name(context, username)
  1967. if real_name:
  1968. return '{} (<a href="?as={}" target="_blank">{}</a>)'.format(
  1969. real_name, username, username
  1970. )
  1971. else:
  1972. return username
  1973. def handle_whdw(context):
  1974. perms = context["cs_user_info"].get("permissions", [])
  1975. if "whdw" not in perms:
  1976. return "You are not allowed to view this page."
  1977. section = str(
  1978. context.get("cs_form", {}).get(
  1979. "section", context.get("cs_user_info").get("section", "default")
  1980. )
  1981. )
  1982. question = context["cs_form"]["question"]
  1983. qtype, qargs = context[_n("name_map")][question]
  1984. display_name = qargs.get("csq_display_name", qargs["csq_name"])
  1985. context["cs_content_header"] += " | {}".format(display_name)
  1986. scores = _get_scores(context)[question]
  1987. groups = (
  1988. context["csm_groups"]
  1989. .list_groups(context, context["cs_path_info"])
  1990. .get(section, None)
  1991. )
  1992. soup = BeautifulSoup("", "html.parser")
  1993. if groups:
  1994. css = soup.new_tag("style")
  1995. css.string = """\
  1996. .whdw-cell {
  1997. border: 1px white solid;
  1998. }
  1999. .whdw-not-tried {
  2000. background-color: #ff6961;
  2001. color: black;
  2002. }
  2003. .whdw-attempted {
  2004. background-color: #ffb347;
  2005. color: black;
  2006. }
  2007. .whdw-completed {
  2008. background-color: #77dd77;
  2009. color: black;
  2010. }
  2011. .whdw-cell ul {
  2012. padding-left: 5px;
  2013. }
  2014. """
  2015. soup.append(css)
  2016. grid = soup.new_tag("div")
  2017. grid["class"] = "row"
  2018. for group, members in sorted(groups.items()):
  2019. min_score = min(
  2020. (scores.get(member, None) for member in members),
  2021. key=lambda x: -1 if x is None else x,
  2022. )
  2023. cell = soup.new_tag("div")
  2024. cell["class"] = "col-sm-3 whdw-cell {}".format(
  2025. {None: "whdw-not-tried", 1: "whdw-completed"}.get(
  2026. min_score, "whdw-attempted"
  2027. )
  2028. )
  2029. grid.append(cell)
  2030. header = soup.new_tag("div")
  2031. header["class"] = "text-center"
  2032. header.string = "{}".format(group)
  2033. cell.append(header)
  2034. people = soup.new_tag("ul")
  2035. header["class"] = "text-center"
  2036. for member in members:
  2037. m = soup.new_tag("li")
  2038. name = soup.new_tag("span")
  2039. name.insert(
  2040. 1, BeautifulSoup(_whdw_name(context, member), "html.parser")
  2041. )
  2042. m.append(name)
  2043. score = soup.new_tag("span")
  2044. score["class"] = "pull-right"
  2045. score.string = str(scores.get(member, None))
  2046. m.append(score)
  2047. people.append(m)
  2048. cell.append(people)
  2049. soup.append(grid)
  2050. return str(soup)
  2051. else:
  2052. states = {"completed": [], "attempted": [], "not tried": []}
  2053. for username, score in scores.items():
  2054. if score is None:
  2055. state = "not tried"
  2056. elif score == 1:
  2057. state = "completed"
  2058. else:
  2059. state = "attempted"
  2060. states[state].append(username)
  2061. for state in ["not tried", "attempted", "completed"]:
  2062. usernames = states[state]
  2063. h3 = soup.new_tag("h3")
  2064. h3.string = "{} ({})".format(state, len(states[state]))
  2065. soup.append(h3)
  2066. grid = soup.new_tag("div")
  2067. grid["class"] = "row"
  2068. for username in sorted(usernames):
  2069. cell = soup.new_tag("div")
  2070. cell.insert(
  2071. 1, BeautifulSoup(_whdw_name(context, username), "html.parser")
  2072. )
  2073. cell["class"] = "col-sm-2"
  2074. grid.append(cell)
  2075. soup.append(grid)
  2076. return str(soup)
  2077. WEBSOCKET_RESPONSE = """
  2078. <div class="callout callout-default" id="cs_partialresults_%(name)s">
  2079. <div id="cs_partialresults_%(name)s_body">
  2080. <span id="cs_partialresults_%(name)s_message">Looking up your submission (id <code>%(magic)s</code>). Watch here for updates.</span><br/>
  2081. <center><img src="%(loading)s"/></center>
  2082. </div>
  2083. </div>
  2084. <small%(id_css)s>ID: <code>%(magic)s</code></small>
  2085. <script type="text/javascript">
  2086. // @license magnet:?xt=urn:btih:0b31508aeb0634b347b8270c7bee4d411b5d4109&dn=agpl-3.0.txt AGPL-v3
  2087. var magic_%(name)s = %(magic)r;
  2088. if (typeof ws_%(name)s != 'undefined'){
  2089. ws_%(name)s.onclose = function(){}
  2090. ws_%(name)s.onmessage = function(){}
  2091. ws_%(name)s.close();
  2092. var ws_%(name)s = undefined;
  2093. }
  2094. document.getElementById('%(name)s_score_display').innerHTML = '<img src="%(loading)s" style="vertical-align: -6px; margin-left: 5px;"/>';
  2095. document.querySelectorAll('#%(name)s_buttons button').forEach(function(b){b.disabled = true});
  2096. var ws_%(name)s = new WebSocket(%(websocket)r);
  2097. ws_%(name)s.onopen = function(){
  2098. ws_%(name)s.send(JSON.stringify({type: "hello", magic: magic_%(name)s}));
  2099. }
  2100. ws_%(name)s.onclose = function(){
  2101. if (this !== ws_%(name)s) return;
  2102. if (ws_%(name)s_state != 2){
  2103. var thediv = document.getElementById('cs_partialresults_%(name)s')
  2104. thediv.innerHTML = 'Your connection to the server was lost. Please reload the page.';
  2105. }
  2106. }
  2107. var ws_%(name)s_state = -1;
  2108. ws_%(name)s.onmessage = function(event){
  2109. var m = event.data;
  2110. var j = JSON.parse(m);
  2111. var thediv = document.getElementById('cs_partialresults_%(name)s');
  2112. var themessage = document.getElementById('cs_partialresults_%(name)s_message');
  2113. if (j.type == 'ping'){
  2114. ws_%(name)s.send(JSON.stringify({type: 'pong'}));
  2115. }else if (j.type == 'inqueue'){
  2116. ws_%(name)s_state = 0;
  2117. try{clearInterval(ws_%(name)s_interval);}catch(err){}
  2118. thediv.classList = 'callout callout-warning';
  2119. themessage.innerHTML = 'Your submission (id <code>%(magic)s</code>) is queued to be checked (position ' + j.position + ').';
  2120. document.querySelectorAll('#%(name)s_buttons button').forEach(function(b){b.disabled = false;});
  2121. }else if (j.type == 'running'){
  2122. ws_%(name)s_state = 1;
  2123. try{clearInterval(ws_%(name)s_interval);}catch(err){}
  2124. thediv.classList = 'callout callout-info';
  2125. themessage.innerHTML = 'Your submission is currently being checked<span id="%(name)s_ws_running_time"></span>.';
  2126. document.querySelectorAll('#%(name)s_buttons button').forEach(function(b){b.disabled = false;});
  2127. var sync = ((new Date()).valueOf()/1000 - j.now);
  2128. ws_%(name)s_interval = setInterval(function(){catsoop.setTimeSince("%(name)s",
  2129. j.started,
  2130. sync);}, 1000);
  2131. }else if (j.type == 'newresult'){
  2132. ws_%(name)s_state = 2;
  2133. try{clearInterval(ws_%(name)s_interval);}catch(err){}
  2134. document.getElementById('%(name)s_score_display').innerHTML = j.score_box;
  2135. thediv.classList = [];
  2136. thediv.innerHTML = j.response;
  2137. catsoop.render_all_math(thediv);
  2138. catsoop.run_all_scripts('cs_partialresults_%(name)s');
  2139. document.querySelectorAll('#%(name)s_buttons button').forEach(function(b){b.disabled = false;});
  2140. }
  2141. }
  2142. ws_%(name)s.onerror = function(event){
  2143. }
  2144. // @license-end
  2145. </script>
  2146. """