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.
 
 
 

238 lines
7.7 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 json
  17. import collections.abc
  18. def default_checkbox_checker(submission, solution):
  19. credit_per_correct = 1 / sum(solution)
  20. correct = (
  21. sum(i == j == True for i, j in zip(submission, solution)) * credit_per_correct
  22. )
  23. incorrect = (
  24. sum(i == True and j == False for i, j in zip(submission, solution))
  25. * credit_per_correct
  26. )
  27. return max(0, correct - incorrect)
  28. defaults = {
  29. "csq_soln": "--",
  30. "csq_npoints": 1,
  31. "csq_check_function": lambda x, y: (x == y) * 1.0,
  32. "csq_checkbox_check_function": default_checkbox_checker,
  33. "csq_msg_function": lambda sub: "",
  34. "csq_options": [],
  35. "csq_show_check": False,
  36. "csq_renderer": "dropdown",
  37. "csq_soln_mode": "value",
  38. }
  39. def total_points(**info):
  40. return info["csq_npoints"]
  41. def handle_submission(submissions, **info):
  42. check = info["csq_check_function"]
  43. soln = info["csq_soln"]
  44. sub = submissions[info["csq_name"]]
  45. if info["csq_renderer"] == "checkbox":
  46. try:
  47. sub = json.loads(sub)
  48. except:
  49. sub = {}
  50. _sub = []
  51. for ix in range(len(info["csq_options"])):
  52. n = "%s_opt%d" % (info["csq_name"], ix)
  53. _sub.append(sub.get(n, False))
  54. sub = _sub
  55. if check is defaults["csq_check_function"]:
  56. check = defaults["csq_checkbox_check_function"]
  57. else:
  58. sub = int(sub)
  59. if info["csq_soln_mode"] == "value":
  60. sub = info["csq_options"][sub] if sub >= 0 else "--"
  61. check_result = check(sub, soln)
  62. if isinstance(check_result, collections.abc.Mapping):
  63. score = check_result["score"]
  64. msg = check_result["msg"]
  65. elif isinstance(check_result, collections.abc.Sequence):
  66. score, msg = check_result
  67. else:
  68. score = check_result
  69. mfunc = info["csq_msg_function"]
  70. try:
  71. msg = mfunc(sub, soln)
  72. except:
  73. try:
  74. msg = mfunc(sub)
  75. except:
  76. msg = ""
  77. percent = float(score)
  78. if info["csq_show_check"]:
  79. if percent == 1.0:
  80. response = '<img src="%s" />' % info["cs_check_image"]
  81. elif percent == 0.0:
  82. response = '<img src="%s" />' % info["cs_cross_image"]
  83. else:
  84. response = ""
  85. else:
  86. response = ""
  87. response += msg
  88. return {"score": percent, "msg": response}
  89. def render_html(last_log, **info):
  90. r = info["csq_renderer"]
  91. if r in _renderers:
  92. return _renderers[r](last_log, **info)
  93. else:
  94. return (
  95. "<font color='red'>"
  96. "Invalid <tt>multiplechoice</tt> renderer: %s"
  97. "</font>"
  98. ) % r
  99. def render_html_dropdown(last_log, **info):
  100. if last_log is None:
  101. last_log = {}
  102. out = '\n<select id="%s" name="%s" >' % (info["csq_name"], info["csq_name"])
  103. for (ix, i) in enumerate(["--"] + info["csq_options"]):
  104. out += '\n<option value="%s" ' % (ix - 1)
  105. if last_log.get(info["csq_name"], "-1") == str(ix - 1):
  106. out += "selected "
  107. out += ">%s</option>" % i
  108. out += "</select>"
  109. return out
  110. def render_html_checkbox(last_log, **info):
  111. if last_log is None:
  112. last_log = {}
  113. out = '<table border="0">'
  114. name = info["csq_name"]
  115. last = last_log.get(info["csq_name"], None)
  116. if last is None:
  117. last = {}
  118. else:
  119. try:
  120. last = json.loads(last)
  121. except:
  122. last = {}
  123. if not isinstance(last, dict):
  124. try:
  125. last = {("%s_opt%d" % (name, last)): True}
  126. except:
  127. last = {}
  128. checked = set()
  129. for (ix, i) in enumerate(info["csq_options"]):
  130. out += '\n<tr><td align="center">'
  131. _n = "%s_opt%d" % (name, ix)
  132. if last.get(_n, False):
  133. _s = " checked"
  134. checked.add(_n)
  135. else:
  136. _s = ""
  137. out += '<input type="checkbox" name="%s" value="%s"%s />' % (_n, ix, _s)
  138. text = csm_language.source_transform_string(info, i)
  139. out += "</td><td>%s</td></tr>" % text
  140. out += "\n</table>"
  141. out += '<input type="hidden" name="%s" id="%s" value="%s">' % (
  142. name,
  143. name,
  144. last or "",
  145. )
  146. checked_str = ",".join(("%r: true" % i) for i in checked)
  147. out += (
  148. '\n<script type="text/javascript">'
  149. "\n// @license magnet:?xt=urn:btih:0b31508aeb0634b347b8270c7bee4d411b5d4109&dn=agpl-3.0.txt AGPL-v3"
  150. "\nvar %s_selected = {%s};"
  151. '\ndocument.getElementById("%s").value = JSON.stringify(%s_selected);'
  152. '\ndocument.querySelectorAll("input[type=checkbox][name^=%s_opt]").forEach(function(r){'
  153. '\n r.addEventListener("click", function(){'
  154. '\n %s_selected[this.getAttribute("name")] = this.checked;'
  155. '\n document.getElementById("%s").value = JSON.stringify(%s_selected);'
  156. "\n });"
  157. "\n});"
  158. "\n// @license-end"
  159. "\n</script>"
  160. ) % ((info["csq_name"], checked_str) + (info["csq_name"],) * 6)
  161. return out
  162. def render_html_radio(last_log, **info):
  163. if last_log is None:
  164. last_log = {}
  165. out = '<table border="0">'
  166. name = info["csq_name"]
  167. last = last_log.get(info["csq_name"], None)
  168. for (ix, i) in enumerate(info["csq_options"]):
  169. out += '\n<tr><td align="center">'
  170. if last == str(ix):
  171. _s = " checked"
  172. else:
  173. _s = ""
  174. out += '<input type="radio" name="%s_opts" value="%s"%s />' % (name, ix, _s)
  175. text = csm_language.source_transform_string(info, i)
  176. out += "</td><td>%s</td></tr>" % text
  177. out += "\n</table>"
  178. out += '<input type="hidden" name="%s" id="%s" value="%s">' % (
  179. name,
  180. name,
  181. last or "",
  182. )
  183. out += (
  184. '\n<script type="text/javascript">'
  185. "\n// @license magnet:?xt=urn:btih:0b31508aeb0634b347b8270c7bee4d411b5d4109&dn=agpl-3.0.txt AGPL-v3"
  186. '\ndocument.querySelectorAll("input[type=radio][name=%s_opts]").forEach(function(r){'
  187. '\n r.addEventListener("click", function(){'
  188. '\n document.getElementById("%s").value = this.value;'
  189. "\n });"
  190. "\n});"
  191. "\n// @license-end"
  192. "\n</script>"
  193. ) % (info["csq_name"], info["csq_name"])
  194. return out
  195. _renderers = {
  196. "dropdown": render_html_dropdown,
  197. "radio": render_html_radio,
  198. "checkbox": render_html_checkbox,
  199. }
  200. def answer_display(**info):
  201. if info["csq_renderer"] == "checkbox":
  202. out = "Solution: <table>"
  203. for c, i in zip(info["csq_soln"], info["csq_options"]):
  204. out += '<tr style="height:30px;"></tr>'
  205. out += '<tr><td align="center">'
  206. _im = "check" if c else "cross"
  207. out += '<img src="BASE/images/%s.png" />' % _im
  208. out += "</td><td>"
  209. text = csm_language.source_transform_string(info, i)
  210. out += text
  211. out += "</td></tr>"
  212. out += "</table>"
  213. else:
  214. soln = info["csq_soln"]
  215. out = "Solution: %s" % (soln,)
  216. return out