diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 715a215f46a..a9c2157e444 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,6 +21,7 @@ jobs: toxenv: [ "django32-drflatest", + "django40-drflatest", "quality", "pii_check", "version_check", diff --git a/CHANGELOG.rst b/CHANGELOG.rst index ecb6aca128a..e94cd47bece 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -15,6 +15,12 @@ Unreleased ~~~~~~~~~~ [4.10.1] - 2022-04-06 +~~~~~~~~~~~~~~~~~~~~~ +* Enabled Django40 testing +* Removed Deprecated and Removed Featured from Django40 + +[4.10.1] - 2022-04-06 +~~~~~~~~~~~~~~~~~~~~~ * Fixed the syntax error in CI workflow to make it work. * Removed Django40 tests for now which will be enabled in subsequent PR diff --git a/edx_proctoring/__init__.py b/edx_proctoring/__init__.py index 67b86fadc5e..3cf5076c638 100644 --- a/edx_proctoring/__init__.py +++ b/edx_proctoring/__init__.py @@ -3,6 +3,6 @@ """ # Be sure to update the version number in edx_proctoring/package.json -__version__ = '4.10.1' +__version__ = '4.10.2' default_app_config = 'edx_proctoring.apps.EdxProctoringConfig' # pylint: disable=invalid-name diff --git a/edx_proctoring/admin.py b/edx_proctoring/admin.py index 73fb95582f1..76c0bd8eb10 100644 --- a/edx_proctoring/admin.py +++ b/edx_proctoring/admin.py @@ -13,7 +13,7 @@ from django.conf import settings from django.contrib import admin, messages from django.db.models import Q -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ from edx_proctoring.api import update_attempt_status from edx_proctoring.exceptions import ProctoredExamIllegalStatusTransition, StudentExamAttemptDoesNotExistsException diff --git a/edx_proctoring/api.py b/edx_proctoring/api.py index fcb46241cc9..e869be5df30 100644 --- a/edx_proctoring/api.py +++ b/edx_proctoring/api.py @@ -18,8 +18,8 @@ from django.core.mail.message import EmailMessage from django.template import loader from django.urls import NoReverseMatch, reverse -from django.utils.translation import ugettext as _ -from django.utils.translation import ugettext_noop +from django.utils.translation import gettext as _ +from django.utils.translation import gettext_noop from edx_proctoring import constants from edx_proctoring.backends import get_backend_provider @@ -2246,47 +2246,47 @@ def _resolve_prerequisite_links(exam, prerequisites): STATUS_SUMMARY_MAP = { '_default': { - 'short_description': ugettext_noop('Taking As Proctored Exam'), + 'short_description': gettext_noop('Taking As Proctored Exam'), 'suggested_icon': 'fa-pencil-square-o', 'in_completed_state': False }, ProctoredExamStudentAttemptStatus.eligible: { - 'short_description': ugettext_noop('Proctored Option Available'), + 'short_description': gettext_noop('Proctored Option Available'), 'suggested_icon': 'fa-pencil-square-o', 'in_completed_state': False }, ProctoredExamStudentAttemptStatus.declined: { - 'short_description': ugettext_noop('Taking As Open Exam'), + 'short_description': gettext_noop('Taking As Open Exam'), 'suggested_icon': 'fa-pencil-square-o', 'in_completed_state': False }, ProctoredExamStudentAttemptStatus.submitted: { - 'short_description': ugettext_noop('Pending Session Review'), + 'short_description': gettext_noop('Pending Session Review'), 'suggested_icon': 'fa-spinner fa-spin', 'in_completed_state': True }, ProctoredExamStudentAttemptStatus.second_review_required: { - 'short_description': ugettext_noop('Pending Session Review'), + 'short_description': gettext_noop('Pending Session Review'), 'suggested_icon': 'fa-spinner fa-spin', 'in_completed_state': True }, ProctoredExamStudentAttemptStatus.verified: { - 'short_description': ugettext_noop('Passed Proctoring'), + 'short_description': gettext_noop('Passed Proctoring'), 'suggested_icon': 'fa-check', 'in_completed_state': True }, ProctoredExamStudentAttemptStatus.rejected: { - 'short_description': ugettext_noop('Failed Proctoring'), + 'short_description': gettext_noop('Failed Proctoring'), 'suggested_icon': 'fa-exclamation-triangle', 'in_completed_state': True }, ProctoredExamStudentAttemptStatus.error: { - 'short_description': ugettext_noop('Failed Proctoring'), + 'short_description': gettext_noop('Failed Proctoring'), 'suggested_icon': 'fa-exclamation-triangle', 'in_completed_state': True }, ProctoredExamStudentAttemptStatus.expired: { - 'short_description': ugettext_noop('Proctored Option No Longer Available'), + 'short_description': gettext_noop('Proctored Option No Longer Available'), 'suggested_icon': 'fa-times-circle', 'in_completed_state': False } @@ -2295,17 +2295,17 @@ def _resolve_prerequisite_links(exam, prerequisites): PRACTICE_STATUS_SUMMARY_MAP = { '_default': { - 'short_description': ugettext_noop('Ungraded Practice Exam'), + 'short_description': gettext_noop('Ungraded Practice Exam'), 'suggested_icon': '', 'in_completed_state': False }, ProctoredExamStudentAttemptStatus.submitted: { - 'short_description': ugettext_noop('Practice Exam Completed'), + 'short_description': gettext_noop('Practice Exam Completed'), 'suggested_icon': 'fa-check', 'in_completed_state': True }, ProctoredExamStudentAttemptStatus.error: { - 'short_description': ugettext_noop('Practice Exam Failed'), + 'short_description': gettext_noop('Practice Exam Failed'), 'suggested_icon': 'fa-exclamation-triangle', 'in_completed_state': True } @@ -2313,7 +2313,7 @@ def _resolve_prerequisite_links(exam, prerequisites): TIMED_EXAM_STATUS_SUMMARY_MAP = { '_default': { - 'short_description': ugettext_noop('Timed Exam'), + 'short_description': gettext_noop('Timed Exam'), 'suggested_icon': 'fa-clock-o', 'in_completed_state': False } diff --git a/edx_proctoring/instructor_dashboard_exam_urls.py b/edx_proctoring/instructor_dashboard_exam_urls.py index 6f16631055b..cfe8ff0081a 100644 --- a/edx_proctoring/instructor_dashboard_exam_urls.py +++ b/edx_proctoring/instructor_dashboard_exam_urls.py @@ -3,7 +3,7 @@ """ from django.conf import settings -from django.conf.urls import url +from django.urls import re_path from edx_proctoring import views @@ -11,7 +11,7 @@ urlpatterns = [ - url( + re_path( fr'edx_proctoring/v1/instructor/{settings.COURSE_ID_PATTERN}/(?P\d+)$', views.InstructorDashboard.as_view(), name='instructor_dashboard_exam' diff --git a/edx_proctoring/models.py b/edx_proctoring/models.py index b6aaa31747c..d1296252a60 100644 --- a/edx_proctoring/models.py +++ b/edx_proctoring/models.py @@ -16,7 +16,7 @@ from django.db import models from django.db.models import Q from django.db.models.base import ObjectDoesNotExist -from django.utils.translation import ugettext_noop +from django.utils.translation import gettext_noop from edx_proctoring.backends import get_backend_provider from edx_proctoring.constants import VERIFICATION_DAYS_VALID @@ -213,6 +213,7 @@ class ProctoredExamStudentAttemptManager(models.Manager): """ Custom manager """ + def get_current_exam_attempt(self, exam_id, user_id): """ Returns the most recent Student Exam Attempt object if found @@ -391,11 +392,11 @@ class ProctoredExamStudentAttempt(TimeStampedModel): # if the user is attempting this as a proctored exam # in case there is an option to opt-out - taking_as_proctored = models.BooleanField(default=False, verbose_name=ugettext_noop("Taking as Proctored")) + taking_as_proctored = models.BooleanField(default=False, verbose_name=gettext_noop("Taking as Proctored")) # Whether this attempt is considered a sample attempt, e.g. to try out # the proctoring software - is_sample_attempt = models.BooleanField(default=False, verbose_name=ugettext_noop("Is Sample Attempt")) + is_sample_attempt = models.BooleanField(default=False, verbose_name=gettext_noop("Is Sample Attempt")) # what review policy was this exam submitted under # Note that this is not a foreign key because @@ -413,15 +414,15 @@ class ProctoredExamStudentAttempt(TimeStampedModel): # marks whether the attempt is able to be resumed by user # Only those attempts which had an error state before, but # has not yet marked submitted is resumable. - is_resumable = models.BooleanField(default=False, verbose_name=ugettext_noop("Is Resumable")) + is_resumable = models.BooleanField(default=False, verbose_name=gettext_noop("Is Resumable")) # marks whether or not an attempt has been marked as ready to resume # by staff. The value of this field does not necessarily mean that an # attempt is ready to resume by a learner, only that the staff has marked it as such. - ready_to_resume = models.BooleanField(default=False, verbose_name=ugettext_noop("Ready to Resume")) + ready_to_resume = models.BooleanField(default=False, verbose_name=gettext_noop("Ready to Resume")) # marks whether or not an attempt has been resumed by a learner. - resumed = models.BooleanField(default=False, verbose_name=ugettext_noop("Resumed")) + resumed = models.BooleanField(default=False, verbose_name=gettext_noop("Resumed")) history = HistoricalRecords(table_name='proctoring_proctoredexamstudentattempt_history') @@ -492,6 +493,7 @@ class QuerySetWithUpdateOverride(models.QuerySet): Custom QuerySet class to make an archive copy every time the object is updated. """ + def update(self, **kwargs): """ Create a copy after update """ super().update(**kwargs) @@ -503,6 +505,7 @@ class ProctoredExamStudentAllowanceManager(models.Manager): Custom manager to override with the custom queryset to enable archiving on Allowance updation. """ + def get_queryset(self): """ Return a specialized queryset @@ -521,8 +524,8 @@ class ProctoredExamStudentAllowance(TimeStampedModel): # DONT EDIT THE KEYS - THE FIRST VALUE OF THE TUPLE - AS ARE THEY ARE STORED IN THE DATABASE # THE SECOND ELEMENT OF THE TUPLE IS A DISPLAY STRING AND CAN BE EDITED - ADDITIONAL_TIME_GRANTED = ('additional_time_granted', ugettext_noop('Additional Time (minutes)')) - REVIEW_POLICY_EXCEPTION = ('review_policy_exception', ugettext_noop('Review Policy Exception')) + ADDITIONAL_TIME_GRANTED = ('additional_time_granted', gettext_noop('Additional Time (minutes)')) + REVIEW_POLICY_EXCEPTION = ('review_policy_exception', gettext_noop('Review Policy Exception')) all_allowances = [ ADDITIONAL_TIME_GRANTED + REVIEW_POLICY_EXCEPTION diff --git a/edx_proctoring/signals.py b/edx_proctoring/signals.py index 61187867fec..5fd01293388 100644 --- a/edx_proctoring/signals.py +++ b/edx_proctoring/signals.py @@ -2,13 +2,4 @@ from django.dispatch import Signal # Signal that is emitted when an attempt status is updated. Added to utils to avoid cyclic import in signals.py file -exam_attempt_status_signal = Signal(providing_args=[ - "attempt_id", - "user_id", - "status", - "full_name", - "profile_name", - "is_practice_exam", - "is_proctored" - "backend_supports_onboarding" -]) +exam_attempt_status_signal = Signal() diff --git a/edx_proctoring/urls.py b/edx_proctoring/urls.py index 9198866adf6..250345d18fb 100644 --- a/edx_proctoring/urls.py +++ b/edx_proctoring/urls.py @@ -3,7 +3,7 @@ """ from django.conf import settings -from django.conf.urls import include, url +from django.urls import include, path, re_path from edx_proctoring import callbacks, instructor_dashboard_exam_urls, views @@ -12,153 +12,128 @@ CONTENT_ID_PATTERN = r'(?P([A-z0-9]+|(?:i4x://?[^/]+/[^/]+/[^/]+/[^@]+(?:@[^/]+)?)|(?:[^/]+)))' urlpatterns = [ - url( - r'edx_proctoring/v1/proctored_exam/exam$', - views.ProctoredExamView.as_view(), - name='proctored_exam.exam' - ), - url( - r'edx_proctoring/v1/proctored_exam/exam/exam_id/(?P\d+)$', - views.ProctoredExamView.as_view(), - name='proctored_exam.exam_by_id' - ), - url( + path('edx_proctoring/v1/proctored_exam/exam', views.ProctoredExamView.as_view(), + name='proctored_exam.exam' + ), + path('edx_proctoring/v1/proctored_exam/exam/exam_id/', views.ProctoredExamView.as_view(), + name='proctored_exam.exam_by_id' + ), + re_path( (fr'edx_proctoring/v1/proctored_exam/exam/course_id/{settings.COURSE_ID_PATTERN}' '/content_id/(?P[A-z0-9]+)$'), views.ProctoredExamView.as_view(), name='proctored_exam.exam_by_content_id' ), - url( + re_path( fr'edx_proctoring/v1/proctored_exam/exam/course_id/{settings.COURSE_ID_PATTERN}$', views.ProctoredExamView.as_view(), name='proctored_exam.exams_by_course_id' ), - url( - r'edx_proctoring/v1/proctored_exam/attempt/(?P\d+)$', - views.StudentProctoredExamAttempt.as_view(), - name='proctored_exam.attempt' - ), - url( + path('edx_proctoring/v1/proctored_exam/attempt/', views.StudentProctoredExamAttempt.as_view(), + name='proctored_exam.attempt' + ), + re_path( fr'edx_proctoring/v1/proctored_exam/attempt/grouped/course_id/{settings.COURSE_ID_PATTERN}$', views.StudentProctoredGroupedExamAttemptsByCourse.as_view(), name='proctored_exam.attempts.grouped.course' ), - url( + re_path( 'edx_proctoring/v1/proctored_exam/attempt/grouped/course_id/' fr'{settings.COURSE_ID_PATTERN}/search/(?P.+)$', views.StudentProctoredGroupedExamAttemptsByCourse.as_view(), name='proctored_exam.attempts.grouped.search' ), - url( - r'edx_proctoring/v1/proctored_exam/attempt$', - views.StudentProctoredExamAttemptCollection.as_view(), - name='proctored_exam.attempt.collection' - ), - url( - r'edx_proctoring/v1/proctored_exam/attempt/(?P\d+)/review_status$', - views.ProctoredExamAttemptReviewStatus.as_view(), - name='proctored_exam.attempt.review_status' - ), - url( - r'edx_proctoring/v1/proctored_exam/attempt/(?P[-\w]+)/ready$', - views.ExamReadyCallback.as_view(), - name='proctored_exam.attempt.ready_callback' - ), - url( - r'edx_proctoring/v1/proctored_exam/attempt/(?P[-\w]+)/reviewed$', - views.ProctoredExamReviewCallback.as_view(), - name='proctored_exam.attempt.callback' - ), - url( + path('edx_proctoring/v1/proctored_exam/attempt', + views.StudentProctoredExamAttemptCollection.as_view(), + name='proctored_exam.attempt.collection' + ), + path('edx_proctoring/v1/proctored_exam/attempt//review_status', + views.ProctoredExamAttemptReviewStatus.as_view(), + name='proctored_exam.attempt.review_status' + ), + path('edx_proctoring/v1/proctored_exam/attempt//ready', + views.ExamReadyCallback.as_view(), + name='proctored_exam.attempt.ready_callback' + ), + path('edx_proctoring/v1/proctored_exam/attempt//reviewed', + views.ProctoredExamReviewCallback.as_view(), + name='proctored_exam.attempt.callback' + ), + re_path( fr'edx_proctoring/v1/proctored_exam/{settings.COURSE_ID_PATTERN}/allowance$', views.ExamAllowanceView.as_view(), name='proctored_exam.allowance' ), - url( - r'edx_proctoring/v1/proctored_exam/allowance$', - views.ExamAllowanceView.as_view(), - name='proctored_exam.allowance' - ), - url( + path('edx_proctoring/v1/proctored_exam/allowance', views.ExamAllowanceView.as_view(), + name='proctored_exam.allowance' + ), + re_path( fr'edx_proctoring/v1/proctored_exam/{settings.COURSE_ID_PATTERN}/bulk_allowance$', views.ExamBulkAllowanceView.as_view(), name='proctored_exam.bulk_allowance' ), - url( - r'edx_proctoring/v1/proctored_exam/bulk_allowance$', - views.ExamBulkAllowanceView.as_view(), - name='proctored_exam.bulk_allowance' - ), - url( + path('edx_proctoring/v1/proctored_exam/bulk_allowance', views.ExamBulkAllowanceView.as_view(), + name='proctored_exam.bulk_allowance' + ), + re_path( fr'edx_proctoring/v1/proctored_exam/{settings.COURSE_ID_PATTERN}/grouped/allowance$', views.GroupedExamAllowancesByStudent.as_view(), name='proctored_exam.allowance.grouped.course' ), - url( - r'edx_proctoring/v1/proctored_exam/active_exams_for_user$', - views.ActiveExamsForUserView.as_view(), - name='proctored_exam.active_exams_for_user' - ), - url( - r'edx_proctoring/v1/user_onboarding/status$', - views.StudentOnboardingStatusView.as_view(), - name='user_onboarding.status' - ), - url( + path('edx_proctoring/v1/proctored_exam/active_exams_for_user', views.ActiveExamsForUserView.as_view(), + name='proctored_exam.active_exams_for_user' + ), + path('edx_proctoring/v1/user_onboarding/status', views.StudentOnboardingStatusView.as_view(), + name='user_onboarding.status' + ), + re_path( fr'edx_proctoring/v1/user_onboarding/status/course_id/{settings.COURSE_ID_PATTERN}$', views.StudentOnboardingStatusByCourseView.as_view(), name='user_onboarding.status.course' ), - url( + re_path( fr'edx_proctoring/v1/instructor/{settings.COURSE_ID_PATTERN}$', views.InstructorDashboard.as_view(), name='instructor_dashboard_course' ), - url( + re_path( r'edx_proctoring/v1/retire_backend_user/(?P[\d]+)/$', views.BackendUserManagementAPI.as_view(), name='backend_user_deletion_api' ), - url( + re_path( r'edx_proctoring/v1/retire_user/(?P[\d]+)/$', views.UserRetirement.as_view(), name='user_retirement_api' ), - url( + re_path( fr'edx_proctoring/v1/proctored_exam/attempt/course_id/{settings.COURSE_ID_PATTERN}$', views.ProctoredExamAttemptView.as_view(), name='proctored_exam.exam_attempts' ), - url( - r'edx_proctoring/v1/proctored_exam/settings/exam_id/(?P\d+)/$', - views.ProctoredSettingsView.as_view(), - name='proctored_exam.proctoring_settings' - ), - url( - r'edx_proctoring/v1/proctored_exam/review_policy/exam_id/(?P\d+)/$', - views.ProctoredExamReviewPolicyView.as_view(), - name='proctored_exam.review_policy' - ), + path('edx_proctoring/v1/proctored_exam/settings/exam_id//', views.ProctoredSettingsView.as_view(), + name='proctored_exam.proctoring_settings' + ), + path('edx_proctoring/v1/proctored_exam/review_policy/exam_id//', + views.ProctoredExamReviewPolicyView.as_view(), + name='proctored_exam.review_policy' + ), # Unauthenticated callbacks from SoftwareSecure. Note we use other # security token measures to protect data # - url( - r'edx_proctoring/proctoring_launch_callback/start_exam/(?P[-\w]+)$', - callbacks.start_exam_callback, - name='anonymous.proctoring_launch_callback.start_exam' - ), - url( - r'edx_proctoring/proctoring_review_callback/$', - views.AnonymousReviewCallback.as_view(), - name='anonymous.proctoring_review_callback' - ), - url( + path('edx_proctoring/proctoring_launch_callback/start_exam/', callbacks.start_exam_callback, + name='anonymous.proctoring_launch_callback.start_exam' + ), + path('edx_proctoring/proctoring_review_callback/', views.AnonymousReviewCallback.as_view(), + name='anonymous.proctoring_review_callback' + ), + re_path( r'edx_proctoring/v1/proctored_exam/exam_id/(?P\d+)/user_id/(?P[\d]+)/reset_attempts$', views.StudentProctoredExamResetAttempts.as_view(), name='proctored_exam.attempts.reset' ), - url(r'^', include('rest_framework.urls', namespace='rest_framework')), + path('', include('rest_framework.urls', namespace='rest_framework')), ] urlpatterns += instructor_dashboard_exam_urls.urlpatterns diff --git a/edx_proctoring/utils.py b/edx_proctoring/utils.py index 085f262c57d..4b034a73a50 100644 --- a/edx_proctoring/utils.py +++ b/edx_proctoring/utils.py @@ -26,7 +26,7 @@ from django.conf import settings from django.urls import reverse -from django.utils.translation import ugettext as _ +from django.utils.translation import gettext as _ from edx_proctoring.models import ProctoredExamStudentAttempt from edx_proctoring.runtime import get_runtime_service diff --git a/edx_proctoring/views.py b/edx_proctoring/views.py index 4cc61ac49da..671b91692b0 100644 --- a/edx_proctoring/views.py +++ b/edx_proctoring/views.py @@ -23,7 +23,7 @@ from django.shortcuts import redirect from django.urls import reverse from django.utils.decorators import method_decorator -from django.utils.translation import ugettext as _ +from django.utils.translation import gettext as _ from edx_proctoring import constants from edx_proctoring.api import ( diff --git a/package.json b/package.json index 585d2b9ca7c..f0170cf1848 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@edx/edx-proctoring", "//": "Note that the version format is slightly different than that of the Python version when using prereleases.", - "version": "4.10.1", + "version": "4.10.2", "main": "edx_proctoring/static/index.js", "scripts": { "test": "gulp test" diff --git a/test_urls.py b/test_urls.py index 9f28e3dbb36..5feadff1d5a 100644 --- a/test_urls.py +++ b/test_urls.py @@ -1,14 +1,15 @@ from django.conf import settings -from django.conf.urls import include, url +from django.conf.urls import include from edx_proctoring import views +from django.urls import path, re_path urlpatterns = [ - url(r'^', include('edx_proctoring.urls', namespace='edx_proctoring')), + path('', include('edx_proctoring.urls', namespace='edx_proctoring')), # Fake view to mock url pattern provided by edx_platform - url( + re_path( r'^courses/{}/jump_to/(?P.*)$'.format( settings.COURSE_ID_PATTERN, ),