Unverified Commit 332d0a5d by Rabia Iftikhar Committed by GitHub

Merge pull request #400 from edx/ri/EDUCATOR-2016-suspicious-proctored-exams

invalidate certificate when marked proctored exam as suspicious
parents 5199cccf f2fd74c3
...@@ -936,6 +936,22 @@ def update_attempt_status(exam_id, user_id, to_status, ...@@ -936,6 +936,22 @@ def update_attempt_status(exam_id, user_id, to_status,
earned_graded=REJECTED_GRADE_OVERRIDE_EARNED earned_graded=REJECTED_GRADE_OVERRIDE_EARNED
) )
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(
user_id=exam_attempt_obj.user_id,
course_id=exam['course_id']
)
)
# invalidate certificate after overriding subsection grade
certificates_service.invalidate_certificate(
user_id=exam_attempt_obj.user_id,
course_key_or_id=exam['course_id']
)
if (to_status == ProctoredExamStudentAttemptStatus.verified and if (to_status == ProctoredExamStudentAttemptStatus.verified and
ProctoredExamStudentAttemptStatus.needs_grade_override(from_status)): ProctoredExamStudentAttemptStatus.needs_grade_override(from_status)):
grades_service = get_runtime_service('grades') grades_service = get_runtime_service('grades')
......
...@@ -43,7 +43,12 @@ from edx_proctoring.models import ( ...@@ -43,7 +43,12 @@ from edx_proctoring.models import (
ProctoredExamStudentAllowance ProctoredExamStudentAllowance
) )
from edx_proctoring.backends.tests.test_review_payload import create_test_review_payload from edx_proctoring.backends.tests.test_review_payload import create_test_review_payload
from edx_proctoring.tests.test_services import MockCreditService, MockInstructorService, MockGradesService from edx_proctoring.tests.test_services import (
MockCreditService,
MockInstructorService,
MockGradesService,
MockCertificateService
)
from edx_proctoring.backends.software_secure import SOFTWARE_SECURE_INVALID_CHARS from edx_proctoring.backends.software_secure import SOFTWARE_SECURE_INVALID_CHARS
...@@ -105,6 +110,7 @@ class SoftwareSecureTests(TestCase): ...@@ -105,6 +110,7 @@ class SoftwareSecureTests(TestCase):
set_runtime_service('credit', MockCreditService()) set_runtime_service('credit', MockCreditService())
set_runtime_service('instructor', MockInstructorService()) set_runtime_service('instructor', MockInstructorService())
set_runtime_service('grades', MockGradesService()) set_runtime_service('grades', MockGradesService())
set_runtime_service('certificates', MockCertificateService())
def tearDown(self): def tearDown(self):
""" """
...@@ -113,6 +119,7 @@ class SoftwareSecureTests(TestCase): ...@@ -113,6 +119,7 @@ class SoftwareSecureTests(TestCase):
super(SoftwareSecureTests, self).tearDown() super(SoftwareSecureTests, self).tearDown()
set_runtime_service('credit', None) set_runtime_service('credit', None)
set_runtime_service('grades', None) set_runtime_service('grades', None)
set_runtime_service('certificates', None)
def test_provider_instance(self): def test_provider_instance(self):
""" """
......
...@@ -15,7 +15,8 @@ from edx_proctoring.management.commands import set_attempt_status ...@@ -15,7 +15,8 @@ from edx_proctoring.management.commands import set_attempt_status
from edx_proctoring.models import ProctoredExamStudentAttemptStatus, ProctoredExamStudentAttempt from edx_proctoring.models import ProctoredExamStudentAttemptStatus, ProctoredExamStudentAttempt
from edx_proctoring.tests.test_services import ( from edx_proctoring.tests.test_services import (
MockCreditService, MockCreditService,
MockGradesService MockGradesService,
MockCertificateService
) )
from edx_proctoring.runtime import set_runtime_service from edx_proctoring.runtime import set_runtime_service
...@@ -33,6 +34,7 @@ class SetAttemptStatusTests(LoggedInTestCase): ...@@ -33,6 +34,7 @@ class SetAttemptStatusTests(LoggedInTestCase):
super(SetAttemptStatusTests, self).setUp() super(SetAttemptStatusTests, self).setUp()
set_runtime_service('credit', MockCreditService()) set_runtime_service('credit', MockCreditService())
set_runtime_service('grades', MockGradesService()) set_runtime_service('grades', MockGradesService())
set_runtime_service('certificates', MockCertificateService())
self.exam_id = create_exam( self.exam_id = create_exam(
course_id='foo', course_id='foo',
content_id='bar', content_id='bar',
......
...@@ -76,7 +76,8 @@ from .test_services import ( ...@@ -76,7 +76,8 @@ from .test_services import (
MockCreditService, MockCreditService,
MockCreditServiceNone, MockCreditServiceNone,
MockCreditServiceWithCourseEndDate, MockCreditServiceWithCourseEndDate,
MockGradesService MockGradesService,
MockCertificateService
) )
from .utils import ProctoredExamTestCase from .utils import ProctoredExamTestCase
...@@ -88,6 +89,20 @@ class ProctoredExamApiTests(ProctoredExamTestCase): ...@@ -88,6 +89,20 @@ class ProctoredExamApiTests(ProctoredExamTestCase):
All tests for the models.py All tests for the models.py
""" """
def setUp(self):
"""
Initialize
"""
super(ProctoredExamApiTests, self).setUp()
set_runtime_service('certificates', MockCertificateService())
def tearDown(self):
"""
When tests are done
"""
super(ProctoredExamApiTests, self).tearDown()
set_runtime_service('certificates', None)
def _add_allowance_for_user(self): def _add_allowance_for_user(self):
""" """
creates allowance for user. creates allowance for user.
...@@ -938,12 +953,14 @@ class ProctoredExamApiTests(ProctoredExamTestCase): ...@@ -938,12 +953,14 @@ class ProctoredExamApiTests(ProctoredExamTestCase):
def test_grade_override(self): def test_grade_override(self):
""" """
Verify that putting an attempt into the rejected state will also override Verify that putting an attempt into the rejected state will override
the learner's subsection grade for the exam the learner's subsection grade for the exam and also invalidate the
learner's certificate
""" """
set_runtime_service('grades', MockGradesService()) set_runtime_service('grades', MockGradesService())
grades_service = get_runtime_service('grades') grades_service = get_runtime_service('grades')
certificates_service = get_runtime_service('certificates')
exam_attempt = self._create_started_exam_attempt() exam_attempt = self._create_started_exam_attempt()
# Pretend learner answered 5 graded questions in the exam correctly # Pretend learner answered 5 graded questions in the exam correctly
grades_service.init_grade( grades_service.init_grade(
...@@ -975,6 +992,26 @@ class ProctoredExamApiTests(ProctoredExamTestCase): ...@@ -975,6 +992,26 @@ class ProctoredExamApiTests(ProctoredExamTestCase):
'earned_graded': 0.0 'earned_graded': 0.0
}) })
# Rejected exam attempt should invalidate learner's certificate
invalid_generated_certificate = certificates_service.get_invalidated_certificate(
user_id=self.user.id,
course_key_or_id=exam_attempt.proctored_exam.course_id
)
self.assertDictEqual({
'verify_uuid': invalid_generated_certificate.verify_uuid,
'download_uuid': invalid_generated_certificate.download_uuid,
'download_url': invalid_generated_certificate.download_url,
'grade': invalid_generated_certificate.grade,
'status': invalid_generated_certificate.status
}, {
'verify_uuid': '',
'download_uuid': '',
'download_url': '',
'grade': '',
'status': 'unavailable'
})
# The MockGradeService updates the PersistentSubsectionGrade synchronously, but in the real GradesService, this # The MockGradeService updates the PersistentSubsectionGrade synchronously, but in the real GradesService, this
# would be updated by an asynchronous recalculation celery task. # would be updated by an asynchronous recalculation celery task.
......
...@@ -19,6 +19,8 @@ from edx_proctoring.runtime import set_runtime_service, get_runtime_service ...@@ -19,6 +19,8 @@ from edx_proctoring.runtime import set_runtime_service, get_runtime_service
from .test_services import ( from .test_services import (
MockCreditService, MockCreditService,
MockGradesService,
MockCertificateService
) )
from .utils import ( from .utils import (
ProctoredExamTestCase, ProctoredExamTestCase,
...@@ -32,6 +34,23 @@ class ProctoredExamEmailTests(ProctoredExamTestCase): ...@@ -32,6 +34,23 @@ class ProctoredExamEmailTests(ProctoredExamTestCase):
All tests for proctored exam emails. All tests for proctored exam emails.
""" """
def setUp(self):
"""
Initialize
"""
super(ProctoredExamEmailTests, self).setUp()
set_runtime_service('grades', MockGradesService())
set_runtime_service('certificates', MockCertificateService())
def tearDown(self):
"""
When tests are done
"""
super(ProctoredExamEmailTests, self).tearDown()
set_runtime_service('grades', None)
set_runtime_service('certificates', None)
@ddt.data( @ddt.data(
[ [
ProctoredExamStudentAttemptStatus.submitted, ProctoredExamStudentAttemptStatus.submitted,
......
...@@ -189,6 +189,26 @@ class MockGradeOverride(object): ...@@ -189,6 +189,26 @@ class MockGradeOverride(object):
self.earned_graded_override = earned_graded self.earned_graded_override = earned_graded
class MockGeneratedCertificate(object):
"""Fake GeneratedCertificate instance."""
def __init__(self):
self.verify_uuid = 'test_verify_uuid'
self.download_uuid = 'test_download_uuid'
self.download_url = 'test_download_url'
self.grade = 1.0
self.status = 'downloadable'
def mock_invalidate(self):
"""
Invalidate Generated Certificate by marking it 'unavailable'.
"""
self.verify_uuid = ''
self.download_uuid = ''
self.download_url = ''
self.grade = ''
self.status = 'unavailable'
class MockGradesService(object): class MockGradesService(object):
""" """
Simple mock of the Grades Service Simple mock of the Grades Service
...@@ -239,3 +259,29 @@ class MockGradesService(object): ...@@ -239,3 +259,29 @@ class MockGradesService(object):
def should_override_grade_on_rejected_exam(self, course_key): def should_override_grade_on_rejected_exam(self, course_key):
"""Mock will always return instance variable: rejected_exam_overrides_grade""" """Mock will always return instance variable: rejected_exam_overrides_grade"""
return self.rejected_exam_overrides_grade return self.rejected_exam_overrides_grade
class MockCertificateService(object):
"""
mock Certificate Service
"""
def __init__(self):
"""
Initialize empty data stores for generated certificate
"""
self.generated_certificate = {}
def invalidate_certificate(self, user_id, course_key_or_id):
"""
Get the generated certificate for key (user_id + course_key) and invalidate certificate
whose grade dropped below passing threshold due to suspicious proctored exam
"""
key = str(user_id) + str(course_key_or_id)
self.generated_certificate[key] = MockGeneratedCertificate()
self.generated_certificate[key].mock_invalidate()
def get_invalidated_certificate(self, user_id, course_key_or_id):
"""
Returns invalidated certificate for key (user_id + course_key)
"""
return self.generated_certificate.get(str(user_id) + str(course_key_or_id))
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment