Merge pull request #8606 from edx/awais786/ECOM-1597-min-grade-update-with-signals
Awais786/ecom 1597 min grade update with signals
This commit is contained in:
@@ -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={}
|
||||
)
|
||||
|
||||
100
openedx/core/djangoapps/credit/tests/test_signals.py
Normal file
100
openedx/core/djangoapps/credit/tests/test_signals.py
Normal file
@@ -0,0 +1,100 @@
|
||||
"""
|
||||
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')
|
||||
0
openedx/core/djangoapps/signals/__init__.py
Normal file
0
openedx/core/djangoapps/signals/__init__.py
Normal file
9
openedx/core/djangoapps/signals/signals.py
Normal file
9
openedx/core/djangoapps/signals/signals.py
Normal file
@@ -0,0 +1,9 @@
|
||||
"""
|
||||
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"])
|
||||
Reference in New Issue
Block a user