Commit bcd762ef 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 9b24735c
......@@ -7,13 +7,10 @@ import logging
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.exceptions import InvalidCreditRequirements, InvalidCreditCourse
from openedx.core.djangoapps.credit.models import (
CreditCourse,
CreditRequirement,
CreditRequirementStatus,
CreditEligibility,
CreditCourse, CreditRequirement, CreditRequirementStatus, CreditEligibility, CreditRequest
)
# TODO: Cleanup this mess! ECOM-2908
......@@ -228,13 +225,21 @@ def set_credit_requirement_status(username, course_key, req_namespace, req_name,
)
"""
# Check if we're already eligible for credit.
# If so, short-circuit this process.
if CreditEligibility.is_user_eligible_for_credit(course_key, username):
# Do not allow students who have requested credit to change their eligibility
if CreditRequest.get_user_request_status(username, course_key):
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(
u'Skipping update of credit requirement with namespace "%s" '
u'and name "%s" because the user "%s" is already eligible for credit '
u'in the course "%s".',
u'Refusing to set status of requirement with namespace "%s" and name "%s" to "failed" because the '
u'user "%s" is already eligible for credit in the course "%s".',
req_namespace, req_name, username, course_key
)
return
......@@ -273,9 +278,9 @@ def set_credit_requirement_status(username, course_key, req_namespace, req_name,
username, req_to_update, status=status, reason=reason
)
# If we're marking this requirement as "satisfied", there's a chance
# that the user has met all eligibility requirements.
if status == "satisfied":
# If we're marking this requirement as "satisfied", there's a chance that the user has met all eligibility
# requirements, and should be notified. However, if the user was already eligible, do not send another notification.
if status == "satisfied" and not eligible_before_update:
is_eligible, eligibility_record_created = CreditEligibility.update_eligibility(reqs, username, course_key)
if eligibility_record_created and is_eligible:
try:
......
......@@ -7,11 +7,10 @@ import logging
from django.dispatch import receiver
from django.utils import timezone
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 openedx.core.djangoapps.credit.verification_access import update_verification_partitions
from openedx.core.djangoapps.signals.signals import GRADES_UPDATED
log = logging.getLogger(__name__)
......@@ -80,12 +79,39 @@ def listen_for_grade_calculation(sender, username, grade_summary, course_key, de
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():
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(
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 (
CreditProvider,
CreditRequirement,
CreditRequirementStatus,
CreditEligibility
CreditEligibility,
CreditRequest
)
from student.tests.factories import UserFactory
from util.date_utils import from_timestamp
......@@ -380,7 +381,7 @@ class CreditRequirementApiTests(CreditApiTestBase):
def test_set_credit_requirement_status(self):
username = "staff"
self.add_credit_course()
credit_course = self.add_credit_course()
requirements = [
{
"namespace": "grade",
......@@ -405,6 +406,16 @@ class CreditRequirementApiTests(CreditApiTestBase):
# 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)
......@@ -532,7 +543,7 @@ class CreditRequirementApiTests(CreditApiTestBase):
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):
with self.assertNumQueries(12):
api.set_credit_requirement_status(
user.username,
self.course_key,
......@@ -544,7 +555,7 @@ class CreditRequirementApiTests(CreditApiTestBase):
self.assertFalse(api.is_user_eligible_for_credit("bob", self.course_key))
# Satisfy the other requirement
with self.assertNumQueries(19):
with self.assertNumQueries(20):
api.set_credit_requirement_status(
"bob",
self.course_key,
......@@ -598,7 +609,7 @@ class CreditRequirementApiTests(CreditApiTestBase):
# Delete the eligibility entries and satisfy the user's eligibility
# requirement again to trigger eligibility notification
CreditEligibility.objects.all().delete()
with self.assertNumQueries(15):
with self.assertNumQueries(16):
api.set_credit_requirement_status(
"bob",
self.course_key,
......
......@@ -70,20 +70,32 @@ class TestMinGradedRequirementStatus(ModuleStoreTestCase):
""" 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)
req_status = get_credit_requirement_status(self.course.id, self.request.user.username, 'grade', 'grade')
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(
(0.6, VALID_DUE_DATE),
(0.52, VALID_DUE_DATE),
(0.70, EXPIRED_DUE_DATE),
(0.52, None),
)
@ddt.unpack
def test_min_grade_requirement_with_valid_grade(self, grade, due_date):
"""Test with valid grades. Deadline date does not effect in case
of valid grade.
"""
"""Test with valid grades submitted before deadline"""
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(
(0.50, 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