Commit a76cbb49 by Tyler Hallada

Override exam grade w/ service on review rejected

parent 580220ce
......@@ -58,6 +58,8 @@ SHOW_EXPIRY_MESSAGE_DURATION = 1 * 60 # duration within which expiry message is
APPROVED_STATUS = 'approved'
REJECTED_GRADE_OVERRIDE_SCORE = 0
def create_exam(course_id, content_id, exam_name, time_limit_mins, due_date=None,
is_proctored=True, is_practice_exam=False, external_id=None, is_active=True, hide_after_due=False):
......@@ -888,6 +890,29 @@ def update_attempt_status(exam_id, user_id, to_status,
cascade_effects=False
)
if ProctoredExamStudentAttemptStatus.needs_grade_override(to_status):
grades_service = get_runtime_service('grades')
log_msg = (
'Overriding exam subsection grade for '
'user_id {user_id} on {course_id} for '
'content_id {content_id}. Override '
'score: {score}'.format(
user_id=exam_attempt_obj.user_id,
course_id=exam['course_id'],
content_id=exam_attempt_obj.proctored_exam.content_id,
score=REJECTED_GRADE_OVERRIDE_SCORE
)
)
log.info(log_msg)
grades_service.override_subsection_grade(
user_id=exam_attempt_obj.user_id,
course_key_or_id=exam['course_id'],
subsection=exam_attempt_obj.proctored_exam.content_id,
score=REJECTED_GRADE_OVERRIDE_SCORE
)
# call service to get course name.
credit_service = get_runtime_service('credit')
credit_state = credit_service.get_credit_state(
......
......@@ -43,7 +43,7 @@ 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
from edx_proctoring.tests.test_services import MockCreditService, MockInstructorService, MockGradesService
from edx_proctoring.backends.software_secure import SOFTWARE_SECURE_INVALID_CHARS
......@@ -104,6 +104,7 @@ class SoftwareSecureTests(TestCase):
set_runtime_service('credit', MockCreditService())
set_runtime_service('instructor', MockInstructorService())
set_runtime_service('grades', MockGradesService())
def tearDown(self):
"""
......@@ -111,6 +112,7 @@ class SoftwareSecureTests(TestCase):
"""
super(SoftwareSecureTests, self).tearDown()
set_runtime_service('credit', None)
set_runtime_service('grades', None)
def test_provider_instance(self):
"""
......
......@@ -15,6 +15,7 @@ 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
)
from edx_proctoring.runtime import set_runtime_service
......@@ -31,6 +32,7 @@ class SetAttemptStatusTests(LoggedInTestCase):
"""
super(SetAttemptStatusTests, self).setUp()
set_runtime_service('credit', MockCreditService())
set_runtime_service('grades', MockGradesService())
self.exam_id = create_exam(
course_id='foo',
content_id='bar',
......
......@@ -214,6 +214,15 @@ class ProctoredExamStudentAttemptStatus(object):
]
@classmethod
def needs_grade_override(cls, to_status):
"""
Returns a boolean if the passed in to_status calls for an override of the learner's grade.
"""
return to_status in [
cls.rejected
]
@classmethod
def is_a_cascadable_failure(cls, to_status):
"""
Returns a boolean if the passed in to_status has a failure that needs to be cascaded
......
......@@ -71,6 +71,7 @@ from .test_services import (
MockCreditService,
MockCreditServiceNone,
MockCreditServiceWithCourseEndDate,
MockGradesService
)
from .utils import ProctoredExamTestCase
......@@ -844,6 +845,7 @@ class ProctoredExamApiTests(ProctoredExamTestCase):
are auto marked as declined
"""
set_runtime_service('grades', MockGradesService())
# create other exams in course
second_exam_id = create_exam(
course_id=self.course_id,
......@@ -912,6 +914,26 @@ class ProctoredExamApiTests(ProctoredExamTestCase):
self.assertIsNone(get_exam_attempt(timed_exam_id, self.user_id))
self.assertIsNone(get_exam_attempt(inactive_exam_id, self.user_id))
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
"""
set_runtime_service('grades', MockGradesService())
exam_attempt = self._create_started_exam_attempt()
update_attempt_status(
exam_attempt.proctored_exam_id,
self.user.id,
ProctoredExamStudentAttemptStatus.rejected
)
grades_service = get_runtime_service('grades')
grades_status = grades_service.get_subsection_grade(user_id=self.user.id,
course_key_or_id=exam_attempt.proctored_exam.course_id,
subsection=exam_attempt.proctored_exam.content_id)
self.assertEqual(grades_status, 0)
@ddt.data(
(ProctoredExamStudentAttemptStatus.declined, ProctoredExamStudentAttemptStatus.eligible),
(ProctoredExamStudentAttemptStatus.timed_out, ProctoredExamStudentAttemptStatus.created),
......@@ -1141,6 +1163,7 @@ class ProctoredExamApiTests(ProctoredExamTestCase):
Assert that we get the expected status summaries
"""
set_runtime_service('grades', MockGradesService())
exam_attempt = self._create_started_exam_attempt()
update_attempt_status(
exam_attempt.proctored_exam_id,
......
......@@ -173,3 +173,20 @@ class TestProctoringService(unittest.TestCase):
service1 = ProctoringService()
service2 = ProctoringService()
self.assertIs(service1, service2)
class MockGradesService(object):
"""
Simple mock of the Grades Service
"""
def __init__(self):
"""Initialize empty data store for grades (a dict)"""
self.grades = {}
def get_subsection_grade(self, user_id, course_key_or_id, subsection):
"""Returns entered grade override for key (user_id + course_key + subsection) or None"""
return self.grades.get(str(user_id) + str(course_key_or_id) + str(subsection))
def override_subsection_grade(self, user_id, course_key_or_id, subsection, score):
"""Sets grade override score for key (user_id + course_key + subsection)"""
self.grades[str(user_id) + str(course_key_or_id) + str(subsection)] = score
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