edx-proctoring 6.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (293) hide show
  1. edx_proctoring/__init__.py +6 -0
  2. edx_proctoring/admin.py +502 -0
  3. edx_proctoring/api.py +3269 -0
  4. edx_proctoring/apps.py +162 -0
  5. edx_proctoring/backends/LICENSE.txt +202 -0
  6. edx_proctoring/backends/README.txt +1 -0
  7. edx_proctoring/backends/__init__.py +20 -0
  8. edx_proctoring/backends/backend.py +162 -0
  9. edx_proctoring/backends/mock.py +105 -0
  10. edx_proctoring/backends/null.py +61 -0
  11. edx_proctoring/backends/rest.py +422 -0
  12. edx_proctoring/backends/software_secure.py +423 -0
  13. edx_proctoring/backends/tests/__init__.py +3 -0
  14. edx_proctoring/backends/tests/test_backend.py +363 -0
  15. edx_proctoring/backends/tests/test_rest.py +624 -0
  16. edx_proctoring/backends/tests/test_review_payload.py +102 -0
  17. edx_proctoring/backends/tests/test_software_secure.py +628 -0
  18. edx_proctoring/callbacks.py +66 -0
  19. edx_proctoring/constants.py +83 -0
  20. edx_proctoring/decorators.py +3 -0
  21. edx_proctoring/exceptions.py +188 -0
  22. edx_proctoring/handlers.py +229 -0
  23. edx_proctoring/instructor_dashboard_exam_urls.py +19 -0
  24. edx_proctoring/locale/ar/LC_MESSAGES/django.mo +0 -0
  25. edx_proctoring/locale/ar/LC_MESSAGES/django.po +1445 -0
  26. edx_proctoring/locale/ar/LC_MESSAGES/djangojs.mo +0 -0
  27. edx_proctoring/locale/ar/LC_MESSAGES/djangojs.po +1443 -0
  28. edx_proctoring/locale/ca/LC_MESSAGES/django.mo +0 -0
  29. edx_proctoring/locale/ca/LC_MESSAGES/django.po +1464 -0
  30. edx_proctoring/locale/ca/LC_MESSAGES/djangojs.mo +0 -0
  31. edx_proctoring/locale/ca/LC_MESSAGES/djangojs.po +1463 -0
  32. edx_proctoring/locale/config.yaml +73 -0
  33. edx_proctoring/locale/cs/LC_MESSAGES/django.mo +0 -0
  34. edx_proctoring/locale/cs/LC_MESSAGES/django.po +1632 -0
  35. edx_proctoring/locale/cs/LC_MESSAGES/djangojs.mo +0 -0
  36. edx_proctoring/locale/cs/LC_MESSAGES/djangojs.po +1634 -0
  37. edx_proctoring/locale/de_DE/LC_MESSAGES/django.mo +0 -0
  38. edx_proctoring/locale/de_DE/LC_MESSAGES/django.po +1461 -0
  39. edx_proctoring/locale/de_DE/LC_MESSAGES/djangojs.mo +0 -0
  40. edx_proctoring/locale/de_DE/LC_MESSAGES/djangojs.po +1461 -0
  41. edx_proctoring/locale/el/LC_MESSAGES/django.mo +0 -0
  42. edx_proctoring/locale/el/LC_MESSAGES/django.po +1389 -0
  43. edx_proctoring/locale/el/LC_MESSAGES/djangojs.mo +0 -0
  44. edx_proctoring/locale/el/LC_MESSAGES/djangojs.po +1387 -0
  45. edx_proctoring/locale/en/LC_MESSAGES/django.mo +0 -0
  46. edx_proctoring/locale/en/LC_MESSAGES/django.po +1476 -0
  47. edx_proctoring/locale/en/LC_MESSAGES/djangojs.mo +0 -0
  48. edx_proctoring/locale/en/LC_MESSAGES/djangojs.po +264 -0
  49. edx_proctoring/locale/eo/LC_MESSAGES/django.mo +0 -0
  50. edx_proctoring/locale/eo/LC_MESSAGES/django.po +1880 -0
  51. edx_proctoring/locale/eo/LC_MESSAGES/djangojs.mo +0 -0
  52. edx_proctoring/locale/eo/LC_MESSAGES/djangojs.po +304 -0
  53. edx_proctoring/locale/es_419/LC_MESSAGES/django.mo +0 -0
  54. edx_proctoring/locale/es_419/LC_MESSAGES/django.po +1783 -0
  55. edx_proctoring/locale/es_419/LC_MESSAGES/djangojs.mo +0 -0
  56. edx_proctoring/locale/es_419/LC_MESSAGES/djangojs.po +1785 -0
  57. edx_proctoring/locale/es_AR/LC_MESSAGES/django.mo +0 -0
  58. edx_proctoring/locale/es_AR/LC_MESSAGES/django.po +1654 -0
  59. edx_proctoring/locale/es_AR/LC_MESSAGES/djangojs.mo +0 -0
  60. edx_proctoring/locale/es_AR/LC_MESSAGES/djangojs.po +1655 -0
  61. edx_proctoring/locale/es_ES/LC_MESSAGES/django.mo +0 -0
  62. edx_proctoring/locale/es_ES/LC_MESSAGES/django.po +1655 -0
  63. edx_proctoring/locale/es_ES/LC_MESSAGES/djangojs.mo +0 -0
  64. edx_proctoring/locale/es_ES/LC_MESSAGES/djangojs.po +1652 -0
  65. edx_proctoring/locale/eu_ES/LC_MESSAGES/django.mo +0 -0
  66. edx_proctoring/locale/eu_ES/LC_MESSAGES/django.po +1378 -0
  67. edx_proctoring/locale/eu_ES/LC_MESSAGES/djangojs.mo +0 -0
  68. edx_proctoring/locale/eu_ES/LC_MESSAGES/djangojs.po +1378 -0
  69. edx_proctoring/locale/fa_IR/LC_MESSAGES/django.mo +0 -0
  70. edx_proctoring/locale/fa_IR/LC_MESSAGES/django.po +1666 -0
  71. edx_proctoring/locale/fa_IR/LC_MESSAGES/djangojs.mo +0 -0
  72. edx_proctoring/locale/fa_IR/LC_MESSAGES/djangojs.po +1665 -0
  73. edx_proctoring/locale/fr/LC_MESSAGES/django.mo +0 -0
  74. edx_proctoring/locale/fr/LC_MESSAGES/django.po +1805 -0
  75. edx_proctoring/locale/fr/LC_MESSAGES/djangojs.mo +0 -0
  76. edx_proctoring/locale/fr/LC_MESSAGES/djangojs.po +1802 -0
  77. edx_proctoring/locale/fr_CA/LC_MESSAGES/django.mo +0 -0
  78. edx_proctoring/locale/fr_CA/LC_MESSAGES/django.po +1790 -0
  79. edx_proctoring/locale/fr_CA/LC_MESSAGES/djangojs.mo +0 -0
  80. edx_proctoring/locale/fr_CA/LC_MESSAGES/djangojs.po +1793 -0
  81. edx_proctoring/locale/he/LC_MESSAGES/django.mo +0 -0
  82. edx_proctoring/locale/he/LC_MESSAGES/django.po +1444 -0
  83. edx_proctoring/locale/he/LC_MESSAGES/djangojs.mo +0 -0
  84. edx_proctoring/locale/he/LC_MESSAGES/djangojs.po +1443 -0
  85. edx_proctoring/locale/id/LC_MESSAGES/django.mo +0 -0
  86. edx_proctoring/locale/id/LC_MESSAGES/django.po +1390 -0
  87. edx_proctoring/locale/id/LC_MESSAGES/djangojs.mo +0 -0
  88. edx_proctoring/locale/id/LC_MESSAGES/djangojs.po +1389 -0
  89. edx_proctoring/locale/it_IT/LC_MESSAGES/django.mo +0 -0
  90. edx_proctoring/locale/it_IT/LC_MESSAGES/django.po +1715 -0
  91. edx_proctoring/locale/it_IT/LC_MESSAGES/djangojs.mo +0 -0
  92. edx_proctoring/locale/it_IT/LC_MESSAGES/djangojs.po +1715 -0
  93. edx_proctoring/locale/ja_JP/LC_MESSAGES/djangojs.mo +0 -0
  94. edx_proctoring/locale/ja_JP/LC_MESSAGES/djangojs.po +1387 -0
  95. edx_proctoring/locale/ka/LC_MESSAGES/django.mo +0 -0
  96. edx_proctoring/locale/ka/LC_MESSAGES/django.po +1380 -0
  97. edx_proctoring/locale/ka/LC_MESSAGES/djangojs.mo +0 -0
  98. edx_proctoring/locale/ka/LC_MESSAGES/djangojs.po +1380 -0
  99. edx_proctoring/locale/lv/LC_MESSAGES/django.mo +0 -0
  100. edx_proctoring/locale/lv/LC_MESSAGES/django.po +1377 -0
  101. edx_proctoring/locale/lv/LC_MESSAGES/djangojs.mo +0 -0
  102. edx_proctoring/locale/lv/LC_MESSAGES/djangojs.po +1377 -0
  103. edx_proctoring/locale/mn/LC_MESSAGES/django.mo +0 -0
  104. edx_proctoring/locale/mn/LC_MESSAGES/django.po +1434 -0
  105. edx_proctoring/locale/mn/LC_MESSAGES/djangojs.mo +0 -0
  106. edx_proctoring/locale/mn/LC_MESSAGES/djangojs.po +1379 -0
  107. edx_proctoring/locale/nb/LC_MESSAGES/django.mo +0 -0
  108. edx_proctoring/locale/nb/LC_MESSAGES/django.po +1393 -0
  109. edx_proctoring/locale/nb/LC_MESSAGES/djangojs.mo +0 -0
  110. edx_proctoring/locale/nb/LC_MESSAGES/djangojs.po +1393 -0
  111. edx_proctoring/locale/pl/LC_MESSAGES/django.mo +0 -0
  112. edx_proctoring/locale/pl/LC_MESSAGES/django.po +1386 -0
  113. edx_proctoring/locale/pl/LC_MESSAGES/djangojs.mo +0 -0
  114. edx_proctoring/locale/pl/LC_MESSAGES/djangojs.po +1386 -0
  115. edx_proctoring/locale/pt_PT/LC_MESSAGES/django.mo +0 -0
  116. edx_proctoring/locale/pt_PT/LC_MESSAGES/django.po +1793 -0
  117. edx_proctoring/locale/pt_PT/LC_MESSAGES/djangojs.mo +0 -0
  118. edx_proctoring/locale/pt_PT/LC_MESSAGES/djangojs.po +1810 -0
  119. edx_proctoring/locale/ro/LC_MESSAGES/django.mo +0 -0
  120. edx_proctoring/locale/ro/LC_MESSAGES/django.po +1382 -0
  121. edx_proctoring/locale/ro/LC_MESSAGES/djangojs.mo +0 -0
  122. edx_proctoring/locale/ro/LC_MESSAGES/djangojs.po +1382 -0
  123. edx_proctoring/locale/ru/LC_MESSAGES/django.mo +0 -0
  124. edx_proctoring/locale/ru/LC_MESSAGES/django.po +1456 -0
  125. edx_proctoring/locale/ru/LC_MESSAGES/djangojs.mo +0 -0
  126. edx_proctoring/locale/ru/LC_MESSAGES/djangojs.po +1459 -0
  127. edx_proctoring/locale/sw_KE/LC_MESSAGES/django.mo +0 -0
  128. edx_proctoring/locale/sw_KE/LC_MESSAGES/django.po +1462 -0
  129. edx_proctoring/locale/sw_KE/LC_MESSAGES/djangojs.mo +0 -0
  130. edx_proctoring/locale/sw_KE/LC_MESSAGES/djangojs.po +1464 -0
  131. edx_proctoring/locale/tr_TR/LC_MESSAGES/django.mo +0 -0
  132. edx_proctoring/locale/tr_TR/LC_MESSAGES/django.po +1527 -0
  133. edx_proctoring/locale/tr_TR/LC_MESSAGES/djangojs.mo +0 -0
  134. edx_proctoring/locale/tr_TR/LC_MESSAGES/djangojs.po +1527 -0
  135. edx_proctoring/locale/uk/LC_MESSAGES/django.mo +0 -0
  136. edx_proctoring/locale/uk/LC_MESSAGES/django.po +1443 -0
  137. edx_proctoring/locale/uk/LC_MESSAGES/djangojs.mo +0 -0
  138. edx_proctoring/locale/uk/LC_MESSAGES/djangojs.po +1443 -0
  139. edx_proctoring/locale/zh_CN/LC_MESSAGES/django.mo +0 -0
  140. edx_proctoring/locale/zh_CN/LC_MESSAGES/django.po +1468 -0
  141. edx_proctoring/locale/zh_CN/LC_MESSAGES/djangojs.mo +0 -0
  142. edx_proctoring/locale/zh_CN/LC_MESSAGES/djangojs.po +1469 -0
  143. edx_proctoring/management/__init__.py +3 -0
  144. edx_proctoring/management/commands/__init__.py +3 -0
  145. edx_proctoring/management/commands/reset_attempts.py +87 -0
  146. edx_proctoring/management/commands/set_attempt_status.py +52 -0
  147. edx_proctoring/management/commands/set_is_attempt_active.py +104 -0
  148. edx_proctoring/management/commands/tests/__init__.py +3 -0
  149. edx_proctoring/management/commands/tests/test_reset_attempts.py +153 -0
  150. edx_proctoring/management/commands/tests/test_set_attempt_status.py +85 -0
  151. edx_proctoring/management/commands/tests/test_set_is_attempt_active.py +85 -0
  152. edx_proctoring/management/commands/tests/test_update_attempt_status_from_review.py +113 -0
  153. edx_proctoring/management/commands/tests/test_update_attempts_for_exam.py +62 -0
  154. edx_proctoring/management/commands/update_attempt_status_from_review.py +81 -0
  155. edx_proctoring/management/commands/update_attempts_for_exam.py +78 -0
  156. edx_proctoring/migrations/0001_initial.py +227 -0
  157. edx_proctoring/migrations/0002_proctoredexamstudentattempt_is_status_acknowledged.py +18 -0
  158. edx_proctoring/migrations/0003_auto_20160101_0525.py +24 -0
  159. edx_proctoring/migrations/0004_auto_20160201_0523.py +19 -0
  160. edx_proctoring/migrations/0005_proctoredexam_hide_after_due.py +19 -0
  161. edx_proctoring/migrations/0006_allowed_time_limit_mins.py +24 -0
  162. edx_proctoring/migrations/0007_proctoredexam_backend.py +20 -0
  163. edx_proctoring/migrations/0008_auto_20181116_1551.py +32 -0
  164. edx_proctoring/migrations/0009_proctoredexamreviewpolicy_remove_rules.py +23 -0
  165. edx_proctoring/migrations/0010_update_backend.py +32 -0
  166. edx_proctoring/migrations/0011_allow_multiple_attempts.py +17 -0
  167. edx_proctoring/migrations/0012_proctoredexamstudentattempt_time_remaining_seconds.py +18 -0
  168. edx_proctoring/migrations/0013_proctoredexamsoftwaresecurereview_is_active_attempt.py +23 -0
  169. edx_proctoring/migrations/0014_add_is_resumable_to_proctoredexamstudentattempt.py +23 -0
  170. edx_proctoring/migrations/0015_rm_proctoredexamstudentattempt_ips.py +29 -0
  171. edx_proctoring/migrations/0016_nullable_proctoredexamstudentattempt_name.py +23 -0
  172. edx_proctoring/migrations/0017_rm_proctoredexamstudentattempt_name.py +21 -0
  173. edx_proctoring/migrations/0018_historicalproctoredexamstudentattempt.py +53 -0
  174. edx_proctoring/migrations/0019_proctoredexamsoftwaresecurereview_encrypted_video_url.py +18 -0
  175. edx_proctoring/migrations/0020_auto_20211028_1915.py +23 -0
  176. edx_proctoring/migrations/0021_auto_20211029_1353.py +26 -0
  177. edx_proctoring/migrations/0022_proctoredexamstudentattempt_add_readytoresume_resumed.py +33 -0
  178. edx_proctoring/migrations/0023_historicalproctoredexam.py +50 -0
  179. edx_proctoring/migrations/0024_delete_proctoredexamstudentattempthistory.py +16 -0
  180. edx_proctoring/migrations/0025_auto_20230727_2112.py +21 -0
  181. edx_proctoring/migrations/__init__.py +0 -0
  182. edx_proctoring/models.py +844 -0
  183. edx_proctoring/rules.py +15 -0
  184. edx_proctoring/runtime.py +21 -0
  185. edx_proctoring/scripts/obscure_user.py +108 -0
  186. edx_proctoring/serializers.py +147 -0
  187. edx_proctoring/services.py +43 -0
  188. edx_proctoring/settings/__init__.py +0 -0
  189. edx_proctoring/settings/common.py +50 -0
  190. edx_proctoring/settings/production.py +13 -0
  191. edx_proctoring/settings/test.py +11 -0
  192. edx_proctoring/signals.py +5 -0
  193. edx_proctoring/static/index.js +41 -0
  194. edx_proctoring/static/proctoring/clippy.swf +0 -0
  195. edx_proctoring/static/proctoring/close-modal-hover.png +0 -0
  196. edx_proctoring/static/proctoring/close-modal.png +0 -0
  197. edx_proctoring/static/proctoring/js/collections/proctored_exam_allowance_collection.js +15 -0
  198. edx_proctoring/static/proctoring/js/collections/proctored_exam_attempt_grouped_collection.js +15 -0
  199. edx_proctoring/static/proctoring/js/collections/proctored_exam_collection.js +16 -0
  200. edx_proctoring/static/proctoring/js/collections/proctored_exam_onboarding_collection.js +13 -0
  201. edx_proctoring/static/proctoring/js/dropdown.js +97 -0
  202. edx_proctoring/static/proctoring/js/exam_action_handler.js +252 -0
  203. edx_proctoring/static/proctoring/js/models/learner_onboarding_model.js +9 -0
  204. edx_proctoring/static/proctoring/js/models/proctored_exam_allowance_model.js +15 -0
  205. edx_proctoring/static/proctoring/js/models/proctored_exam_attempt_model.js +15 -0
  206. edx_proctoring/static/proctoring/js/models/proctored_exam_bulk_allowance_model.js +15 -0
  207. edx_proctoring/static/proctoring/js/models/proctored_exam_model.js +56 -0
  208. edx_proctoring/static/proctoring/js/proctored_app.js +16 -0
  209. edx_proctoring/static/proctoring/js/vendor/backbone.js +1608 -0
  210. edx_proctoring/static/proctoring/js/vendor/date.js +145 -0
  211. edx_proctoring/static/proctoring/js/vendor/i18n.js +10 -0
  212. edx_proctoring/static/proctoring/js/vendor/jquery.js +4 -0
  213. edx_proctoring/static/proctoring/js/vendor/underscore.js +1416 -0
  214. edx_proctoring/static/proctoring/js/views/Backbone.ModalDialog.js +338 -0
  215. edx_proctoring/static/proctoring/js/views/proctored_exam_add_bulk_allowance_view.js +300 -0
  216. edx_proctoring/static/proctoring/js/views/proctored_exam_allowance_view.js +204 -0
  217. edx_proctoring/static/proctoring/js/views/proctored_exam_attempt_view.js +307 -0
  218. edx_proctoring/static/proctoring/js/views/proctored_exam_edit_allowance_view.js +165 -0
  219. edx_proctoring/static/proctoring/js/views/proctored_exam_info.js +232 -0
  220. edx_proctoring/static/proctoring/js/views/proctored_exam_instructor_launch.js +41 -0
  221. edx_proctoring/static/proctoring/js/views/proctored_exam_onboarding_view.js +288 -0
  222. edx_proctoring/static/proctoring/js/views/proctored_exam_view.js +267 -0
  223. edx_proctoring/static/proctoring/templates/add-new-bulk-allowance.underscore +105 -0
  224. edx_proctoring/static/proctoring/templates/course_grouped_allowances.underscore +70 -0
  225. edx_proctoring/static/proctoring/templates/dashboard.underscore +3 -0
  226. edx_proctoring/static/proctoring/templates/edit-allowance.underscore +52 -0
  227. edx_proctoring/static/proctoring/templates/proctored-exam-info.underscore +46 -0
  228. edx_proctoring/static/proctoring/templates/student-onboarding-status.underscore +153 -0
  229. edx_proctoring/static/proctoring/templates/student-proctored-exam-attempts-grouped.underscore +287 -0
  230. edx_proctoring/statuses.py +330 -0
  231. edx_proctoring/templates/common/waiting_banner.html +12 -0
  232. edx_proctoring/templates/emails/proctoring_attempt_satisfactory_email.html +28 -0
  233. edx_proctoring/templates/emails/proctoring_attempt_submitted_email.html +20 -0
  234. edx_proctoring/templates/emails/proctoring_attempt_unsatisfactory_email.html +33 -0
  235. edx_proctoring/templates/onboarding_exam/entrance.html +65 -0
  236. edx_proctoring/templates/onboarding_exam/error.html +18 -0
  237. edx_proctoring/templates/onboarding_exam/ready_to_submit.html +10 -0
  238. edx_proctoring/templates/onboarding_exam/rejected.html +33 -0
  239. edx_proctoring/templates/onboarding_exam/submitted.html +60 -0
  240. edx_proctoring/templates/onboarding_exam/verified.html +23 -0
  241. edx_proctoring/templates/practice_exam/entrance.html +90 -0
  242. edx_proctoring/templates/practice_exam/error.html +42 -0
  243. edx_proctoring/templates/practice_exam/submitted.html +31 -0
  244. edx_proctoring/templates/proctored_exam/confirm-decline.html +55 -0
  245. edx_proctoring/templates/proctored_exam/entrance.html +29 -0
  246. edx_proctoring/templates/proctored_exam/error.html +26 -0
  247. edx_proctoring/templates/proctored_exam/error_modal.html +26 -0
  248. edx_proctoring/templates/proctored_exam/error_wrong_browser.html +32 -0
  249. edx_proctoring/templates/proctored_exam/expired.html +13 -0
  250. edx_proctoring/templates/proctored_exam/failed-prerequisites.html +46 -0
  251. edx_proctoring/templates/proctored_exam/footer.html +80 -0
  252. edx_proctoring/templates/proctored_exam/inactive_account.html +38 -0
  253. edx_proctoring/templates/proctored_exam/instructions.html +227 -0
  254. edx_proctoring/templates/proctored_exam/onboarding_error.html +45 -0
  255. edx_proctoring/templates/proctored_exam/pending-prerequisites.html +40 -0
  256. edx_proctoring/templates/proctored_exam/proctoring_launch_callback.html +183 -0
  257. edx_proctoring/templates/proctored_exam/proctoring_opt_out_button.html +8 -0
  258. edx_proctoring/templates/proctored_exam/ready_to_resume.html +33 -0
  259. edx_proctoring/templates/proctored_exam/ready_to_start.html +70 -0
  260. edx_proctoring/templates/proctored_exam/ready_to_submit.html +39 -0
  261. edx_proctoring/templates/proctored_exam/rejected.html +30 -0
  262. edx_proctoring/templates/proctored_exam/submitted.html +24 -0
  263. edx_proctoring/templates/proctored_exam/verified.html +19 -0
  264. edx_proctoring/templates/proctored_exam/visit_exam_content.html +38 -0
  265. edx_proctoring/templates/timed_exam/entrance.html +79 -0
  266. edx_proctoring/templates/timed_exam/expired.html +13 -0
  267. edx_proctoring/templates/timed_exam/footer.html +10 -0
  268. edx_proctoring/templates/timed_exam/ready_to_submit.html +56 -0
  269. edx_proctoring/templates/timed_exam/submitted.html +32 -0
  270. edx_proctoring/tests/__init__.py +56 -0
  271. edx_proctoring/tests/test_api.py +3878 -0
  272. edx_proctoring/tests/test_email.py +325 -0
  273. edx_proctoring/tests/test_handlers.py +56 -0
  274. edx_proctoring/tests/test_mfe_views.py +579 -0
  275. edx_proctoring/tests/test_models.py +483 -0
  276. edx_proctoring/tests/test_reviews.py +844 -0
  277. edx_proctoring/tests/test_serializer.py +34 -0
  278. edx_proctoring/tests/test_services.py +429 -0
  279. edx_proctoring/tests/test_student_view.py +1676 -0
  280. edx_proctoring/tests/test_utils/factories.py +35 -0
  281. edx_proctoring/tests/test_utils/test_utils.py +207 -0
  282. edx_proctoring/tests/test_utils/utils.py +402 -0
  283. edx_proctoring/tests/test_views.py +6501 -0
  284. edx_proctoring/tests/test_workerconfig.py +96 -0
  285. edx_proctoring/urls.py +149 -0
  286. edx_proctoring/utils.py +488 -0
  287. edx_proctoring/views.py +2333 -0
  288. edx_proctoring-6.2.0.dist-info/METADATA +152 -0
  289. edx_proctoring-6.2.0.dist-info/RECORD +293 -0
  290. edx_proctoring-6.2.0.dist-info/WHEEL +5 -0
  291. edx_proctoring-6.2.0.dist-info/entry_points.txt +11 -0
  292. edx_proctoring-6.2.0.dist-info/licenses/LICENSE.txt +674 -0
  293. edx_proctoring-6.2.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,6 @@
1
+ """
2
+ The exam proctoring subsystem for the Open edX platform.
3
+ """
4
+ from importlib.metadata import version
5
+
6
+ __version__ = version("edx-proctoring")
@@ -0,0 +1,502 @@
1
+ """
2
+ Django Admin pages
3
+ """
4
+ # pylint: disable=no-self-argument, no-member
5
+
6
+
7
+ from datetime import datetime, timedelta
8
+
9
+ import pytz
10
+ from simple_history.admin import SimpleHistoryAdmin
11
+
12
+ from django import forms
13
+ from django.conf import settings
14
+ from django.contrib import admin, messages
15
+ from django.db.models import Q
16
+ from django.utils.translation import gettext_lazy as _
17
+
18
+ from edx_proctoring.api import update_attempt_status
19
+ from edx_proctoring.exceptions import ProctoredExamIllegalStatusTransition, StudentExamAttemptDoesNotExistsException
20
+ from edx_proctoring.models import (
21
+ ProctoredExam,
22
+ ProctoredExamReviewPolicy,
23
+ ProctoredExamSoftwareSecureReview,
24
+ ProctoredExamSoftwareSecureReviewHistory,
25
+ ProctoredExamStudentAttempt
26
+ )
27
+ from edx_proctoring.statuses import ProctoredExamStudentAttemptStatus
28
+ from edx_proctoring.utils import locate_attempt_by_attempt_code
29
+
30
+
31
+ class ProctoredExamForm(forms.ModelForm):
32
+ """
33
+ Admin form for reading/updating a Proctored Exam
34
+ """
35
+ def __init__(self, *args, **kwargs):
36
+ super().__init__(*args, **kwargs)
37
+ self.fields['external_id'].required = False
38
+ self.fields['due_date'].required = False
39
+ self.fields['backend'].required = False
40
+
41
+ class Meta:
42
+ model = ProctoredExam
43
+ fields = '__all__'
44
+
45
+
46
+ @admin.register(ProctoredExam)
47
+ class ProctoredExamAdmin(admin.ModelAdmin):
48
+ """
49
+ Admin panel for Proctored Exams
50
+ """
51
+ form = ProctoredExamForm
52
+ search_fields = ['course_id', 'exam_name']
53
+
54
+
55
+ @admin.register(ProctoredExamReviewPolicy)
56
+ class ProctoredExamReviewPolicyAdmin(admin.ModelAdmin):
57
+ """
58
+ The admin panel for Review Policies
59
+ """
60
+ readonly_fields = ['set_by_user']
61
+
62
+ def course_id(obj):
63
+ """
64
+ Return course_id of related model
65
+ """
66
+ return obj.proctored_exam.course_id
67
+
68
+ def exam_name(obj):
69
+ """
70
+ Return exam name of related model
71
+ """
72
+ return obj.proctored_exam.exam_name
73
+
74
+ list_display = [
75
+ course_id,
76
+ exam_name,
77
+ ]
78
+ list_select_related = True
79
+ search_fields = ['proctored_exam__course_id', 'proctored_exam__exam_name']
80
+
81
+ def save_model(self, request, obj, form, change):
82
+ """
83
+ Override callback so that we can inject the user_id that made the change
84
+ """
85
+ obj.set_by_user = request.user
86
+ obj.save()
87
+
88
+
89
+ class ProctoredExamSoftwareSecureReviewForm(forms.ModelForm):
90
+ """Admin Form to display for reading/updating a Review"""
91
+ class Meta:
92
+ """Meta class"""
93
+ model = ProctoredExamSoftwareSecureReview
94
+ fields = '__all__'
95
+
96
+ REVIEW_STATUS_CHOICES = [
97
+ ('Clean', 'Clean'),
98
+ ('Rules Violation', 'Rules Violation'),
99
+ ('Suspicious', 'Suspicious'),
100
+ ('Not Reviewed', 'Not Reviewed'),
101
+ ]
102
+
103
+ review_status = forms.ChoiceField(choices=REVIEW_STATUS_CHOICES)
104
+ raw_data = forms.CharField(widget=forms.Textarea, label='Reviewer Notes')
105
+
106
+
107
+ class ReviewListFilter(admin.SimpleListFilter):
108
+ """
109
+ Quick filter to allow admins to see which reviews have not been reviewed internally
110
+ """
111
+
112
+ title = _('internally reviewed')
113
+
114
+ parameter_name = 'reviewed_by'
115
+
116
+ def lookups(self, request, model_admin):
117
+ """
118
+ List of values to allow admin to select
119
+ """
120
+ return (
121
+ ('all_unreviewed', _('All Unreviewed')),
122
+ ('all_unreviewed_failures', _('All Unreviewed Failures')),
123
+ )
124
+
125
+ def queryset(self, request, queryset):
126
+ """
127
+ Return the filtered queryset
128
+ """
129
+
130
+ if self.value() == 'all_unreviewed':
131
+ return queryset.filter(reviewed_by__isnull=True)
132
+ if self.value() == 'all_unreviewed_failures':
133
+ return queryset.filter(reviewed_by__isnull=True, review_status='Suspicious')
134
+ return queryset
135
+
136
+
137
+ class ProctoredExamListFilter(admin.SimpleListFilter):
138
+ """
139
+ Quick filter to allow admins to see which reviews have not been reviewed internally
140
+ """
141
+
142
+ title = _('active proctored exams')
143
+
144
+ parameter_name = 'exam__id'
145
+
146
+ def lookups(self, request, model_admin):
147
+ """
148
+ List of values to allow admin to select
149
+ """
150
+
151
+ now_utc = datetime.now(pytz.UTC)
152
+ thirty_days_ago = now_utc - timedelta(days=30)
153
+ one_week_from_now = now_utc + timedelta(days=7)
154
+
155
+ # only consider proctored (not practice) exams that have a due date of no more than
156
+ # a month ago as well as a week into the future. This is to help keep the list of
157
+ # quick filters small and reasonable in length
158
+ exams = ProctoredExam.objects.filter(
159
+ Q(is_proctored=True) &
160
+ Q(is_active=True) &
161
+ Q(is_practice_exam=False) &
162
+ Q(
163
+ Q(due_date__gte=thirty_days_ago, due_date__lte=one_week_from_now) |
164
+ Q(due_date__isnull=True)
165
+ )
166
+ )
167
+
168
+ lookups = (())
169
+
170
+ for exam in exams:
171
+ course_id = exam.course_id
172
+
173
+ # to help disambiguate exam names,
174
+ # prepend the exam_name with a parsed out course_id
175
+ lookups += ((
176
+ exam.id,
177
+ f'{prettify_course_id(course_id)}: {exam.exam_name}'
178
+ ),)
179
+
180
+ return lookups
181
+
182
+ def queryset(self, request, queryset):
183
+ """
184
+ Return the filtered queryset
185
+ """
186
+ if not self.value():
187
+ return queryset
188
+
189
+ return queryset.filter(exam__id=self.value())
190
+
191
+
192
+ class ProctoredExamCoursesListFilter(admin.SimpleListFilter):
193
+ """
194
+ Quick filter to allow admins to see which reviews have not been reviewed internally
195
+ """
196
+
197
+ title = _('courses with active proctored exams')
198
+
199
+ parameter_name = 'exam__course_id'
200
+
201
+ def lookups(self, request, model_admin):
202
+ """
203
+ List of values to allow admin to select
204
+ """
205
+
206
+ now_utc = datetime.now(pytz.UTC)
207
+ thirty_days_ago = now_utc - timedelta(days=30)
208
+ one_week_from_now = now_utc + timedelta(days=7)
209
+
210
+ # only consider proctored (not practice) exams that have a due date of no more than
211
+ # a month ago as well as a week into the future. This is to help keep the list of
212
+ # quick filters small and reasonable in length
213
+ exams = ProctoredExam.objects.filter(
214
+ is_proctored=True,
215
+ is_active=True,
216
+ is_practice_exam=False,
217
+ due_date__gte=thirty_days_ago,
218
+ due_date__lte=one_week_from_now
219
+ )
220
+
221
+ lookups = (())
222
+
223
+ existing_course_ids = []
224
+
225
+ for exam in exams:
226
+ # make sure we don't have duplicate course_ids
227
+ if exam.course_id not in existing_course_ids:
228
+ lookups += ((exam.course_id, exam.course_id),)
229
+ existing_course_ids.append(exam.course_id)
230
+
231
+ return lookups
232
+
233
+ def queryset(self, request, queryset):
234
+ """
235
+ Return the filtered queryset
236
+ """
237
+
238
+ if not self.value():
239
+ return queryset
240
+
241
+ return queryset.filter(
242
+ exam__course_id=self.value(),
243
+ exam__is_proctored=True,
244
+ exam__is_active=True,
245
+ exam__is_practice_exam=False
246
+ )
247
+
248
+
249
+ @admin.register(ProctoredExamSoftwareSecureReview)
250
+ class ProctoredExamSoftwareSecureReviewAdmin(admin.ModelAdmin):
251
+ """
252
+ The admin panel for SoftwareSecure Review records
253
+ """
254
+
255
+ readonly_fields = ['attempt_code', 'is_attempt_active', 'exam', 'student', 'reviewed_by', 'modified']
256
+ list_filter = [
257
+ ReviewListFilter,
258
+ ProctoredExamListFilter,
259
+ ProctoredExamCoursesListFilter,
260
+ 'review_status'
261
+ ]
262
+ list_select_related = True
263
+ search_fields = ['student__username', 'attempt_code']
264
+ ordering = ['-modified']
265
+ form = ProctoredExamSoftwareSecureReviewForm
266
+
267
+ def _get_exam_from_attempt_code(self, code):
268
+ """Get exam from attempt code. Note that the attempt code could be an archived one"""
269
+ attempt_obj = locate_attempt_by_attempt_code(code)[0]
270
+ return attempt_obj.proctored_exam if attempt_obj else None
271
+
272
+ def course_id_for_review(self, obj):
273
+ """Return course_id associated with review"""
274
+ if obj.exam:
275
+ return obj.exam.course_id
276
+ exam = self._get_exam_from_attempt_code(obj.attempt_code)
277
+ return exam.exam_name if exam else '(none)'
278
+
279
+ def exam_name_for_review(self, obj):
280
+ """Return course_id associated with review"""
281
+ if obj.exam:
282
+ return obj.exam.exam_name
283
+ exam = self._get_exam_from_attempt_code(obj.attempt_code)
284
+ return exam.exam_name if exam else '(none)'
285
+
286
+ def student_username_for_review(self, obj):
287
+ """Return username of student who took the test"""
288
+ if obj.student:
289
+ return obj.student.username
290
+ attempt_obj = locate_attempt_by_attempt_code(obj.attempt_code)[0]
291
+ return attempt_obj.user.username if attempt_obj else '(None)'
292
+
293
+ list_display = [
294
+ 'course_id_for_review',
295
+ 'exam_name_for_review',
296
+ 'student_username_for_review',
297
+ 'attempt_code',
298
+ 'is_attempt_active',
299
+ 'modified',
300
+ 'reviewed_by',
301
+ 'review_status'
302
+ ]
303
+
304
+ def has_add_permission(self, request):
305
+ """Don't allow adds"""
306
+ return False
307
+
308
+ def has_delete_permission(self, request, obj=None):
309
+ """ Allow deletes """
310
+ return True
311
+
312
+ def save_model(self, request, obj, form, change):
313
+ """
314
+ Override callback so that we can inject the user_id that made the change
315
+ """
316
+ obj.reviewed_by = request.user
317
+ obj.save()
318
+
319
+ def lookup_allowed(self, lookup, value, request=None):
320
+ """ Checks if lookup allowed or not """
321
+ if lookup == 'exam__course_id':
322
+ return True
323
+ return super().lookup_allowed(lookup, value, request)
324
+
325
+
326
+ @admin.register(ProctoredExamSoftwareSecureReviewHistory)
327
+ class ProctoredExamSoftwareSecureReviewHistoryAdmin(ProctoredExamSoftwareSecureReviewAdmin):
328
+ """
329
+ The admin panel for SoftwareSecure Review records
330
+ """
331
+
332
+ readonly_fields = [
333
+ 'review_status',
334
+ 'raw_data',
335
+ 'attempt_code',
336
+ 'exam',
337
+ 'student',
338
+ 'reviewed_by',
339
+ 'modified',
340
+ ]
341
+
342
+ def save_model(self, request, obj, form, change):
343
+ """
344
+ History can't be updated
345
+ """
346
+ return
347
+
348
+
349
+ class ExamAttemptFilterByCourseId(admin.SimpleListFilter):
350
+ """
351
+ Quick filter to allow admins to see attempts by "course_id"
352
+ """
353
+
354
+ title = _('Course Id')
355
+ parameter_name = 'proctored_exam__course_id'
356
+
357
+ def lookups(self, request, model_admin):
358
+ """
359
+ List of values to allow admin to select
360
+ """
361
+ lookups = (())
362
+ unique_course_ids = ProctoredExamStudentAttempt.objects.values_list(
363
+ 'proctored_exam__course_id',
364
+ flat=True
365
+ ).distinct()
366
+
367
+ if unique_course_ids:
368
+ lookups = [(course_id, prettify_course_id(course_id)) for course_id in unique_course_ids]
369
+
370
+ return lookups
371
+
372
+ def queryset(self, request, queryset):
373
+ """
374
+ Return the filtered queryset
375
+ """
376
+ if self.value():
377
+ return queryset.filter(proctored_exam__course_id=self.value())
378
+ return queryset
379
+
380
+
381
+ class ProctoredExamAttemptForm(forms.ModelForm):
382
+ """
383
+ Admin Form to display for reading/updating a Proctored Exam Attempt
384
+ """
385
+
386
+ class Meta:
387
+ """ Meta class """
388
+ model = ProctoredExamStudentAttempt
389
+ fields = '__all__'
390
+
391
+ STATUS_CHOICES = [
392
+ (ProctoredExamStudentAttemptStatus.created, _('Created')),
393
+ (ProctoredExamStudentAttemptStatus.download_software_clicked, _('Download Software Clicked')),
394
+ (ProctoredExamStudentAttemptStatus.ready_to_start, _('Ready To Start')),
395
+ (ProctoredExamStudentAttemptStatus.started, _('Started')),
396
+ (ProctoredExamStudentAttemptStatus.ready_to_submit, _('Ready To Submit')),
397
+ (ProctoredExamStudentAttemptStatus.declined, _('Declined')),
398
+ (ProctoredExamStudentAttemptStatus.timed_out, _('Timed Out')),
399
+ (ProctoredExamStudentAttemptStatus.submitted, _('Submitted')),
400
+ (ProctoredExamStudentAttemptStatus.second_review_required, _('Second Review Required')),
401
+ (ProctoredExamStudentAttemptStatus.verified, _('Verified')),
402
+ (ProctoredExamStudentAttemptStatus.rejected, _('Rejected')),
403
+ (ProctoredExamStudentAttemptStatus.error, _('Error')),
404
+ ]
405
+ if settings.DEBUG:
406
+ STATUS_CHOICES.extend([
407
+ (ProctoredExamStudentAttemptStatus.onboarding_missing, _('Onboarding Missing')),
408
+ (ProctoredExamStudentAttemptStatus.onboarding_failed, _('Onboarding Failed')),
409
+ (ProctoredExamStudentAttemptStatus.onboarding_pending, _('Onboarding Pending')),
410
+ (ProctoredExamStudentAttemptStatus.onboarding_expired, _('Onboarding Expired')),
411
+ ])
412
+ status = forms.ChoiceField(choices=STATUS_CHOICES)
413
+
414
+
415
+ @admin.register(ProctoredExamStudentAttempt)
416
+ class ProctoredExamStudentAttemptAdmin(SimpleHistoryAdmin):
417
+ """
418
+ Admin panel for Proctored Exam Attempts
419
+ """
420
+
421
+ readonly_fields = [
422
+ 'user',
423
+ 'proctored_exam',
424
+ 'started_at',
425
+ 'completed_at',
426
+ 'attempt_code',
427
+ 'external_id',
428
+ 'allowed_time_limit_mins',
429
+ 'taking_as_proctored',
430
+ 'is_sample_attempt',
431
+ 'review_policy_id',
432
+ 'is_status_acknowledged',
433
+ 'time_remaining_seconds'
434
+ ]
435
+
436
+ list_display = [
437
+ 'username',
438
+ 'exam_name',
439
+ 'course_id',
440
+ 'taking_as_proctored',
441
+ 'is_sample_attempt',
442
+ 'attempt_code',
443
+ 'status',
444
+ 'modified'
445
+ ]
446
+
447
+ history_list_display = [
448
+ 'status'
449
+ ]
450
+
451
+ search_fields = [
452
+ 'user__username',
453
+ 'attempt_code'
454
+ ]
455
+
456
+ list_filter = [
457
+ 'status',
458
+ 'taking_as_proctored',
459
+ 'is_sample_attempt',
460
+ ExamAttemptFilterByCourseId
461
+ ]
462
+
463
+ form = ProctoredExamAttemptForm
464
+
465
+ def username(self, obj):
466
+ """ Return user's username of attempt"""
467
+ return obj.user.username
468
+
469
+ def exam_name(self, obj):
470
+ """ Return exam_name of attempt"""
471
+ return obj.proctored_exam.exam_name
472
+
473
+ def course_id(self, obj):
474
+ """ Return course_id of attempt"""
475
+ return obj.proctored_exam.course_id
476
+
477
+ def save_model(self, request, obj, form, change):
478
+ """
479
+ Override callback so that we can change the status by "update_attempt_status" function
480
+ """
481
+ try:
482
+ if change:
483
+ update_attempt_status(obj.id, form.cleaned_data['status'])
484
+ except (ProctoredExamIllegalStatusTransition, StudentExamAttemptDoesNotExistsException) as ex:
485
+ # prevent showing success message inappropriately
486
+ messages.set_level(request, messages.ERROR)
487
+ messages.error(request, str(ex))
488
+
489
+ def has_add_permission(self, request):
490
+ """Don't allow adds"""
491
+ return False
492
+
493
+ def has_delete_permission(self, request, obj=None):
494
+ """Don't allow deletes"""
495
+ return False
496
+
497
+
498
+ def prettify_course_id(course_id):
499
+ """
500
+ Prettify the COURSE ID string
501
+ """
502
+ return course_id.replace('+', ' ').replace('/', ' ').replace('course-v1:', '')