Commit 7fa9d73a by Ahsan Ulhaq

Update Credit Eligibility Table for Skips

ECOM-2551
parent 6fb89fee
...@@ -1045,6 +1045,9 @@ def _credit_course_requirements(course_key, student): ...@@ -1045,6 +1045,9 @@ def _credit_course_requirements(course_key, student):
if not (settings.FEATURES.get("ENABLE_CREDIT_ELIGIBILITY", False) and is_credit_course(course_key)): if not (settings.FEATURES.get("ENABLE_CREDIT_ELIGIBILITY", False) and is_credit_course(course_key)):
return None return None
# Credit requirement statuses for which user does not remain eligible to get credit.
non_eligible_statuses = ['failed', 'declined']
# Retrieve the status of the user for each eligibility requirement in the course. # Retrieve the status of the user for each eligibility requirement in the course.
# For each requirement, the user's status is either "satisfied", "failed", or None. # For each requirement, the user's status is either "satisfied", "failed", or None.
# In this context, `None` means that we don't know the user's status, either because # In this context, `None` means that we don't know the user's status, either because
...@@ -1068,7 +1071,7 @@ def _credit_course_requirements(course_key, student): ...@@ -1068,7 +1071,7 @@ def _credit_course_requirements(course_key, student):
# If the user has *failed* any requirements (for example, if a photo verification is denied), # If the user has *failed* any requirements (for example, if a photo verification is denied),
# then the user is NOT eligible for credit. # then the user is NOT eligible for credit.
elif any(requirement['status'] == 'failed' for requirement in requirement_statuses): elif any(requirement['status'] in non_eligible_statuses for requirement in requirement_statuses):
eligibility_status = "not_eligible" eligibility_status = "not_eligible"
# Otherwise, the user may be eligible for credit, but the user has not # Otherwise, the user may be eligible for credit, but the user has not
......
...@@ -95,6 +95,7 @@ class ReverificationService(object): ...@@ -95,6 +95,7 @@ class ReverificationService(object):
course_id=course_key, course_id=course_key,
checkpoint_location=related_assessment_location checkpoint_location=related_assessment_location
) )
user = User.objects.get(id=user_id)
# user can skip a reverification attempt only if that user has not already # user can skip a reverification attempt only if that user has not already
# skipped an attempt # skipped an attempt
...@@ -102,6 +103,24 @@ class ReverificationService(object): ...@@ -102,6 +103,24 @@ class ReverificationService(object):
SkippedReverification.add_skipped_reverification_attempt(checkpoint, user_id, course_key) SkippedReverification.add_skipped_reverification_attempt(checkpoint, user_id, course_key)
except IntegrityError: except IntegrityError:
log.exception("Skipped attempt already exists for user %s: with course %s:", user_id, unicode(course_id)) log.exception("Skipped attempt already exists for user %s: with course %s:", user_id, unicode(course_id))
return
try:
# Avoid circular import
from openedx.core.djangoapps.credit.api import set_credit_requirement_status
# As a user skips the reverification it declines to fulfill the requirement so
# requirement sets to declined.
set_credit_requirement_status(
user.username,
course_key,
'reverification',
checkpoint.checkpoint_location,
status='declined'
)
except Exception as err: # pylint: disable=broad-except
log.error("Unable to add credit requirement status for user with id %d: %s", user_id, err)
def get_attempts(self, user_id, course_id, related_assessment_location): def get_attempts(self, user_id, course_id, related_assessment_location):
"""Get re-verification attempts against a user for a given 'checkpoint' """Get re-verification attempts against a user for a given 'checkpoint'
......
...@@ -4,6 +4,8 @@ Tests of re-verification service. ...@@ -4,6 +4,8 @@ Tests of re-verification service.
import ddt import ddt
from opaque_keys.edx.keys import CourseKey
from course_modes.models import CourseMode from course_modes.models import CourseMode
from course_modes.tests.factories import CourseModeFactory from course_modes.tests.factories import CourseModeFactory
from student.models import CourseEnrollment from student.models import CourseEnrollment
...@@ -11,6 +13,8 @@ from student.tests.factories import UserFactory ...@@ -11,6 +13,8 @@ from student.tests.factories import UserFactory
from verify_student.models import VerificationCheckpoint, VerificationStatus, SkippedReverification from verify_student.models import VerificationCheckpoint, VerificationStatus, SkippedReverification
from verify_student.services import ReverificationService from verify_student.services import ReverificationService
from openedx.core.djangoapps.credit.api import get_credit_requirement_status, set_credit_requirements
from openedx.core.djangoapps.credit.models import CreditCourse
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
...@@ -25,20 +29,22 @@ class TestReverificationService(ModuleStoreTestCase): ...@@ -25,20 +29,22 @@ class TestReverificationService(ModuleStoreTestCase):
super(TestReverificationService, self).setUp() super(TestReverificationService, self).setUp()
self.user = UserFactory.create(username="rusty", password="test") self.user = UserFactory.create(username="rusty", password="test")
course = CourseFactory.create(org='Robot', number='999', display_name='Test Course') self.course = CourseFactory.create(org='Robot', number='999', display_name='Test Course')
self.course_key = course.id self.course_id = self.course.id
CourseModeFactory( CourseModeFactory(
mode_slug="verified", mode_slug="verified",
course_id=self.course_key, course_id=self.course_id,
min_price=100, min_price=100,
) )
self.item = ItemFactory.create(parent=course, category='chapter', display_name='Test Section') self.course_key = CourseKey.from_string(unicode(self.course_id))
self.item = ItemFactory.create(parent=self.course, category='chapter', display_name='Test Section')
self.final_checkpoint_location = u'i4x://{org}/{course}/edx-reverification-block/final_uuid'.format( self.final_checkpoint_location = u'i4x://{org}/{course}/edx-reverification-block/final_uuid'.format(
org=self.course_key.org, course=self.course_key.course org=self.course_id.org, course=self.course_id.course
) )
# Enroll in a verified mode # Enroll in a verified mode
self.enrollment = CourseEnrollment.enroll(self.user, self.course_key, mode=CourseMode.VERIFIED) self.enrollment = CourseEnrollment.enroll(self.user, self.course_id, mode=CourseMode.VERIFIED)
@ddt.data('final', 'midterm') @ddt.data('final', 'midterm')
def test_start_verification(self, checkpoint_name): def test_start_verification(self, checkpoint_name):
...@@ -50,16 +56,16 @@ class TestReverificationService(ModuleStoreTestCase): ...@@ -50,16 +56,16 @@ class TestReverificationService(ModuleStoreTestCase):
""" """
reverification_service = ReverificationService() reverification_service = ReverificationService()
checkpoint_location = u'i4x://{org}/{course}/edx-reverification-block/{checkpoint}'.format( checkpoint_location = u'i4x://{org}/{course}/edx-reverification-block/{checkpoint}'.format(
org=self.course_key.org, course=self.course_key.course, checkpoint=checkpoint_name org=self.course_id.org, course=self.course_id.course, checkpoint=checkpoint_name
) )
expected_url = ( expected_url = (
'/verify_student/reverify' '/verify_student/reverify'
'/{course_key}' '/{course_key}'
'/{checkpoint_location}/' '/{checkpoint_location}/'
).format(course_key=unicode(self.course_key), checkpoint_location=checkpoint_location) ).format(course_key=unicode(self.course_id), checkpoint_location=checkpoint_location)
self.assertEqual( self.assertEqual(
reverification_service.start_verification(unicode(self.course_key), checkpoint_location), reverification_service.start_verification(unicode(self.course_id), checkpoint_location),
expected_url expected_url
) )
...@@ -69,22 +75,22 @@ class TestReverificationService(ModuleStoreTestCase): ...@@ -69,22 +75,22 @@ class TestReverificationService(ModuleStoreTestCase):
""" """
reverification_service = ReverificationService() reverification_service = ReverificationService()
self.assertIsNone( self.assertIsNone(
reverification_service.get_status(self.user.id, unicode(self.course_key), self.final_checkpoint_location) reverification_service.get_status(self.user.id, unicode(self.course_id), self.final_checkpoint_location)
) )
checkpoint_obj = VerificationCheckpoint.objects.create( checkpoint_obj = VerificationCheckpoint.objects.create(
course_id=unicode(self.course_key), course_id=unicode(self.course_id),
checkpoint_location=self.final_checkpoint_location checkpoint_location=self.final_checkpoint_location
) )
VerificationStatus.objects.create(checkpoint=checkpoint_obj, user=self.user, status='submitted') VerificationStatus.objects.create(checkpoint=checkpoint_obj, user=self.user, status='submitted')
self.assertEqual( self.assertEqual(
reverification_service.get_status(self.user.id, unicode(self.course_key), self.final_checkpoint_location), reverification_service.get_status(self.user.id, unicode(self.course_id), self.final_checkpoint_location),
'submitted' 'submitted'
) )
VerificationStatus.objects.create(checkpoint=checkpoint_obj, user=self.user, status='approved') VerificationStatus.objects.create(checkpoint=checkpoint_obj, user=self.user, status='approved')
self.assertEqual( self.assertEqual(
reverification_service.get_status(self.user.id, unicode(self.course_key), self.final_checkpoint_location), reverification_service.get_status(self.user.id, unicode(self.course_id), self.final_checkpoint_location),
'approved' 'approved'
) )
...@@ -94,36 +100,68 @@ class TestReverificationService(ModuleStoreTestCase): ...@@ -94,36 +100,68 @@ class TestReverificationService(ModuleStoreTestCase):
""" """
reverification_service = ReverificationService() reverification_service = ReverificationService()
VerificationCheckpoint.objects.create( VerificationCheckpoint.objects.create(
course_id=unicode(self.course_key), course_id=unicode(self.course_id),
checkpoint_location=self.final_checkpoint_location checkpoint_location=self.final_checkpoint_location
) )
reverification_service.skip_verification(self.user.id, unicode(self.course_key), self.final_checkpoint_location) reverification_service.skip_verification(self.user.id, unicode(self.course_id), self.final_checkpoint_location)
self.assertEqual( self.assertEqual(
SkippedReverification.objects.filter(user=self.user, course_id=self.course_key).count(), SkippedReverification.objects.filter(user=self.user, course_id=self.course_id).count(),
1 1
) )
# now test that a user can have only one entry for a skipped # now test that a user can have only one entry for a skipped
# reverification for a course # reverification for a course
reverification_service.skip_verification(self.user.id, unicode(self.course_key), self.final_checkpoint_location) reverification_service.skip_verification(self.user.id, unicode(self.course_id), self.final_checkpoint_location)
self.assertEqual( self.assertEqual(
SkippedReverification.objects.filter(user=self.user, course_id=self.course_key).count(), SkippedReverification.objects.filter(user=self.user, course_id=self.course_id).count(),
1 1
) )
# testing service for skipped attempt. # testing service for skipped attempt.
self.assertEqual( self.assertEqual(
reverification_service.get_status(self.user.id, unicode(self.course_key), self.final_checkpoint_location), reverification_service.get_status(self.user.id, unicode(self.course_id), self.final_checkpoint_location),
'skipped' 'skipped'
) )
def test_declined_verification_on_skip(self):
"""Test that status with value 'declined' is added in credit
requirement status model when a user skip's an ICRV.
"""
reverification_service = ReverificationService()
checkpoint = VerificationCheckpoint.objects.create(
course_id=unicode(self.course_id),
checkpoint_location=self.final_checkpoint_location
)
# Create credit course and set credit requirements.
CreditCourse.objects.create(course_key=self.course_key, enabled=True)
set_credit_requirements(
self.course_key,
[
{
"namespace": "reverification",
"name": checkpoint.checkpoint_location,
"display_name": "Assessment 1",
"criteria": {},
}
]
)
reverification_service.skip_verification(self.user.id, unicode(self.course_id), self.final_checkpoint_location)
requirement_status = get_credit_requirement_status(
self.course_key, self.user.username, 'reverification', checkpoint.checkpoint_location
)
self.assertEqual(SkippedReverification.objects.filter(user=self.user, course_id=self.course_id).count(), 1)
self.assertEqual(len(requirement_status), 1)
self.assertEqual(requirement_status[0].get('name'), checkpoint.checkpoint_location)
self.assertEqual(requirement_status[0].get('status'), 'declined')
def test_get_attempts(self): def test_get_attempts(self):
"""Check verification attempts count against a user for a given """Check verification attempts count against a user for a given
'checkpoint' and 'course_id'. 'checkpoint' and 'course_id'.
""" """
reverification_service = ReverificationService() reverification_service = ReverificationService()
course_id = unicode(self.course_key) course_id = unicode(self.course_id)
self.assertEqual( self.assertEqual(
reverification_service.get_attempts(self.user.id, course_id, self.final_checkpoint_location), reverification_service.get_attempts(self.user.id, course_id, self.final_checkpoint_location),
0 0
...@@ -147,5 +185,5 @@ class TestReverificationService(ModuleStoreTestCase): ...@@ -147,5 +185,5 @@ class TestReverificationService(ModuleStoreTestCase):
# Should be marked as "skipped" (opted out) # Should be marked as "skipped" (opted out)
service = ReverificationService() service = ReverificationService()
status = service.get_status(self.user.id, unicode(self.course_key), self.final_checkpoint_location) status = service.get_status(self.user.id, unicode(self.course_id), self.final_checkpoint_location)
self.assertEqual(status, service.NON_VERIFIED_TRACK) self.assertEqual(status, service.NON_VERIFIED_TRACK)
...@@ -132,10 +132,13 @@ from django.utils.http import urlquote_plus ...@@ -132,10 +132,13 @@ from django.utils.http import urlquote_plus
%if requirement['status'] == 'submitted': %if requirement['status'] == 'submitted':
<span class="requirement-submitted">${_("Verification Submitted")}</span> <span class="requirement-submitted">${_("Verification Submitted")}</span>
%elif requirement['status'] == 'failed': %elif requirement['status'] == 'failed':
<i class="fa fa-times"></i> <i class="fa fa-times" aria-hidden="true"></i>
<span>${_("Verification Failed" )}</span> <span>${_("Verification Failed" )}</span>
%elif requirement['status'] == 'declined':
<i class="fa fa-times" aria-hidden="true"></i>
<span>${_("Verification Declined" )}</span>
%elif requirement['status'] == 'satisfied': %elif requirement['status'] == 'satisfied':
<i class="fa fa-check"></i> <i class="fa fa-check" aria-hidden="true"></i>
% if requirement['namespace'] == 'grade' and 'final_grade' in requirement['reason']: % if requirement['namespace'] == 'grade' and 'final_grade' in requirement['reason']:
<span>${int(requirement['reason']['final_grade'] * 100)}%</span> <span>${int(requirement['reason']['final_grade'] * 100)}%</span>
% else: % else:
...@@ -149,7 +152,7 @@ from django.utils.http import urlquote_plus ...@@ -149,7 +152,7 @@ from django.utils.http import urlquote_plus
</div> </div>
%endfor %endfor
</div> </div>
<button class="detail-collapse" aria-live="polite"><i class="fa fa-caret-up"></i> <button class="detail-collapse" aria-live="polite"><i class="fa fa-caret-up" aria-hidden="true"></i>
<span class="requirement-detail">${_("Less")}</span> <span class="requirement-detail">${_("Less")}</span>
</button> </button>
</div> </div>
......
...@@ -411,6 +411,7 @@ class CreditRequirementStatus(TimeStampedModel): ...@@ -411,6 +411,7 @@ class CreditRequirementStatus(TimeStampedModel):
REQUIREMENT_STATUS_CHOICES = ( REQUIREMENT_STATUS_CHOICES = (
("satisfied", "satisfied"), ("satisfied", "satisfied"),
("failed", "failed"), ("failed", "failed"),
("declined", "declined"),
) )
username = models.CharField(max_length=255, db_index=True) username = models.CharField(max_length=255, db_index=True)
......
...@@ -315,6 +315,21 @@ class CreditRequirementApiTests(CreditApiTestBase): ...@@ -315,6 +315,21 @@ class CreditRequirementApiTests(CreditApiTestBase):
req_status = api.get_credit_requirement_status(self.course_key, "staff", namespace="grade", name="grade") req_status = api.get_credit_requirement_status(self.course_key, "staff", namespace="grade", name="grade")
self.assertEqual(req_status[0]["status"], "failed") self.assertEqual(req_status[0]["status"], "failed")
# Set the requirement to "declined" and check that it's actually set
api.set_credit_requirement_status(
"staff", self.course_key,
"reverification",
"i4x://edX/DemoX/edx-reverification-block/assessment_uuid",
status="declined"
)
req_status = api.get_credit_requirement_status(
self.course_key,
"staff",
namespace="reverification",
name="i4x://edX/DemoX/edx-reverification-block/assessment_uuid"
)
self.assertEqual(req_status[0]["status"], "declined")
def test_remove_credit_requirement_status(self): def test_remove_credit_requirement_status(self):
self.add_credit_course() self.add_credit_course()
requirements = [ requirements = [
......
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