Commit 78f4af7d by Clinton Blackburn Committed by Clinton Blackburn

Updated credit requirements logic

Credit eligibility can be updated up to the point that the course is closed or the student submits a request for credit. Credit eligibility cannot be removed.

ECOM-4379
parent 17e92f55
...@@ -7,13 +7,10 @@ import logging ...@@ -7,13 +7,10 @@ import logging
from opaque_keys.edx.keys import CourseKey from opaque_keys.edx.keys import CourseKey
from openedx.core.djangoapps.credit.exceptions import InvalidCreditRequirements, InvalidCreditCourse
from openedx.core.djangoapps.credit.email_utils import send_credit_notifications from openedx.core.djangoapps.credit.email_utils import send_credit_notifications
from openedx.core.djangoapps.credit.exceptions import InvalidCreditRequirements, InvalidCreditCourse
from openedx.core.djangoapps.credit.models import ( from openedx.core.djangoapps.credit.models import (
CreditCourse, CreditCourse, CreditRequirement, CreditRequirementStatus, CreditEligibility, CreditRequest
CreditRequirement,
CreditRequirementStatus,
CreditEligibility,
) )
# TODO: Cleanup this mess! ECOM-2908 # TODO: Cleanup this mess! ECOM-2908
...@@ -228,13 +225,21 @@ def set_credit_requirement_status(username, course_key, req_namespace, req_name, ...@@ -228,13 +225,21 @@ def set_credit_requirement_status(username, course_key, req_namespace, req_name,
) )
""" """
# Check if we're already eligible for credit. # Do not allow students who have requested credit to change their eligibility
# If so, short-circuit this process. if CreditRequest.get_user_request_status(username, course_key):
if CreditEligibility.is_user_eligible_for_credit(course_key, username): log.info(
u'Refusing to set status of requirement with namespace "%s" and name "%s" because the '
u'user "%s" has already requested credit for the course "%s".',
req_namespace, req_name, username, course_key
)
return
# Do not allow a student who has earned eligibility to un-earn eligibility
eligible_before_update = CreditEligibility.is_user_eligible_for_credit(course_key, username)
if eligible_before_update and status == 'failed':
log.info( log.info(
u'Skipping update of credit requirement with namespace "%s" ' u'Refusing to set status of requirement with namespace "%s" and name "%s" to "failed" because the '
u'and name "%s" because the user "%s" is already eligible for credit ' u'user "%s" is already eligible for credit in the course "%s".',
u'in the course "%s".',
req_namespace, req_name, username, course_key req_namespace, req_name, username, course_key
) )
return return
...@@ -273,9 +278,9 @@ def set_credit_requirement_status(username, course_key, req_namespace, req_name, ...@@ -273,9 +278,9 @@ def set_credit_requirement_status(username, course_key, req_namespace, req_name,
username, req_to_update, status=status, reason=reason username, req_to_update, status=status, reason=reason
) )
# If we're marking this requirement as "satisfied", there's a chance # If we're marking this requirement as "satisfied", there's a chance that the user has met all eligibility
# that the user has met all eligibility requirements. # requirements, and should be notified. However, if the user was already eligible, do not send another notification.
if status == "satisfied": if status == "satisfied" and not eligible_before_update:
is_eligible, eligibility_record_created = CreditEligibility.update_eligibility(reqs, username, course_key) is_eligible, eligibility_record_created = CreditEligibility.update_eligibility(reqs, username, course_key)
if eligibility_record_created and is_eligible: if eligibility_record_created and is_eligible:
try: try:
......
...@@ -7,11 +7,10 @@ import logging ...@@ -7,11 +7,10 @@ import logging
from django.dispatch import receiver from django.dispatch import receiver
from django.utils import timezone from django.utils import timezone
from opaque_keys.edx.keys import CourseKey from opaque_keys.edx.keys import CourseKey
from openedx.core.djangoapps.signals.signals import GRADES_UPDATED
from openedx.core.djangoapps.credit.verification_access import update_verification_partitions
from xmodule.modulestore.django import SignalHandler from xmodule.modulestore.django import SignalHandler
from openedx.core.djangoapps.credit.verification_access import update_verification_partitions
from openedx.core.djangoapps.signals.signals import GRADES_UPDATED
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
...@@ -80,12 +79,39 @@ def listen_for_grade_calculation(sender, username, grade_summary, course_key, de ...@@ -80,12 +79,39 @@ def listen_for_grade_calculation(sender, username, grade_summary, course_key, de
criteria = requirements[0].get('criteria') criteria = requirements[0].get('criteria')
if criteria: if criteria:
min_grade = criteria.get('min_grade') min_grade = criteria.get('min_grade')
if grade_summary['percent'] >= min_grade: passing_grade = grade_summary['percent'] >= min_grade
reason_dict = {'final_grade': grade_summary['percent']} now = timezone.now()
api.set_credit_requirement_status( status = None
username, course_id, 'grade', 'grade', status="satisfied", reason=reason_dict reason = None
)
elif deadline and deadline < timezone.now(): 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( api.set_credit_requirement_status(
username, course_id, 'grade', 'grade', status="failed", reason={} username, course_id, 'grade', 'grade', status=status, reason=reason
) )
...@@ -33,7 +33,8 @@ from openedx.core.djangoapps.credit.models import ( ...@@ -33,7 +33,8 @@ from openedx.core.djangoapps.credit.models import (
CreditProvider, CreditProvider,
CreditRequirement, CreditRequirement,
CreditRequirementStatus, CreditRequirementStatus,
CreditEligibility CreditEligibility,
CreditRequest
) )
from student.tests.factories import UserFactory from student.tests.factories import UserFactory
from util.date_utils import from_timestamp from util.date_utils import from_timestamp
...@@ -380,7 +381,7 @@ class CreditRequirementApiTests(CreditApiTestBase): ...@@ -380,7 +381,7 @@ class CreditRequirementApiTests(CreditApiTestBase):
def test_set_credit_requirement_status(self): def test_set_credit_requirement_status(self):
username = "staff" username = "staff"
self.add_credit_course() credit_course = self.add_credit_course()
requirements = [ requirements = [
{ {
"namespace": "grade", "namespace": "grade",
...@@ -405,6 +406,16 @@ class CreditRequirementApiTests(CreditApiTestBase): ...@@ -405,6 +406,16 @@ class CreditRequirementApiTests(CreditApiTestBase):
# Initially, the status should be None # Initially, the status should be None
self.assert_grade_requirement_status(None, 0) 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 # Set the requirement to "satisfied" and check that it's actually set
api.set_credit_requirement_status(username, self.course_key, "grade", "grade") api.set_credit_requirement_status(username, self.course_key, "grade", "grade")
self.assert_grade_requirement_status('satisfied', 0) self.assert_grade_requirement_status('satisfied', 0)
...@@ -532,7 +543,7 @@ class CreditRequirementApiTests(CreditApiTestBase): ...@@ -532,7 +543,7 @@ class CreditRequirementApiTests(CreditApiTestBase):
user = UserFactory.create(username=self.USER_INFO['username'], password=self.USER_INFO['password']) user = UserFactory.create(username=self.USER_INFO['username'], password=self.USER_INFO['password'])
# Satisfy one of the requirements, but not the other # Satisfy one of the requirements, but not the other
with self.assertNumQueries(11): with self.assertNumQueries(12):
api.set_credit_requirement_status( api.set_credit_requirement_status(
user.username, user.username,
self.course_key, self.course_key,
...@@ -544,7 +555,7 @@ class CreditRequirementApiTests(CreditApiTestBase): ...@@ -544,7 +555,7 @@ class CreditRequirementApiTests(CreditApiTestBase):
self.assertFalse(api.is_user_eligible_for_credit("bob", self.course_key)) self.assertFalse(api.is_user_eligible_for_credit("bob", self.course_key))
# Satisfy the other requirement # Satisfy the other requirement
with self.assertNumQueries(19): with self.assertNumQueries(20):
api.set_credit_requirement_status( api.set_credit_requirement_status(
"bob", "bob",
self.course_key, self.course_key,
...@@ -598,7 +609,7 @@ class CreditRequirementApiTests(CreditApiTestBase): ...@@ -598,7 +609,7 @@ class CreditRequirementApiTests(CreditApiTestBase):
# Delete the eligibility entries and satisfy the user's eligibility # Delete the eligibility entries and satisfy the user's eligibility
# requirement again to trigger eligibility notification # requirement again to trigger eligibility notification
CreditEligibility.objects.all().delete() CreditEligibility.objects.all().delete()
with self.assertNumQueries(15): with self.assertNumQueries(16):
api.set_credit_requirement_status( api.set_credit_requirement_status(
"bob", "bob",
self.course_key, self.course_key,
......
...@@ -70,20 +70,32 @@ class TestMinGradedRequirementStatus(ModuleStoreTestCase): ...@@ -70,20 +70,32 @@ class TestMinGradedRequirementStatus(ModuleStoreTestCase):
""" Verify the user's credit requirement status is as expected after simulating a grading calculation. """ """ Verify the user's credit requirement status is as expected after simulating a grading calculation. """
listen_for_grade_calculation(None, self.user.username, {'percent': grade}, self.course.id, due_date) listen_for_grade_calculation(None, self.user.username, {'percent': grade}, self.course.id, due_date)
req_status = get_credit_requirement_status(self.course.id, self.request.user.username, 'grade', 'grade') req_status = get_credit_requirement_status(self.course.id, self.request.user.username, 'grade', 'grade')
self.assertEqual(req_status[0]['status'], expected_status) self.assertEqual(req_status[0]['status'], expected_status)
if expected_status == 'satisfied':
expected_reason = {'final_grade': grade}
self.assertEqual(req_status[0]['reason'], expected_reason)
@ddt.data( @ddt.data(
(0.6, VALID_DUE_DATE), (0.6, VALID_DUE_DATE),
(0.52, VALID_DUE_DATE), (0.52, None),
(0.70, EXPIRED_DUE_DATE),
) )
@ddt.unpack @ddt.unpack
def test_min_grade_requirement_with_valid_grade(self, grade, due_date): def test_min_grade_requirement_with_valid_grade(self, grade, due_date):
"""Test with valid grades. Deadline date does not effect in case """Test with valid grades submitted before deadline"""
of valid grade.
"""
self.assert_requirement_status(grade, due_date, 'satisfied') self.assert_requirement_status(grade, due_date, 'satisfied')
def test_grade_changed(self):
""" Verify successive calls to update a satisfied grade requirement are recorded. """
self.assert_requirement_status(0.6, self.VALID_DUE_DATE, 'satisfied')
self.assert_requirement_status(0.75, self.VALID_DUE_DATE, 'satisfied')
self.assert_requirement_status(0.70, self.VALID_DUE_DATE, 'satisfied')
def test_min_grade_requirement_with_valid_grade_and_expired_deadline(self):
""" Verify the status is set to failure if a passing grade is received past the submission deadline. """
self.assert_requirement_status(0.70, self.EXPIRED_DUE_DATE, 'failed')
@ddt.data( @ddt.data(
(0.50, None), (0.50, None),
(0.51, None), (0.51, None),
......
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