Merge pull request #29792 from raccoongang/dyudyunov/fix-certificate-generation-without-persistent-grades

Fix certificate generation without persistent grades
This commit is contained in:
Maria Grimaldi
2022-04-08 12:02:53 -04:00
committed by GitHub
3 changed files with 70 additions and 30 deletions

View File

@@ -53,7 +53,7 @@ def generate_allowlist_certificate_task(user, course_key, generation_mode=None,
Create a task to generate an allowlist certificate for this user in this course run.
"""
enrollment_mode = _get_enrollment_mode(user, course_key)
course_grade = _get_course_grade(user, course_key)
course_grade = _get_course_grade(user, course_key, send_course_grade_signals=False)
if _can_generate_allowlist_certificate(user, course_key, enrollment_mode):
return _generate_certificate_task(user=user, course_key=course_key, enrollment_mode=enrollment_mode,
course_grade=course_grade, generation_mode=generation_mode,
@@ -72,7 +72,7 @@ def _generate_regular_certificate_task(user, course_key, generation_mode=None, d
eligible and a certificate can be generated.
"""
enrollment_mode = _get_enrollment_mode(user, course_key)
course_grade = _get_course_grade(user, course_key)
course_grade = _get_course_grade(user, course_key, send_course_grade_signals=False)
if _can_generate_regular_certificate(user, course_key, enrollment_mode, course_grade):
return _generate_certificate_task(user=user, course_key=course_key, enrollment_mode=enrollment_mode,
course_grade=course_grade, generation_mode=generation_mode,
@@ -377,11 +377,14 @@ def _get_grade_value(course_grade):
return ''
def _get_course_grade(user, course_key):
def _get_course_grade(user, course_key, send_course_grade_signals=True):
"""
Get the user's course grade in this course run. Note that this may be None.
Use send_course_grade_signals=False to avoid firing the course grade signals recursively.
See details in lms/djangoapps/grades/course_grade_factory.py _update method.
"""
return CourseGradeFactory().read(user, course_key=course_key)
return CourseGradeFactory().read(user, course_key=course_key, send_course_grade_signals=send_course_grade_signals)
def _get_enrollment_mode(user, course_key):

View File

@@ -33,6 +33,7 @@ class CourseGradeFactory:
course_structure=None,
course_key=None,
create_if_needed=True,
send_course_grade_signals=True,
):
"""
Returns the CourseGrade for the given user in the course.
@@ -51,7 +52,7 @@ class CourseGradeFactory:
if assume_zero_if_absent(course_data.course_key):
return self._create_zero(user, course_data)
elif create_if_needed:
return self._update(user, course_data)
return self._update(user, course_data, send_course_grade_signals=send_course_grade_signals)
else:
return None
@@ -160,13 +161,16 @@ class CourseGradeFactory:
)
@staticmethod
def _update(user, course_data, force_update_subsections=False):
def _update(user, course_data, force_update_subsections=False, send_course_grade_signals=True):
"""
Computes, saves, and returns a CourseGrade object for the
given user and course.
Sends a COURSE_GRADE_CHANGED signal to listeners and
COURSE_GRADE_NOW_PASSED if learner has passed course or
COURSE_GRADE_NOW_FAILED if learner is now failing course
Computes, saves, and returns a CourseGrade object for the given user and course.
send_course_grade_signals defines if signals should be sent. Use it to avoid recursion issues in
cases when the signal listener trying to get grades but Persistent Grades are disabled.
If True - sends:
COURSE_GRADE_CHANGED signal to listeners and
COURSE_GRADE_NOW_PASSED if learner has passed course or
COURSE_GRADE_NOW_FAILED if learner is now failing course
"""
should_persist = should_persist_grades(course_data.course_key)
if should_persist and force_update_subsections:
@@ -193,26 +197,27 @@ class CourseGradeFactory:
passed=course_grade.passed,
)
COURSE_GRADE_CHANGED.send_robust(
sender=None,
user=user,
course_grade=course_grade,
course_key=course_data.course_key,
deadline=course_data.course.end,
)
if course_grade.passed:
COURSE_GRADE_NOW_PASSED.send(
sender=CourseGradeFactory,
if send_course_grade_signals:
COURSE_GRADE_CHANGED.send_robust(
sender=None,
user=user,
course_id=course_data.course_key,
)
else:
COURSE_GRADE_NOW_FAILED.send(
sender=CourseGradeFactory,
user=user,
course_id=course_data.course_key,
grade=course_grade,
course_grade=course_grade,
course_key=course_data.course_key,
deadline=course_data.course.end,
)
if course_grade.passed:
COURSE_GRADE_NOW_PASSED.send(
sender=CourseGradeFactory,
user=user,
course_id=course_data.course_key,
)
else:
COURSE_GRADE_NOW_FAILED.send(
sender=CourseGradeFactory,
user=user,
course_id=course_data.course_key,
grade=course_grade,
)
log.info(
'Grades: Update, %s, User: %s, %s, persisted: %s',

View File

@@ -2,16 +2,19 @@
Tests for the CourseGradeFactory class.
"""
import itertools
from unittest.mock import patch
from unittest.mock import patch, Mock
import ddt
from django.conf import settings
from edx_toggles.toggles.testutils import override_waffle_switch
import pytest
from common.djangoapps.student.tests.factories import UserFactory
from lms.djangoapps.certificates.config import AUTO_CERTIFICATE_GENERATION
from lms.djangoapps.courseware.access import has_access
from lms.djangoapps.grades.config.tests.utils import persistent_grades_feature_flags
from openedx.core.djangoapps.content.block_structure.factory import BlockStructureFactory
from openedx.core.djangoapps.signals.signals import COURSE_GRADE_NOW_PASSED
from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase # lint-amnesty, pylint: disable=wrong-import-order
from xmodule.modulestore.tests.factories import CourseFactory # lint-amnesty, pylint: disable=wrong-import-order
@@ -73,6 +76,35 @@ class TestCourseGradeFactory(GradeTestBase):
grade_factory.read(self.request.user, self.course)
assert mock_read_grade.called == (feature_flag and course_setting)
@patch.dict(settings.FEATURES, {'PERSISTENT_GRADES_ENABLED_FOR_ALL_TESTS': False})
def test_no_recursion_without_persistent_grades(self):
"""
Course grade signals should not be fired recursively when persistent grades are disabled.
"""
self.mock_process_signal = Mock() # pylint: disable=attribute-defined-outside-init
def handler(**kwargs):
"""
Mock signal receiver.
"""
self.mock_process_signal()
with persistent_grades_feature_flags(
global_flag=False,
enabled_for_all_courses=False,
course_id=self.course.id,
enabled_for_course=False
):
with override_waffle_switch(AUTO_CERTIFICATE_GENERATION, active=True), mock_get_score(2, 2):
COURSE_GRADE_NOW_PASSED.connect(handler)
try:
CourseGradeFactory().update(self.request.user, self.course)
except RecursionError:
pytest.fail("The COURSE_GRADE_NOW_PASSED signal fired recursively.")
self.mock_process_signal.assert_called_once()
COURSE_GRADE_NOW_PASSED.disconnect(handler)
def test_read_and_update(self):
grade_factory = CourseGradeFactory()