Commit f2fd74c3 by rabiaiftikhar

invalidate certificate when marked proctored exam as suspicious

parent 5199cccf
......@@ -936,6 +936,22 @@ def update_attempt_status(exam_id, user_id, to_status,
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
ProctoredExamStudentAttemptStatus.needs_grade_override(from_status)):
grades_service = get_runtime_service('grades')
......
......@@ -43,7 +43,12 @@ from edx_proctoring.models import (
ProctoredExamStudentAllowance
)
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
......@@ -105,6 +110,7 @@ class SoftwareSecureTests(TestCase):
set_runtime_service('credit', MockCreditService())
set_runtime_service('instructor', MockInstructorService())
set_runtime_service('grades', MockGradesService())
set_runtime_service('certificates', MockCertificateService())
def tearDown(self):
"""
......@@ -113,6 +119,7 @@ class SoftwareSecureTests(TestCase):
super(SoftwareSecureTests, self).tearDown()
set_runtime_service('credit', None)
set_runtime_service('grades', None)
set_runtime_service('certificates', None)
def test_provider_instance(self):
"""
......
......@@ -15,7 +15,8 @@ from edx_proctoring.management.commands import set_attempt_status
from edx_proctoring.models import ProctoredExamStudentAttemptStatus, ProctoredExamStudentAttempt
from edx_proctoring.tests.test_services import (
MockCreditService,
MockGradesService
MockGradesService,
MockCertificateService
)
from edx_proctoring.runtime import set_runtime_service
......@@ -33,6 +34,7 @@ class SetAttemptStatusTests(LoggedInTestCase):
super(SetAttemptStatusTests, self).setUp()
set_runtime_service('credit', MockCreditService())
set_runtime_service('grades', MockGradesService())
set_runtime_service('certificates', MockCertificateService())
self.exam_id = create_exam(
course_id='foo',
content_id='bar',
......
......@@ -76,7 +76,8 @@ from .test_services import (
MockCreditService,
MockCreditServiceNone,
MockCreditServiceWithCourseEndDate,
MockGradesService
MockGradesService,
MockCertificateService
)
from .utils import ProctoredExamTestCase
......@@ -88,6 +89,20 @@ class ProctoredExamApiTests(ProctoredExamTestCase):
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):
"""
creates allowance for user.
......@@ -938,12 +953,14 @@ class ProctoredExamApiTests(ProctoredExamTestCase):
def test_grade_override(self):
"""
Verify that putting an attempt into the rejected state will also override
the learner's subsection grade for the exam
Verify that putting an attempt into the rejected state will override
the learner's subsection grade for the exam and also invalidate the
learner's certificate
"""
set_runtime_service('grades', MockGradesService())
grades_service = get_runtime_service('grades')
certificates_service = get_runtime_service('certificates')
exam_attempt = self._create_started_exam_attempt()
# Pretend learner answered 5 graded questions in the exam correctly
grades_service.init_grade(
......@@ -975,6 +992,26 @@ class ProctoredExamApiTests(ProctoredExamTestCase):
'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
# would be updated by an asynchronous recalculation celery task.
......
......@@ -19,6 +19,8 @@ from edx_proctoring.runtime import set_runtime_service, get_runtime_service
from .test_services import (
MockCreditService,
MockGradesService,
MockCertificateService
)
from .utils import (
ProctoredExamTestCase,
......@@ -32,6 +34,23 @@ class ProctoredExamEmailTests(ProctoredExamTestCase):
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(
[
ProctoredExamStudentAttemptStatus.submitted,
......
......@@ -189,6 +189,26 @@ class MockGradeOverride(object):
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):
"""
Simple mock of the Grades Service
......@@ -239,3 +259,29 @@ class MockGradesService(object):
def should_override_grade_on_rejected_exam(self, course_key):
"""Mock will always return instance variable: 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