From 7fa9d73a4c5963789b8f2937097597ac92de450a Mon Sep 17 00:00:00 2001 From: Ahsan Ulhaq <ahsan.haq@arbisoft.com> Date: Thu, 15 Oct 2015 15:22:08 +0500 Subject: [PATCH] Update Credit Eligibility Table for Skips ECOM-2551 --- lms/djangoapps/courseware/views.py | 5 +- lms/djangoapps/verify_student/services.py | 19 +++++ .../verify_student/tests/test_services.py | 80 ++++++++++++++----- lms/templates/courseware/progress.html | 9 ++- openedx/core/djangoapps/credit/models.py | 1 + .../core/djangoapps/credit/tests/test_api.py | 15 ++++ 6 files changed, 104 insertions(+), 25 deletions(-) diff --git a/lms/djangoapps/courseware/views.py b/lms/djangoapps/courseware/views.py index b3ce0215684..024a12909ff 100644 --- a/lms/djangoapps/courseware/views.py +++ b/lms/djangoapps/courseware/views.py @@ -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 diff --git a/lms/djangoapps/verify_student/services.py b/lms/djangoapps/verify_student/services.py index cdf0a38df43..0269d8ed3c3 100644 --- a/lms/djangoapps/verify_student/services.py +++ b/lms/djangoapps/verify_student/services.py @@ -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' diff --git a/lms/djangoapps/verify_student/tests/test_services.py b/lms/djangoapps/verify_student/tests/test_services.py index 85568c3d9fb..5c5a8b9dc12 100644 --- a/lms/djangoapps/verify_student/tests/test_services.py +++ b/lms/djangoapps/verify_student/tests/test_services.py @@ -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) diff --git a/lms/templates/courseware/progress.html b/lms/templates/courseware/progress.html index 9483e8efea3..8666bd941fc 100644 --- a/lms/templates/courseware/progress.html +++ b/lms/templates/courseware/progress.html @@ -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> diff --git a/openedx/core/djangoapps/credit/models.py b/openedx/core/djangoapps/credit/models.py index 1bc67198e62..895faaa2df8 100644 --- a/openedx/core/djangoapps/credit/models.py +++ b/openedx/core/djangoapps/credit/models.py @@ -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) diff --git a/openedx/core/djangoapps/credit/tests/test_api.py b/openedx/core/djangoapps/credit/tests/test_api.py index 11ea560876b..17508938408 100644 --- a/openedx/core/djangoapps/credit/tests/test_api.py +++ b/openedx/core/djangoapps/credit/tests/test_api.py @@ -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 = [ -- GitLab