Commit 16f06bf6 by Gregory Martin

Generate eligible certificates on learner track change

parent 32618fa0
......@@ -15,8 +15,10 @@ from certificates.models import \
GeneratedCertificate
from certificates.tasks import generate_certificate
from courseware import courses
from lms.djangoapps.grades.new.course_grade_factory import CourseGradeFactory
from openedx.core.djangoapps.models.course_details import COURSE_PACING_CHANGE
from openedx.core.djangoapps.signals.signals import COURSE_GRADE_NOW_PASSED
from openedx.core.djangoapps.signals.signals import COURSE_GRADE_NOW_PASSED, LEARNER_NOW_VERIFIED
from student.models import CourseEnrollment
log = logging.getLogger(__name__)
......@@ -76,7 +78,6 @@ def _listen_for_passing_grade(sender, user, course_id, **kwargs): # pylint: dis
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
......@@ -86,19 +87,55 @@ def _listen_for_passing_grade(sender, user, course_id, **kwargs): # pylint: dis
# 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:
if not courses.get_course_by_id(course_id, 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:
if waffle.waffle().is_enabled(waffle.INSTRUCTOR_PACED_ONLY):
if courses.get_course_by_id(course_id, 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,
)
if fire_ungenerated_certificate_task(
user=user,
course_id=course_id
):
log.info(u'Certificate generation task initiated for {user} : {course} via passing grade'.format(
user=user.id,
course=course_id
))
@receiver(LEARNER_NOW_VERIFIED, dispatch_uid="learner_track_changed")
def _listen_for_track_change(sender, user, **kwargs): # pylint: disable=unused-argument
"""
Catches a track change signal, determines user status,
calls fire_ungenerated_certificate_task for passing grades
"""
if (
not waffle.waffle().is_enabled(waffle.SELF_PACED_ONLY) and
not waffle.waffle().is_enabled(waffle.INSTRUCTOR_PACED_ONLY)
):
return
user_enrollments = CourseEnrollment.enrollments_for_user(user=user)
grade_factory = CourseGradeFactory()
for enrollment in user_enrollments:
if grade_factory.read(user=user, course=enrollment.course).passed:
if fire_ungenerated_certificate_task(
user=user,
course_id=enrollment.course.id
):
log.info(u'Certificate generation task initiated for {user} : {course} via track change'.format(
user=user.id,
course=enrollment.course.id
))
def fire_ungenerated_certificate_task(user, course_id):
"""
Helper function to fire un-generated certificate tasks
"""
if GeneratedCertificate.certificate_for_student(user, course_id) is None:
generate_certificate.apply_async(
student=user,
course_key=course_id
)
return True
......@@ -6,10 +6,15 @@ import mock
from certificates import api as certs_api
from certificates.config import waffle
from certificates.models import CertificateGenerationConfiguration, CertificateWhitelist
from certificates.models import \
CertificateGenerationConfiguration, \
CertificateWhitelist, \
GeneratedCertificate, \
CertificateStatuses
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 lms.djangoapps.grades.tests.utils import mock_passing_grade
from lms.djangoapps.verify_student.models import SoftwareSecurePhotoVerification
from openedx.core.djangoapps.self_paced.models import SelfPacedConfiguration
from student.tests.factories import CourseEnrollmentFactory, UserFactory
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
......@@ -72,10 +77,7 @@ class WhitelistGeneratedCertificatesTest(ModuleStoreTestCase):
user=self.user,
course_id=self.course.id
)
mock_generate_certificate_apply_async.assert_not_called(
student=self.user,
course_key=self.course.id
)
mock_generate_certificate_apply_async.assert_not_called()
with waffle.waffle().override(waffle.SELF_PACED_ONLY, active=True):
CertificateWhitelist.objects.create(
user=self.user,
......@@ -100,10 +102,7 @@ class WhitelistGeneratedCertificatesTest(ModuleStoreTestCase):
user=self.user,
course_id=self.ip_course.id
)
mock_generate_certificate_apply_async.assert_not_called(
student=self.user,
course_key=self.ip_course.id
)
mock_generate_certificate_apply_async.assert_not_called()
with waffle.waffle().override(waffle.INSTRUCTOR_PACED_ONLY, active=True):
CertificateWhitelist.objects.create(
user=self.user,
......@@ -121,7 +120,9 @@ class PassingGradeCertsTest(ModuleStoreTestCase):
"""
def setUp(self):
super(PassingGradeCertsTest, self).setUp()
self.course = CourseFactory.create(self_paced=True)
self.course = CourseFactory.create(
self_paced=True,
)
self.user = UserFactory.create()
self.enrollment = CourseEnrollmentFactory(
user=self.user,
......@@ -130,6 +131,12 @@ class PassingGradeCertsTest(ModuleStoreTestCase):
mode="verified",
)
self.ip_course = CourseFactory.create(self_paced=False)
self.ip_enrollment = CourseEnrollmentFactory(
user=self.user,
course_id=self.ip_course.id,
is_active=True,
mode="verified",
)
def test_cert_generation_on_passing_self_paced(self):
with mock.patch(
......@@ -138,22 +145,13 @@ class PassingGradeCertsTest(ModuleStoreTestCase):
) 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):
# Not passing
grade_factory.update(self.user, self.course)
mock_generate_certificate_apply_async.assert_not_called()
# Certs fired after passing
with mock_passing_grade():
grade_factory.update(self.user, self.course)
mock_generate_certificate_apply_async.assert_not_called(
mock_generate_certificate_apply_async.assert_called_with(
student=self.user,
course_key=self.course.id
)
......@@ -165,22 +163,96 @@ class PassingGradeCertsTest(ModuleStoreTestCase):
) 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):
# Not passing
grade_factory.update(self.user, self.ip_course)
mock_generate_certificate_apply_async.assert_not_called()
# Certs fired after passing
with mock_passing_grade():
grade_factory.update(self.user, self.ip_course)
mock_generate_certificate_apply_async.assert_not_called(
mock_generate_certificate_apply_async.assert_called_with(
student=self.user,
course_key=self.ip_course.id
)
def test_cert_already_generated(self):
with mock.patch(
'lms.djangoapps.certificates.signals.generate_certificate.apply_async',
return_value=None
) as mock_generate_certificate_apply_async:
grade_factory = CourseGradeFactory()
# Create the certificate
GeneratedCertificate.eligible_certificates.create(
user=self.user,
course_id=self.course.id,
status=CertificateStatuses.downloadable
)
# Certs are not re-fired after passing
with mock_passing_grade():
grade_factory.update(self.user, self.course)
mock_generate_certificate_apply_async.assert_not_called()
class LearnerTrackChangeCertsTest(ModuleStoreTestCase):
"""
Tests for certificate generation task firing on learner verification
"""
def setUp(self):
super(LearnerTrackChangeCertsTest, self).setUp()
self.course_one = CourseFactory.create(self_paced=True)
self.user_one = UserFactory.create()
self.enrollment_one = CourseEnrollmentFactory(
user=self.user_one,
course_id=self.course_one.id,
is_active=True,
mode='honor',
)
self.user_two = UserFactory.create()
self.course_two = CourseFactory.create(self_paced=False)
self.enrollment_two = CourseEnrollmentFactory(
user=self.user_two,
course_id=self.course_two.id,
is_active=True,
mode='honor'
)
def test_cert_generation_on_photo_verification_self_paced(self):
with mock.patch(
'lms.djangoapps.certificates.signals.generate_certificate.apply_async',
return_value=None
) as mock_generate_certificate_apply_async:
with mock_passing_grade():
grade_factory = CourseGradeFactory()
grade_factory.update(self.user_one, self.course_one)
with waffle.waffle().override(waffle.SELF_PACED_ONLY, active=True):
mock_generate_certificate_apply_async.assert_not_called()
attempt = SoftwareSecurePhotoVerification.objects.create(
user=self.user_one,
status='submitted'
)
attempt.approve()
mock_generate_certificate_apply_async.assert_called_with(
student=self.user_one,
course_key=self.course_one.id
)
def test_cert_generation_on_photo_verification_instructor_paced(self):
with mock.patch(
'lms.djangoapps.certificates.signals.generate_certificate.apply_async',
return_value=None
) as mock_generate_certificate_apply_async:
with mock_passing_grade():
grade_factory = CourseGradeFactory()
grade_factory.update(self.user_two, self.course_two)
with waffle.waffle().override(waffle.INSTRUCTOR_PACED_ONLY, active=True):
mock_generate_certificate_apply_async.assert_not_called()
attempt = SoftwareSecurePhotoVerification.objects.create(
user=self.user_two,
status='submitted'
)
attempt.approve()
mock_generate_certificate_apply_async.assert_called_with(
student=self.user_two,
course_key=self.course_two.id
)
......@@ -159,6 +159,7 @@ class CourseGradeFactory(object):
persistent_grade.letter_grade,
persistent_grade.passed_timestamp is not None,
)
log.info(u'Grades: Read, %s, User: %s, %s', unicode(course_data), user.id, persistent_grade)
return course_grade, persistent_grade.grading_policy_hash
......@@ -199,11 +200,11 @@ class CourseGradeFactory(object):
course_key=course_data.course_key,
deadline=course_data.course.end,
)
if course_grade.passed is True:
COURSE_GRADE_NOW_PASSED.send_robust(
if course_grade.passed:
COURSE_GRADE_NOW_PASSED.send(
sender=CourseGradeFactory,
user=user,
course_key=course_data.course_key,
course_id=course_data.course_key,
)
log.info(
......
......@@ -186,7 +186,7 @@ class TestCourseGradeFactory(GradeTestBase):
self.assertEqual(course_grade.letter_grade, u'Pass' if expected_pass else None)
self.assertEqual(course_grade.percent, 0.5)
with self.assertNumQueries(11), mock_get_score(1, 2):
with self.assertNumQueries(13), mock_get_score(1, 2):
_assert_create(expected_pass=True)
with self.assertNumQueries(13), mock_get_score(1, 2):
......
......@@ -41,11 +41,13 @@ from lms.djangoapps.verify_student.ssencrypt import (
random_aes_key,
rsa_encrypt
)
from openedx.core.djangoapps.signals.signals import LEARNER_NOW_VERIFIED
from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers
from openedx.core.djangoapps.xmodule_django.models import CourseKeyField
from openedx.core.djangolib.model_mixins import DeprecatedModelMixin
from openedx.core.storage import get_storage
log = logging.getLogger(__name__)
......@@ -516,6 +518,11 @@ class PhotoVerification(StatusModel):
self.reviewing_service = service
self.status = "approved"
self.save()
# Emit signal to find and generate eligible certificates
LEARNER_NOW_VERIFIED.send_robust(
sender=PhotoVerification,
user=self.user
)
@status_before_must_be("must_retry", "submitted", "approved", "denied")
def deny(self,
......
"""
Signal handlers are registered at startup here.
"""
from django.apps import AppConfig
class SignalConfig(AppConfig):
"""
Application Configuration for Signals.
"""
name = u'openedx.core.djangoapps.signals'
def ready(self):
"""
Connect handlers.
"""
# Can't import models at module level in AppConfigs, and models get
# included from the signal handlers
from .signals import handlers # pylint: disable=unused-variable
......@@ -16,6 +16,9 @@ COURSE_CERT_AWARDED = Signal(providing_args=["user", "course_key", "mode", "stat
COURSE_GRADE_NOW_PASSED = Signal(
providing_args=[
'user', # user object
'course_key', # course.id
'course_id', # course.id
]
)
# Signal that indicates that a user has become verified
LEARNER_NOW_VERIFIED = Signal(providing_args=['user'])
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