Commit 8543a750 by Gregory Martin Committed by GitHub

Merge pull request #15411 from edx/yro/fire-certs-on-pass

Enqueue Generate Certs Task on Passing Grade
parents 1d8d44ee b9425c5b
...@@ -9,10 +9,14 @@ from django.dispatch import receiver ...@@ -9,10 +9,14 @@ from django.dispatch import receiver
from opaque_keys.edx.keys import CourseKey from opaque_keys.edx.keys import CourseKey
from .config import waffle from .config import waffle
from certificates.models import CertificateGenerationCourseSetting, CertificateWhitelist from certificates.models import \
CertificateGenerationCourseSetting, \
CertificateWhitelist, \
GeneratedCertificate
from certificates.tasks import generate_certificate from certificates.tasks import generate_certificate
from courseware import courses from courseware import courses
from openedx.core.djangoapps.models.course_details import COURSE_PACING_CHANGE from openedx.core.djangoapps.models.course_details import COURSE_PACING_CHANGE
from openedx.core.djangoapps.signals.signals import COURSE_GRADE_NOW_PASSED
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
...@@ -64,3 +68,37 @@ def toggle_self_generated_certs(course_key, course_self_paced): ...@@ -64,3 +68,37 @@ def toggle_self_generated_certs(course_key, course_self_paced):
""" """
course_key = CourseKey.from_string(course_key) course_key = CourseKey.from_string(course_key)
CertificateGenerationCourseSetting.set_enabled_for_course(course_key, course_self_paced) CertificateGenerationCourseSetting.set_enabled_for_course(course_key, course_self_paced)
@receiver(COURSE_GRADE_NOW_PASSED, dispatch_uid="new_passing_learner")
def _listen_for_passing_grade(sender, user, course_id, **kwargs): # pylint: disable=unused-argument
"""
Listen for a learner passing a course, send cert generation task,
downstream signal from COURSE_GRADE_CHANGED
"""
# No flags enabled
if (
not waffle.waffle().is_enabled(waffle.SELF_PACED_ONLY) and
not waffle.waffle().is_enabled(waffle.INSTRUCTOR_PACED_ONLY)
):
return
# Only SELF_PACED_ONLY flag enabled
if waffle.waffle().is_enabled(waffle.SELF_PACED_ONLY):
if not courses.get_course_by_id(course_key, depth=0).self_paced:
return
# Only INSTRUCTOR_PACED_ONLY flag enabled
elif waffle.waffle().is_enabled(waffle.INSTRUCTOR_PACED_ONLY):
if courses.get_course_by_id(course_key, depth=0).self_paced:
return
if GeneratedCertificate.certificate_for_student(self.user, self.course_id) is None:
generate_certificate.apply_async(
student=user,
course_key=course_id,
)
log.info(u'Certificate generation task initiated for {user} : {course} via passing grade'.format(
user=user.id,
course=course_id
))
...@@ -8,8 +8,10 @@ from certificates import api as certs_api ...@@ -8,8 +8,10 @@ from certificates import api as certs_api
from certificates.config import waffle from certificates.config import waffle
from certificates.models import CertificateGenerationConfiguration, CertificateWhitelist from certificates.models import CertificateGenerationConfiguration, CertificateWhitelist
from certificates.signals import _listen_for_course_pacing_changed from certificates.signals import _listen_for_course_pacing_changed
from lms.djangoapps.grades.new.course_grade_factory import CourseGradeFactory
from lms.djangoapps.grades.tests.utils import mock_get_score
from openedx.core.djangoapps.self_paced.models import SelfPacedConfiguration from openedx.core.djangoapps.self_paced.models import SelfPacedConfiguration
from student.tests.factories import UserFactory from student.tests.factories import CourseEnrollmentFactory, UserFactory
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory from xmodule.modulestore.tests.factories import CourseFactory
...@@ -56,9 +58,10 @@ class WhitelistGeneratedCertificatesTest(ModuleStoreTestCase): ...@@ -56,9 +58,10 @@ class WhitelistGeneratedCertificatesTest(ModuleStoreTestCase):
self.user = UserFactory.create() self.user = UserFactory.create()
self.ip_course = CourseFactory.create(self_paced=False) self.ip_course = CourseFactory.create(self_paced=False)
def test_cert_generation_on_whitelist_append(self): def test_cert_generation_on_whitelist_append_self_paced(self):
""" """
Verify that signal is sent, received, and fires task based on various flag configs Verify that signal is sent, received, and fires task
based on 'SELF_PACED_ONLY' flag
""" """
with mock.patch( with mock.patch(
'lms.djangoapps.certificates.signals.generate_certificate.apply_async', 'lms.djangoapps.certificates.signals.generate_certificate.apply_async',
...@@ -82,6 +85,16 @@ class WhitelistGeneratedCertificatesTest(ModuleStoreTestCase): ...@@ -82,6 +85,16 @@ class WhitelistGeneratedCertificatesTest(ModuleStoreTestCase):
student=self.user, student=self.user,
course_key=self.course.id, course_key=self.course.id,
) )
def test_cert_generation_on_whitelist_append_instructor_paced(self):
"""
Verify that signal is sent, received, and fires task
based on 'INSTRUCTOR_PACED_ONLY' flag
"""
with mock.patch(
'lms.djangoapps.certificates.signals.generate_certificate.apply_async',
return_value=None
) as mock_generate_certificate_apply_async:
with waffle.waffle().override(waffle.INSTRUCTOR_PACED_ONLY, active=False): with waffle.waffle().override(waffle.INSTRUCTOR_PACED_ONLY, active=False):
CertificateWhitelist.objects.create( CertificateWhitelist.objects.create(
user=self.user, user=self.user,
...@@ -100,3 +113,74 @@ class WhitelistGeneratedCertificatesTest(ModuleStoreTestCase): ...@@ -100,3 +113,74 @@ class WhitelistGeneratedCertificatesTest(ModuleStoreTestCase):
student=self.user, student=self.user,
course_key=self.ip_course.id course_key=self.ip_course.id
) )
class PassingGradeCertsTest(ModuleStoreTestCase):
"""
Tests for certificate generation task firing on passing grade receipt
"""
def setUp(self):
super(PassingGradeCertsTest, self).setUp()
self.course = CourseFactory.create(self_paced=True)
self.user = UserFactory.create()
self.enrollment = CourseEnrollmentFactory(
user=self.user,
course_id=self.course.id,
is_active=True,
mode="verified",
)
self.ip_course = CourseFactory.create(self_paced=False)
def test_cert_generation_on_passing_self_paced(self):
with mock.patch(
'lms.djangoapps.certificates.signals.generate_certificate.apply_async',
return_value=None
) as mock_generate_certificate_apply_async:
with waffle.waffle().override(waffle.SELF_PACED_ONLY, active=True):
grade_factory = CourseGradeFactory()
with mock_get_score(0, 2):
grade_factory.update(self.user, self.course)
mock_generate_certificate_apply_async.assert_not_called(
student=self.user,
course_key=self.course.id
)
with mock_get_score(1, 2):
grade_factory.update(self.user, self.course)
mock_generate_certificate_apply_async.assert_called(
student=self.user,
course_key=self.course.id
)
# Certs are not re-fired after passing
with mock_get_score(2, 2):
grade_factory.update(self.user, self.course)
mock_generate_certificate_apply_async.assert_not_called(
student=self.user,
course_key=self.course.id
)
def test_cert_generation_on_passing_instructor_paced(self):
with mock.patch(
'lms.djangoapps.certificates.signals.generate_certificate.apply_async',
return_value=None
) as mock_generate_certificate_apply_async:
with waffle.waffle().override(waffle.INSTRUCTOR_PACED_ONLY, active=True):
grade_factory = CourseGradeFactory()
with mock_get_score(0, 2):
grade_factory.update(self.user, self.ip_course)
mock_generate_certificate_apply_async.assert_not_called(
student=self.user,
course_key=self.ip_course.id
)
with mock_get_score(1, 2):
grade_factory.update(self.user, self.ip_course)
mock_generate_certificate_apply_async.assert_called(
student=self.user,
course_key=self.ip_course.id
)
# Certs are not re-fired after passing
with mock_get_score(2, 2):
grade_factory.update(self.user, self.ip_course)
mock_generate_certificate_apply_async.assert_not_called(
student=self.user,
course_key=self.ip_course.id
)
...@@ -3,7 +3,8 @@ from contextlib import contextmanager ...@@ -3,7 +3,8 @@ from contextlib import contextmanager
from logging import getLogger from logging import getLogger
import dogstats_wrapper as dog_stats_api import dogstats_wrapper as dog_stats_api
from openedx.core.djangoapps.signals.signals import COURSE_GRADE_CHANGED
from openedx.core.djangoapps.signals.signals import COURSE_GRADE_CHANGED, COURSE_GRADE_NOW_PASSED
from ..config import assume_zero_if_absent, should_persist_grades from ..config import assume_zero_if_absent, should_persist_grades
from ..config.waffle import WRITE_ONLY_IF_ENGAGED, waffle from ..config.waffle import WRITE_ONLY_IF_ENGAGED, waffle
...@@ -167,7 +168,8 @@ class CourseGradeFactory(object): ...@@ -167,7 +168,8 @@ class CourseGradeFactory(object):
""" """
Computes, saves, and returns a CourseGrade object for the Computes, saves, and returns a CourseGrade object for the
given user and course. given user and course.
Sends a COURSE_GRADE_CHANGED signal to listeners. Sends a COURSE_GRADE_CHANGED signal to listeners and a
COURSE_GRADE_NOW_PASSED if learner has passed course.
""" """
course_grade = CourseGrade(user, course_data) course_grade = CourseGrade(user, course_data)
course_grade.update() course_grade.update()
...@@ -197,6 +199,12 @@ class CourseGradeFactory(object): ...@@ -197,6 +199,12 @@ class CourseGradeFactory(object):
course_key=course_data.course_key, course_key=course_data.course_key,
deadline=course_data.course.end, deadline=course_data.course.end,
) )
if course_grade.passed is True:
COURSE_GRADE_NOW_PASSED.send_robust(
sender=CourseGradeFactory,
user=user,
course_key=course_data.course_key,
)
log.info( log.info(
u'Grades: Update, %s, User: %s, %s, persisted: %s', u'Grades: Update, %s, User: %s, %s, persisted: %s',
......
...@@ -11,3 +11,11 @@ COURSE_GRADE_CHANGED = Signal(providing_args=["user", "course_grade", "course_ke ...@@ -11,3 +11,11 @@ COURSE_GRADE_CHANGED = Signal(providing_args=["user", "course_grade", "course_ke
# TODO: runtime coupling between apps will be reduced if this event is changed to carry a username # TODO: runtime coupling between apps will be reduced if this event is changed to carry a username
# rather than a User object; however, this will require changes to the milestones and badges APIs # rather than a User object; however, this will require changes to the milestones and badges APIs
COURSE_CERT_AWARDED = Signal(providing_args=["user", "course_key", "mode", "status"]) COURSE_CERT_AWARDED = Signal(providing_args=["user", "course_key", "mode", "status"])
# Signal that indicates that a user has passed a course.
COURSE_GRADE_NOW_PASSED = Signal(
providing_args=[
'user', # user object
'course_key', # course.id
]
)
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