Commit b8e04f30 by Awais

ECOM-1597 adding signals to update min-grade requirement.

parent 614bcafb
......@@ -173,18 +173,18 @@ class TestFieldOverrideMongoPerformance(FieldOverridePerformanceTestCase):
TEST_DATA = {
# (providers, course_width, enable_ccx): # of sql queries, # of mongo queries, # of xblocks
('no_overrides', 1, True): (26, 7, 14),
('no_overrides', 2, True): (134, 7, 85),
('no_overrides', 3, True): (594, 7, 336),
('ccx', 1, True): (26, 7, 14),
('ccx', 2, True): (134, 7, 85),
('ccx', 3, True): (594, 7, 336),
('no_overrides', 1, False): (26, 7, 14),
('no_overrides', 2, False): (134, 7, 85),
('no_overrides', 3, False): (594, 7, 336),
('ccx', 1, False): (26, 7, 14),
('ccx', 2, False): (134, 7, 85),
('ccx', 3, False): (594, 7, 336),
('no_overrides', 1, True): (27, 7, 14),
('no_overrides', 2, True): (135, 7, 85),
('no_overrides', 3, True): (595, 7, 336),
('ccx', 1, True): (27, 7, 14),
('ccx', 2, True): (135, 7, 85),
('ccx', 3, True): (595, 7, 336),
('no_overrides', 1, False): (27, 7, 14),
('no_overrides', 2, False): (135, 7, 85),
('no_overrides', 3, False): (595, 7, 336),
('ccx', 1, False): (27, 7, 14),
('ccx', 2, False): (135, 7, 85),
('ccx', 3, False): (595, 7, 336),
}
......@@ -196,16 +196,16 @@ class TestFieldOverrideSplitPerformance(FieldOverridePerformanceTestCase):
__test__ = True
TEST_DATA = {
('no_overrides', 1, True): (26, 4, 9),
('no_overrides', 2, True): (134, 19, 54),
('no_overrides', 3, True): (594, 84, 215),
('ccx', 1, True): (26, 4, 9),
('ccx', 2, True): (134, 19, 54),
('ccx', 3, True): (594, 84, 215),
('no_overrides', 1, False): (26, 4, 9),
('no_overrides', 2, False): (134, 19, 54),
('no_overrides', 3, False): (594, 84, 215),
('ccx', 1, False): (26, 4, 9),
('ccx', 2, False): (134, 19, 54),
('ccx', 3, False): (594, 84, 215),
('no_overrides', 1, True): (27, 4, 9),
('no_overrides', 2, True): (135, 19, 54),
('no_overrides', 3, True): (595, 84, 215),
('ccx', 1, True): (27, 4, 9),
('ccx', 2, True): (135, 19, 54),
('ccx', 3, True): (595, 84, 215),
('no_overrides', 1, False): (27, 4, 9),
('no_overrides', 2, False): (135, 19, 54),
('no_overrides', 3, False): (595, 84, 215),
('ccx', 1, False): (27, 4, 9),
('ccx', 2, False): (135, 19, 54),
('ccx', 3, False): (595, 84, 215),
}
......@@ -26,6 +26,7 @@ from submissions import api as sub_api # installed from the edx-submissions rep
from opaque_keys import InvalidKeyError
from opaque_keys.edx.keys import CourseKey
from openedx.core.djangoapps.signals.signals import GRADES_UPDATED
log = logging.getLogger("edx.courseware")
......@@ -126,9 +127,22 @@ def grade(student, request, course, keep_raw_scores=False):
"""
Wraps "_grade" with the manual_transaction context manager just in case
there are unanticipated errors.
Send a signal to update the minimum grade requirement status.
"""
with manual_transaction():
return _grade(student, request, course, keep_raw_scores)
grade_summary = _grade(student, request, course, keep_raw_scores)
responses = GRADES_UPDATED.send_robust(
sender=None,
username=request.user.username,
grade_summary=grade_summary,
course_key=course.id,
deadline=course.end
)
for receiver, response in responses:
log.info('Signal fired when student grade is calculated. Receiver: %s. Response: %s', receiver, response)
return grade_summary
def _grade(student, request, course, keep_raw_scores):
......
......@@ -26,6 +26,10 @@ from student.models import anonymous_id_for_user
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
from xmodule.partitions.partitions import Group, UserPartition
from openedx.core.djangoapps.credit.api import (
set_credit_requirements, get_credit_requirement_status
)
from openedx.core.djangoapps.credit.models import CreditCourse, CreditProvider
from openedx.core.djangoapps.user_api.tests.factories import UserCourseTagFactory
......@@ -238,6 +242,7 @@ class TestSubmittingProblems(ModuleStoreTestCase, LoginEnrollmentTestCase):
reverse('progress', kwargs={'course_id': self.course.id.to_deprecated_string()})
)
fake_request.user = self.student_user
return grades.grade(self.student_user, fake_request, self.course)
def get_progress_summary(self):
......@@ -594,6 +599,43 @@ class TestCourseGrader(TestSubmittingProblems):
self.assertEqual(self.earned_hw_scores(), [1.0, 2.0, 2.0]) # Order matters
self.assertEqual(self.score_for_hw('homework3'), [1.0, 1.0])
def test_min_grade_credit_requirements_status(self):
"""
Test for credit course. If user passes minimum grade requirement then
status will be updated as satisfied in requirement status table.
"""
self.basic_setup()
self.submit_question_answer('p1', {'2_1': 'Correct'})
self.submit_question_answer('p2', {'2_1': 'Correct'})
# Enable the course for credit
credit_course = CreditCourse.objects.create(
course_key=self.course.id,
enabled=True,
)
# Configure a credit provider for the course
credit_provider = CreditProvider.objects.create(
provider_id="ASU",
enable_integration=True,
provider_url="https://credit.example.com/request",
)
credit_course.providers.add(credit_provider)
credit_course.save()
requirements = [{
"namespace": "grade",
"name": "grade",
"display_name": "Grade",
"criteria": {"min_grade": 0.52}
}]
# Add a single credit requirement (final grade)
set_credit_requirements(self.course.id, requirements)
self.get_grade_summary()
req_status = get_credit_requirement_status(self.course.id, self.student_user.username, 'grade', 'grade')
self.assertEqual(req_status[0]["status"], 'satisfied')
@attr('shard_1')
class ProblemWithUploadedFilesTest(TestSubmittingProblems):
......
......@@ -946,6 +946,7 @@ class IsCoursePassedTests(ModuleStoreTestCase):
grade_cutoffs={'cutoff': 0.75, 'Pass': self.SUCCESS_CUTOFF}
)
self.request = RequestFactory()
self.request.user = self.student
def test_user_fails_if_not_clear_exam(self):
# If user has not grade then false will return
......
......@@ -3,8 +3,11 @@ This file contains receivers of course publication signals.
"""
from django.dispatch import receiver
from django.utils import timezone
from opaque_keys.edx.keys import CourseKey
from xmodule.modulestore.django import SignalHandler
from openedx.core.djangoapps.signals.signals import GRADES_UPDATED
@receiver(SignalHandler.course_published)
......@@ -18,3 +21,42 @@ def listen_for_course_publish(sender, course_key, **kwargs): # pylint: disable=
from .tasks import update_credit_course_requirements
update_credit_course_requirements.delay(unicode(course_key))
@receiver(GRADES_UPDATED)
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
"""
from openedx.core.djangoapps.credit.api import (
is_credit_course, get_credit_requirement, set_credit_requirement_status
)
course_id = CourseKey.from_string(unicode(course_key))
is_credit = is_credit_course(course_id)
if is_credit:
requirement = get_credit_requirement(course_id, 'grade', 'grade')
if requirement:
criteria = requirement.get('criteria')
if criteria:
min_grade = criteria.get('min_grade')
if grade_summary['percent'] >= min_grade:
reason_dict = {'final_grade': grade_summary['percent']}
set_credit_requirement_status(
username, course_id, 'grade', 'grade', status="satisfied", reason=reason_dict
)
elif deadline and deadline < timezone.now():
set_credit_requirement_status(
username, course_id, 'grade', 'grade', status="failed", reason={}
)
"""
Tests for minimum grade requirement status
"""
import pytz
import ddt
from datetime import timedelta, datetime
from django.test.client import RequestFactory
from openedx.core.djangoapps.credit.api import (
set_credit_requirements, get_credit_requirement_status
)
from openedx.core.djangoapps.credit.models import CreditCourse, CreditProvider
from openedx.core.djangoapps.credit.signals import listen_for_grade_calculation
from student.tests.factories import UserFactory
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory
@ddt.ddt
class TestMinGradedRequirementStatus(ModuleStoreTestCase):
"""Test cases to check the minimum grade requirement status updated.
If user grade is above or equal to min-grade then status will be
satisfied. But if student grade is less than and deadline is passed then
user will be marked as failed.
"""
VALID_DUE_DATE = datetime.now(pytz.UTC) + timedelta(days=20)
EXPIRED_DUE_DATE = datetime.now(pytz.UTC) - timedelta(days=20)
def setUp(self):
super(TestMinGradedRequirementStatus, self).setUp()
self.course = CourseFactory.create(
org='Robot', number='999', display_name='Test Course'
)
self.user = UserFactory()
self.request = RequestFactory().get('/')
self.request.user = self.user
self.client.login(username=self.user.username, password=self.user.password)
# Enable the course for credit
credit_course = CreditCourse.objects.create(
course_key=self.course.id,
enabled=True,
)
# Configure a credit provider for the course
credit_provider = CreditProvider.objects.create(
provider_id="ASU",
enable_integration=True,
provider_url="https://credit.example.com/request",
)
credit_course.providers.add(credit_provider)
credit_course.save()
requirements = [{
"namespace": "grade",
"name": "grade",
"display_name": "Grade",
"criteria": {"min_grade": 0.52}
}]
# Add a single credit requirement (final grade)
set_credit_requirements(self.course.id, requirements)
@ddt.data(
(0.6, VALID_DUE_DATE),
(0.52, VALID_DUE_DATE),
(0.70, EXPIRED_DUE_DATE),
)
@ddt.unpack
def test_min_grade_requirement_with_valid_grade(self, grade_achieved, due_date):
"""Test with valid grades. Deadline date does not effect in case
of valid grade.
"""
listen_for_grade_calculation(None, self.user.username, {'percent': grade_achieved}, 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"], 'satisfied')
@ddt.data(
(0.50, None),
(0.51, None),
(0.40, VALID_DUE_DATE),
)
@ddt.unpack
def test_min_grade_requirement_failed_grade_valid_deadline(self, grade_achieved, due_date):
"""Test with failed grades and deadline is still open or not defined."""
listen_for_grade_calculation(None, self.user.username, {'percent': grade_achieved}, 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"], None)
def test_min_grade_requirement_failed_grade_expired_deadline(self):
"""Test with failed grades and deadline expire"""
listen_for_grade_calculation(None, self.user.username, {'percent': 0.22}, self.course.id, self.EXPIRED_DUE_DATE)
req_status = get_credit_requirement_status(self.course.id, self.request.user.username, 'grade', 'grade')
self.assertEqual(req_status[0]["status"], 'failed')
"""
This module contains all signals.
"""
from django.dispatch import Signal
# Signal that fires when a user is graded (in lms/courseware/grades.py)
GRADES_UPDATED = Signal(providing_args=["username", "grade_summary", "course_key", "deadline"])
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