Update Credit Eligibility Table for Skips
ECOM-2551
This commit is contained in:
@@ -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 = [
|
||||
|
||||
Reference in New Issue
Block a user