def test_credit_requirements_eligible(self): # Mark the user as eligible for all requirements credit_api.set_credit_requirement_status(self.user.username, self.course.id, "grade", "grade", status="satisfied", reason={"final_grade": 0.95}) credit_api.set_credit_requirement_status(self.user.username, self.course.id, "reverification", "midterm", status="satisfied", reason={}) # Check the progress page display response = self._get_progress_page() self.assertContains(response, self.MIN_GRADE_REQ_DISPLAY) self.assertContains(response, self.VERIFICATION_REQ_DISPLAY) self.assertContains( response, "{}, you have met the requirements for credit in this course.". format(self.USER_FULL_NAME)) self.assertContains( response, "Verified on {date}".format(date=self._now_formatted_date())) self.assertContains(response, "95%")
def test_eligibility_email_with_providers(self, providers_list, providers_email_message, expected_subject): """ Test the credit requirements, eligibility notification, email for different providers combinations. """ # Configure a course with two credit requirements self.add_credit_course() CourseFactory.create(org='edX', number='DemoX', display_name='Demo_Course') requirements = [ { "namespace": "grade", "name": "grade", "display_name": "Grade", "criteria": { "min_grade": 0.8 }, }, { "namespace": "reverification", "name": "i4x://edX/DemoX/edx-reverification-block/assessment_uuid", "display_name": "Assessment 1", "criteria": {}, } ] api.set_credit_requirements(self.course_key, requirements) user = UserFactory.create(username=self.USER_INFO['username'], password=self.USER_INFO['password']) # Satisfy one of the requirements, but not the other api.set_credit_requirement_status( user.username, self.course_key, requirements[0]["namespace"], requirements[0]["name"] ) # Satisfy the other requirement. And mocked the api to return different kind of data. with mock.patch('openedx.core.djangoapps.credit.email_utils.get_credit_provider_display_names') as mock_method: mock_method.return_value = providers_list api.set_credit_requirement_status( "bob", self.course_key, requirements[1]["namespace"], requirements[1]["name"] ) # Now the user should be eligible self.assertTrue(api.is_user_eligible_for_credit("bob", self.course_key)) # Credit eligibility email should be sent self.assertEqual(len(mail.outbox), 1) # Verify the email subject self.assertEqual(mail.outbox[0].subject, expected_subject) # Now verify them email content email_payload_first = mail.outbox[0].attachments[0]._payload # pylint: disable=protected-access html_content_first = email_payload_first[0]._payload[1]._payload # pylint: disable=protected-access self.assertIn(providers_email_message, html_content_first) # test text email text_content_first = email_payload_first[0]._payload[0]._payload # pylint: disable=protected-access self.assertIn(providers_email_message, text_content_first)
def test_credit_requirements_eligible(self): # Mark the user as eligible for all requirements credit_api.set_credit_requirement_status( self.user.username, self.course.id, "grade", "grade", status="satisfied", reason={"final_grade": 0.95} ) credit_api.set_credit_requirement_status( self.user.username, self.course.id, "reverification", "midterm", status="satisfied", reason={} ) # Check the progress page display response = self._get_progress_page() self.assertContains(response, self.MIN_GRADE_REQ_DISPLAY) self.assertContains(response, self.VERIFICATION_REQ_DISPLAY) self.assertContains( response, "{}, you have met the requirements for credit in this course.".format(self.USER_FULL_NAME) ) self.assertContains(response, "Verified on {date}".format(date=self._now_formatted_date())) self.assertContains(response, "95%")
def test_credit_requirements_eligible(self): """ Mark the user as eligible for all requirements. Requirements are only displayed for credit and verified enrollments. """ credit_api.set_credit_requirement_status( self.user, self.course.id, "grade", "grade", status="satisfied", reason={"final_grade": 0.95} ) credit_api.set_credit_requirement_status( self.user, self.course.id, "reverification", "midterm", status="satisfied", reason={} ) # Check the progress page display response = self._get_progress_page() self.assertContains(response, self.MIN_GRADE_REQ_DISPLAY) self.assertContains(response, self.VERIFICATION_REQ_DISPLAY) self.assertContains( response, "{}, you have met the requirements for credit in this course.".format(self.USER_FULL_NAME) ) self.assertContains(response, "Completed by {date}") credit_requirements = credit_api.get_credit_requirement_status(self.course.id, self.user.username) for requirement in credit_requirements: self.assertContains(response, requirement['status_date'].strftime('%Y-%m-%d %H:%M')) self.assertNotContains(response, "95%")
def test_credit_requirements_eligible(self): """ Mark the user as eligible for all requirements. Requirements are only displayed for credit and verified enrollments. """ credit_api.set_credit_requirement_status( self.user, self.course.id, "grade", "grade", status="satisfied", reason={"final_grade": 0.95} ) credit_api.set_credit_requirement_status( self.user, self.course.id, "reverification", "midterm", status="satisfied", reason={} ) # Check the progress page display response = self._get_progress_page() self.assertContains(response, self.MIN_GRADE_REQ_DISPLAY) self.assertContains(response, self.VERIFICATION_REQ_DISPLAY) self.assertContains( response, f"{self.USER_FULL_NAME}, you have met the requirements for credit in this course." ) self.assertContains(response, "Completed by {date}") credit_requirements = credit_api.get_credit_requirement_status(self.course.id, self.user.username) for requirement in credit_requirements: self.assertContains(response, requirement['status_date'].strftime('%Y-%m-%d %H:%M')) self.assertNotContains(response, "95%")
def test_set_credit_requirement_status(self): self.add_credit_course() requirements = [ {"namespace": "grade", "name": "grade", "display_name": "Grade", "criteria": {"min_grade": 0.8}}, { "namespace": "reverification", "name": "i4x://edX/DemoX/edx-reverification-block/assessment_uuid", "display_name": "Assessment 1", "criteria": {}, }, ] api.set_credit_requirements(self.course_key, requirements) course_requirements = api.get_credit_requirements(self.course_key) self.assertEqual(len(course_requirements), 2) # Initially, the status should be None req_status = api.get_credit_requirement_status(self.course_key, "staff", namespace="grade", name="grade") self.assertEqual(req_status[0]["status"], None) # Set the requirement to "satisfied" and check that it's actually set api.set_credit_requirement_status("staff", self.course_key, "grade", "grade") req_status = api.get_credit_requirement_status(self.course_key, "staff", namespace="grade", name="grade") self.assertEqual(req_status[0]["status"], "satisfied") # Set the requirement to "failed" and check that it's actually set api.set_credit_requirement_status("staff", self.course_key, "grade", "grade", status="failed") req_status = api.get_credit_requirement_status(self.course_key, "staff", namespace="grade", name="grade") self.assertEqual(req_status[0]["status"], "failed")
def test_remove_credit_requirement_status(self): self.add_credit_course() requirements = [ {"namespace": "grade", "name": "grade", "display_name": "Grade", "criteria": {"min_grade": 0.8}}, { "namespace": "reverification", "name": "i4x://edX/DemoX/edx-reverification-block/assessment_uuid", "display_name": "Assessment 1", "criteria": {}, }, ] api.set_credit_requirements(self.course_key, requirements) course_requirements = api.get_credit_requirements(self.course_key) self.assertEqual(len(course_requirements), 2) # before setting credit_requirement_status api.remove_credit_requirement_status("staff", self.course_key, "grade", "grade") req_status = api.get_credit_requirement_status(self.course_key, "staff", namespace="grade", name="grade") self.assertIsNone(req_status[0]["status"]) self.assertIsNone(req_status[0]["status_date"]) self.assertIsNone(req_status[0]["reason"]) # Set the requirement to "satisfied" and check that it's actually set api.set_credit_requirement_status("staff", self.course_key, "grade", "grade") req_status = api.get_credit_requirement_status(self.course_key, "staff", namespace="grade", name="grade") self.assertEqual(len(req_status), 1) self.assertEqual(req_status[0]["status"], "satisfied") # remove the credit requirement status and check that it's actually removed api.remove_credit_requirement_status("staff", self.course_key, "grade", "grade") req_status = api.get_credit_requirement_status(self.course_key, "staff", namespace="grade", name="grade") self.assertIsNone(req_status[0]["status"]) self.assertIsNone(req_status[0]["status_date"]) self.assertIsNone(req_status[0]["reason"])
def _make_eligible(self): """Make the user eligible for credit in the course. """ credit_api.set_credit_requirement_status( self.user, self.course.id, # pylint: disable=no-member "grade", "grade", status="satisfied", reason={"final_grade": 0.95})
def _make_eligible(self): """Make the user eligible for credit in the course. """ credit_api.set_credit_requirement_status( self.USERNAME, self.course.id, # pylint: disable=no-member "grade", "grade", status="satisfied", reason={"final_grade": 0.95}, )
def test_remove_credit_requirement_status(self): self.add_credit_course() requirements = [{ "namespace": "grade", "name": "grade", "display_name": "Grade", "criteria": { "min_grade": 0.8 }, }, { "namespace": "reverification", "name": "i4x://edX/DemoX/edx-reverification-block/assessment_uuid", "display_name": "Assessment 1", "criteria": {}, }] api.set_credit_requirements(self.course_key, requirements) course_requirements = api.get_credit_requirements(self.course_key) self.assertEqual(len(course_requirements), 2) # before setting credit_requirement_status api.remove_credit_requirement_status("staff", self.course_key, "grade", "grade") req_status = api.get_credit_requirement_status(self.course_key, "staff", namespace="grade", name="grade") self.assertIsNone(req_status[0]["status"]) self.assertIsNone(req_status[0]["status_date"]) self.assertIsNone(req_status[0]["reason"]) # Set the requirement to "satisfied" and check that it's actually set api.set_credit_requirement_status("staff", self.course_key, "grade", "grade") req_status = api.get_credit_requirement_status(self.course_key, "staff", namespace="grade", name="grade") self.assertEqual(len(req_status), 1) self.assertEqual(req_status[0]["status"], "satisfied") # remove the credit requirement status and check that it's actually removed api.remove_credit_requirement_status("staff", self.course_key, "grade", "grade") req_status = api.get_credit_requirement_status(self.course_key, "staff", namespace="grade", name="grade") self.assertIsNone(req_status[0]["status"]) self.assertIsNone(req_status[0]["status_date"]) self.assertIsNone(req_status[0]["reason"])
def test_set_credit_requirement_status_req_not_configured(self): # Configure a credit course with no requirements self.add_credit_course() # A user satisfies a requirement. This could potentially # happen if there's a lag when the requirements are updated # after the course is published. api.set_credit_requirement_status("bob", self.course_key, "grade", "grade") # Since the requirement hasn't been published yet, it won't show # up in the list of requirements. req_status = api.get_credit_requirement_status(self.course_key, "bob", namespace="grade", name="grade") self.assertEqual(req_status, []) # Now add the requirements, simulating what happens when a course is published. requirements = [{ "namespace": "grade", "name": "grade", "display_name": "Grade", "criteria": { "min_grade": 0.8 }, }, { "namespace": "reverification", "name": "i4x://edX/DemoX/edx-reverification-block/assessment_uuid", "display_name": "Assessment 1", "criteria": {}, }] api.set_credit_requirements(self.course_key, requirements) # The user should not have satisfied the requirements, since they weren't # in effect when the user completed the requirement req_status = api.get_credit_requirement_status(self.course_key, "bob") self.assertEqual(len(req_status), 2) self.assertEqual(req_status[0]["status"], None) self.assertEqual(req_status[0]["status"], None) # The user should *not* have satisfied the reverification requirement req_status = api.get_credit_requirement_status( self.course_key, "bob", namespace=requirements[1]["namespace"], name=requirements[1]["name"]) self.assertEqual(len(req_status), 1) self.assertEqual(req_status[0]["status"], None)
def test_set_credit_requirement_status_req_not_configured(self): # Configure a credit course with no requirements self.add_credit_course() # A user satisfies a requirement. This could potentially # happen if there's a lag when the requirements are updated # after the course is published. api.set_credit_requirement_status("bob", self.course_key, "grade", "grade") # Since the requirement hasn't been published yet, it won't show # up in the list of requirements. req_status = api.get_credit_requirement_status(self.course_key, "bob", namespace="grade", name="grade") self.assertEqual(req_status, []) # Now add the requirements, simulating what happens when a course is published. requirements = [ { "namespace": "grade", "name": "grade", "display_name": "Grade", "criteria": { "min_grade": 0.8 }, }, { "namespace": "reverification", "name": "i4x://edX/DemoX/edx-reverification-block/assessment_uuid", "display_name": "Assessment 1", "criteria": {}, } ] api.set_credit_requirements(self.course_key, requirements) # The user should not have satisfied the requirements, since they weren't # in effect when the user completed the requirement req_status = api.get_credit_requirement_status(self.course_key, "bob") self.assertEqual(len(req_status), 2) self.assertEqual(req_status[0]["status"], None) self.assertEqual(req_status[0]["status"], None) # The user should *not* have satisfied the reverification requirement req_status = api.get_credit_requirement_status( self.course_key, "bob", namespace=requirements[1]["namespace"], name=requirements[1]["name"] ) self.assertEqual(len(req_status), 1) self.assertEqual(req_status[0]["status"], None)
def test_set_credit_requirement_status(self): self.add_credit_course() requirements = [{ "namespace": "grade", "name": "grade", "display_name": "Grade", "criteria": { "min_grade": 0.8 }, }, { "namespace": "reverification", "name": "i4x://edX/DemoX/edx-reverification-block/assessment_uuid", "display_name": "Assessment 1", "criteria": {}, }] api.set_credit_requirements(self.course_key, requirements) course_requirements = api.get_credit_requirements(self.course_key) self.assertEqual(len(course_requirements), 2) # Initially, the status should be None req_status = api.get_credit_requirement_status(self.course_key, "staff", namespace="grade", name="grade") self.assertEqual(req_status[0]["status"], None) # Set the requirement to "satisfied" and check that it's actually set api.set_credit_requirement_status("staff", self.course_key, "grade", "grade") req_status = api.get_credit_requirement_status(self.course_key, "staff", namespace="grade", name="grade") self.assertEqual(req_status[0]["status"], "satisfied") # Set the requirement to "failed" and check that it's actually set api.set_credit_requirement_status("staff", self.course_key, "grade", "grade", status="failed") req_status = api.get_credit_requirement_status(self.course_key, "staff", namespace="grade", name="grade") self.assertEqual(req_status[0]["status"], "failed")
def test_credit_requirements_not_eligible(self): # Mark the user as having failed both requirements credit_api.set_credit_requirement_status( self.user.username, self.course.id, "reverification", "midterm", status="failed", reason={} ) # Check the progress page display response = self._get_progress_page() self.assertContains(response, self.MIN_GRADE_REQ_DISPLAY) self.assertContains(response, self.VERIFICATION_REQ_DISPLAY) self.assertContains( response, "{}, you are no longer eligible for credit in this course.".format(self.USER_FULL_NAME) ) self.assertContains(response, "Verification Failed")
def skip_verification(self, user_id, course_id, related_assessment_location): """Add skipped verification attempt entry for a user against a given 'checkpoint'. Args: user_id(str): User Id string course_id(str): A string of course_id related_assessment_location(str): Location of Reverification XBlock Returns: None """ course_key = CourseKey.from_string(course_id) checkpoint = VerificationCheckpoint.objects.get( 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 try: 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, 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 listen_for_grade_calculation(sender, username, grade_summary, course_key, deadline, **kwargs): # pylint: disable=unused-argument """Receive 'MIN_GRADE_REQUIREMENT_STATUS' signal and update minimum grade requirement status. Args: sender: None username(string): user name grade_summary(dict): Dict containing output from the course grader course_key(CourseKey): The key for the course deadline(datetime): Course end date or None Kwargs: kwargs : None """ # This needs to be imported here to avoid a circular dependency # that can cause syncdb to fail. from openedx.core.djangoapps.credit import api course_id = CourseKey.from_string(unicode(course_key)) is_credit = api.is_credit_course(course_id) if is_credit: requirements = api.get_credit_requirements(course_id, namespace='grade') if requirements: criteria = requirements[0].get('criteria') if criteria: min_grade = criteria.get('min_grade') if grade_summary['percent'] >= min_grade: reason_dict = {'final_grade': grade_summary['percent']} api.set_credit_requirement_status(username, course_id, 'grade', 'grade', status="satisfied", reason=reason_dict) elif deadline and deadline < timezone.now(): api.set_credit_requirement_status(username, course_id, 'grade', 'grade', status="failed", reason={})
def skip_verification(self, user_id, course_id, related_assessment_location): """Add skipped verification attempt entry for a user against a given 'checkpoint'. Args: user_id(str): User Id string course_id(str): A string of course_id related_assessment_location(str): Location of Reverification XBlock Returns: None """ course_key = CourseKey.from_string(course_id) checkpoint = VerificationCheckpoint.objects.get( 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 try: 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 test_credit_requirements_not_eligible(self): """ Mark the user as having failed both requirements. Requirements are only displayed for credit and verified enrollments. """ credit_api.set_credit_requirement_status( self.user, self.course.id, "reverification", "midterm", status="failed", reason={} ) # Check the progress page display response = self._get_progress_page() self.assertContains(response, self.MIN_GRADE_REQ_DISPLAY) self.assertContains(response, self.VERIFICATION_REQ_DISPLAY) self.assertContains( response, f"{self.USER_FULL_NAME}, you are no longer eligible for credit in this course." ) self.assertContains(response, "Verification Failed")
def test_set_credit_requirement_status(self): self.add_credit_course() requirements = [ { "namespace": "grade", "name": "grade", "display_name": "Grade", "criteria": { "min_grade": 0.8 } }, { "namespace": "reverification", "name": "i4x://edX/DemoX/edx-reverification-block/assessment_uuid", "display_name": "Assessment 1", "criteria": {} } ] set_credit_requirements(self.course_key, requirements) course_requirements = CreditRequirement.get_course_requirements(self.course_key) self.assertEqual(len(course_requirements), 2) requirement = get_credit_requirement(self.course_key, "grade", "grade") set_credit_requirement_status("staff", requirement, 'satisfied', {}) course_requirement = CreditRequirement.get_course_requirement( requirement['course_key'], requirement['namespace'], requirement['name'] ) status = CreditRequirementStatus.objects.get(username="******", requirement=course_requirement) self.assertEqual(status.requirement.namespace, requirement['namespace']) self.assertEqual(status.status, "satisfied") set_credit_requirement_status( "staff", requirement, 'failed', {'failure_reason': "requirements not satisfied"} ) status = CreditRequirementStatus.objects.get(username="******", requirement=course_requirement) self.assertEqual(status.requirement.namespace, requirement['namespace']) self.assertEqual(status.status, "failed")
def listen_for_grade_calculation(sender, username, grade_summary, course_key, deadline, **kwargs): # pylint: disable=unused-argument """Receive 'MIN_GRADE_REQUIREMENT_STATUS' signal and update minimum grade requirement status. Args: sender: None username(string): user name grade_summary(dict): Dict containing output from the course grader course_key(CourseKey): The key for the course deadline(datetime): Course end date or None Kwargs: kwargs : None """ # This needs to be imported here to avoid a circular dependency # that can cause syncdb to fail. from openedx.core.djangoapps.credit import api course_id = CourseKey.from_string(unicode(course_key)) is_credit = api.is_credit_course(course_id) if is_credit: requirements = api.get_credit_requirements(course_id, namespace='grade') if requirements: criteria = requirements[0].get('criteria') if criteria: min_grade = criteria.get('min_grade') if grade_summary['percent'] >= min_grade: reason_dict = {'final_grade': grade_summary['percent']} api.set_credit_requirement_status( username, course_id, 'grade', 'grade', status="satisfied", reason=reason_dict ) elif deadline and deadline < timezone.now(): api.set_credit_requirement_status( username, course_id, 'grade', 'grade', status="failed", reason={} )
def _set_user_requirement_status(attempt, namespace, status, reason=None): """Sets the status of a credit requirement for the user, based on a verification checkpoint. """ checkpoint = None try: checkpoint = VerificationCheckpoint.objects.get(photo_verification=attempt) except VerificationCheckpoint.DoesNotExist: log.error("Unable to find checkpoint for user with id %d", attempt.user.id) if checkpoint is not None: try: set_credit_requirement_status( attempt.user.username, checkpoint.course_id, namespace, checkpoint.checkpoint_location, status=status, reason=reason, ) except Exception: # pylint: disable=broad-except # Catch exception if unable to add credit requirement # status for user log.error("Unable to add Credit requirement status for user with id %d", attempt.user.id)
def test_satisfy_all_requirements(self): # Configure a course with two credit requirements self.add_credit_course() requirements = [ { "namespace": "grade", "name": "grade", "display_name": "Grade", "criteria": { "min_grade": 0.8 }, }, { "namespace": "reverification", "name": "i4x://edX/DemoX/edx-reverification-block/assessment_uuid", "display_name": "Assessment 1", "criteria": {}, } ] api.set_credit_requirements(self.course_key, requirements) # Satisfy one of the requirements, but not the other with self.assertNumQueries(7): api.set_credit_requirement_status( "bob", self.course_key, requirements[0]["namespace"], requirements[0]["name"] ) # The user should not be eligible (because only one requirement is satisfied) self.assertFalse(api.is_user_eligible_for_credit("bob", self.course_key)) # Satisfy the other requirement with self.assertNumQueries(10): api.set_credit_requirement_status( "bob", self.course_key, requirements[1]["namespace"], requirements[1]["name"] ) # Now the user should be eligible self.assertTrue(api.is_user_eligible_for_credit("bob", self.course_key)) # The user should remain eligible even if the requirement status is later changed api.set_credit_requirement_status( "bob", self.course_key, requirements[0]["namespace"], requirements[0]["name"], status="failed" ) self.assertTrue(api.is_user_eligible_for_credit("bob", self.course_key))
def test_satisfy_all_requirements(self): # Configure a course with two credit requirements self.add_credit_course() CourseFactory.create(org='edX', number='DemoX', display_name='Demo_Course') requirements = [ { "namespace": "grade", "name": "grade", "display_name": "Grade", "criteria": { "min_grade": 0.8 }, }, { "namespace": "reverification", "name": "i4x://edX/DemoX/edx-reverification-block/assessment_uuid", "display_name": "Assessment 1", "criteria": {}, } ] api.set_credit_requirements(self.course_key, requirements) user = UserFactory.create(username=self.USER_INFO['username'], password=self.USER_INFO['password']) # Satisfy one of the requirements, but not the other with self.assertNumQueries(7): api.set_credit_requirement_status( user.username, self.course_key, requirements[0]["namespace"], requirements[0]["name"] ) # The user should not be eligible (because only one requirement is satisfied) self.assertFalse(api.is_user_eligible_for_credit("bob", self.course_key)) # Satisfy the other requirement with self.assertNumQueries(11): api.set_credit_requirement_status( "bob", self.course_key, requirements[1]["namespace"], requirements[1]["name"] ) # Now the user should be eligible self.assertTrue(api.is_user_eligible_for_credit("bob", self.course_key)) # Credit eligible mail should be sent self.assertEqual(len(mail.outbox), 1) self.assertEqual(mail.outbox[0].subject, 'Course Credit Eligibility') # The user should remain eligible even if the requirement status is later changed api.set_credit_requirement_status( "bob", self.course_key, requirements[0]["namespace"], requirements[0]["name"], status="failed" ) self.assertTrue(api.is_user_eligible_for_credit("bob", self.course_key))
def test_set_credit_requirement_status(self): username = "******" credit_course = self.add_credit_course() requirements = [ { "namespace": "grade", "name": "grade", "display_name": "Grade", "criteria": { "min_grade": 0.8 }, }, { "namespace": "reverification", "name": "i4x://edX/DemoX/edx-reverification-block/assessment_uuid", "display_name": "Assessment 1", "criteria": {}, } ] api.set_credit_requirements(self.course_key, requirements) course_requirements = api.get_credit_requirements(self.course_key) self.assertEqual(len(course_requirements), 2) # Initially, the status should be None self.assert_grade_requirement_status(None, 0) # Requirement statuses cannot be changed if a CreditRequest exists credit_request = CreditRequest.objects.create( course=credit_course, provider=CreditProvider.objects.first(), username=username, ) api.set_credit_requirement_status(username, self.course_key, "grade", "grade") self.assert_grade_requirement_status(None, 0) credit_request.delete() # Set the requirement to "satisfied" and check that it's actually set api.set_credit_requirement_status(username, self.course_key, "grade", "grade") self.assert_grade_requirement_status('satisfied', 0) # Set the requirement to "failed" and check that it's actually set api.set_credit_requirement_status(username, self.course_key, "grade", "grade", status="failed") self.assert_grade_requirement_status('failed', 0) req_status = api.get_credit_requirement_status(self.course_key, "staff") self.assertEqual(req_status[0]["status"], "failed") self.assertEqual(req_status[0]["order"], 0) # make sure the 'order' on the 2nd requirement is set correctly (aka 1) self.assertEqual(req_status[1]["status"], None) self.assertEqual(req_status[1]["order"], 1) # Set the requirement to "declined" and check that it's actually set api.set_credit_requirement_status( username, self.course_key, "reverification", "i4x://edX/DemoX/edx-reverification-block/assessment_uuid", status="declined" ) req_status = api.get_credit_requirement_status( self.course_key, username, namespace="reverification", name="i4x://edX/DemoX/edx-reverification-block/assessment_uuid" ) self.assertEqual(req_status[0]["status"], "declined")
def listen_for_grade_calculation(sender, user, grade_summary, course_key, deadline, **kwargs): # pylint: disable=unused-argument """Receive 'MIN_GRADE_REQUIREMENT_STATUS' signal and update minimum grade requirement status. Args: sender: None user(User): User Model object grade_summary(dict): Dict containing output from the course grader course_key(CourseKey): The key for the course deadline(datetime): Course end date or None Kwargs: kwargs : None """ # This needs to be imported here to avoid a circular dependency # that can cause syncdb to fail. from openedx.core.djangoapps.credit import api course_id = CourseKey.from_string(unicode(course_key)) is_credit = api.is_credit_course(course_id) if is_credit: requirements = api.get_credit_requirements(course_id, namespace='grade') if requirements: criteria = requirements[0].get('criteria') if criteria: min_grade = criteria.get('min_grade') passing_grade = grade_summary['percent'] >= min_grade now = timezone.now() status = None reason = None if (deadline and now < deadline) or not deadline: # Student completed coursework on-time if passing_grade: # Student received a passing grade status = 'satisfied' reason = {'final_grade': grade_summary['percent']} else: # Submission after deadline if passing_grade: # Grade was good, but submission arrived too late status = 'failed' reason = {'current_date': now, 'deadline': deadline} else: # Student failed to receive minimum grade status = 'failed' reason = { 'final_grade': grade_summary['percent'], 'minimum_grade': min_grade } # We do not record a status if the user has not yet earned the minimum grade, but still has # time to do so. if status and reason: api.set_credit_requirement_status(user, course_id, 'grade', 'grade', status=status, reason=reason)
def test_satisfy_all_requirements(self): """ Test the credit requirements, eligibility notification, email content caching for a credit course. """ # Configure a course with two credit requirements self.add_credit_course() CourseFactory.create(org='edX', number='DemoX', display_name='Demo_Course') requirements = [{ "namespace": "grade", "name": "grade", "display_name": "Grade", "criteria": { "min_grade": 0.8 }, }, { "namespace": "reverification", "name": "i4x://edX/DemoX/edx-reverification-block/assessment_uuid", "display_name": "Assessment 1", "criteria": {}, }] api.set_credit_requirements(self.course_key, requirements) user = UserFactory.create(username=self.USER_INFO['username'], password=self.USER_INFO['password']) # Satisfy one of the requirements, but not the other with self.assertNumQueries(7): api.set_credit_requirement_status(user.username, self.course_key, requirements[0]["namespace"], requirements[0]["name"]) # The user should not be eligible (because only one requirement is satisfied) self.assertFalse( api.is_user_eligible_for_credit("bob", self.course_key)) # Satisfy the other requirement with self.assertNumQueries(11): api.set_credit_requirement_status("bob", self.course_key, requirements[1]["namespace"], requirements[1]["name"]) # Now the user should be eligible self.assertTrue(api.is_user_eligible_for_credit( "bob", self.course_key)) # Credit eligibility email should be sent self.assertEqual(len(mail.outbox), 1) self.assertEqual(mail.outbox[0].subject, 'Course Credit Eligibility') # Now verify them email content email_payload_first = mail.outbox[0].attachments[0]._payload # pylint: disable=protected-access # Test that email has two payloads [multipart (plain text and html # content), attached image] self.assertEqual(len(email_payload_first), 2) # pylint: disable=protected-access self.assertIn('text/plain', email_payload_first[0]._payload[0]['Content-Type']) # pylint: disable=protected-access self.assertIn('text/html', email_payload_first[0]._payload[1]['Content-Type']) self.assertIn('image/png', email_payload_first[1]['Content-Type']) # Now check that html email content has same logo image 'Content-ID' # as the attached logo image 'Content-ID' email_image = email_payload_first[1] html_content_first = email_payload_first[0]._payload[1]._payload # pylint: disable=protected-access # strip enclosing angle brackets from 'logo_image' cache 'Content-ID' image_id = email_image.get('Content-ID', '')[1:-1] self.assertIsNotNone(image_id) self.assertIn(image_id, html_content_first) # Delete the eligibility entries and satisfy the user's eligibility # requirement again to trigger eligibility notification CreditEligibility.objects.all().delete() with self.assertNumQueries(12): api.set_credit_requirement_status("bob", self.course_key, requirements[1]["namespace"], requirements[1]["name"]) # Credit eligibility email should be sent self.assertEqual(len(mail.outbox), 2) # Now check that on sending eligibility notification again cached # logo image is used email_payload_second = mail.outbox[1].attachments[0]._payload # pylint: disable=protected-access html_content_second = email_payload_second[0]._payload[1]._payload # pylint: disable=protected-access self.assertIn(image_id, html_content_second) # The user should remain eligible even if the requirement status is later changed api.set_credit_requirement_status("bob", self.course_key, requirements[0]["namespace"], requirements[0]["name"], status="failed") self.assertTrue(api.is_user_eligible_for_credit( "bob", self.course_key))
def test_satisfy_all_requirements(self): """ Test the credit requirements, eligibility notification, email content caching for a credit course. """ # Configure a course with two credit requirements self.add_credit_course() CourseFactory.create(org='edX', number='DemoX', display_name='Demo_Course') requirements = [ { "namespace": "grade", "name": "grade", "display_name": "Grade", "criteria": { "min_grade": 0.8 }, }, { "namespace": "reverification", "name": "i4x://edX/DemoX/edx-reverification-block/assessment_uuid", "display_name": "Assessment 1", "criteria": {}, } ] api.set_credit_requirements(self.course_key, requirements) user = UserFactory.create(username=self.USER_INFO['username'], password=self.USER_INFO['password']) # Satisfy one of the requirements, but not the other with self.assertNumQueries(11): api.set_credit_requirement_status( user.username, self.course_key, requirements[0]["namespace"], requirements[0]["name"] ) # The user should not be eligible (because only one requirement is satisfied) self.assertFalse(api.is_user_eligible_for_credit("bob", self.course_key)) # Satisfy the other requirement with self.assertNumQueries(15): api.set_credit_requirement_status( "bob", self.course_key, requirements[1]["namespace"], requirements[1]["name"] ) # Now the user should be eligible self.assertTrue(api.is_user_eligible_for_credit("bob", self.course_key)) # Credit eligibility email should be sent self.assertEqual(len(mail.outbox), 1) self.assertEqual(mail.outbox[0].subject, 'Course Credit Eligibility') # Now verify them email content email_payload_first = mail.outbox[0].attachments[0]._payload # pylint: disable=protected-access # Test that email has two payloads [multipart (plain text and html # content), attached image] self.assertEqual(len(email_payload_first), 2) # pylint: disable=protected-access self.assertIn('text/plain', email_payload_first[0]._payload[0]['Content-Type']) # pylint: disable=protected-access self.assertIn('text/html', email_payload_first[0]._payload[1]['Content-Type']) self.assertIn('image/png', email_payload_first[1]['Content-Type']) # Now check that html email content has same logo image 'Content-ID' # as the attached logo image 'Content-ID' email_image = email_payload_first[1] html_content_first = email_payload_first[0]._payload[1]._payload # pylint: disable=protected-access # strip enclosing angle brackets from 'logo_image' cache 'Content-ID' image_id = email_image.get('Content-ID', '')[1:-1] self.assertIsNotNone(image_id) self.assertIn(image_id, html_content_first) # Delete the eligibility entries and satisfy the user's eligibility # requirement again to trigger eligibility notification CreditEligibility.objects.all().delete() with self.assertNumQueries(13): api.set_credit_requirement_status( "bob", self.course_key, requirements[1]["namespace"], requirements[1]["name"] ) # Credit eligibility email should be sent self.assertEqual(len(mail.outbox), 2) # Now check that on sending eligibility notification again cached # logo image is used email_payload_second = mail.outbox[1].attachments[0]._payload # pylint: disable=protected-access html_content_second = email_payload_second[0]._payload[1]._payload # pylint: disable=protected-access self.assertIn(image_id, html_content_second) # The user should remain eligible even if the requirement status is later changed api.set_credit_requirement_status( "bob", self.course_key, requirements[0]["namespace"], requirements[0]["name"], status="failed" ) self.assertTrue(api.is_user_eligible_for_credit("bob", self.course_key))
def listen_for_grade_calculation(sender, user, course_grade, course_key, deadline, **kwargs): # pylint: disable=unused-argument """Receive 'MIN_GRADE_REQUIREMENT_STATUS' signal and update minimum grade requirement status. Args: sender: None user(User): User Model object course_grade(CourseGrade): CourseGrade object course_key(CourseKey): The key for the course deadline(datetime): Course end date or None Kwargs: kwargs : None """ # This needs to be imported here to avoid a circular dependency # that can cause syncdb to fail. from openedx.core.djangoapps.credit import api course_id = CourseKey.from_string(unicode(course_key)) is_credit = api.is_credit_course(course_id) if is_credit: requirements = api.get_credit_requirements(course_id, namespace='grade') if requirements: criteria = requirements[0].get('criteria') if criteria: min_grade = criteria.get('min_grade') passing_grade = course_grade.percent >= min_grade now = timezone.now() status = None reason = None if (deadline and now < deadline) or not deadline: # Student completed coursework on-time if passing_grade: # Student received a passing grade status = 'satisfied' reason = {'final_grade': course_grade.percent} else: # Submission after deadline if passing_grade: # Grade was good, but submission arrived too late status = 'failed' reason = { 'current_date': now, 'deadline': deadline } else: # Student failed to receive minimum grade status = 'failed' reason = { 'final_grade': course_grade.percent, 'minimum_grade': min_grade } # We do not record a status if the user has not yet earned the minimum grade, but still has # time to do so. if status and reason: api.set_credit_requirement_status( user, course_id, 'grade', 'grade', status=status, reason=reason )