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):
if not (settings.FEATURES.get("ENABLE_CREDIT_ELIGIBILITY", False) and is_credit_course(course_key)):
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.
# 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
......@@ -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),
# 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"
# Otherwise, the user may be eligible for credit, but the user has not
......
......@@ -95,6 +95,7 @@ class ReverificationService(object):
course_id=course_key,
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
# skipped an attempt
......@@ -102,6 +103,24 @@ class ReverificationService(object):
SkippedReverification.add_skipped_reverification_attempt(checkpoint, user_id, course_key)
except IntegrityError:
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):
"""Get re-verification attempts against a user for a given 'checkpoint'
......
......@@ -4,6 +4,8 @@ Tests of re-verification service.
import ddt
from opaque_keys.edx.keys import CourseKey
from course_modes.models import CourseMode
from course_modes.tests.factories import CourseModeFactory
from student.models import CourseEnrollment
......@@ -11,6 +13,8 @@ from student.tests.factories import UserFactory
from verify_student.models import VerificationCheckpoint, VerificationStatus, SkippedReverification
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.factories import CourseFactory, ItemFactory
......@@ -25,20 +29,22 @@ class TestReverificationService(ModuleStoreTestCase):
super(TestReverificationService, self).setUp()
self.user = UserFactory.create(username="rusty", password="test")
course = CourseFactory.create(org='Robot', number='999', display_name='Test Course')
self.course_key = course.id
self.course = CourseFactory.create(org='Robot', number='999', display_name='Test Course')
self.course_id = self.course.id
CourseModeFactory(
mode_slug="verified",
course_id=self.course_key,
course_id=self.course_id,
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(
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
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')
def test_start_verification(self, checkpoint_name):
......@@ -50,16 +56,16 @@ class TestReverificationService(ModuleStoreTestCase):
"""
reverification_service = ReverificationService()
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 = (
'/verify_student/reverify'
'/{course_key}'
'/{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(
reverification_service.start_verification(unicode(self.course_key), checkpoint_location),
reverification_service.start_verification(unicode(self.course_id), checkpoint_location),
expected_url
)
......@@ -69,22 +75,22 @@ class TestReverificationService(ModuleStoreTestCase):
"""
reverification_service = ReverificationService()
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(
course_id=unicode(self.course_key),
course_id=unicode(self.course_id),
checkpoint_location=self.final_checkpoint_location
)
VerificationStatus.objects.create(checkpoint=checkpoint_obj, user=self.user, status='submitted')
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'
)
VerificationStatus.objects.create(checkpoint=checkpoint_obj, user=self.user, status='approved')
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'
)
......@@ -94,36 +100,68 @@ class TestReverificationService(ModuleStoreTestCase):
"""
reverification_service = ReverificationService()
VerificationCheckpoint.objects.create(
course_id=unicode(self.course_key),
course_id=unicode(self.course_id),
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(
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
)
# now test that a user can have only one entry for a skipped
# 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(
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
)
# testing service for skipped attempt.
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'
)
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):
"""Check verification attempts count against a user for a given
'checkpoint' and 'course_id'.
"""
reverification_service = ReverificationService()
course_id = unicode(self.course_key)
course_id = unicode(self.course_id)
self.assertEqual(
reverification_service.get_attempts(self.user.id, course_id, self.final_checkpoint_location),
0
......@@ -147,5 +185,5 @@ class TestReverificationService(ModuleStoreTestCase):
# Should be marked as "skipped" (opted out)
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)
......@@ -132,10 +132,13 @@ from django.utils.http import urlquote_plus
%if requirement['status'] == 'submitted':
<span class="requirement-submitted">${_("Verification Submitted")}</span>
%elif requirement['status'] == 'failed':
<i class="fa fa-times"></i>
<i class="fa fa-times" aria-hidden="true"></i>
<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':
<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']:
<span>${int(requirement['reason']['final_grade'] * 100)}%</span>
% else:
......@@ -149,7 +152,7 @@ from django.utils.http import urlquote_plus
</div>
%endfor
</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>
</button>
</div>
......
......@@ -411,6 +411,7 @@ class CreditRequirementStatus(TimeStampedModel):
REQUIREMENT_STATUS_CHOICES = (
("satisfied", "satisfied"),
("failed", "failed"),
("declined", "declined"),
)
username = models.CharField(max_length=255, db_index=True)
......
......@@ -315,6 +315,21 @@ class CreditRequirementApiTests(CreditApiTestBase):
req_status = api.get_credit_requirement_status(self.course_key, "staff", namespace="grade", name="grade")
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):
self.add_credit_course()
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