From a7cd6847716524173ea193f1b1a5128b15972aff Mon Sep 17 00:00:00 2001 From: Ayub khan Date: Wed, 23 Oct 2019 16:30:34 +0500 Subject: [PATCH] -Ran isort to fix import order -Ran python-modernize to make python3 compatible -Fixed tests for py3 -Added py3 to travis and tox -Updated quality requirements -Moved quality control setting from tox.ini to setup.cfg with others. -Fixed all pylint issues -Fixed all pycodestyle issues -Fixed all pydocstyle issues -Version Bump --- .travis.yml | 1 + AUTHORS | 10 - edx_proctoring/__init__.py | 2 +- edx_proctoring/admin.py | 35 ++-- edx_proctoring/api.py | 195 +++++++++--------- edx_proctoring/apps.py | 21 +- edx_proctoring/backends/__init__.py | 2 + edx_proctoring/backends/backend.py | 3 + edx_proctoring/backends/mock.py | 2 +- edx_proctoring/backends/rest.py | 34 +-- edx_proctoring/backends/software_secure.py | 34 +-- edx_proctoring/backends/tests/test_backend.py | 10 +- edx_proctoring/backends/tests/test_rest.py | 13 +- .../backends/tests/test_review_payload.py | 2 + .../backends/tests/test_software_secure.py | 43 ++-- edx_proctoring/callbacks.py | 13 +- edx_proctoring/exceptions.py | 3 + .../instructor_dashboard_exam_urls.py | 2 + .../management/commands/set_attempt_status.py | 6 +- .../commands/tests/test_set_attempt_status.py | 14 +- edx_proctoring/migrations/0001_initial.py | 5 +- ...amstudentattempt_is_status_acknowledged.py | 2 +- .../migrations/0003_auto_20160101_0525.py | 2 +- .../migrations/0004_auto_20160201_0523.py | 2 +- .../0005_proctoredexam_hide_after_due.py | 2 +- .../0006_allowed_time_limit_mins.py | 2 +- .../migrations/0007_proctoredexam_backend.py | 2 +- .../migrations/0008_auto_20181116_1551.py | 5 +- ..._proctoredexamreviewpolicy_remove_rules.py | 2 +- .../migrations/0010_update_backend.py | 3 +- edx_proctoring/models.py | 21 +- edx_proctoring/rules.py | 2 +- edx_proctoring/serializers.py | 12 +- edx_proctoring/services.py | 2 +- edx_proctoring/signals.py | 14 +- edx_proctoring/statuses.py | 6 +- edx_proctoring/tests/__init__.py | 1 + edx_proctoring/tests/test_api.py | 12 +- edx_proctoring/tests/test_email.py | 30 +-- edx_proctoring/tests/test_models.py | 20 +- edx_proctoring/tests/test_reviews.py | 11 +- edx_proctoring/tests/test_services.py | 9 +- edx_proctoring/tests/test_student_view.py | 26 +-- edx_proctoring/tests/test_views.py | 41 ++-- edx_proctoring/tests/test_workerconfig.py | 11 +- edx_proctoring/tests/utils.py | 25 +-- edx_proctoring/urls.py | 4 +- edx_proctoring/utils.py | 32 +-- edx_proctoring/views.py | 163 ++++++--------- mock_apps/apps.py | 2 +- mock_apps/models.py | 4 +- package.json | 2 +- requirements/base.txt | 6 +- requirements/dev.in | 2 +- requirements/dev.txt | 16 +- requirements/doc.txt | 6 +- requirements/quality.in | 2 +- requirements/quality.txt | 10 +- requirements/test.txt | 10 +- setup.cfg | 15 ++ test_settings.py | 5 +- test_urls.py | 2 + tox.ini | 18 +- 63 files changed, 468 insertions(+), 546 deletions(-) delete mode 100644 AUTHORS diff --git a/.travis.yml b/.travis.yml index bff27661c36..0ef9704e2c6 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,6 +2,7 @@ language: python cache: pip python: - 2.7 +- 3.6 services: - xvfb env: diff --git a/AUTHORS b/AUTHORS deleted file mode 100644 index 758059362d4..00000000000 --- a/AUTHORS +++ /dev/null @@ -1,10 +0,0 @@ -Chris Dodge -Muhammad Shoaib -Afzal Wali -Mushtaq Ali -Christina Roberts -Dennis Jen -Tyler Hallada -Dave St.Germain -Matthew Hughes -Michael Roytman diff --git a/edx_proctoring/__init__.py b/edx_proctoring/__init__.py index 4ba51f3a529..d369aa7b9e6 100644 --- a/edx_proctoring/__init__.py +++ b/edx_proctoring/__init__.py @@ -5,6 +5,6 @@ from __future__ import absolute_import # Be sure to update the version number in edx_proctoring/package.json -__version__ = '2.1.2' +__version__ = '2.1.3' default_app_config = 'edx_proctoring.apps.EdxProctoringConfig' # pylint: disable=invalid-name diff --git a/edx_proctoring/admin.py b/edx_proctoring/admin.py index 83c153548af..6196dd96f24 100644 --- a/edx_proctoring/admin.py +++ b/edx_proctoring/admin.py @@ -6,28 +6,21 @@ from __future__ import absolute_import from datetime import datetime, timedelta + import pytz from django import forms -from django.db.models import Q from django.conf import settings -from django.contrib import admin -from django.contrib import messages +from django.contrib import admin, messages +from django.db.models import Q from django.utils.translation import ugettext_lazy as _ -from edx_proctoring.models import ( - ProctoredExam, - ProctoredExamReviewPolicy, - ProctoredExamSoftwareSecureReview, - ProctoredExamSoftwareSecureReviewHistory, - ProctoredExamStudentAttempt, -) + from edx_proctoring.api import update_attempt_status -from edx_proctoring.utils import locate_attempt_by_attempt_code -from edx_proctoring.exceptions import ( - ProctoredExamIllegalStatusTransition, - StudentExamAttemptDoesNotExistsException, -) +from edx_proctoring.exceptions import ProctoredExamIllegalStatusTransition, StudentExamAttemptDoesNotExistsException +from edx_proctoring.models import (ProctoredExam, ProctoredExamReviewPolicy, ProctoredExamSoftwareSecureReview, + ProctoredExamSoftwareSecureReviewHistory, ProctoredExamStudentAttempt) from edx_proctoring.statuses import ProctoredExamStudentAttemptStatus +from edx_proctoring.utils import locate_attempt_by_attempt_code class ProctoredExamReviewPolicyAdmin(admin.ModelAdmin): @@ -38,13 +31,13 @@ class ProctoredExamReviewPolicyAdmin(admin.ModelAdmin): def course_id(obj): """ - return course_id of related model + Return course_id of related model """ return obj.proctored_exam.course_id def exam_name(obj): """ - return exam name of related model + Return exam name of related model """ return obj.proctored_exam.exam_name @@ -65,7 +58,8 @@ def save_model(self, request, obj, form, change): class ProctoredExamSoftwareSecureReviewForm(forms.ModelForm): """Admin Form to display for reading/updating a Review""" - class Meta(object): # pylint: disable=missing-docstring + class Meta(object): + """Meta class""" model = ProctoredExamSoftwareSecureReview fields = '__all__' @@ -294,12 +288,14 @@ def save_model(self, request, review, form, change): # pylint: disable=argument review.save() def get_form(self, request, obj=None, **kwargs): + """ Returns software secure review form """ form = super(ProctoredExamSoftwareSecureReviewAdmin, self).get_form(request, obj, **kwargs) if 'video_url' in form.base_fields: del form.base_fields['video_url'] return form def lookup_allowed(self, key, value): # pylint: disable=arguments-differ + """ Checks if lookup allowed or not """ if key == 'exam__course_id': return True return super(ProctoredExamSoftwareSecureReviewAdmin, self).lookup_allowed(key, value) @@ -364,7 +360,8 @@ class ProctoredExamAttemptForm(forms.ModelForm): Admin Form to display for reading/updating a Proctored Exam Attempt """ - class Meta(object): # pylint: disable=missing-docstring + class Meta(object): + """ Meta class """ model = ProctoredExamStudentAttempt fields = '__all__' diff --git a/edx_proctoring/api.py b/edx_proctoring/api.py index 8c5b1836ed2..2b1ab53ec52 100644 --- a/edx_proctoring/api.py +++ b/edx_proctoring/api.py @@ -12,6 +12,7 @@ import pytz import six +from waffle import switch_is_active from django.conf import settings from django.contrib.auth import get_user_model @@ -23,21 +24,14 @@ from edx_proctoring import constants from edx_proctoring.backends import get_backend_provider -from edx_proctoring.exceptions import ( - BackendProviderCannotRegisterAttempt, - BackendProviderOnboardingException, - BackendProviderSentNoAttemptID, - ProctoredExamAlreadyExists, - ProctoredExamIllegalStatusTransition, - ProctoredExamNotActiveException, - ProctoredExamNotFoundException, - ProctoredExamPermissionDenied, - ProctoredExamReviewPolicyAlreadyExists, - ProctoredExamReviewPolicyNotFoundException, - StudentExamAttemptAlreadyExistsException, - StudentExamAttemptDoesNotExistsException, - StudentExamAttemptedAlreadyStarted, -) +from edx_proctoring.exceptions import (BackendProviderCannotRegisterAttempt, BackendProviderOnboardingException, + BackendProviderSentNoAttemptID, ProctoredExamAlreadyExists, + ProctoredExamIllegalStatusTransition, ProctoredExamNotActiveException, + ProctoredExamNotFoundException, ProctoredExamPermissionDenied, + ProctoredExamReviewPolicyAlreadyExists, + ProctoredExamReviewPolicyNotFoundException, + StudentExamAttemptAlreadyExistsException, + StudentExamAttemptDoesNotExistsException, StudentExamAttemptedAlreadyStarted) from edx_proctoring.models import (ProctoredExam, ProctoredExamReviewPolicy, ProctoredExamSoftwareSecureReview, ProctoredExamStudentAllowance, ProctoredExamStudentAttempt) from edx_proctoring.runtime import get_runtime_service @@ -46,7 +40,6 @@ from edx_proctoring.statuses import ProctoredExamStudentAttemptStatus from edx_proctoring.utils import (emit_event, get_exam_due_date, has_due_date_passed, humanized_time, is_reattempting_exam, obscured_user_id, verify_and_add_wait_deadline) -from waffle import switch_is_active log = logging.getLogger(__name__) @@ -168,11 +161,11 @@ def update_review_policy(exam_id, set_by_user_id, review_policy): exam_review_policy.set_by_user_id = set_by_user_id exam_review_policy.review_policy = review_policy exam_review_policy.save() - msg = 'Updated exam review policy with {exam_id}'.format(exam_id=exam_id) + msg = u'Updated exam review policy with {exam_id}'.format(exam_id=exam_id) log.info(msg) else: exam_review_policy.delete() - msg = 'removed exam review policy with {exam_id}'.format(exam_id=exam_id) + msg = u'removed exam review policy with {exam_id}'.format(exam_id=exam_id) log.info(msg) @@ -325,7 +318,7 @@ def get_exam_by_content_id(course_id, content_id): proctored_exam = ProctoredExam.get_exam_by_content_id(course_id, content_id) if proctored_exam is None: log.exception( - 'Cannot find the proctored exam in this course %s with content_id: %s', + u'Cannot find the proctored exam in this course %s with content_id: %s', course_id, content_id ) raise ProctoredExamNotFoundException @@ -340,8 +333,8 @@ def add_allowance_for_user(exam_id, user_info, key, value): """ log_msg = ( - 'Adding allowance "{key}" with value "{value}" for exam_id {exam_id} ' - 'for user {user_info} '.format( + u'Adding allowance "{key}" with value "{value}" for exam_id {exam_id} ' + u'for user {user_info} '.format( key=key, value=value, exam_id=exam_id, user_info=user_info ) ) @@ -379,7 +372,7 @@ def remove_allowance_for_user(exam_id, user_id, key): Deletes an allowance for a user within a given exam. """ log_msg = ( - 'Removing allowance "{key}" for exam_id {exam_id} for user_id {user_id} '.format( + u'Removing allowance "{key}" for exam_id {exam_id} for user_id {user_id} '.format( key=key, exam_id=exam_id, user_id=user_id ) ) @@ -495,12 +488,12 @@ def get_exam_attempt_by_code(attempt_code): def update_exam_attempt(attempt_id, **kwargs): """ - update exam_attempt + Update exam_attempt """ exam_attempt_obj = ProctoredExamStudentAttempt.objects.get_exam_attempt_by_id(attempt_id) if not exam_attempt_obj: err_msg = ( - 'Attempted to access of attempt object with attempt_id {attempt_id} but ' + u'Attempted to access of attempt object with attempt_id {attempt_id} but ' 'it does not exist.'.format( attempt_id=attempt_id ) @@ -512,8 +505,8 @@ def update_exam_attempt(attempt_id, **kwargs): # namely because status transitions can trigger workflow if key not in ['last_poll_timestamp', 'last_poll_ipaddr', 'is_status_acknowledged']: err_msg = ( - 'You cannot call into update_exam_attempt to change ' - 'field {key}'.format(key=key) + u'You cannot call into update_exam_attempt to change ' + u'field {key}'.format(key=key) ) raise ProctoredExamPermissionDenied(err_msg) setattr(exam_attempt_obj, key, value) @@ -530,7 +523,7 @@ def is_exam_passed_due(exam, user=None): def _was_review_status_acknowledged(is_status_acknowledged, exam): """ - return True if review status has been acknowledged and due date has been passed + Return True if review status has been acknowledged and due date has been passed """ return is_status_acknowledged and is_exam_passed_due(exam) @@ -559,8 +552,8 @@ def create_exam_attempt(exam_id, user_id, taking_as_proctored=False): # for now the student is allowed the exam default log_msg = ( - 'Creating exam attempt for exam_id {exam_id} for ' - 'user_id {user_id} with taking as proctored = {taking_as_proctored}'.format( + u'Creating exam attempt for exam_id {exam_id} for ' + u'user_id {user_id} with taking as proctored = {taking_as_proctored}'.format( exam_id=exam_id, user_id=user_id, taking_as_proctored=taking_as_proctored ) ) @@ -574,8 +567,8 @@ def create_exam_attempt(exam_id, user_id, taking_as_proctored=False): existing_attempt.delete_exam_attempt() else: err_msg = ( - 'Cannot create new exam attempt for exam_id = {exam_id} and ' - 'user_id = {user_id} because it already exists!' + u'Cannot create new exam attempt for exam_id = {exam_id} and ' + u'user_id = {user_id} because it already exists!' ).format(exam_id=exam_id, user_id=user_id) raise StudentExamAttemptAlreadyExistsException(err_msg) @@ -638,11 +631,11 @@ def create_exam_attempt(exam_id, user_id, taking_as_proctored=False): ) except BackendProviderSentNoAttemptID as ex: log_message = ( - 'Failed to get the attempt ID for {user_id}' - 'in {exam_id} from the backend because the backend' - 'did not provide the id in API response, even when the' - 'HTTP response status is {status}, ' - 'Response: {response}'.format( + u'Failed to get the attempt ID for {user_id}' + u'in {exam_id} from the backend because the backend' + u'did not provide the id in API response, even when the' + u'HTTP response status is {status}, ' + u'Response: {response}'.format( user_id=user_id, exam_id=exam_id, response=six.text_type(ex), @@ -653,10 +646,10 @@ def create_exam_attempt(exam_id, user_id, taking_as_proctored=False): raise ex except BackendProviderCannotRegisterAttempt as ex: log_message = ( - 'Failed to create attempt for {user_id} ' - 'in {exam_id} because backend was unable ' - 'to register the attempt. Status: {status}, ' - 'Reponse: {response}'.format( + u'Failed to create attempt for {user_id} ' + u'in {exam_id} because backend was unable ' + u'to register the attempt. Status: {status}, ' + u'Reponse: {response}'.format( user_id=user_id, exam_id=exam_id, response=six.text_type(ex), @@ -668,9 +661,9 @@ def create_exam_attempt(exam_id, user_id, taking_as_proctored=False): except BackendProviderOnboardingException as ex: force_status = ex.status log_msg = ( - 'Failed to create attempt for {user_id} ' - 'in {exam_id} because of onboarding failure: ' - '{force_status}'.format(**locals()) + u'Failed to create attempt for {user_id} ' + u'in {exam_id} because of onboarding failure: ' + u'{force_status}'.format(**locals()) ) log.error(log_msg) @@ -690,10 +683,10 @@ def create_exam_attempt(exam_id, user_id, taking_as_proctored=False): emit_event(exam, attempt.status, attempt=_get_exam_attempt(attempt)) log_msg = ( - 'Created exam attempt ({attempt_id}) for exam_id {exam_id} for ' - 'user_id {user_id} with taking as proctored = {taking_as_proctored} ' - 'Attempt_code {attempt_code} was generated which has a ' - 'external_id of {external_id}'.format( + u'Created exam attempt ({attempt_id}) for exam_id {exam_id} for ' + u'user_id {user_id} with taking as proctored = {taking_as_proctored} ' + u'Attempt_code {attempt_code} was generated which has a ' + u'external_id of {external_id}'.format( attempt_id=attempt.id, exam_id=exam_id, user_id=user_id, taking_as_proctored=taking_as_proctored, attempt_code=attempt_code, @@ -717,8 +710,8 @@ def start_exam_attempt(exam_id, user_id): if not existing_attempt: err_msg = ( - 'Cannot start exam attempt for exam_id = {exam_id} ' - 'and user_id = {user_id} because it does not exist!' + u'Cannot start exam attempt for exam_id = {exam_id} ' + u'and user_id = {user_id} because it does not exist!' ).format(exam_id=exam_id, user_id=user_id) raise StudentExamAttemptDoesNotExistsException(err_msg) @@ -736,8 +729,8 @@ def start_exam_attempt_by_code(attempt_code): if not existing_attempt: err_msg = ( - 'Cannot start exam attempt for attempt_code = {attempt_code} ' - 'because it does not exist!' + u'Cannot start exam attempt for attempt_code = {attempt_code} ' + u'because it does not exist!' ).format(attempt_code=attempt_code) raise StudentExamAttemptDoesNotExistsException(err_msg) @@ -753,8 +746,8 @@ def _start_exam_attempt(existing_attempt): if existing_attempt.started_at and existing_attempt.status == ProctoredExamStudentAttemptStatus.started: # cannot restart an attempt err_msg = ( - 'Cannot start exam attempt for exam_id = {exam_id} ' - 'and user_id = {user_id} because it has already started!' + u'Cannot start exam attempt for exam_id = {exam_id} ' + u'and user_id = {user_id} because it has already started!' ).format(exam_id=existing_attempt.proctored_exam.id, user_id=existing_attempt.user_id) raise StudentExamAttemptedAlreadyStarted(err_msg) @@ -789,6 +782,7 @@ def mark_exam_attempt_as_ready(exam_id, user_id): return update_attempt_status(exam_id, user_id, ProctoredExamStudentAttemptStatus.ready_to_start) +# pylint: disable=inconsistent-return-statements def update_attempt_status(exam_id, user_id, to_status, raise_if_not_found=True, cascade_effects=True, timeout_timestamp=None, update_attributable_to=None): @@ -806,8 +800,8 @@ def update_attempt_status(exam_id, user_id, to_status, from_status = exam_attempt_obj.status log_msg = ( - 'Updating attempt status for exam_id {exam_id} ' - 'for user_id {user_id} from status "{from_status}" to "{to_status}"'.format( + u'Updating attempt status for exam_id {exam_id} ' + u'for user_id {user_id} from status "{from_status}" to "{to_status}"'.format( exam_id=exam_id, user_id=user_id, from_status=from_status, to_status=to_status ) ) @@ -830,9 +824,9 @@ def update_attempt_status(exam_id, user_id, to_status, if in_completed_status and to_incompleted_status: err_msg = ( - 'A status transition from {from_status} to {to_status} was attempted ' - 'on exam_id {exam_id} for user_id {user_id}. This is not ' - 'allowed!'.format( + u'A status transition from {from_status} to {to_status} was attempted ' + u'on exam_id {exam_id} for user_id {user_id}. This is not ' + u'allowed!'.format( from_status=from_status, to_status=to_status, exam_id=exam_id, @@ -879,9 +873,9 @@ def update_attempt_status(exam_id, user_id, to_status, credit_requirement_status = 'failed' log_msg = ( - 'Calling set_credit_requirement_status for ' - 'user_id {user_id} on {course_id} for ' - 'content_id {content_id}. Status: {status}'.format( + u'Calling set_credit_requirement_status for ' + u'user_id {user_id} on {course_id} for ' + u'content_id {content_id}. Status: {status}'.format( user_id=exam_attempt_obj.user_id, course_id=exam['course_id'], content_id=exam_attempt_obj.proctored_exam.content_id, @@ -952,11 +946,11 @@ def update_attempt_status(exam_id, user_id, to_status, if grades_service.should_override_grade_on_rejected_exam(exam['course_id']): log_msg = ( - 'Overriding exam subsection grade for ' - 'user_id {user_id} on {course_id} for ' - 'content_id {content_id}. Override ' - 'earned_all: {earned_all}, ' - 'earned_graded: {earned_graded}.'.format( + u'Overriding exam subsection grade for ' + u'user_id {user_id} on {course_id} for ' + u'content_id {content_id}. Override ' + u'earned_all: {earned_all}, ' + u'earned_graded: {earned_graded}.'.format( user_id=exam_attempt_obj.user_id, course_id=exam['course_id'], content_id=exam_attempt_obj.proctored_exam.content_id, @@ -973,7 +967,7 @@ def update_attempt_status(exam_id, user_id, to_status, earned_all=REJECTED_GRADE_OVERRIDE_EARNED, earned_graded=REJECTED_GRADE_OVERRIDE_EARNED, overrider=update_attributable_to, - comment=('Failed {backend} proctoring'.format(backend=backend.verbose_name) + comment=(u'Failed {backend} proctoring'.format(backend=backend.verbose_name) if backend else 'Failed Proctoring') ) @@ -981,8 +975,8 @@ def update_attempt_status(exam_id, user_id, to_status, certificates_service = get_runtime_service('certificates') log.info( - 'Invalidating certificate for user_id {user_id} in course {course_id} whose ' - 'grade dropped below passing threshold due to suspicious proctored exam'.format( + u'Invalidating certificate for user_id {user_id} in course {course_id} whose ' + u'grade dropped below passing threshold due to suspicious proctored exam'.format( user_id=exam_attempt_obj.user_id, course_id=exam['course_id'] ) @@ -1000,11 +994,11 @@ def update_attempt_status(exam_id, user_id, to_status, if grades_service.should_override_grade_on_rejected_exam(exam['course_id']): log_msg = ( - 'Deleting override of exam subsection grade for ' - 'user_id {user_id} on {course_id} for ' - 'content_id {content_id}. Override ' - 'earned_all: {earned_all}, ' - 'earned_graded: {earned_graded}.'.format( + u'Deleting override of exam subsection grade for ' + u'user_id {user_id} on {course_id} for ' + u'content_id {content_id}. Override ' + u'earned_all: {earned_all}, ' + u'earned_graded: {earned_graded}.'.format( user_id=exam_attempt_obj.user_id, course_id=exam['course_id'], content_id=exam_attempt_obj.proctored_exam.content_id, @@ -1034,7 +1028,7 @@ def update_attempt_status(exam_id, user_id, to_status, else: course_name = default_name log.info( - "Could not find credit_state for user id %r in the course %r.", + u"Could not find credit_state for user id %r in the course %r.", exam_attempt_obj.user_id, exam_attempt_obj.proctored_exam.course_id ) @@ -1090,7 +1084,7 @@ def create_proctoring_attempt_status_email(user_id, exam_attempt_obj, course_nam user = USER_MODEL.objects.get(id=user_id) course_info_url = '' email_subject = ( - _('Proctoring Results For {course_name} {exam_name}').format( + _(u'Proctoring Results For {course_name} {exam_name}').format( course_name=course_name, exam_name=exam_attempt_obj.proctored_exam.exam_name ) @@ -1099,7 +1093,7 @@ def create_proctoring_attempt_status_email(user_id, exam_attempt_obj, course_nam if status == ProctoredExamStudentAttemptStatus.submitted: email_template_path = 'emails/proctoring_attempt_submitted_email.html' email_subject = ( - _('Proctoring Review In Progress For {course_name} {exam_name}').format( + _(u'Proctoring Review In Progress For {course_name} {exam_name}').format( course_name=course_name, exam_name=exam_attempt_obj.proctored_exam.exam_name ) @@ -1115,7 +1109,7 @@ def create_proctoring_attempt_status_email(user_id, exam_attempt_obj, course_nam try: course_info_url = reverse('info', args=[exam_attempt_obj.proctored_exam.course_id]) except NoReverseMatch: - log.exception("Can't find course info url for course %s", exam_attempt_obj.proctored_exam.course_id) + log.exception(u"Can't find course info url for course %s", exam_attempt_obj.proctored_exam.course_id) scheme = 'https' if getattr(settings, 'HTTPS', 'on') == 'on' else 'http' course_url = '{scheme}://{site_name}{course_info_url}'.format( @@ -1124,7 +1118,7 @@ def create_proctoring_attempt_status_email(user_id, exam_attempt_obj, course_nam course_info_url=course_info_url ) exam_name = exam_attempt_obj.proctored_exam.exam_name - support_email_subject = _('Proctored exam {exam_name} in {course_name} for user {username}').format( + support_email_subject = _(u'Proctored exam {exam_name} in {course_name} for user {username}').format( exam_name=exam_name, course_name=course_name, username=user.username, @@ -1160,15 +1154,15 @@ def remove_exam_attempt(attempt_id, requesting_user): """ log_msg = ( - 'Removing exam attempt {attempt_id}'.format(attempt_id=attempt_id) + u'Removing exam attempt {attempt_id}'.format(attempt_id=attempt_id) ) log.info(log_msg) existing_attempt = ProctoredExamStudentAttempt.objects.get_exam_attempt_by_id(attempt_id) if not existing_attempt: err_msg = ( - 'Cannot remove attempt for attempt_id = {attempt_id} ' - 'because it does not exist!' + u'Cannot remove attempt for attempt_id = {attempt_id} ' + u'because it does not exist!' ).format(attempt_id=attempt_id) raise StudentExamAttemptDoesNotExistsException(err_msg) @@ -1253,7 +1247,7 @@ def get_all_exam_attempts(course_id): def get_filtered_exam_attempts(course_id, search_by): """ - returns all exam attempts for a course id filtered by the search_by string in user names and emails. + Returns all exam attempts for a course id filtered by the search_by string in user names and emails. """ exam_attempts = ProctoredExamStudentAttempt.objects.get_filtered_exam_attempts(course_id, search_by) return [ProctoredExamStudentAttemptSerializer(active_exam).data for active_exam in exam_attempts] @@ -1261,7 +1255,7 @@ def get_filtered_exam_attempts(course_id, search_by): def get_last_exam_completion_date(course_id, username): """ - return the completion date of last proctoring exam for the given course and username if + Return the completion date of last proctoring exam for the given course and username if all the proctored exams are attempted and completed otherwise None """ exam_attempts = ProctoredExamStudentAttempt.objects.get_proctored_exam_attempts(course_id, username) @@ -1336,9 +1330,12 @@ def _get_ordered_prerequisites(prerequisites_statuses, filter_out_namespaces=Non return sorted_list -def _are_prerequirements_satisfied(prerequisites_statuses, evaluate_for_requirement_name=None, - filter_out_namespaces=None): - """ +def _are_prerequirements_satisfied( + prerequisites_statuses, + evaluate_for_requirement_name=None, + filter_out_namespaces=None +): + u""" Returns a dict about the fulfillment of any pre-requisites in order to this exam as proctored. The pre-requisites are taken from the credit requirements table. So if ordering of requirements are - say - ICRV1, Proctoring1, ICRV2, and Proctoring2, then the user cannot take @@ -1442,7 +1439,7 @@ def _resolve_prerequisite_links(exam, prerequisites): try: jumpto_url = reverse('jump_to', args=[exam['course_id'], prerequisite['name']]) except NoReverseMatch: - log.exception("Can't find jumpto url for course %s", exam['course_id']) + log.exception(u"Can't find jumpto url for course %s", exam['course_id']) prerequisite['jumpto_url'] = jumpto_url @@ -1549,7 +1546,7 @@ def get_attempt_status_summary(user_id, course_id, content_id): except ProctoredExamNotFoundException: # this really shouldn't happen, but log it at least log.exception( - 'Cannot find the proctored exam in this course %s with content_id: %s', + u'Cannot find the proctored exam in this course %s with content_id: %s', course_id, content_id ) return None @@ -1619,6 +1616,7 @@ def _does_time_remain(attempt): return does_time_remain +# pylint: disable=inconsistent-return-statements def _get_timed_exam_view(exam, context, exam_id, user_id, course_id): """ Returns a rendered view for the Timed Exams @@ -1680,7 +1678,7 @@ def _get_timed_exam_view(exam, context, exam_id, user_id, course_id): try: progress_page_url = reverse('progress', args=[course_id]) except NoReverseMatch: - log.exception("Can't find progress url for course %s", course_id) + log.exception(u"Can't find progress url for course %s", course_id) context.update({ 'total_time': total_time, @@ -1738,7 +1736,7 @@ def _get_proctored_exam_context(exam, attempt, user_id, course_id, is_practice_e try: progress_page_url = reverse('progress', args=[course_id]) except NoReverseMatch: - log.exception("Can't find progress url for course %s", course_id) + log.exception(u"Can't find progress url for course %s", course_id) provider = get_backend_provider(exam) @@ -1751,7 +1749,7 @@ def _get_proctored_exam_context(exam, attempt, user_id, course_id, is_practice_e 'has_due_date': has_due_date, 'has_due_date_passed': is_exam_passed_due(exam, user=user_id), 'able_to_reenter_exam': _does_time_remain(attempt) and not provider.should_block_access_to_exam_material(), - 'is_rpnow4_enabled': switch_is_active(constants.RPNOWV4_WAFFLE_NAME), + 'is_rpnow4_enabled': switch_is_active(constants.RPNOWV4_WAFFLE_NAME), # pylint: disable=illegal-waffle-usage 'enter_exam_endpoint': reverse('edx_proctoring:proctored_exam.attempt.collection'), 'exam_started_poll_url': reverse( 'edx_proctoring:proctored_exam.attempt', @@ -1791,6 +1789,7 @@ def _get_proctored_exam_context(exam, attempt, user_id, course_id, is_practice_e return context +# pylint: disable=inconsistent-return-statements def _get_practice_exam_view(exam, context, exam_id, user_id, course_id): """ Returns a rendered view for the practice Exams @@ -1828,6 +1827,7 @@ def _get_practice_exam_view(exam, context, exam_id, user_id, course_id): return template.render(context) +# pylint: disable=inconsistent-return-statements def _get_onboarding_exam_view(exam, context, exam_id, user_id, course_id): """ Returns a rendered view for onboarding exams, which for some backends establish a user's profile @@ -1871,6 +1871,7 @@ def _get_onboarding_exam_view(exam, context, exam_id, user_id, course_id): return template.render(context) +# pylint: disable=inconsistent-return-statements def _get_proctored_exam_view(exam, context, exam_id, user_id, course_id): """ Returns a rendered view for the Proctored Exams @@ -2008,7 +2009,7 @@ def _get_proctored_exam_view(exam, context, exam_id, user_id, course_id): try: context['onboarding_link'] = reverse('jump_to', args=[course_id, onboarding_exam.content_id]) except (NoReverseMatch, AttributeError): - log.exception("Can't find onboarding exam for %s", course_id) + log.exception(u"Can't find onboarding exam for %s", course_id) if student_view_template: template = loader.get_template(student_view_template) @@ -2123,14 +2124,14 @@ def get_exam_violation_report(course_id, include_practice_exams=False): attempts_by_code[attempt_code]['review_status'] = review.review_status for comment in review.proctoredexamsoftwaresecurecomment_set.all(): - comments_key = '{status} Comments'.format(status=comment.status) + comments_key = u'{status} Comments'.format(status=comment.status) if comments_key not in attempts_by_code[attempt_code]: attempts_by_code[attempt_code][comments_key] = [] attempts_by_code[attempt_code][comments_key].append(comment.comment) - return sorted(attempts_by_code.values(), key=lambda a: a['exam_name']) + return sorted(list(attempts_by_code.values()), key=lambda a: a['exam_name']) def is_backend_dashboard_available(course_id): @@ -2155,7 +2156,7 @@ def does_backend_support_onboarding(backend): return get_backend_provider(name=backend).supports_onboarding except NotImplementedError: log.exception( - "No proctoring backend configured for '{}'.".format(backend) + u"No proctoring backend configured for '{}'.".format(backend) ) return False @@ -2169,7 +2170,7 @@ def get_exam_configuration_dashboard_url(course_id, content_id): exam = get_exam_by_content_id(course_id, content_id) except ProctoredExamNotFoundException: log.exception( - 'Cannot find the proctored exam in this course %s with content_id: %s', + u'Cannot find the proctored exam in this course %s with content_id: %s', course_id, content_id ) return None diff --git a/edx_proctoring/apps.py b/edx_proctoring/apps.py index 7635e5fae32..f5d609258e0 100644 --- a/edx_proctoring/apps.py +++ b/edx_proctoring/apps.py @@ -10,10 +10,11 @@ import os.path import warnings +from stevedore.extension import ExtensionManager + from django.apps import AppConfig from django.conf import settings from django.core.exceptions import ImproperlyConfigured -from stevedore.extension import ExtensionManager def make_worker_config(backends, out='/tmp/workers.json'): @@ -35,16 +36,16 @@ def make_worker_config(backends, out='/tmp/workers.json'): # no npm module defined continue except IOError: - warnings.warn('Proctoring backend %s defined an npm module,' - 'but it is not installed at %r' % (backend.__class__, package_file)) + warnings.warn(u'Proctoring backend %s defined an npm module,' + u'but it is not installed at %r' % (backend.__class__, package_file)) except KeyError: - warnings.warn('%r does not contain a `main` entry' % package_file) + warnings.warn(u'%r does not contain a `main` entry' % package_file) if config: try: with open(out, 'wb+') as outfp: - json.dump(config, outfp) + outfp.write(json.dumps(config).encode('utf-8')) except IOError: - warnings.warn("Could not write worker config to %s" % out) + warnings.warn(u"Could not write worker config to %s" % out) else: # make sure that this file is group writable, because it may be written by different users os.chmod(out, 0o664) @@ -103,12 +104,12 @@ def get_backend(self, name=None): try: name = settings.PROCTORING_BACKENDS['DEFAULT'] except (KeyError, AttributeError): - raise ImproperlyConfigured("No default proctoring backend set in settings.PROCTORING_BACKENDS") + raise ImproperlyConfigured(u"No default proctoring backend set in settings.PROCTORING_BACKENDS") try: return self.backends[name] except KeyError: - raise NotImplementedError("No proctoring backend configured for '{}'. " - "Available: {}".format(name, list(self.backends))) + raise NotImplementedError(u"No proctoring backend configured for '{}'. " + u"Available: {}".format(name, list(self.backends))) def ready(self): """ @@ -125,4 +126,4 @@ def ready(self): self.backends[name] = extension.plugin(**options) except KeyError: pass - make_worker_config(self.backends.values(), out=os.path.join(settings.ENV_ROOT, 'workers.json')) + make_worker_config(list(self.backends.values()), out=os.path.join(settings.ENV_ROOT, 'workers.json')) diff --git a/edx_proctoring/backends/__init__.py b/edx_proctoring/backends/__init__.py index 99eead980bd..1a71b856d8a 100644 --- a/edx_proctoring/backends/__init__.py +++ b/edx_proctoring/backends/__init__.py @@ -1,6 +1,8 @@ """ All supporting Proctoring backends """ +from __future__ import absolute_import + from django.apps import apps diff --git a/edx_proctoring/backends/backend.py b/edx_proctoring/backends/backend.py index d693e504e14..01e0c9f859a 100644 --- a/edx_proctoring/backends/backend.py +++ b/edx_proctoring/backends/backend.py @@ -2,7 +2,10 @@ Defines the abstract base class that all backends should derive from """ +from __future__ import absolute_import + import abc + import six from edx_proctoring import constants diff --git a/edx_proctoring/backends/mock.py b/edx_proctoring/backends/mock.py index 07a2d9d5217..cea5fd656e3 100644 --- a/edx_proctoring/backends/mock.py +++ b/edx_proctoring/backends/mock.py @@ -7,8 +7,8 @@ import threading -from edx_proctoring.callbacks import start_exam_callback from edx_proctoring.backends.backend import ProctoringBackendProvider +from edx_proctoring.callbacks import start_exam_callback class MockProctoringBackendProvider(ProctoringBackendProvider): diff --git a/edx_proctoring/backends/rest.py b/edx_proctoring/backends/rest.py index 189dc11d4bb..d70dab76fef 100644 --- a/edx_proctoring/backends/rest.py +++ b/edx_proctoring/backends/rest.py @@ -2,17 +2,18 @@ Base implementation of a REST backend, following the API documented in docs/backends.rst """ +from __future__ import absolute_import + import logging -import warnings import time import uuid -import jwt +import warnings -# this is a known PyLint false positive (https://github.com/PyCQA/pylint/issues/1640) -# that is fixed in PyLint v1.8 -from six.moves.urllib.parse import urlparse # pylint: disable=import-error -from webpack_loader.utils import get_files +import jwt +from edx_rest_api_client.client import OAuthAPIClient +from six.moves.urllib.parse import urlparse # pylint: disable=import-error, wrong-import-order from webpack_loader.exceptions import BaseWebpackLoaderException, WebpackBundleLookupError +from webpack_loader.utils import get_files from django.conf import settings @@ -21,10 +22,9 @@ BackendProviderCannotRegisterAttempt, BackendProviderCannotRetireUser, BackendProviderOnboardingException, - BackendProviderSentNoAttemptID, + BackendProviderSentNoAttemptID ) from edx_proctoring.statuses import ProctoredExamStudentAttemptStatus, SoftwareSecureReviewStatus -from edx_rest_api_client.client import OAuthAPIClient log = logging.getLogger(__name__) @@ -149,7 +149,7 @@ def get_proctoring_config(self): Returns the metadata and configuration options for the proctoring service """ url = self.config_url - log.debug('Requesting config from %r', url) + log.debug(u'Requesting config from %r', url) response = self.session.get(url, headers=self._get_language_headers()).json() return response @@ -158,7 +158,7 @@ def get_exam(self, exam): Returns the exam metadata stored by the proctoring service """ url = self.exam_url.format(exam_id=exam['id']) - log.debug('Requesting exam from %r', url) + log.debug(u'Requesting exam from %r', url) response = self.session.get(url).json() return response @@ -185,7 +185,7 @@ def register_exam_attempt(self, exam, context): payload['status'] = 'created' # attempt code isn't needed in this API payload.pop('attempt_code', False) - log.debug('Creating exam attempt for %r at %r', exam['external_id'], url) + log.debug(u'Creating exam attempt for %r at %r', exam['external_id'], url) response = self.session.post(url, json=payload) if response.status_code != 200: raise BackendProviderCannotRegisterAttempt(response.content, response.status_code) @@ -265,7 +265,7 @@ def on_exam_saved(self, exam): url = self.exam_url.format(exam_id=external_id) else: url = self.create_exam_url - log.info('Saving exam to %r', url) + log.info(u'Saving exam to %r', url) response = None try: response = self.session.post(url, json=exam) @@ -276,7 +276,7 @@ def on_exam_saved(self, exam): content = exc.response.content if hasattr(exc, 'response') else response.content else: content = None - log.exception('failed to save exam. %r', content) + log.exception(u'failed to save exam. %r', content) data = {} return data.get('id') @@ -302,10 +302,10 @@ def get_instructor_url(self, course_id, user, exam_id=None, attempt_id=None, sho token['config'] = True if attempt_id: token['attempt_id'] = attempt_id - encoded = jwt.encode(token, self.client_secret) + encoded = jwt.encode(token, self.client_secret).decode('utf-8') url = self.instructor_url.format(client_id=self.client_id, jwt=encoded) - log.debug('Created instructor url for %r %r %r', course_id, exam_id, attempt_id) + log.debug(u'Created instructor url for %r %r %r', course_id, exam_id, attempt_id) return url def retire_user(self, user_id): @@ -350,11 +350,11 @@ def _make_attempt_request(self, exam, attempt, method='POST', status=None, **pay headers = {} if method == 'GET': headers.update(self._get_language_headers()) - log.debug('Making %r attempt request at %r', method, url) + log.debug(u'Making %r attempt request at %r', method, url) response = self.session.request(method, url, json=payload, headers=headers) try: data = response.json() except ValueError: - log.exception("Decoding attempt %r -> %r", attempt, response.content) + log.exception(u"Decoding attempt %r -> %r", attempt, response.content) data = {} return data diff --git a/edx_proctoring/backends/software_secure.py b/edx_proctoring/backends/software_secure.py index 7ac5a697179..563f58a2a3d 100644 --- a/edx_proctoring/backends/software_secure.py +++ b/edx_proctoring/backends/software_secure.py @@ -7,28 +7,24 @@ import base64 import binascii import datetime -from hashlib import sha256 import hmac import json import logging import unicodedata -import six +from hashlib import sha256 import requests - +import six from crum import get_current_request +from Cryptodome.Cipher import DES3 from waffle import switch_is_active + from django.conf import settings from django.urls import reverse -from Cryptodome.Cipher import DES3 - -from edx_proctoring.backends.backend import ProctoringBackendProvider from edx_proctoring import constants -from edx_proctoring.exceptions import ( - BackendProviderCannotRegisterAttempt, - ProctoredExamSuspiciousLookup, -) +from edx_proctoring.backends.backend import ProctoringBackendProvider +from edx_proctoring.exceptions import BackendProviderCannotRegisterAttempt, ProctoredExamSuspiciousLookup from edx_proctoring.statuses import SoftwareSecureReviewStatus log = logging.getLogger(__name__) @@ -79,6 +75,7 @@ def register_exam_attempt(self, exam, context): headers = { "Content-Type": 'application/json' } + # pylint: disable=unicode-format-string http_date = datetime.datetime.utcnow().strftime("%a, %d %b %Y %H:%M:%S GMT") signature = self._sign_doc(data, 'POST', headers, http_date) @@ -87,7 +84,7 @@ def register_exam_attempt(self, exam, context): if status not in [200, 201]: err_msg = ( u'Could not register attempt_code = {attempt_code}. ' - 'HTTP Status code was {status_code} and response was {response}.'.format( + u'HTTP Status code was {status_code} and response was {response}.'.format( attempt_code=attempt_code, status_code=status, response=response @@ -147,9 +144,9 @@ def on_review_callback(self, attempt, payload): ) if not match: err_msg = ( - 'Found attempt_code {attempt_code}, but the recorded external_id did not ' - 'match the ssiRecordLocator that had been recorded previously. Has {existing} ' - 'but received {received}!'.format( + u'Found attempt_code {attempt_code}, but the recorded external_id did not ' + u'match the ssiRecordLocator that had been recorded previously. Has {existing} ' + u'but received {received}!'.format( attempt_code=attempt['attempt_code'], existing=attempt['external_id'], received=received_id @@ -162,7 +159,7 @@ def on_review_callback(self, attempt, payload): del payload['videoReviewLink'] log_msg = ( - 'Received callback from SoftwareSecure with review data: {payload}'.format( + u'Received callback from SoftwareSecure with review data: {payload}'.format( payload=payload ) ) @@ -248,7 +245,7 @@ def _get_payload(self, exam, context): # combined with any exceptions granted to the particular student reviewer_notes = review_policy if review_policy_exception: - reviewer_notes = '{notes}; {exception}'.format( + reviewer_notes = u'{notes}; {exception}'.format( notes=reviewer_notes, exception=review_policy_exception ) @@ -256,7 +253,9 @@ def _get_payload(self, exam, context): (first_name, last_name) = self._split_fullname(full_name) now = datetime.datetime.utcnow() + # pylint: disable=unicode-format-string start_time_str = now.strftime("%a, %d %b %Y %H:%M:%S GMT") + # pylint: disable=unicode-format-string end_time_str = (now + datetime.timedelta(minutes=time_limit_mins)).strftime("%a, %d %b %Y %H:%M:%S GMT") # remove all illegal characters from the exam name exam_name = exam['exam_name'] @@ -359,7 +358,7 @@ def _sign_doc(self, body_json, method, headers, date): message = method_string + headers_str + body_str log_msg = ( - 'About to send payload to SoftwareSecure: examCode: {examCode}, courseID: {courseID}'. + u'About to send payload to SoftwareSecure: examCode: {examCode}, courseID: {courseID}'. format(examCode=body_json.get('examCode'), courseID=body_json.get('orgExtra').get('courseID')) ) log.info(log_msg) @@ -394,4 +393,5 @@ def should_block_access_to_exam_material(self): browser other than PSI's secure browser """ req = get_current_request() + # pylint: disable=illegal-waffle-usage return switch_is_active(constants.RPNOWV4_WAFFLE_NAME) and not req.get_signed_cookie('exam', default=False) diff --git a/edx_proctoring/backends/tests/test_backend.py b/edx_proctoring/backends/tests/test_backend.py index ba944100218..f5115dc8f97 100644 --- a/edx_proctoring/backends/tests/test_backend.py +++ b/edx_proctoring/backends/tests/test_backend.py @@ -5,6 +5,7 @@ from __future__ import absolute_import import time + from mock import patch from django.core.exceptions import ImproperlyConfigured @@ -12,13 +13,10 @@ from edx_proctoring.backends import get_backend_provider from edx_proctoring.backends.backend import ProctoringBackendProvider -from edx_proctoring.backends.null import NullBackendProvider from edx_proctoring.backends.mock import MockProctoringBackendProvider -from edx_proctoring.exceptions import ( - BackendProviderCannotRetireUser, - BackendProviderOnboardingException, - BackendProviderSentNoAttemptID, -) +from edx_proctoring.backends.null import NullBackendProvider +from edx_proctoring.exceptions import (BackendProviderCannotRetireUser, BackendProviderOnboardingException, + BackendProviderSentNoAttemptID) # pragma pylint: disable=useless-super-delegation diff --git a/edx_proctoring/backends/tests/test_rest.py b/edx_proctoring/backends/tests/test_rest.py index eadcd01b629..d18cdbc3d17 100644 --- a/edx_proctoring/backends/tests/test_rest.py +++ b/edx_proctoring/backends/tests/test_rest.py @@ -1,11 +1,12 @@ """ Tests for the REST backend """ +from __future__ import absolute_import + import json import ddt import jwt - import responses from mock import patch @@ -13,12 +14,8 @@ from django.utils import translation from edx_proctoring.backends.rest import BaseRestProctoringProvider -from edx_proctoring.exceptions import ( - BackendProviderCannotRegisterAttempt, - BackendProviderCannotRetireUser, - BackendProviderOnboardingException, - BackendProviderSentNoAttemptID, -) +from edx_proctoring.exceptions import (BackendProviderCannotRegisterAttempt, BackendProviderCannotRetireUser, + BackendProviderOnboardingException, BackendProviderSentNoAttemptID) from edx_proctoring.statuses import ProctoredExamStudentAttemptStatus @@ -27,7 +24,7 @@ class RESTBackendTests(TestCase): """ Tests for the REST backend """ - def setUp(self): + def setUp(self): # pylint: disable=super-method-not-called "setup tests" BaseRestProctoringProvider.base_url = 'http://rr.fake' self.provider = BaseRestProctoringProvider('client_id', 'client_secret') diff --git a/edx_proctoring/backends/tests/test_review_payload.py b/edx_proctoring/backends/tests/test_review_payload.py index 648a6c060de..4775ee8a643 100644 --- a/edx_proctoring/backends/tests/test_review_payload.py +++ b/edx_proctoring/backends/tests/test_review_payload.py @@ -2,6 +2,8 @@ Some canned data for SoftwareSecure callback testing. """ +from __future__ import absolute_import + import json MOCK_EXAM_ID = "4d07a01a-1502-422e-b943-93ac04dc6ced" diff --git a/edx_proctoring/backends/tests/test_software_secure.py b/edx_proctoring/backends/tests/test_software_secure.py index 218657fa9cd..7f387e2400d 100644 --- a/edx_proctoring/backends/tests/test_software_secure.py +++ b/edx_proctoring/backends/tests/test_software_secure.py @@ -7,41 +7,26 @@ from __future__ import absolute_import import json + import ddt +from httmock import HTTMock, all_requests from mock import MagicMock, patch -from httmock import all_requests, HTTMock -from django.test import TestCase from django.contrib.auth.models import User +from django.test import TestCase from django.urls import reverse -from edx_proctoring.runtime import set_runtime_service +from edx_proctoring import constants +from edx_proctoring.api import add_allowance_for_user, create_exam, create_exam_attempt, get_exam_attempt_by_id from edx_proctoring.backends import get_backend_provider +from edx_proctoring.backends.software_secure import SOFTWARE_SECURE_INVALID_CHARS, SoftwareSecureBackendProvider +from edx_proctoring.backends.tests.test_review_payload import create_test_review_payload from edx_proctoring.exceptions import BackendProviderCannotRegisterAttempt -from edx_proctoring import constants - -from edx_proctoring.api import ( - get_exam_attempt_by_id, - create_exam, - create_exam_attempt, - add_allowance_for_user - -) - -from edx_proctoring.models import ( - ProctoredExamReviewPolicy, - ProctoredExamStudentAllowance -) +from edx_proctoring.models import ProctoredExamReviewPolicy, ProctoredExamStudentAllowance +from edx_proctoring.runtime import set_runtime_service from edx_proctoring.statuses import ProctoredExamStudentAttemptStatus - -from edx_proctoring.backends.tests.test_review_payload import create_test_review_payload -from edx_proctoring.tests.test_services import ( - MockCreditService, - MockInstructorService, - MockGradesService, - MockCertificateService -) -from edx_proctoring.backends.software_secure import SoftwareSecureBackendProvider, SOFTWARE_SECURE_INVALID_CHARS +from edx_proctoring.tests.test_services import (MockCertificateService, MockCreditService, MockGradesService, + MockInstructorService) @all_requests @@ -267,7 +252,7 @@ def assert_get_payload_mock(self, exam, context): # assert that this is in the 'reviewerNotes' field that is passed to SoftwareSecure expected = context['review_policy'] if review_policy_exception: - expected = '{base}; {exception}'.format( + expected = u'{base}; {exception}'.format( base=expected, exception=review_policy_exception ) @@ -329,8 +314,8 @@ def assert_get_payload_mock_no_policy(self, exam, context): for illegal_char in SOFTWARE_SECURE_INVALID_CHARS: exam_id = create_exam( course_id='foo/bar/baz', - content_id='content with {}'.format(illegal_char), - exam_name='Sample Exam with {} character'.format(illegal_char), + content_id=u'content with {}'.format(illegal_char), + exam_name=u'Sample Exam with {} character'.format(illegal_char), time_limit_mins=10, is_proctored=True, backend='software_secure', diff --git a/edx_proctoring/callbacks.py b/edx_proctoring/callbacks.py index f17c980ceab..817f1f2106f 100644 --- a/edx_proctoring/callbacks.py +++ b/edx_proctoring/callbacks.py @@ -2,8 +2,12 @@ Various callback paths that support callbacks from SoftwareSecure """ +from __future__ import absolute_import + import logging +from waffle import switch_is_active + from django.conf import settings from django.http import HttpResponse, HttpResponseRedirect from django.template import loader @@ -12,7 +16,6 @@ from edx_proctoring.api import get_exam_attempt_by_code, mark_exam_attempt_as_ready, update_attempt_status from edx_proctoring.constants import RPNOWV4_WAFFLE_NAME from edx_proctoring.statuses import ProctoredExamStudentAttemptStatus -from waffle import switch_is_active log = logging.getLogger(__name__) @@ -30,7 +33,7 @@ def start_exam_callback(request, attempt_code): # pylint: disable=unused-argume """ attempt = get_exam_attempt_by_code(attempt_code) if not attempt: - log.warning("Attempt code %r cannot be found.", attempt_code) + log.warning(u"Attempt code %r cannot be found.", attempt_code) return HttpResponse( content='You have entered an exam code that is not valid.', status=404 @@ -46,13 +49,13 @@ def start_exam_callback(request, attempt_code): # pylint: disable=unused-argume if ProctoredExamStudentAttemptStatus.is_in_progress_status(attempt_status): update_attempt_status(proctored_exam_id, user_id, ProctoredExamStudentAttemptStatus.submitted) else: - log.warning("Attempted to enter proctored exam attempt {attempt_id} when status was {attempt_status}" + log.warning(u"Attempted to enter proctored exam attempt {attempt_id} when status was {attempt_status}" .format( attempt_id=attempt['id'], attempt_status=attempt_status, )) - if switch_is_active(RPNOWV4_WAFFLE_NAME): + if switch_is_active(RPNOWV4_WAFFLE_NAME): # pylint: disable=illegal-waffle-usage course_id = attempt['proctored_exam']['course_id'] content_id = attempt['proctored_exam']['content_id'] @@ -60,7 +63,7 @@ def start_exam_callback(request, attempt_code): # pylint: disable=unused-argume try: exam_url = reverse('jump_to', args=[course_id, content_id]) except NoReverseMatch: - log.exception("BLOCKING ERROR: Can't find course info url for course %s", course_id) + log.exception(u"BLOCKING ERROR: Can't find course info url for course %s", course_id) response = HttpResponseRedirect(exam_url) response.set_signed_cookie('exam', attempt['attempt_code']) return response diff --git a/edx_proctoring/exceptions.py b/edx_proctoring/exceptions.py index 06c8240def0..fa7a58cdd3d 100644 --- a/edx_proctoring/exceptions.py +++ b/edx_proctoring/exceptions.py @@ -20,6 +20,7 @@ class ProctoredExamNotFoundException(ProctoredBaseException): Raised when a look up fails. """ def __init__(self, *args): + """ Init method of exception """ ProctoredBaseException.__init__(self, u'The exam_id does not exist.', *args) @@ -77,6 +78,7 @@ class BackendProviderCannotRegisterAttempt(ProctoredBaseException): """ def __init__(self, content, http_status): + """ Init method of exception """ super(BackendProviderCannotRegisterAttempt, self).__init__(self, content) self.http_status = http_status @@ -94,6 +96,7 @@ class BackendProviderOnboardingException(ProctoredBaseException): because of missing/failed onboarding requirements """ def __init__(self, status): + """ Init method of exception """ super(BackendProviderOnboardingException, self).__init__(self, status) self.status = status diff --git a/edx_proctoring/instructor_dashboard_exam_urls.py b/edx_proctoring/instructor_dashboard_exam_urls.py index 6396c3ebcd8..05a46cac439 100644 --- a/edx_proctoring/instructor_dashboard_exam_urls.py +++ b/edx_proctoring/instructor_dashboard_exam_urls.py @@ -2,6 +2,8 @@ URL mapping for the exam instructor dashboard. """ +from __future__ import absolute_import + from django.conf import settings from django.conf.urls import url diff --git a/edx_proctoring/management/commands/set_attempt_status.py b/edx_proctoring/management/commands/set_attempt_status.py index 867917060d3..8c09f4bf91b 100644 --- a/edx_proctoring/management/commands/set_attempt_status.py +++ b/edx_proctoring/management/commands/set_attempt_status.py @@ -52,8 +52,8 @@ def handle(self, *args, **options): to_status = options['to_status'] msg = ( - 'Running management command to update user {user_id} ' - 'attempt status on exam_id {exam_id} to {to_status}'.format( + u'Running management command to update user {user_id} ' + u'attempt status on exam_id {exam_id} to {to_status}'.format( user_id=user_id, exam_id=exam_id, to_status=to_status @@ -62,7 +62,7 @@ def handle(self, *args, **options): self.stdout.write(msg) if not ProctoredExamStudentAttemptStatus.is_valid_status(to_status): - raise CommandError('{to_status} is not a valid attempt status!'.format(to_status=to_status)) + raise CommandError(u'{to_status} is not a valid attempt status!'.format(to_status=to_status)) # get exam, this will throw exception if does not exist, so let it bomb out get_exam_by_id(exam_id) diff --git a/edx_proctoring/management/commands/tests/test_set_attempt_status.py b/edx_proctoring/management/commands/tests/test_set_attempt_status.py index fe5fce7c76b..de64ec525ef 100644 --- a/edx_proctoring/management/commands/tests/test_set_attempt_status.py +++ b/edx_proctoring/management/commands/tests/test_set_attempt_status.py @@ -5,22 +5,18 @@ from __future__ import absolute_import from datetime import datetime -from mock import MagicMock, patch + import pytz +from mock import MagicMock, patch from django.core.management import call_command -from edx_proctoring.tests.utils import LoggedInTestCase from edx_proctoring.api import create_exam, get_exam_attempt - from edx_proctoring.models import ProctoredExamStudentAttempt -from edx_proctoring.statuses import ProctoredExamStudentAttemptStatus -from edx_proctoring.tests.test_services import ( - MockCreditService, - MockGradesService, - MockCertificateService -) from edx_proctoring.runtime import set_runtime_service +from edx_proctoring.statuses import ProctoredExamStudentAttemptStatus +from edx_proctoring.tests.test_services import MockCertificateService, MockCreditService, MockGradesService +from edx_proctoring.tests.utils import LoggedInTestCase @patch('django.urls.reverse', MagicMock) diff --git a/edx_proctoring/migrations/0001_initial.py b/edx_proctoring/migrations/0001_initial.py index 588e3a5577a..dc7f6f64237 100644 --- a/edx_proctoring/migrations/0001_initial.py +++ b/edx_proctoring/migrations/0001_initial.py @@ -1,9 +1,10 @@ # -*- coding: utf-8 -*- -from __future__ import unicode_literals +from __future__ import absolute_import, unicode_literals -from django.db import migrations, models import django.utils.timezone from django.conf import settings +from django.db import migrations, models + import model_utils.fields diff --git a/edx_proctoring/migrations/0002_proctoredexamstudentattempt_is_status_acknowledged.py b/edx_proctoring/migrations/0002_proctoredexamstudentattempt_is_status_acknowledged.py index c739856851d..5b97803e06b 100644 --- a/edx_proctoring/migrations/0002_proctoredexamstudentattempt_is_status_acknowledged.py +++ b/edx_proctoring/migrations/0002_proctoredexamstudentattempt_is_status_acknowledged.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import unicode_literals +from __future__ import absolute_import, unicode_literals from django.db import migrations, models diff --git a/edx_proctoring/migrations/0003_auto_20160101_0525.py b/edx_proctoring/migrations/0003_auto_20160101_0525.py index 49967526e40..a00f66419a6 100644 --- a/edx_proctoring/migrations/0003_auto_20160101_0525.py +++ b/edx_proctoring/migrations/0003_auto_20160101_0525.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import unicode_literals +from __future__ import absolute_import, unicode_literals from django.db import migrations, models diff --git a/edx_proctoring/migrations/0004_auto_20160201_0523.py b/edx_proctoring/migrations/0004_auto_20160201_0523.py index 6ba0e6ae65a..c94eaa2ac32 100644 --- a/edx_proctoring/migrations/0004_auto_20160201_0523.py +++ b/edx_proctoring/migrations/0004_auto_20160201_0523.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import unicode_literals +from __future__ import absolute_import, unicode_literals from django.db import migrations, models diff --git a/edx_proctoring/migrations/0005_proctoredexam_hide_after_due.py b/edx_proctoring/migrations/0005_proctoredexam_hide_after_due.py index 305345bca63..2cfa2abe84d 100644 --- a/edx_proctoring/migrations/0005_proctoredexam_hide_after_due.py +++ b/edx_proctoring/migrations/0005_proctoredexam_hide_after_due.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import unicode_literals +from __future__ import absolute_import, unicode_literals from django.db import migrations, models diff --git a/edx_proctoring/migrations/0006_allowed_time_limit_mins.py b/edx_proctoring/migrations/0006_allowed_time_limit_mins.py index d96365e84e4..7a59712842b 100644 --- a/edx_proctoring/migrations/0006_allowed_time_limit_mins.py +++ b/edx_proctoring/migrations/0006_allowed_time_limit_mins.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import unicode_literals +from __future__ import absolute_import, unicode_literals from django.db import migrations, models diff --git a/edx_proctoring/migrations/0007_proctoredexam_backend.py b/edx_proctoring/migrations/0007_proctoredexam_backend.py index 1c20b648e13..bf74841e2aa 100644 --- a/edx_proctoring/migrations/0007_proctoredexam_backend.py +++ b/edx_proctoring/migrations/0007_proctoredexam_backend.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # Generated by Django 1.11.15 on 2018-10-02 19:10 -from __future__ import unicode_literals +from __future__ import absolute_import, unicode_literals from django.db import migrations, models diff --git a/edx_proctoring/migrations/0008_auto_20181116_1551.py b/edx_proctoring/migrations/0008_auto_20181116_1551.py index e2c3f9a6dee..53c90336f1b 100644 --- a/edx_proctoring/migrations/0008_auto_20181116_1551.py +++ b/edx_proctoring/migrations/0008_auto_20181116_1551.py @@ -1,10 +1,11 @@ # -*- coding: utf-8 -*- # Generated by Django 1.11.16 on 2018-11-16 15:51 -from __future__ import unicode_literals +from __future__ import absolute_import, unicode_literals -from django.db import migrations, models import jsonfield.fields +from django.db import migrations, models + class Migration(migrations.Migration): diff --git a/edx_proctoring/migrations/0009_proctoredexamreviewpolicy_remove_rules.py b/edx_proctoring/migrations/0009_proctoredexamreviewpolicy_remove_rules.py index 158ad39157e..58ea1bfb056 100644 --- a/edx_proctoring/migrations/0009_proctoredexamreviewpolicy_remove_rules.py +++ b/edx_proctoring/migrations/0009_proctoredexamreviewpolicy_remove_rules.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # Generated by Django 1.11.16 on 2018-12-10 16:47 -from __future__ import unicode_literals +from __future__ import absolute_import, unicode_literals from django.db import migrations diff --git a/edx_proctoring/migrations/0010_update_backend.py b/edx_proctoring/migrations/0010_update_backend.py index f7d12217c1d..e5235cee08a 100644 --- a/edx_proctoring/migrations/0010_update_backend.py +++ b/edx_proctoring/migrations/0010_update_backend.py @@ -1,8 +1,9 @@ # -*- coding: utf-8 -*- # Generated by Django 1.11.18 on 2019-04-29 15:44 -from __future__ import unicode_literals +from __future__ import absolute_import, unicode_literals import logging + from django.db import migrations diff --git a/edx_proctoring/models.py b/edx_proctoring/models.py index b0892712018..4cf8f1e8e94 100644 --- a/edx_proctoring/models.py +++ b/edx_proctoring/models.py @@ -6,6 +6,7 @@ # pylint: disable=model-missing-unicode from __future__ import absolute_import + import six from django.contrib.auth import get_user_model @@ -17,11 +18,8 @@ from model_utils.models import TimeStampedModel from edx_proctoring.backends import get_backend_provider -from edx_proctoring.exceptions import ( - UserNotFoundException, - ProctoredExamNotActiveException, - AllowanceValueNotAllowedException, -) +from edx_proctoring.exceptions import (AllowanceValueNotAllowedException, ProctoredExamNotActiveException, + UserNotFoundException) from edx_proctoring.statuses import ProctoredExamStudentAttemptStatus, SoftwareSecureReviewStatus USER_MODEL = get_user_model() @@ -72,6 +70,7 @@ class Meta: db_table = 'proctoring_proctoredexam' def __str__(self): + """ String representation """ # pragma: no cover return u"{course_id}: {exam_name} ({active})".format( course_id=self.course_id, @@ -134,6 +133,7 @@ class ProctoredExamReviewPolicy(TimeStampedModel): review_policy = models.TextField(default='') def __str__(self): + """ String representation """ # pragma: no cover return u"ProctoredExamReviewPolicy: {set_by_user} ({proctored_exam})".format( set_by_user=self.set_by_user, @@ -240,7 +240,7 @@ def get_all_exam_attempts(self, course_id): Returns the Student Exam Attempts for the given course_id. """ filtered_query = Q(proctored_exam__course_id=course_id) - return self.filter(filtered_query).order_by('-created') + return self.filter(filtered_query).order_by('-created') # pylint: disable=no-member def get_filtered_exam_attempts(self, course_id, search_by): """ @@ -255,6 +255,7 @@ def get_proctored_exam_attempts(self, course_id, username): """ Returns the Student's Proctored Exam Attempts for the given course_id. """ + # pylint: disable=no-member return self.filter( proctored_exam__course_id=course_id, user__username=username, @@ -278,6 +279,7 @@ def clear_onboarding_errors(self, user_id): Removes any attempts in the onboarding error states. (They will automatically be saved to the attempt history table) """ + # pylint: disable=no-member self.filter(user_id=user_id, status__in=ProctoredExamStudentAttemptStatus.onboarding_errors).delete() @@ -365,7 +367,7 @@ def create_exam_attempt(cls, exam_id, user_id, student_name, attempt_code, def delete_exam_attempt(self): """ - deletes the exam attempt object and archives it to the ProctoredExamStudentAttemptHistory table. + Deletes the exam attempt object and archives it to the ProctoredExamStudentAttemptHistory table. """ self.delete() @@ -467,6 +469,7 @@ class QuerySetWithUpdateOverride(models.QuerySet): every time the object is updated. """ def update(self, **kwargs): + """ Create a copy after update """ super(QuerySetWithUpdateOverride, self).update(**kwargs) archive_model(ProctoredExamStudentAllowanceHistory, self.get(), id='allowance_id') @@ -552,7 +555,7 @@ def add_allowance_for_user(cls, exam_id, user_info, key, value): if not cls.is_allowance_value_valid(key, value): err_msg = ( - 'allowance_value "{value}" should be non-negative integer value.' + u'allowance_value "{value}" should be non-negative integer value.' ).format(value=value) raise AllowanceValueNotAllowedException(err_msg) # were we passed a PK? @@ -566,7 +569,7 @@ def add_allowance_for_user(cls, exam_id, user_info, key, value): if not users.exists(): err_msg = ( - 'Cannot find user against {user_info}' + u'Cannot find user against {user_info}' ).format(user_info=user_info) raise UserNotFoundException(err_msg) diff --git a/edx_proctoring/rules.py b/edx_proctoring/rules.py index 9b00aa67cd1..74dcd424e54 100644 --- a/edx_proctoring/rules.py +++ b/edx_proctoring/rules.py @@ -1,4 +1,4 @@ -"Django Rules for edx_proctoring" +"""Django Rules for edx_proctoring""" from __future__ import absolute_import import rules diff --git a/edx_proctoring/serializers.py b/edx_proctoring/serializers.py index 59ce8b6eaa3..200350482f6 100644 --- a/edx_proctoring/serializers.py +++ b/edx_proctoring/serializers.py @@ -2,17 +2,13 @@ from __future__ import absolute_import -from django.contrib.auth.models import User - from rest_framework import serializers from rest_framework.fields import DateTimeField -from edx_proctoring.models import ( - ProctoredExam, - ProctoredExamStudentAttempt, - ProctoredExamStudentAllowance, - ProctoredExamReviewPolicy -) +from django.contrib.auth.models import User + +from edx_proctoring.models import (ProctoredExam, ProctoredExamReviewPolicy, ProctoredExamStudentAllowance, + ProctoredExamStudentAttempt) class ProctoredExamSerializer(serializers.ModelSerializer): diff --git a/edx_proctoring/services.py b/edx_proctoring/services.py index fec943aca3c..2647bb78655 100644 --- a/edx_proctoring/services.py +++ b/edx_proctoring/services.py @@ -35,7 +35,7 @@ def __init__(self): def _bind_to_module_functions(self, module): """ - bind module functions. Since we use underscores to mean private methods, let's exclude those. + Bind module functions. Since we use underscores to mean private methods, let's exclude those. """ for attr_name in dir(module): attr = getattr(module, attr_name, None) diff --git a/edx_proctoring/signals.py b/edx_proctoring/signals.py index c5544c3c2ed..5aeea7b1464 100644 --- a/edx_proctoring/signals.py +++ b/edx_proctoring/signals.py @@ -1,15 +1,15 @@ -"edx-proctoring signals" +"""edx-proctoring signals""" +from __future__ import absolute_import + import logging -from django.db.models.signals import pre_save, post_save, pre_delete +from django.db.models.signals import post_save, pre_delete, pre_save from django.dispatch import receiver -from edx_proctoring import api -from edx_proctoring import constants -from edx_proctoring import models +from edx_proctoring import api, constants, models +from edx_proctoring.backends import get_backend_provider from edx_proctoring.statuses import ProctoredExamStudentAttemptStatus, SoftwareSecureReviewStatus from edx_proctoring.utils import emit_event, locate_attempt_by_attempt_code -from edx_proctoring.backends import get_backend_provider log = logging.getLogger(__name__) @@ -117,7 +117,7 @@ def on_attempt_changed(sender, instance, signal, **kwargs): # pylint: disable=u if backend: result = backend.remove_exam_attempt(instance.proctored_exam.external_id, instance.external_id) if not result: - log.error('Failed to remove attempt %d from %s', instance.id, backend.verbose_name) + log.error(u'Failed to remove attempt %d from %s', instance.id, backend.verbose_name) models.archive_model(models.ProctoredExamStudentAttemptHistory, instance, id='attempt_id') diff --git a/edx_proctoring/statuses.py b/edx_proctoring/statuses.py index b3a755c2fcb..4b93c0a2e5a 100644 --- a/edx_proctoring/statuses.py +++ b/edx_proctoring/statuses.py @@ -1,6 +1,8 @@ """ Status enums for edx-proctoring """ +from __future__ import absolute_import + from edx_proctoring.exceptions import ProctoredExamBadReviewStatus @@ -211,8 +213,8 @@ def validate(cls, status): """ if status not in cls.passing_statuses + cls.failing_statuses: err_msg = ( - 'Received unexpected reviewStatus field value from payload. ' - 'Was {review_status}.'.format(review_status=status) + u'Received unexpected reviewStatus field value from payload. ' + u'Was {review_status}.'.format(review_status=status) ) raise ProctoredExamBadReviewStatus(err_msg) return True diff --git a/edx_proctoring/tests/__init__.py b/edx_proctoring/tests/__init__.py index 5fb8d670c2e..e69c8912622 100644 --- a/edx_proctoring/tests/__init__.py +++ b/edx_proctoring/tests/__init__.py @@ -4,6 +4,7 @@ from __future__ import absolute_import import contextlib + import rules diff --git a/edx_proctoring/tests/test_api.py b/edx_proctoring/tests/test_api.py index 60c0dd290cc..aba069ce4a8 100644 --- a/edx_proctoring/tests/test_api.py +++ b/edx_proctoring/tests/test_api.py @@ -12,9 +12,11 @@ import ddt import pytz -import six + from freezegun import freeze_time from mock import MagicMock, patch +import six +from six.moves import range from edx_proctoring.api import (_are_prerequirements_satisfied, _check_for_attempt_timeout, _get_ordered_prerequisites, _get_review_policy_by_exam_id, add_allowance_for_user, create_exam, create_exam_attempt, @@ -1949,14 +1951,14 @@ def test_get_exam_violation_report(self): 'CCCCCC', 'DDDDDD' ]) - self.assertTrue('Rules Violation Comments' in report[3]) + self.assertTrue('Rules Violation Comments' in report[3]) # pylint: disable=wrong-assert-type self.assertEqual(len(report[3]['Rules Violation Comments']), 1) - self.assertTrue('Suspicious Comments' in report[3]) + self.assertTrue('Suspicious Comments' in report[3]) # pylint: disable=wrong-assert-type self.assertEqual(len(report[3]['Suspicious Comments']), 2) self.assertEqual(report[3]['review_status'], 'Suspicious') - self.assertTrue('Suspicious Comments' not in report[2]) - self.assertTrue('Rules Violation Comments' in report[2]) + self.assertTrue('Suspicious Comments' not in report[2]) # pylint: disable=wrong-assert-type + self.assertTrue('Rules Violation Comments' in report[2]) # pylint: disable=wrong-assert-type self.assertEqual(len(report[2]['Rules Violation Comments']), 1) self.assertEqual(report[2]['review_status'], 'Rules Violation') diff --git a/edx_proctoring/tests/test_email.py b/edx_proctoring/tests/test_email.py index 3fbf8693df6..1c423b218a3 100644 --- a/edx_proctoring/tests/test_email.py +++ b/edx_proctoring/tests/test_email.py @@ -6,27 +6,17 @@ from __future__ import absolute_import import ddt -from django.core import mail from mock import MagicMock, patch -from edx_proctoring.api import ( - update_attempt_status, - get_integration_specific_email -) +from django.core import mail + +from edx_proctoring.api import get_integration_specific_email, update_attempt_status from edx_proctoring.backends import get_backend_provider -from edx_proctoring.runtime import set_runtime_service, get_runtime_service -from edx_proctoring.statuses import ( - ProctoredExamStudentAttemptStatus, -) +from edx_proctoring.runtime import get_runtime_service, set_runtime_service +from edx_proctoring.statuses import ProctoredExamStudentAttemptStatus -from .test_services import ( - MockCreditService, - MockGradesService, - MockCertificateService -) -from .utils import ( - ProctoredExamTestCase, -) +from .test_services import MockCertificateService, MockCreditService, MockGradesService +from .utils import ProctoredExamTestCase @patch('django.urls.reverse', MagicMock) @@ -213,7 +203,7 @@ def test_correct_edx_email(self, status, integration_specific_email,): # Verify the edX email expected_email = get_integration_specific_email(test_backend) actual_body = self._normalize_whitespace(mail.outbox[0].body) - self.assertIn('contact Open edX support at ' - ' ' - '{email} '.format(email=expected_email), + self.assertIn(u'contact Open edX support at ' + u' ' + u'{email} '.format(email=expected_email), actual_body) diff --git a/edx_proctoring/tests/test_models.py b/edx_proctoring/tests/test_models.py index 1eb6201662e..d4a9dff001f 100644 --- a/edx_proctoring/tests/test_models.py +++ b/edx_proctoring/tests/test_models.py @@ -7,20 +7,14 @@ from __future__ import absolute_import import six -from edx_proctoring.models import ( - ProctoredExam, - ProctoredExamStudentAllowance, - ProctoredExamStudentAllowanceHistory, - ProctoredExamStudentAttempt, - ProctoredExamStudentAttemptHistory, - ProctoredExamReviewPolicy, - ProctoredExamReviewPolicyHistory, -) +from six.moves import range + +from edx_proctoring.models import (ProctoredExam, ProctoredExamReviewPolicy, ProctoredExamReviewPolicyHistory, + ProctoredExamStudentAllowance, ProctoredExamStudentAllowanceHistory, + ProctoredExamStudentAttempt, ProctoredExamStudentAttemptHistory) from edx_proctoring.statuses import ProctoredExamStudentAttemptStatus -from .utils import ( - LoggedInTestCase -) +from .utils import LoggedInTestCase # pragma pylint: disable=useless-super-delegation @@ -82,6 +76,7 @@ def test_save_proctored_exam_student_allowance_history(self): # pylint: disable self.assertEqual(len(proctored_exam_student_history), 0) # Update the allowance object twice + # pylint: disable=no-member ProctoredExamStudentAllowance.objects.filter( user_id=1, proctored_exam=proctored_exam, @@ -92,6 +87,7 @@ def test_save_proctored_exam_student_allowance_history(self): # pylint: disable value='10 minutes' ) + # pylint: disable=no-member ProctoredExamStudentAllowance.objects.filter( user_id=1, proctored_exam=proctored_exam, diff --git a/edx_proctoring/tests/test_reviews.py b/edx_proctoring/tests/test_reviews.py index d534b46669f..90eb1e21eb7 100644 --- a/edx_proctoring/tests/test_reviews.py +++ b/edx_proctoring/tests/test_reviews.py @@ -5,9 +5,9 @@ import json -from crum import set_current_request import ddt -from mock import patch, call +from crum import set_current_request +from mock import call, patch from django.contrib.auth.models import User from django.test import RequestFactory @@ -17,7 +17,7 @@ from edx_proctoring.api import create_exam, create_exam_attempt, get_exam_attempt_by_id, remove_exam_attempt from edx_proctoring.backends import get_backend_provider from edx_proctoring.backends.tests.test_review_payload import create_test_review_payload -from edx_proctoring.exceptions import (ProctoredExamBadReviewStatus, ProctoredExamReviewAlreadyExists) +from edx_proctoring.exceptions import ProctoredExamBadReviewStatus, ProctoredExamReviewAlreadyExists from edx_proctoring.models import (ProctoredExamSoftwareSecureComment, ProctoredExamSoftwareSecureReview, ProctoredExamSoftwareSecureReviewHistory, ProctoredExamStudentAttemptHistory) from edx_proctoring.runtime import get_runtime_service, set_runtime_service @@ -499,8 +499,8 @@ def test_reviewed_by_is_course_or_global_staff(self, logger_mock): ) log_format_string = ( - 'User %(user)s does not have the required permissions ' - 'to submit a review for attempt_code %(attempt_code)s.' + u'User %(user)s does not have the required permissions ' + u'to submit a review for attempt_code %(attempt_code)s.' ) log_format_dictionary = { @@ -520,6 +520,7 @@ def test_reviewed_by_is_course_or_global_staff(self, logger_mock): ProctoredExamReviewCallback().make_review(self.attempt, test_payload) # the mock API doesn't have a "assert_not_called_with" method + # pylint: disable=wrong-assert-type self.assertFalse( call(log_format_string, log_format_dictionary) in logger_mock.call_args_list ) diff --git a/edx_proctoring/tests/test_services.py b/edx_proctoring/tests/test_services.py index 9dc21431e20..c9cd94e8580 100644 --- a/edx_proctoring/tests/test_services.py +++ b/edx_proctoring/tests/test_services.py @@ -6,17 +6,16 @@ from __future__ import absolute_import -from datetime import datetime, timedelta import types import unittest +from datetime import datetime, timedelta + import pytz import six -from edx_proctoring.services import ( - ProctoringService -) -from edx_proctoring.exceptions import UserNotFoundException from edx_proctoring import api as edx_proctoring_api +from edx_proctoring.exceptions import UserNotFoundException +from edx_proctoring.services import ProctoringService class MockCreditService(object): diff --git a/edx_proctoring/tests/test_student_view.py b/edx_proctoring/tests/test_student_view.py index 9aa8ef163b1..37747ce0a11 100644 --- a/edx_proctoring/tests/test_student_view.py +++ b/edx_proctoring/tests/test_student_view.py @@ -8,33 +8,23 @@ from __future__ import absolute_import from datetime import datetime, timedelta + import ddt +import pytz +import six from freezegun import freeze_time from mock import MagicMock, patch -import pytz from waffle.testutils import override_flag -import six - -from edx_proctoring.api import ( - update_exam, - get_exam_by_id, - add_allowance_for_user, - get_exam_attempt, - get_student_view, - update_attempt_status, -) +from edx_proctoring.api import (add_allowance_for_user, get_exam_attempt, get_exam_by_id, get_student_view, + update_attempt_status, update_exam) from edx_proctoring.constants import RPNOWV4_WAFFLE_NAME -from edx_proctoring.models import ( - ProctoredExam, - ProctoredExamStudentAllowance, - ProctoredExamStudentAttempt, -) +from edx_proctoring.models import ProctoredExam, ProctoredExamStudentAllowance, ProctoredExamStudentAttempt from edx_proctoring.runtime import set_runtime_service from edx_proctoring.statuses import ProctoredExamStudentAttemptStatus from edx_proctoring.tests import mock_perm -from .test_services import MockCreditServiceWithCourseEndDate, MockCreditServiceNone +from .test_services import MockCreditServiceNone, MockCreditServiceWithCourseEndDate from .utils import ProctoredExamTestCase @@ -55,7 +45,7 @@ def setUp(self): # Messages for get_student_view self.start_an_exam_msg = 'This exam is proctored' self.exam_expired_msg = 'The due date for this exam has passed' - self.timed_exam_msg = '{exam_name} is a Timed Exam' + self.timed_exam_msg = u'{exam_name} is a Timed Exam' self.timed_exam_submitted = 'You have submitted your timed exam.' self.timed_exam_expired = 'The time allotted for this exam has expired.' self.submitted_timed_exam_msg_with_due_date = 'After the due date has passed,' diff --git a/edx_proctoring/tests/test_views.py b/edx_proctoring/tests/test_views.py index 8c23aa5664d..7b53d7c0d3c 100644 --- a/edx_proctoring/tests/test_views.py +++ b/edx_proctoring/tests/test_views.py @@ -5,42 +5,32 @@ from __future__ import absolute_import -from datetime import datetime, timedelta import json +from datetime import datetime, timedelta + import ddt +import pytz from freezegun import freeze_time from httmock import HTTMock from mock import Mock, patch -import pytz +from six.moves import range -from django.test.client import Client -from django.urls import reverse, NoReverseMatch from django.contrib.auth.models import User +from django.test.client import Client +from django.urls import NoReverseMatch, reverse -from edx_proctoring.models import ( - ProctoredExam, - ProctoredExamStudentAttempt, - ProctoredExamStudentAllowance, -) -from edx_proctoring.exceptions import ( - ProctoredExamIllegalStatusTransition, - StudentExamAttemptDoesNotExistsException, ProctoredExamPermissionDenied) -from edx_proctoring.views import require_staff, require_course_or_global_staff -from edx_proctoring.api import ( - create_exam, - create_exam_attempt, - get_exam_attempt_by_id, - update_attempt_status, - _calculate_allowed_mins, - get_backend_provider, -) -from edx_proctoring.statuses import ProctoredExamStudentAttemptStatus -from edx_proctoring.serializers import ProctoredExamSerializer +from edx_proctoring.api import (_calculate_allowed_mins, create_exam, create_exam_attempt, get_backend_provider, + get_exam_attempt_by_id, update_attempt_status) from edx_proctoring.backends.tests.test_review_payload import create_test_review_payload from edx_proctoring.backends.tests.test_software_secure import mock_response_content -from edx_proctoring.runtime import set_runtime_service, get_runtime_service +from edx_proctoring.exceptions import (ProctoredExamIllegalStatusTransition, ProctoredExamPermissionDenied, + StudentExamAttemptDoesNotExistsException) +from edx_proctoring.models import ProctoredExam, ProctoredExamStudentAllowance, ProctoredExamStudentAttempt +from edx_proctoring.runtime import get_runtime_service, set_runtime_service +from edx_proctoring.serializers import ProctoredExamSerializer +from edx_proctoring.statuses import ProctoredExamStudentAttemptStatus from edx_proctoring.urls import urlpatterns - +from edx_proctoring.views import require_course_or_global_staff, require_staff from mock_apps.models import Profile from .test_services import MockCreditService, MockInstructorService @@ -1689,6 +1679,7 @@ def test_get_expired_exam_attempt(self): ) self.assertEqual(response.status_code, 200) + # pylint: disable=no-member ProctoredExamStudentAttempt.objects.filter( proctored_exam_id=proctored_exam.id, user_id=self.user.id, diff --git a/edx_proctoring/tests/test_workerconfig.py b/edx_proctoring/tests/test_workerconfig.py index 96ee833855d..ce101b891a4 100644 --- a/edx_proctoring/tests/test_workerconfig.py +++ b/edx_proctoring/tests/test_workerconfig.py @@ -1,13 +1,16 @@ "Tests for generating webpack config json" +from __future__ import absolute_import + import json import os import os.path import tempfile import unittest +from mock import patch + from django.conf import settings -from mock import patch from edx_proctoring.apps import make_worker_config from edx_proctoring.backends.tests.test_backend import TestBackendProvider @@ -15,12 +18,12 @@ class TestWorkerConfig(unittest.TestCase): "Tests for generating webpack config json" - def setUp(self): + def setUp(self): # pylint: disable=super-method-not-called super(TestWorkerConfig, self).setUp() self.outfile = tempfile.mktemp(prefix='test-%d' % os.getpid()) self.to_del = [self.outfile] - def tearDown(self): + def tearDown(self): # pylint: disable=super-method-not-called for path in self.to_del: if os.path.exists(path): os.unlink(path) @@ -39,7 +42,7 @@ def _make_npm_module(self, package, main=None): except OSError: pass with open(package_file, 'wb') as package_fp: - json.dump(package_json, package_fp) + package_fp.write(json.dumps(package_json).encode('utf-8')) self.to_del.append(package_file) return package diff --git a/edx_proctoring/tests/utils.py b/edx_proctoring/tests/utils.py index 0a421904558..b7a792d936e 100644 --- a/edx_proctoring/tests/utils.py +++ b/edx_proctoring/tests/utils.py @@ -9,30 +9,23 @@ from datetime import datetime from importlib import import_module + import pytz +from eventtracking import tracker +from eventtracking.tracker import TRACKERS, Tracker from django.conf import settings from django.contrib.auth import login +from django.contrib.auth.models import User from django.http import HttpRequest -from django.test.client import Client from django.test import TestCase -from django.contrib.auth.models import User - -from edx_proctoring.api import ( - create_exam, -) -from edx_proctoring.models import ( - ProctoredExamStudentAttempt, -) -from edx_proctoring.statuses import ProctoredExamStudentAttemptStatus +from django.test.client import Client -from edx_proctoring.tests.test_services import ( - MockCreditService, - MockInstructorService, -) +from edx_proctoring.api import create_exam +from edx_proctoring.models import ProctoredExamStudentAttempt from edx_proctoring.runtime import set_runtime_service -from eventtracking import tracker -from eventtracking.tracker import Tracker, TRACKERS +from edx_proctoring.statuses import ProctoredExamStudentAttemptStatus +from edx_proctoring.tests.test_services import MockCreditService, MockInstructorService class TestClient(Client): diff --git a/edx_proctoring/urls.py b/edx_proctoring/urls.py index c32a3165446..6c0d6ce3723 100644 --- a/edx_proctoring/urls.py +++ b/edx_proctoring/urls.py @@ -5,9 +5,9 @@ from __future__ import absolute_import from django.conf import settings -from django.conf.urls import url, include +from django.conf.urls import include, url -from edx_proctoring import views, callbacks, instructor_dashboard_exam_urls +from edx_proctoring import callbacks, instructor_dashboard_exam_urls, views app_name = u'edx_proctoring' diff --git a/edx_proctoring/utils.py b/edx_proctoring/utils.py index d9d85eb5fff..e3549beb961 100644 --- a/edx_proctoring/utils.py +++ b/edx_proctoring/utils.py @@ -11,6 +11,11 @@ import pytz import six +from edx_rest_framework_extensions.auth.jwt.authentication import JwtAuthentication +from edx_when import api as when_api +from eventtracking import tracker +from opaque_keys import InvalidKeyError +from opaque_keys.edx.keys import CourseKey from rest_framework.authentication import SessionAuthentication from rest_framework.permissions import IsAuthenticated from rest_framework.views import APIView @@ -20,11 +25,6 @@ from edx_proctoring.models import ProctoredExamStudentAttempt, ProctoredExamStudentAttemptHistory from edx_proctoring.statuses import ProctoredExamStudentAttemptStatus -from edx_rest_framework_extensions.auth.jwt.authentication import JwtAuthentication -from edx_when import api as when_api -from eventtracking import tracker -from opaque_keys import InvalidKeyError -from opaque_keys.edx.keys import CourseKey log = logging.getLogger(__name__) @@ -75,10 +75,10 @@ def humanized_time(time_in_minutes): hours_present = False template = "" elif hours == 1: - template = _("{num_of_hours} hour") + template = _(u"{num_of_hours} hour") hours_present = True elif hours >= 2: - template = _("{num_of_hours} hours") + template = _(u"{num_of_hours} hours") hours_present = True else: template = "error" @@ -86,17 +86,17 @@ def humanized_time(time_in_minutes): if template != "error": if minutes == 0: if not hours_present: - template = _("{num_of_minutes} minutes") + template = _(u"{num_of_minutes} minutes") elif minutes == 1: if hours_present: - template += _(" and {num_of_minutes} minute") + template += _(u" and {num_of_minutes} minute") else: - template += _("{num_of_minutes} minute") + template += _(u"{num_of_minutes} minute") else: if hours_present: - template += _(" and {num_of_minutes} minutes") + template += _(u" and {num_of_minutes} minutes") else: - template += _("{num_of_minutes} minutes") + template += _(u"{num_of_minutes} minutes") human_time = template.format(num_of_hours=hours, num_of_minutes=minutes) return human_time @@ -117,7 +117,7 @@ def locate_attempt_by_attempt_code(attempt_code): if not attempt_obj: # still can't find, error out err_msg = ( - 'Could not locate attempt_code: {attempt_code}'.format(attempt_code=attempt_code) + u'Could not locate attempt_code: {attempt_code}'.format(attempt_code=attempt_code) ) log.error(err_msg) is_archived = None @@ -214,8 +214,8 @@ def _emit_event(name, context, data): # This happens when a default tracker has not been registered by the host application # aka LMS. This is normal when running unit tests in isolation. log.warning( - 'Analytics tracker not properly configured. ' - 'If this message appears in a production environment, please investigate' + u'Analytics tracker not properly configured. ' + u'If this message appears in a production environment, please investigate' ) @@ -232,7 +232,7 @@ def obscured_user_id(user_id, *extra): def has_due_date_passed(due_datetime): """ - return True if due date is lesser than current datetime, otherwise False + Return True if due date is lesser than current datetime, otherwise False and if due_datetime is None then we don't have to consider the due date for return False """ diff --git a/edx_proctoring/views.py b/edx_proctoring/views.py index 2da914bca18..97955d37787 100644 --- a/edx_proctoring/views.py +++ b/edx_proctoring/views.py @@ -6,76 +6,41 @@ import json import logging + import six import waffle - from crum import get_current_request +from rest_framework import status +from rest_framework.negotiation import BaseContentNegotiation +from rest_framework.response import Response +from rest_framework.views import APIView + from django.conf import settings from django.contrib.auth import get_user_model from django.core.exceptions import ObjectDoesNotExist -from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger -from django.urls import reverse, NoReverseMatch +from django.core.paginator import EmptyPage, PageNotAnInteger, Paginator from django.shortcuts import redirect -from django.utils.translation import ugettext as _ +from django.urls import NoReverseMatch, reverse from django.utils.decorators import method_decorator - -from rest_framework import status -from rest_framework.negotiation import BaseContentNegotiation -from rest_framework.response import Response -from rest_framework.views import APIView +from django.utils.translation import ugettext as _ from edx_proctoring import constants -from edx_proctoring.api import ( - create_exam, - update_exam, - get_exam_by_id, - get_exam_by_content_id, - start_exam_attempt, - stop_exam_attempt, - add_allowance_for_user, - remove_allowance_for_user, - get_active_exams_for_user, - create_exam_attempt, - get_allowances_for_course, - get_all_exams_for_course, - get_exam_attempt_by_id, - get_exam_attempt_by_external_id, - remove_exam_attempt, - update_attempt_status, - update_exam_attempt, - is_exam_passed_due, - get_backend_provider, - mark_exam_attempt_as_ready, -) +from edx_proctoring.api import (add_allowance_for_user, create_exam, create_exam_attempt, get_active_exams_for_user, + get_all_exams_for_course, get_allowances_for_course, get_backend_provider, + get_exam_attempt_by_external_id, get_exam_attempt_by_id, get_exam_by_content_id, + get_exam_by_id, is_exam_passed_due, mark_exam_attempt_as_ready, + remove_allowance_for_user, remove_exam_attempt, start_exam_attempt, stop_exam_attempt, + update_attempt_status, update_exam, update_exam_attempt) from edx_proctoring.constants import PING_FAILURE_PASSTHROUGH_TEMPLATE - -from edx_proctoring.exceptions import ( - ProctoredBaseException, - ProctoredExamReviewAlreadyExists, - ProctoredExamPermissionDenied, - StudentExamAttemptDoesNotExistsException, -) +from edx_proctoring.exceptions import (ProctoredBaseException, ProctoredExamPermissionDenied, + ProctoredExamReviewAlreadyExists, StudentExamAttemptDoesNotExistsException) +from edx_proctoring.models import (ProctoredExam, ProctoredExamSoftwareSecureComment, ProctoredExamSoftwareSecureReview, + ProctoredExamStudentAttempt) from edx_proctoring.runtime import get_runtime_service from edx_proctoring.serializers import ProctoredExamSerializer, ProctoredExamStudentAttemptSerializer -from edx_proctoring.models import ( - ProctoredExamStudentAttempt, - ProctoredExam, - ProctoredExamSoftwareSecureComment, - ProctoredExamSoftwareSecureReview, -) -from edx_proctoring.statuses import ( - ProctoredExamStudentAttemptStatus, - ReviewStatus, - SoftwareSecureReviewStatus, -) - -from edx_proctoring.utils import ( - AuthenticatedAPIView, - get_time_remaining_for_attempt, - locate_attempt_by_attempt_code, - humanized_time, - obscured_user_id, -) +from edx_proctoring.statuses import ProctoredExamStudentAttemptStatus, ReviewStatus, SoftwareSecureReviewStatus +from edx_proctoring.utils import (AuthenticatedAPIView, get_time_remaining_for_attempt, humanized_time, + locate_attempt_by_attempt_code, obscured_user_id) ATTEMPTS_PER_PAGE = 25 @@ -129,17 +94,17 @@ def wrapped(request, *args, **kwargs): # pylint: disable=missing-docstring def is_user_course_or_global_staff(user, course_id): """ - Return whether a user is course staff for a given course, described by the course_id, - or is global staff. + Return whether a user is course staff for a given course, described by the course_id, + or is global staff. """ instructor_service = get_runtime_service('instructor') return user.is_staff or instructor_service.is_course_staff(user, course_id) -def handle_proctored_exception(exc, name=None): +def handle_proctored_exception(exc, name=None): # pylint: disable=inconsistent-return-statements """ - converts proctoring exceptions into standard restframework responses + Converts proctoring exceptions into standard restframework responses """ if isinstance(exc, ProctoredBaseException): LOG.exception(name) @@ -152,7 +117,7 @@ class ProctoredAPIView(AuthenticatedAPIView): """ def handle_exception(self, exc): """ - converts proctoring exceptions into standard restframework responses + Converts proctoring exceptions into standard restframework responses """ resp = handle_proctored_exception(exc, name=self.__class__.__name__) if not resp: @@ -329,8 +294,8 @@ def get(self, request, attempt_id): if not attempt: err_msg = ( - 'Attempted to access attempt_id {attempt_id} but ' - 'it does not exist.'.format( + u'Attempted to access attempt_id {attempt_id} but ' + u'it does not exist.'.format( attempt_id=attempt_id ) ) @@ -339,8 +304,8 @@ def get(self, request, attempt_id): # make sure the the attempt belongs to the calling user_id if attempt['user']['id'] != request.user.id: err_msg = ( - 'Attempted to access attempt_id {attempt_id} but ' - 'does not have access to it.'.format( + u'Attempted to access attempt_id {attempt_id} but ' + u'does not have access to it.'.format( attempt_id=attempt_id ) ) @@ -351,13 +316,13 @@ def get(self, request, attempt_id): attempt['time_remaining_seconds'] = time_remaining_seconds - accessibility_time_string = _('you have {remaining_time} remaining').format( + accessibility_time_string = _(u'you have {remaining_time} remaining').format( remaining_time=humanized_time(int(round(time_remaining_seconds / 60.0, 0)))) # special case if we are less than a minute, since we don't produce # text translations of granularity at the seconds range if time_remaining_seconds < 60: - accessibility_time_string = _('you have less than a minute remaining') + accessibility_time_string = _(u'you have less than a minute remaining') attempt['accessibility_time_string'] = accessibility_time_string return Response(attempt) @@ -370,8 +335,8 @@ def put(self, request, attempt_id): if not attempt: err_msg = ( - 'Attempted to access attempt_id {attempt_id} but ' - 'it does not exist.'.format( + u'Attempted to access attempt_id {attempt_id} but ' + u'it does not exist.'.format( attempt_id=attempt_id ) ) @@ -380,8 +345,8 @@ def put(self, request, attempt_id): # make sure the the attempt belongs to the calling user_id if attempt['user']['id'] != request.user.id: err_msg = ( - 'Attempted to access attempt_id {attempt_id} but ' - 'does not have access to it.'.format( + u'Attempted to access attempt_id {attempt_id} but ' + u'does not have access to it.'.format( attempt_id=attempt_id ) ) @@ -447,8 +412,8 @@ def delete(self, request, attempt_id): # pylint: disable=unused-argument if not attempt: err_msg = ( - 'Attempted to access attempt_id {attempt_id} but ' - 'it does not exist.'.format( + u'Attempted to access attempt_id {attempt_id} but ' + u'it does not exist.'.format( attempt_id=attempt_id ) ) @@ -538,7 +503,7 @@ def get(self, request): # pylint: disable=unused-argument # a same process as the LMS exam_url_path = reverse('jump_to', args=[exam['course_id'], exam['content_id']]) except NoReverseMatch: - LOG.exception("Can't find exam url for course %s", exam['course_id']) + LOG.exception(u"Can't find exam url for course %s", exam['course_id']) response_dict = { 'in_timed_exam': True, @@ -555,7 +520,7 @@ def get(self, request): # pylint: disable=unused-argument 'critically_low_threshold_sec': critically_low_threshold, 'course_id': exam['course_id'], 'attempt_id': attempt['id'], - 'accessibility_time_string': _('you have {remaining_time} remaining').format( + 'accessibility_time_string': _(u'you have {remaining_time} remaining').format( remaining_time=humanized_time(int(round(time_remaining_seconds / 60.0, 0))) ), 'attempt_status': attempt['status'], @@ -593,7 +558,7 @@ def post(self, request): # because student can attempt the practice after the due date if not exam.get("is_practice_exam") and is_exam_passed_due(exam, request.user): raise ProctoredExamPermissionDenied( - 'Attempted to access expired exam with exam_id {exam_id}'.format(exam_id=exam_id) + u'Attempted to access expired exam with exam_id {exam_id}'.format(exam_id=exam_id) ) exam_attempt_id = create_exam_attempt( @@ -754,7 +719,7 @@ class ActiveExamsForUserView(ProctoredAPIView): """ def get(self, request): """ - returns the get_active_exams_for_user + Returns the get_active_exams_for_user """ return Response(get_active_exams_for_user( user_id=request.data.get('user_id', None), @@ -779,8 +744,8 @@ def put(self, request, attempt_id): # pylint: disable=unused-argument # make sure the the attempt belongs to the calling user_id if attempt and attempt['user']['id'] != request.user.id: err_msg = ( - 'Attempted to access attempt_id {attempt_id} but ' - 'does not have access to it.'.format( + u'Attempted to access attempt_id {attempt_id} but ' + u'does not have access to it.'.format( attempt_id=attempt_id ) ) @@ -802,7 +767,7 @@ def post(self, request, external_id): # pylint: disable=unused-argument """ attempt = get_exam_attempt_by_external_id(external_id) if not attempt: - LOG.warning("Attempt code %r cannot be found.", external_id) + LOG.warning(u"Attempt code %r cannot be found.", external_id) return Response(data='You have entered an exam code that is not valid.', status=404) if attempt['status'] in [ProctoredExamStudentAttemptStatus.created, ProctoredExamStudentAttemptStatus.download_software_clicked]: @@ -832,8 +797,8 @@ def make_review(self, attempt, data, backend=None): if review: if not constants.ALLOW_REVIEW_UPDATES: err_msg = ( - 'We already have a review submitted regarding ' - 'attempt_code {attempt_code}. We do not allow for updates!'.format( + u'We already have a review submitted regarding ' + u'attempt_code {attempt_code}. We do not allow for updates!'.format( attempt_code=attempt_code ) ) @@ -841,9 +806,9 @@ def make_review(self, attempt, data, backend=None): # we allow updates warn_msg = ( - 'We already have a review submitted from our proctoring provider regarding ' - 'attempt_code {attempt_code}. We have been configured to allow for ' - 'updates and will continue...'.format( + u'We already have a review submitted from our proctoring provider regarding ' + u'attempt_code {attempt_code}. We have been configured to allow for ' + u'updates and will continue...'.format( attempt_code=attempt_code ) ) @@ -875,8 +840,8 @@ def make_review(self, attempt, data, backend=None): course_id = attempt['proctored_exam']['course_id'] if review.reviewed_by is not None and not is_user_course_or_global_staff(review.reviewed_by, course_id): LOG.warning( - 'User %(user)s does not have the required permissions to submit ' - 'a review for attempt_code %(attempt_code)s.', + u'User %(user)s does not have the required permissions to submit ' + u'a review for attempt_code %(attempt_code)s.', {'user': review.reviewed_by, 'attempt_code': attempt_code} ) @@ -967,6 +932,7 @@ class AnonymousReviewCallback(BaseReviewCallback, APIView): content_negotiation_class = IgnoreClientContentNegotiation def handle_exception(self, exc): + """ Helper method for exception handling """ resp = handle_proctored_exception(exc, name=self.__class__.__name__) if not resp: resp = APIView.handle_exception(self, exc) @@ -984,7 +950,7 @@ def post(self, request): if not attempt_obj: # still can't find, error out err_msg = ( - 'Could not locate attempt_code: {attempt_code}'.format(attempt_code=attempt_code) + u'Could not locate attempt_code: {attempt_code}'.format(attempt_code=attempt_code) ) raise StudentExamAttemptDoesNotExistsException(err_msg) serialized = ProctoredExamStudentAttemptSerializer(attempt_obj).data @@ -1027,14 +993,16 @@ def get(self, request, course_id, exam_id=None): # In this case, what are we supposed to do?! # It should not be possible to get in this state, because # course teams will be prevented from updating the backend after the course start date - error_message = "Multiple backends for course %r %r != %r" % (course_id, - found_backend, - exam['backend']) + error_message = u"Multiple backends for course %r %r != %r" % ( + course_id, + found_backend, + exam['backend'] + ) return Response(data=error_message, status=400) else: found_backend = exam_backend if exam is None: - error = _('No exams in course {course_id}.').format(course_id=course_id) + error = _(u'No exams in course {course_id}.').format(course_id=course_id) else: backend = get_backend_provider(exam) if backend: @@ -1054,10 +1022,10 @@ def get(self, request, course_id, exam_id=None): if url: return redirect(url) else: - error = _('No instructor dashboard for {proctor_service}').format( + error = _(u'No instructor dashboard for {proctor_service}').format( proctor_service=backend.verbose_name) else: - error = _('No proctored exams in course {course_id}').format(course_id=course_id) + error = _(u'No proctored exams in course {course_id}').format(course_id=course_id) return Response(data=error, status=404, headers={'X-Frame-Options': 'sameorigin'}) @@ -1075,6 +1043,7 @@ def post(self, request, user_id): # pylint: disable=unused-argument results = {} code = 200 seen = set() + # pylint: disable=no-member attempts = ProctoredExamStudentAttempt.objects.filter(user_id=user_id).select_related('proctored_exam') if attempts: for attempt in attempts: @@ -1082,11 +1051,11 @@ def post(self, request, user_id): # pylint: disable=unused-argument if backend_name in seen or not attempt.taking_as_proctored: continue backend_user_id = obscured_user_id(user_id, backend_name) - LOG.info('retiring user %s from %s', user_id, backend_name) + LOG.info(u'retiring user %s from %s', user_id, backend_name) try: result = get_backend_provider(name=backend_name).retire_user(backend_user_id) except ProctoredBaseException: - LOG.exception('attempting to delete %s (%s) from %s', user_id, backend_user_id, backend_name) + LOG.exception(u'attempting to delete %s (%s) from %s', user_id, backend_user_id, backend_name) result = False if result is not None: results[backend_name] = result diff --git a/mock_apps/apps.py b/mock_apps/apps.py index 4745e1132fb..afbc721d82f 100644 --- a/mock_apps/apps.py +++ b/mock_apps/apps.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import unicode_literals +from __future__ import absolute_import, unicode_literals from django.apps import AppConfig diff --git a/mock_apps/models.py b/mock_apps/models.py index 9d1968e975c..1766d14f0d2 100644 --- a/mock_apps/models.py +++ b/mock_apps/models.py @@ -1,10 +1,10 @@ # -*- coding: utf-8 -*- -from __future__ import unicode_literals +from __future__ import absolute_import, unicode_literals from django.contrib.auth import get_user_model from django.db import models + class Profile(models.Model): user = models.OneToOneField(get_user_model()) name = models.CharField(max_length=100) - diff --git a/package.json b/package.json index 9a0fbb58fb6..ed04e85d587 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "@edx/edx-proctoring", "//": "Be sure to update the version number in edx_proctoring/__init__.py", "//": "Note that the version format is slightly different than that of the Python version when using prereleases.", - "version": "2.1.2", + "version": "2.1.3", "main": "edx_proctoring/static/index.js", "repository": { "type": "git", diff --git a/requirements/base.txt b/requirements/base.txt index 08e0f0f7673..d1f1fda4f99 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -8,7 +8,7 @@ appdirs==1.4.3 # via fs backports.os==0.1.1 # via fs certifi==2019.9.11 # via requests chardet==3.0.4 # via requests -django-crum==0.7.3 +django-crum==0.7.4 django-ipware==2.1.0 django-model-utils==3.2.0 django-waffle==0.17.0 @@ -17,14 +17,14 @@ django==1.11.25 djangorestframework-jwt==1.11.0 # via edx-drf-extensions djangorestframework==3.9.4 edx-django-utils==2.0.1 # via edx-drf-extensions -edx-drf-extensions==2.4.1 +edx-drf-extensions==2.4.2 edx-opaque-keys==2.0.0 edx-rest-api-client==1.9.2 edx-when==0.5.1 enum34==1.1.6 # via fs event-tracking==0.2.9 fs==2.4.11 # via xblock -future==0.18.0 # via backports.os, pyjwkest +future==0.18.1 # via backports.os, pyjwkest idna==2.8 # via requests jsonfield==2.0.2 lxml==4.4.1 # via xblock diff --git a/requirements/dev.in b/requirements/dev.in index 5468627079a..58a9f3c601a 100644 --- a/requirements/dev.in +++ b/requirements/dev.in @@ -4,7 +4,7 @@ -r quality.in diff-cover # Changeset diff test coverage -edx_lint==0.5.5 # Python code linting rules +edx_lint # Python code linting rules edx-i18n-tools # For i18n_tool dummy pip-tools # Requirements file management tox # virtualenv management for tests diff --git a/requirements/dev.txt b/requirements/dev.txt index 8382eb6f937..d94cea57332 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -5,26 +5,26 @@ # make upgrade # argparse==1.4.0 # via caniusepython3 -astroid==1.5.2 # via edx-lint, pylint, pylint-celery +astroid==1.6.6 # via pylint, pylint-celery backports.functools-lru-cache==1.5 # via astroid, caniusepython3, isort, pylint backports.os==0.1.1 # via path.py bleach==3.1.0 # via readme-renderer caniusepython3==7.1.0 certifi==2019.9.11 # via requests chardet==3.0.4 # via requests -click-log==0.1.8 # via edx-lint +click-log==0.3.2 # via edx-lint click==7.0 # via click-log, edx-lint, pip-tools configparser==4.0.2 # via importlib-metadata, pydocstyle, pylint contextlib2==0.6.0.post1 # via importlib-metadata -diff-cover==2.3.0 +diff-cover==2.4.0 distlib==0.2.9.post0 # via caniusepython3 django==1.11.25 docutils==0.15.2 # via readme-renderer edx-i18n-tools==0.4.8 -edx_lint==0.5.5 +edx-lint==1.4.1 enum34==1.1.6 # via astroid filelock==3.0.12 # via tox -future==0.18.0 # via backports.os +future==0.18.1 # via backports.os futures==3.3.0 ; python_version == "2.7" # via caniusepython3, isort idna==2.8 # via requests importlib-metadata==0.23 # via path.py, pluggy, tox @@ -48,9 +48,9 @@ pycodestyle==2.5.0 pydocstyle==3.0.0 pygments==2.4.2 # via diff-cover, readme-renderer pylint-celery==0.3 # via edx-lint -pylint-django==0.7.2 # via edx-lint +pylint-django==0.11.1 # via edx-lint pylint-plugin-utils==0.6 # via pylint-celery, pylint-django -pylint==1.7.1 # via edx-lint, pylint-celery, pylint-django, pylint-plugin-utils +pylint==1.9.5 # via edx-lint, pylint-celery, pylint-django, pylint-plugin-utils pyparsing==2.4.2 # via packaging pytz==2019.3 # via django pyyaml==5.1.2 # via edx-i18n-tools @@ -67,7 +67,7 @@ tox==3.14.0 tqdm==4.36.1 # via twine twine==1.15.0 urllib3==1.25.6 # via requests -virtualenv==16.7.5 # via tox +virtualenv==16.7.7 # via tox webencodings==0.5.1 # via bleach wheel==0.33.6 wrapt==1.11.2 # via astroid diff --git a/requirements/doc.txt b/requirements/doc.txt index a232da79652..0b1d5131385 100644 --- a/requirements/doc.txt +++ b/requirements/doc.txt @@ -11,7 +11,7 @@ backports.os==0.1.1 # via fs bleach==3.1.0 # via readme-renderer certifi==2019.9.11 # via requests chardet==3.0.4 # via doc8, requests -django-crum==0.7.3 +django-crum==0.7.4 django-ipware==2.1.0 django-model-utils==3.2.0 django-waffle==0.17.0 @@ -22,7 +22,7 @@ djangorestframework==3.9.4 doc8==0.8.0 docutils==0.15.2 # via doc8, readme-renderer, restructuredtext-lint, sphinx edx-django-utils==2.0.1 # via edx-drf-extensions -edx-drf-extensions==2.4.1 +edx-drf-extensions==2.4.2 edx-opaque-keys==2.0.0 edx-rest-api-client==1.9.2 edx-sphinx-theme==1.5.0 @@ -30,7 +30,7 @@ edx-when==0.5.1 enum34==1.1.6 # via fs event-tracking==0.2.9 fs==2.4.11 # via xblock -future==0.18.0 # via backports.os, pyjwkest +future==0.18.1 # via backports.os, pyjwkest idna==2.8 # via requests imagesize==1.1.0 # via sphinx jinja2==2.10.3 # via sphinx diff --git a/requirements/quality.in b/requirements/quality.in index 697d9d72764..d34dad4134a 100644 --- a/requirements/quality.in +++ b/requirements/quality.in @@ -2,7 +2,7 @@ -c constraints.txt caniusepython3 # Additional Python 3 compatibility pylint checks -edx_lint==0.5.5 # Python code linting rules +edx_lint # Python code linting rules isort # to standardize order of imports pycodestyle # PEP 8 compliance validation pydocstyle # PEP 257 compliance validation diff --git a/requirements/quality.txt b/requirements/quality.txt index 87cc4e5cd50..0db6b9d3567 100644 --- a/requirements/quality.txt +++ b/requirements/quality.txt @@ -5,17 +5,17 @@ # make upgrade # argparse==1.4.0 # via caniusepython3 -astroid==1.5.2 # via edx-lint, pylint, pylint-celery +astroid==1.6.6 # via pylint, pylint-celery backports.functools-lru-cache==1.5 # via astroid, caniusepython3, isort, pylint caniusepython3==7.1.0 certifi==2019.9.11 # via requests chardet==3.0.4 # via requests -click-log==0.1.8 # via edx-lint +click-log==0.3.2 # via edx-lint click==7.0 # via click-log, edx-lint configparser==4.0.2 # via pydocstyle, pylint distlib==0.2.9.post0 # via caniusepython3 django==1.11.25 -edx_lint==0.5.5 +edx-lint==1.4.1 enum34==1.1.6 # via astroid futures==3.3.0 ; python_version == "2.7" # via caniusepython3, isort idna==2.8 # via requests @@ -26,9 +26,9 @@ packaging==19.2 # via caniusepython3 pycodestyle==2.5.0 pydocstyle==3.0.0 pylint-celery==0.3 # via edx-lint -pylint-django==0.7.2 # via edx-lint +pylint-django==0.11.1 # via edx-lint pylint-plugin-utils==0.6 # via pylint-celery, pylint-django -pylint==1.7.1 # via edx-lint, pylint-celery, pylint-django, pylint-plugin-utils +pylint==1.9.5 # via edx-lint, pylint-celery, pylint-django, pylint-plugin-utils pyparsing==2.4.2 # via packaging pytz==2019.3 # via django requests==2.22.0 # via caniusepython3 diff --git a/requirements/test.txt b/requirements/test.txt index 3b8470dec41..c879ef27c77 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -17,14 +17,14 @@ contextlib2==0.6.0.post1 # via importlib-metadata cookies==2.2.1 # via responses coverage==4.5.4 # via pytest-cov ddt==1.2.1 -django-crum==0.7.3 +django-crum==0.7.4 django-ipware==2.1.0 django-model-utils==3.2.0 django-waffle==0.17.0 django-webpack-loader==0.6.0 djangorestframework-jwt==1.11.0 # via edx-drf-extensions edx-django-utils==2.0.1 # via edx-drf-extensions -edx-drf-extensions==2.4.1 +edx-drf-extensions==2.4.2 edx-i18n-tools==0.4.8 edx-opaque-keys==2.0.0 edx-rest-api-client==1.9.2 @@ -35,7 +35,7 @@ execnet==1.7.1 # via pytest-xdist freezegun==0.3.12 fs==2.4.11 # via xblock funcsigs==1.0.2 # via mock, pytest -future==0.18.0 # via backports.os, pyjwkest +future==0.18.1 # via backports.os, pyjwkest httmock==1.3.0 httpretty==0.9.7 idna==2.8 # via requests @@ -62,8 +62,8 @@ pyjwt==1.7.1 # via djangorestframework-jwt, edx-rest-api-client pymongo==3.9.0 # via edx-opaque-keys, event-tracking pyparsing==2.4.2 # via packaging pytest-cov==2.8.1 -pytest-django==3.5.1 -pytest-forked==1.1.1 # via pytest-xdist +pytest-django==3.6.0 +pytest-forked==1.1.3 # via pytest-xdist pytest-xdist==1.30.0 pytest==4.6.6 # via pytest-cov, pytest-django, pytest-forked, pytest-xdist python-dateutil==2.8.0 diff --git a/setup.cfg b/setup.cfg index 2830de93501..a15cd2de953 100644 --- a/setup.cfg +++ b/setup.cfg @@ -8,3 +8,18 @@ sections = FUTURE,STDLIB,THIRDPARTY,DJANGO,DJANGOAPP,EDX,FIRSTPARTY,LOCALFOLDER [wheel] universal = 1 + +[doc8] +max-line-length = 120 + +[pycodestyle] +exclude = .git,.tox,migrations +max-line-length = 120 + +[pydocstyle] +; D101 = Missing docstring in public class +; D200 = One-line docstring should fit on one line with quotes +; D203 = 1 blank line required before class docstring +; D212 = Multi-line docstring summary should start at the first line +ignore = D101,D200,D202,D203,D204,D205,D210,D212,D400,D401,D404 +match-dir = (?!migrations) \ No newline at end of file diff --git a/test_settings.py b/test_settings.py index 9cc90019787..32bc0cf9cb9 100644 --- a/test_settings.py +++ b/test_settings.py @@ -7,11 +7,10 @@ from __future__ import absolute_import, unicode_literals -import sys - - # Build paths inside the project like this: os.path.join(BASE_DIR, ...) import os +import sys + BASE_DIR = os.path.dirname(__file__) ENV_ROOT = os.path.dirname(BASE_DIR) diff --git a/test_urls.py b/test_urls.py index b1677b66090..35568035c5a 100644 --- a/test_urls.py +++ b/test_urls.py @@ -1,3 +1,5 @@ +from __future__ import absolute_import + from django.conf.urls import include, url urlpatterns = [url(r'^', include('edx_proctoring.urls', namespace='edx_proctoring'))] diff --git a/tox.ini b/tox.ini index fb926f2c498..886a509485c 100644 --- a/tox.ini +++ b/tox.ini @@ -1,20 +1,5 @@ [tox] -envlist = {py27}-django{111}-drf{37,38,39,latest} - -[doc8] -max-line-length = 120 - -[pycodestyle] -exclude = .git,.tox,migrations -max-line-length = 120 - -[pydocstyle] -; D101 = Missing docstring in public class -; D200 = One-line docstring should fit on one line with quotes -; D203 = 1 blank line required before class docstring -; D212 = Multi-line docstring summary should start at the first line -ignore = D101,D200,D203,D212 -match-dir = (?!migrations) +envlist = py{27,36}-django{111}-drf{37,38,39,latest} [testenv] deps = @@ -55,6 +40,7 @@ deps = commands = pylint edx_proctoring pycodestyle edx_proctoring + pydocstyle edx_proctoring [testenv:translations] whitelist_externals =