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.
- edx_proctoring/__init__.py +6 -0
- edx_proctoring/admin.py +502 -0
- edx_proctoring/api.py +3269 -0
- edx_proctoring/apps.py +162 -0
- edx_proctoring/backends/LICENSE.txt +202 -0
- edx_proctoring/backends/README.txt +1 -0
- edx_proctoring/backends/__init__.py +20 -0
- edx_proctoring/backends/backend.py +162 -0
- edx_proctoring/backends/mock.py +105 -0
- edx_proctoring/backends/null.py +61 -0
- edx_proctoring/backends/rest.py +422 -0
- edx_proctoring/backends/software_secure.py +423 -0
- edx_proctoring/backends/tests/__init__.py +3 -0
- edx_proctoring/backends/tests/test_backend.py +363 -0
- edx_proctoring/backends/tests/test_rest.py +624 -0
- edx_proctoring/backends/tests/test_review_payload.py +102 -0
- edx_proctoring/backends/tests/test_software_secure.py +628 -0
- edx_proctoring/callbacks.py +66 -0
- edx_proctoring/constants.py +83 -0
- edx_proctoring/decorators.py +3 -0
- edx_proctoring/exceptions.py +188 -0
- edx_proctoring/handlers.py +229 -0
- edx_proctoring/instructor_dashboard_exam_urls.py +19 -0
- edx_proctoring/locale/ar/LC_MESSAGES/django.mo +0 -0
- edx_proctoring/locale/ar/LC_MESSAGES/django.po +1445 -0
- edx_proctoring/locale/ar/LC_MESSAGES/djangojs.mo +0 -0
- edx_proctoring/locale/ar/LC_MESSAGES/djangojs.po +1443 -0
- edx_proctoring/locale/ca/LC_MESSAGES/django.mo +0 -0
- edx_proctoring/locale/ca/LC_MESSAGES/django.po +1464 -0
- edx_proctoring/locale/ca/LC_MESSAGES/djangojs.mo +0 -0
- edx_proctoring/locale/ca/LC_MESSAGES/djangojs.po +1463 -0
- edx_proctoring/locale/config.yaml +73 -0
- edx_proctoring/locale/cs/LC_MESSAGES/django.mo +0 -0
- edx_proctoring/locale/cs/LC_MESSAGES/django.po +1632 -0
- edx_proctoring/locale/cs/LC_MESSAGES/djangojs.mo +0 -0
- edx_proctoring/locale/cs/LC_MESSAGES/djangojs.po +1634 -0
- edx_proctoring/locale/de_DE/LC_MESSAGES/django.mo +0 -0
- edx_proctoring/locale/de_DE/LC_MESSAGES/django.po +1461 -0
- edx_proctoring/locale/de_DE/LC_MESSAGES/djangojs.mo +0 -0
- edx_proctoring/locale/de_DE/LC_MESSAGES/djangojs.po +1461 -0
- edx_proctoring/locale/el/LC_MESSAGES/django.mo +0 -0
- edx_proctoring/locale/el/LC_MESSAGES/django.po +1389 -0
- edx_proctoring/locale/el/LC_MESSAGES/djangojs.mo +0 -0
- edx_proctoring/locale/el/LC_MESSAGES/djangojs.po +1387 -0
- edx_proctoring/locale/en/LC_MESSAGES/django.mo +0 -0
- edx_proctoring/locale/en/LC_MESSAGES/django.po +1476 -0
- edx_proctoring/locale/en/LC_MESSAGES/djangojs.mo +0 -0
- edx_proctoring/locale/en/LC_MESSAGES/djangojs.po +264 -0
- edx_proctoring/locale/eo/LC_MESSAGES/django.mo +0 -0
- edx_proctoring/locale/eo/LC_MESSAGES/django.po +1880 -0
- edx_proctoring/locale/eo/LC_MESSAGES/djangojs.mo +0 -0
- edx_proctoring/locale/eo/LC_MESSAGES/djangojs.po +304 -0
- edx_proctoring/locale/es_419/LC_MESSAGES/django.mo +0 -0
- edx_proctoring/locale/es_419/LC_MESSAGES/django.po +1783 -0
- edx_proctoring/locale/es_419/LC_MESSAGES/djangojs.mo +0 -0
- edx_proctoring/locale/es_419/LC_MESSAGES/djangojs.po +1785 -0
- edx_proctoring/locale/es_AR/LC_MESSAGES/django.mo +0 -0
- edx_proctoring/locale/es_AR/LC_MESSAGES/django.po +1654 -0
- edx_proctoring/locale/es_AR/LC_MESSAGES/djangojs.mo +0 -0
- edx_proctoring/locale/es_AR/LC_MESSAGES/djangojs.po +1655 -0
- edx_proctoring/locale/es_ES/LC_MESSAGES/django.mo +0 -0
- edx_proctoring/locale/es_ES/LC_MESSAGES/django.po +1655 -0
- edx_proctoring/locale/es_ES/LC_MESSAGES/djangojs.mo +0 -0
- edx_proctoring/locale/es_ES/LC_MESSAGES/djangojs.po +1652 -0
- edx_proctoring/locale/eu_ES/LC_MESSAGES/django.mo +0 -0
- edx_proctoring/locale/eu_ES/LC_MESSAGES/django.po +1378 -0
- edx_proctoring/locale/eu_ES/LC_MESSAGES/djangojs.mo +0 -0
- edx_proctoring/locale/eu_ES/LC_MESSAGES/djangojs.po +1378 -0
- edx_proctoring/locale/fa_IR/LC_MESSAGES/django.mo +0 -0
- edx_proctoring/locale/fa_IR/LC_MESSAGES/django.po +1666 -0
- edx_proctoring/locale/fa_IR/LC_MESSAGES/djangojs.mo +0 -0
- edx_proctoring/locale/fa_IR/LC_MESSAGES/djangojs.po +1665 -0
- edx_proctoring/locale/fr/LC_MESSAGES/django.mo +0 -0
- edx_proctoring/locale/fr/LC_MESSAGES/django.po +1805 -0
- edx_proctoring/locale/fr/LC_MESSAGES/djangojs.mo +0 -0
- edx_proctoring/locale/fr/LC_MESSAGES/djangojs.po +1802 -0
- edx_proctoring/locale/fr_CA/LC_MESSAGES/django.mo +0 -0
- edx_proctoring/locale/fr_CA/LC_MESSAGES/django.po +1790 -0
- edx_proctoring/locale/fr_CA/LC_MESSAGES/djangojs.mo +0 -0
- edx_proctoring/locale/fr_CA/LC_MESSAGES/djangojs.po +1793 -0
- edx_proctoring/locale/he/LC_MESSAGES/django.mo +0 -0
- edx_proctoring/locale/he/LC_MESSAGES/django.po +1444 -0
- edx_proctoring/locale/he/LC_MESSAGES/djangojs.mo +0 -0
- edx_proctoring/locale/he/LC_MESSAGES/djangojs.po +1443 -0
- edx_proctoring/locale/id/LC_MESSAGES/django.mo +0 -0
- edx_proctoring/locale/id/LC_MESSAGES/django.po +1390 -0
- edx_proctoring/locale/id/LC_MESSAGES/djangojs.mo +0 -0
- edx_proctoring/locale/id/LC_MESSAGES/djangojs.po +1389 -0
- edx_proctoring/locale/it_IT/LC_MESSAGES/django.mo +0 -0
- edx_proctoring/locale/it_IT/LC_MESSAGES/django.po +1715 -0
- edx_proctoring/locale/it_IT/LC_MESSAGES/djangojs.mo +0 -0
- edx_proctoring/locale/it_IT/LC_MESSAGES/djangojs.po +1715 -0
- edx_proctoring/locale/ja_JP/LC_MESSAGES/djangojs.mo +0 -0
- edx_proctoring/locale/ja_JP/LC_MESSAGES/djangojs.po +1387 -0
- edx_proctoring/locale/ka/LC_MESSAGES/django.mo +0 -0
- edx_proctoring/locale/ka/LC_MESSAGES/django.po +1380 -0
- edx_proctoring/locale/ka/LC_MESSAGES/djangojs.mo +0 -0
- edx_proctoring/locale/ka/LC_MESSAGES/djangojs.po +1380 -0
- edx_proctoring/locale/lv/LC_MESSAGES/django.mo +0 -0
- edx_proctoring/locale/lv/LC_MESSAGES/django.po +1377 -0
- edx_proctoring/locale/lv/LC_MESSAGES/djangojs.mo +0 -0
- edx_proctoring/locale/lv/LC_MESSAGES/djangojs.po +1377 -0
- edx_proctoring/locale/mn/LC_MESSAGES/django.mo +0 -0
- edx_proctoring/locale/mn/LC_MESSAGES/django.po +1434 -0
- edx_proctoring/locale/mn/LC_MESSAGES/djangojs.mo +0 -0
- edx_proctoring/locale/mn/LC_MESSAGES/djangojs.po +1379 -0
- edx_proctoring/locale/nb/LC_MESSAGES/django.mo +0 -0
- edx_proctoring/locale/nb/LC_MESSAGES/django.po +1393 -0
- edx_proctoring/locale/nb/LC_MESSAGES/djangojs.mo +0 -0
- edx_proctoring/locale/nb/LC_MESSAGES/djangojs.po +1393 -0
- edx_proctoring/locale/pl/LC_MESSAGES/django.mo +0 -0
- edx_proctoring/locale/pl/LC_MESSAGES/django.po +1386 -0
- edx_proctoring/locale/pl/LC_MESSAGES/djangojs.mo +0 -0
- edx_proctoring/locale/pl/LC_MESSAGES/djangojs.po +1386 -0
- edx_proctoring/locale/pt_PT/LC_MESSAGES/django.mo +0 -0
- edx_proctoring/locale/pt_PT/LC_MESSAGES/django.po +1793 -0
- edx_proctoring/locale/pt_PT/LC_MESSAGES/djangojs.mo +0 -0
- edx_proctoring/locale/pt_PT/LC_MESSAGES/djangojs.po +1810 -0
- edx_proctoring/locale/ro/LC_MESSAGES/django.mo +0 -0
- edx_proctoring/locale/ro/LC_MESSAGES/django.po +1382 -0
- edx_proctoring/locale/ro/LC_MESSAGES/djangojs.mo +0 -0
- edx_proctoring/locale/ro/LC_MESSAGES/djangojs.po +1382 -0
- edx_proctoring/locale/ru/LC_MESSAGES/django.mo +0 -0
- edx_proctoring/locale/ru/LC_MESSAGES/django.po +1456 -0
- edx_proctoring/locale/ru/LC_MESSAGES/djangojs.mo +0 -0
- edx_proctoring/locale/ru/LC_MESSAGES/djangojs.po +1459 -0
- edx_proctoring/locale/sw_KE/LC_MESSAGES/django.mo +0 -0
- edx_proctoring/locale/sw_KE/LC_MESSAGES/django.po +1462 -0
- edx_proctoring/locale/sw_KE/LC_MESSAGES/djangojs.mo +0 -0
- edx_proctoring/locale/sw_KE/LC_MESSAGES/djangojs.po +1464 -0
- edx_proctoring/locale/tr_TR/LC_MESSAGES/django.mo +0 -0
- edx_proctoring/locale/tr_TR/LC_MESSAGES/django.po +1527 -0
- edx_proctoring/locale/tr_TR/LC_MESSAGES/djangojs.mo +0 -0
- edx_proctoring/locale/tr_TR/LC_MESSAGES/djangojs.po +1527 -0
- edx_proctoring/locale/uk/LC_MESSAGES/django.mo +0 -0
- edx_proctoring/locale/uk/LC_MESSAGES/django.po +1443 -0
- edx_proctoring/locale/uk/LC_MESSAGES/djangojs.mo +0 -0
- edx_proctoring/locale/uk/LC_MESSAGES/djangojs.po +1443 -0
- edx_proctoring/locale/zh_CN/LC_MESSAGES/django.mo +0 -0
- edx_proctoring/locale/zh_CN/LC_MESSAGES/django.po +1468 -0
- edx_proctoring/locale/zh_CN/LC_MESSAGES/djangojs.mo +0 -0
- edx_proctoring/locale/zh_CN/LC_MESSAGES/djangojs.po +1469 -0
- edx_proctoring/management/__init__.py +3 -0
- edx_proctoring/management/commands/__init__.py +3 -0
- edx_proctoring/management/commands/reset_attempts.py +87 -0
- edx_proctoring/management/commands/set_attempt_status.py +52 -0
- edx_proctoring/management/commands/set_is_attempt_active.py +104 -0
- edx_proctoring/management/commands/tests/__init__.py +3 -0
- edx_proctoring/management/commands/tests/test_reset_attempts.py +153 -0
- edx_proctoring/management/commands/tests/test_set_attempt_status.py +85 -0
- edx_proctoring/management/commands/tests/test_set_is_attempt_active.py +85 -0
- edx_proctoring/management/commands/tests/test_update_attempt_status_from_review.py +113 -0
- edx_proctoring/management/commands/tests/test_update_attempts_for_exam.py +62 -0
- edx_proctoring/management/commands/update_attempt_status_from_review.py +81 -0
- edx_proctoring/management/commands/update_attempts_for_exam.py +78 -0
- edx_proctoring/migrations/0001_initial.py +227 -0
- edx_proctoring/migrations/0002_proctoredexamstudentattempt_is_status_acknowledged.py +18 -0
- edx_proctoring/migrations/0003_auto_20160101_0525.py +24 -0
- edx_proctoring/migrations/0004_auto_20160201_0523.py +19 -0
- edx_proctoring/migrations/0005_proctoredexam_hide_after_due.py +19 -0
- edx_proctoring/migrations/0006_allowed_time_limit_mins.py +24 -0
- edx_proctoring/migrations/0007_proctoredexam_backend.py +20 -0
- edx_proctoring/migrations/0008_auto_20181116_1551.py +32 -0
- edx_proctoring/migrations/0009_proctoredexamreviewpolicy_remove_rules.py +23 -0
- edx_proctoring/migrations/0010_update_backend.py +32 -0
- edx_proctoring/migrations/0011_allow_multiple_attempts.py +17 -0
- edx_proctoring/migrations/0012_proctoredexamstudentattempt_time_remaining_seconds.py +18 -0
- edx_proctoring/migrations/0013_proctoredexamsoftwaresecurereview_is_active_attempt.py +23 -0
- edx_proctoring/migrations/0014_add_is_resumable_to_proctoredexamstudentattempt.py +23 -0
- edx_proctoring/migrations/0015_rm_proctoredexamstudentattempt_ips.py +29 -0
- edx_proctoring/migrations/0016_nullable_proctoredexamstudentattempt_name.py +23 -0
- edx_proctoring/migrations/0017_rm_proctoredexamstudentattempt_name.py +21 -0
- edx_proctoring/migrations/0018_historicalproctoredexamstudentattempt.py +53 -0
- edx_proctoring/migrations/0019_proctoredexamsoftwaresecurereview_encrypted_video_url.py +18 -0
- edx_proctoring/migrations/0020_auto_20211028_1915.py +23 -0
- edx_proctoring/migrations/0021_auto_20211029_1353.py +26 -0
- edx_proctoring/migrations/0022_proctoredexamstudentattempt_add_readytoresume_resumed.py +33 -0
- edx_proctoring/migrations/0023_historicalproctoredexam.py +50 -0
- edx_proctoring/migrations/0024_delete_proctoredexamstudentattempthistory.py +16 -0
- edx_proctoring/migrations/0025_auto_20230727_2112.py +21 -0
- edx_proctoring/migrations/__init__.py +0 -0
- edx_proctoring/models.py +844 -0
- edx_proctoring/rules.py +15 -0
- edx_proctoring/runtime.py +21 -0
- edx_proctoring/scripts/obscure_user.py +108 -0
- edx_proctoring/serializers.py +147 -0
- edx_proctoring/services.py +43 -0
- edx_proctoring/settings/__init__.py +0 -0
- edx_proctoring/settings/common.py +50 -0
- edx_proctoring/settings/production.py +13 -0
- edx_proctoring/settings/test.py +11 -0
- edx_proctoring/signals.py +5 -0
- edx_proctoring/static/index.js +41 -0
- edx_proctoring/static/proctoring/clippy.swf +0 -0
- edx_proctoring/static/proctoring/close-modal-hover.png +0 -0
- edx_proctoring/static/proctoring/close-modal.png +0 -0
- edx_proctoring/static/proctoring/js/collections/proctored_exam_allowance_collection.js +15 -0
- edx_proctoring/static/proctoring/js/collections/proctored_exam_attempt_grouped_collection.js +15 -0
- edx_proctoring/static/proctoring/js/collections/proctored_exam_collection.js +16 -0
- edx_proctoring/static/proctoring/js/collections/proctored_exam_onboarding_collection.js +13 -0
- edx_proctoring/static/proctoring/js/dropdown.js +97 -0
- edx_proctoring/static/proctoring/js/exam_action_handler.js +252 -0
- edx_proctoring/static/proctoring/js/models/learner_onboarding_model.js +9 -0
- edx_proctoring/static/proctoring/js/models/proctored_exam_allowance_model.js +15 -0
- edx_proctoring/static/proctoring/js/models/proctored_exam_attempt_model.js +15 -0
- edx_proctoring/static/proctoring/js/models/proctored_exam_bulk_allowance_model.js +15 -0
- edx_proctoring/static/proctoring/js/models/proctored_exam_model.js +56 -0
- edx_proctoring/static/proctoring/js/proctored_app.js +16 -0
- edx_proctoring/static/proctoring/js/vendor/backbone.js +1608 -0
- edx_proctoring/static/proctoring/js/vendor/date.js +145 -0
- edx_proctoring/static/proctoring/js/vendor/i18n.js +10 -0
- edx_proctoring/static/proctoring/js/vendor/jquery.js +4 -0
- edx_proctoring/static/proctoring/js/vendor/underscore.js +1416 -0
- edx_proctoring/static/proctoring/js/views/Backbone.ModalDialog.js +338 -0
- edx_proctoring/static/proctoring/js/views/proctored_exam_add_bulk_allowance_view.js +300 -0
- edx_proctoring/static/proctoring/js/views/proctored_exam_allowance_view.js +204 -0
- edx_proctoring/static/proctoring/js/views/proctored_exam_attempt_view.js +307 -0
- edx_proctoring/static/proctoring/js/views/proctored_exam_edit_allowance_view.js +165 -0
- edx_proctoring/static/proctoring/js/views/proctored_exam_info.js +232 -0
- edx_proctoring/static/proctoring/js/views/proctored_exam_instructor_launch.js +41 -0
- edx_proctoring/static/proctoring/js/views/proctored_exam_onboarding_view.js +288 -0
- edx_proctoring/static/proctoring/js/views/proctored_exam_view.js +267 -0
- edx_proctoring/static/proctoring/templates/add-new-bulk-allowance.underscore +105 -0
- edx_proctoring/static/proctoring/templates/course_grouped_allowances.underscore +70 -0
- edx_proctoring/static/proctoring/templates/dashboard.underscore +3 -0
- edx_proctoring/static/proctoring/templates/edit-allowance.underscore +52 -0
- edx_proctoring/static/proctoring/templates/proctored-exam-info.underscore +46 -0
- edx_proctoring/static/proctoring/templates/student-onboarding-status.underscore +153 -0
- edx_proctoring/static/proctoring/templates/student-proctored-exam-attempts-grouped.underscore +287 -0
- edx_proctoring/statuses.py +330 -0
- edx_proctoring/templates/common/waiting_banner.html +12 -0
- edx_proctoring/templates/emails/proctoring_attempt_satisfactory_email.html +28 -0
- edx_proctoring/templates/emails/proctoring_attempt_submitted_email.html +20 -0
- edx_proctoring/templates/emails/proctoring_attempt_unsatisfactory_email.html +33 -0
- edx_proctoring/templates/onboarding_exam/entrance.html +65 -0
- edx_proctoring/templates/onboarding_exam/error.html +18 -0
- edx_proctoring/templates/onboarding_exam/ready_to_submit.html +10 -0
- edx_proctoring/templates/onboarding_exam/rejected.html +33 -0
- edx_proctoring/templates/onboarding_exam/submitted.html +60 -0
- edx_proctoring/templates/onboarding_exam/verified.html +23 -0
- edx_proctoring/templates/practice_exam/entrance.html +90 -0
- edx_proctoring/templates/practice_exam/error.html +42 -0
- edx_proctoring/templates/practice_exam/submitted.html +31 -0
- edx_proctoring/templates/proctored_exam/confirm-decline.html +55 -0
- edx_proctoring/templates/proctored_exam/entrance.html +29 -0
- edx_proctoring/templates/proctored_exam/error.html +26 -0
- edx_proctoring/templates/proctored_exam/error_modal.html +26 -0
- edx_proctoring/templates/proctored_exam/error_wrong_browser.html +32 -0
- edx_proctoring/templates/proctored_exam/expired.html +13 -0
- edx_proctoring/templates/proctored_exam/failed-prerequisites.html +46 -0
- edx_proctoring/templates/proctored_exam/footer.html +80 -0
- edx_proctoring/templates/proctored_exam/inactive_account.html +38 -0
- edx_proctoring/templates/proctored_exam/instructions.html +227 -0
- edx_proctoring/templates/proctored_exam/onboarding_error.html +45 -0
- edx_proctoring/templates/proctored_exam/pending-prerequisites.html +40 -0
- edx_proctoring/templates/proctored_exam/proctoring_launch_callback.html +183 -0
- edx_proctoring/templates/proctored_exam/proctoring_opt_out_button.html +8 -0
- edx_proctoring/templates/proctored_exam/ready_to_resume.html +33 -0
- edx_proctoring/templates/proctored_exam/ready_to_start.html +70 -0
- edx_proctoring/templates/proctored_exam/ready_to_submit.html +39 -0
- edx_proctoring/templates/proctored_exam/rejected.html +30 -0
- edx_proctoring/templates/proctored_exam/submitted.html +24 -0
- edx_proctoring/templates/proctored_exam/verified.html +19 -0
- edx_proctoring/templates/proctored_exam/visit_exam_content.html +38 -0
- edx_proctoring/templates/timed_exam/entrance.html +79 -0
- edx_proctoring/templates/timed_exam/expired.html +13 -0
- edx_proctoring/templates/timed_exam/footer.html +10 -0
- edx_proctoring/templates/timed_exam/ready_to_submit.html +56 -0
- edx_proctoring/templates/timed_exam/submitted.html +32 -0
- edx_proctoring/tests/__init__.py +56 -0
- edx_proctoring/tests/test_api.py +3878 -0
- edx_proctoring/tests/test_email.py +325 -0
- edx_proctoring/tests/test_handlers.py +56 -0
- edx_proctoring/tests/test_mfe_views.py +579 -0
- edx_proctoring/tests/test_models.py +483 -0
- edx_proctoring/tests/test_reviews.py +844 -0
- edx_proctoring/tests/test_serializer.py +34 -0
- edx_proctoring/tests/test_services.py +429 -0
- edx_proctoring/tests/test_student_view.py +1676 -0
- edx_proctoring/tests/test_utils/factories.py +35 -0
- edx_proctoring/tests/test_utils/test_utils.py +207 -0
- edx_proctoring/tests/test_utils/utils.py +402 -0
- edx_proctoring/tests/test_views.py +6501 -0
- edx_proctoring/tests/test_workerconfig.py +96 -0
- edx_proctoring/urls.py +149 -0
- edx_proctoring/utils.py +488 -0
- edx_proctoring/views.py +2333 -0
- edx_proctoring-6.2.0.dist-info/METADATA +152 -0
- edx_proctoring-6.2.0.dist-info/RECORD +293 -0
- edx_proctoring-6.2.0.dist-info/WHEEL +5 -0
- edx_proctoring-6.2.0.dist-info/entry_points.txt +11 -0
- edx_proctoring-6.2.0.dist-info/licenses/LICENSE.txt +674 -0
- edx_proctoring-6.2.0.dist-info/top_level.txt +1 -0
edx_proctoring/admin.py
ADDED
|
@@ -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:', '')
|