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 ...@@ -58,6 +58,8 @@ SHOW_EXPIRY_MESSAGE_DURATION = 1 * 60 # duration within which expiry message is
APPROVED_STATUS = 'approved' APPROVED_STATUS = 'approved'
REJECTED_GRADE_OVERRIDE_SCORE = 0
def create_exam(course_id, content_id, exam_name, time_limit_mins, due_date=None, 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): 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, ...@@ -888,6 +890,29 @@ def update_attempt_status(exam_id, user_id, to_status,
cascade_effects=False 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. # call service to get course name.
credit_service = get_runtime_service('credit') credit_service = get_runtime_service('credit')
credit_state = credit_service.get_credit_state( credit_state = credit_service.get_credit_state(
......
...@@ -43,7 +43,7 @@ from edx_proctoring.models import ( ...@@ -43,7 +43,7 @@ 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 from edx_proctoring.tests.test_services import MockCreditService, MockInstructorService, MockGradesService
from edx_proctoring.backends.software_secure import SOFTWARE_SECURE_INVALID_CHARS from edx_proctoring.backends.software_secure import SOFTWARE_SECURE_INVALID_CHARS
...@@ -104,6 +104,7 @@ class SoftwareSecureTests(TestCase): ...@@ -104,6 +104,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())
def tearDown(self): def tearDown(self):
""" """
...@@ -111,6 +112,7 @@ class SoftwareSecureTests(TestCase): ...@@ -111,6 +112,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)
def test_provider_instance(self): def test_provider_instance(self):
""" """
......
...@@ -15,6 +15,7 @@ from edx_proctoring.management.commands import set_attempt_status ...@@ -15,6 +15,7 @@ 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
) )
from edx_proctoring.runtime import set_runtime_service from edx_proctoring.runtime import set_runtime_service
...@@ -31,6 +32,7 @@ class SetAttemptStatusTests(LoggedInTestCase): ...@@ -31,6 +32,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())
self.exam_id = create_exam( self.exam_id = create_exam(
course_id='foo', course_id='foo',
content_id='bar', content_id='bar',
......
...@@ -214,6 +214,15 @@ class ProctoredExamStudentAttemptStatus(object): ...@@ -214,6 +214,15 @@ class ProctoredExamStudentAttemptStatus(object):
] ]
@classmethod @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): 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 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 ( ...@@ -71,6 +71,7 @@ from .test_services import (
MockCreditService, MockCreditService,
MockCreditServiceNone, MockCreditServiceNone,
MockCreditServiceWithCourseEndDate, MockCreditServiceWithCourseEndDate,
MockGradesService
) )
from .utils import ProctoredExamTestCase from .utils import ProctoredExamTestCase
...@@ -844,6 +845,7 @@ class ProctoredExamApiTests(ProctoredExamTestCase): ...@@ -844,6 +845,7 @@ class ProctoredExamApiTests(ProctoredExamTestCase):
are auto marked as declined are auto marked as declined
""" """
set_runtime_service('grades', MockGradesService())
# create other exams in course # create other exams in course
second_exam_id = create_exam( second_exam_id = create_exam(
course_id=self.course_id, course_id=self.course_id,
...@@ -912,6 +914,26 @@ class ProctoredExamApiTests(ProctoredExamTestCase): ...@@ -912,6 +914,26 @@ class ProctoredExamApiTests(ProctoredExamTestCase):
self.assertIsNone(get_exam_attempt(timed_exam_id, self.user_id)) self.assertIsNone(get_exam_attempt(timed_exam_id, self.user_id))
self.assertIsNone(get_exam_attempt(inactive_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( @ddt.data(
(ProctoredExamStudentAttemptStatus.declined, ProctoredExamStudentAttemptStatus.eligible), (ProctoredExamStudentAttemptStatus.declined, ProctoredExamStudentAttemptStatus.eligible),
(ProctoredExamStudentAttemptStatus.timed_out, ProctoredExamStudentAttemptStatus.created), (ProctoredExamStudentAttemptStatus.timed_out, ProctoredExamStudentAttemptStatus.created),
...@@ -1141,6 +1163,7 @@ class ProctoredExamApiTests(ProctoredExamTestCase): ...@@ -1141,6 +1163,7 @@ class ProctoredExamApiTests(ProctoredExamTestCase):
Assert that we get the expected status summaries Assert that we get the expected status summaries
""" """
set_runtime_service('grades', MockGradesService())
exam_attempt = self._create_started_exam_attempt() exam_attempt = self._create_started_exam_attempt()
update_attempt_status( update_attempt_status(
exam_attempt.proctored_exam_id, exam_attempt.proctored_exam_id,
......
...@@ -173,3 +173,20 @@ class TestProctoringService(unittest.TestCase): ...@@ -173,3 +173,20 @@ class TestProctoringService(unittest.TestCase):
service1 = ProctoringService() service1 = ProctoringService()
service2 = ProctoringService() service2 = ProctoringService()
self.assertIs(service1, service2) 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