syncing certificates on course update on credential side.

This commit is contained in:
SaadYousaf
2020-05-12 05:38:11 +05:00
parent 4fc7dbd35e
commit 52cfe647b3
8 changed files with 190 additions and 5 deletions

View File

@@ -1403,6 +1403,9 @@ INSTALLED_APPS = [
# Catalog integration
'openedx.core.djangoapps.catalog',
# Programs support
'openedx.core.djangoapps.programs.apps.ProgramsConfig',
# django-oauth-toolkit
'oauth2_provider',

View File

@@ -0,0 +1,39 @@
"""
This module contains various configuration settings via
waffle switches for the course_details view.
"""
from openedx.core.djangoapps.waffle_utils import (
CourseWaffleFlag,
WaffleFlagNamespace,
WaffleSwitchNamespace
)
COURSE_DETAIL_WAFFLE_NAMESPACE = 'course_detail'
COURSE_DETAIL_WAFFLE_FLAG_NAMESPACE = WaffleFlagNamespace(name=COURSE_DETAIL_WAFFLE_NAMESPACE)
WAFFLE_SWITCHES = WaffleSwitchNamespace(name=COURSE_DETAIL_WAFFLE_NAMESPACE)
# Course Override Flag
COURSE_DETAIL_UPDATE_CERTIFICATE_DATE = u'course_detail_update_certificate_date'
def waffle_flags():
"""
Returns the namespaced, cached, audited Waffle flags dictionary for course detail.
"""
return {
COURSE_DETAIL_UPDATE_CERTIFICATE_DATE: CourseWaffleFlag(
waffle_namespace=COURSE_DETAIL_WAFFLE_NAMESPACE,
flag_name=COURSE_DETAIL_UPDATE_CERTIFICATE_DATE,
flag_undefined_default=False,
)
}
def enable_course_detail_update_certificate_date(course_id):
"""
Returns True if course_detail_update_certificate_date course override flag is enabled,
otherwise False.
"""
return waffle_flags()[COURSE_DETAIL_UPDATE_CERTIFICATE_DATE].is_enabled(course_id)

View File

@@ -6,8 +6,11 @@ CourseDetails
import logging
import re
import six
from django.conf import settings
from openedx.core.djangoapps.models.config.waffle import enable_course_detail_update_certificate_date
from openedx.core.djangoapps.signals.signals import COURSE_CERT_DATE_CHANGE
from openedx.core.djangolib.markup import HTML
from openedx.core.lib.courses import course_image_url
from xmodule.fields import Date
@@ -243,6 +246,8 @@ class CourseDetails(object):
if converted != descriptor.certificate_available_date:
dirty = True
descriptor.certificate_available_date = converted
if enable_course_detail_update_certificate_date(course_key):
COURSE_CERT_DATE_CHANGE.send_robust(sender=cls, course_key=six.text_type(course_key))
if 'course_image_name' in jsondict and jsondict['course_image_name'] != descriptor.course_image:
descriptor.course_image = jsondict['course_image_name']

View File

@@ -6,8 +6,12 @@ This module contains signals / handlers related to programs.
import logging
from django.dispatch import receiver
from openedx.core.djangoapps.signals.signals import COURSE_CERT_AWARDED, COURSE_CERT_CHANGED, COURSE_CERT_REVOKED
from openedx.core.djangoapps.signals.signals import (
COURSE_CERT_AWARDED,
COURSE_CERT_CHANGED,
COURSE_CERT_DATE_CHANGE,
COURSE_CERT_REVOKED
)
from openedx.core.djangoapps.site_configuration import helpers
LOGGER = logging.getLogger(__name__)
@@ -174,3 +178,36 @@ def handle_course_cert_revoked(sender, user, course_key, mode, status, **kwargs)
# import here, because signal is registered at startup, but items in tasks are not yet able to be loaded
from openedx.core.djangoapps.programs.tasks.v1.tasks import revoke_program_certificates
revoke_program_certificates.delay(user.username, course_key)
@receiver(COURSE_CERT_DATE_CHANGE, dispatch_uid='course_certificate_date_change_handler')
def handle_course_cert_date_change(sender, course_key, **kwargs):
"""
If course is updated and the certificate_available_date is changed,
schedule a celery task to update visible_date for all certificates
within course.
Args:
course_key:
refers to the course whose certificate_available_date was updated.
Returns:
None
"""
# Import here instead of top of file since this module gets imported before
# the credentials app is loaded, resulting in a Django deprecation warning.
from openedx.core.djangoapps.credentials.models import CredentialsApiConfig
# Avoid scheduling new tasks if certification is disabled.
if not CredentialsApiConfig.current().is_learner_issuance_enabled:
return
# schedule background task to process
LOGGER.info(
'handling COURSE_CERT_DATE_CHANGE for course %s',
course_key,
)
# import here, because signal is registered at startup, but items in tasks are not yet loaded
from openedx.core.djangoapps.programs.tasks.v1.tasks import update_certificate_visible_date_on_course_update
update_certificate_visible_date_on_course_update.delay(course_key)

View File

@@ -520,3 +520,41 @@ def revoke_program_certificates(self, username, course_key):
LOGGER.info(u'There is no program certificates for user %s to revoke', username)
LOGGER.info(u'Successfully completed the task revoke_program_certificates for username %s', username)
@task(bind=True, ignore_result=True, routing_key=PROGRAM_CERTIFICATES_ROUTING_KEY)
def update_certificate_visible_date_on_course_update(self, course_key):
"""
This task is designed to be called whenever a course is updated with
certificate_available_date so that visible_date is updated on credential
service as well.
It will get all users within the course that have a certificate and call
the credentials API to update all these certificates visible_date value
to keep certificates in sync on both sides.
Args:
course_key (str): The course identifier
Returns:
None
"""
countdown = 2 ** self.request.retries
# If the credentials config model is disabled for this
# feature, it may indicate a condition where processing of such tasks
# has been temporarily disabled. Since this is a recoverable situation,
# mark this task for retry instead of failing it altogether.
if not CredentialsApiConfig.current().is_learner_issuance_enabled:
LOGGER.info(
'Task update_certificate_visible_date_on_course_update cannot be executed when credentials issuance is '
'disabled in API config',
)
raise self.retry(countdown=countdown, max_retries=MAX_RETRIES)
users_with_certificates_in_course = GeneratedCertificate.eligible_available_certificates.filter(
course_id=course_key).values_list('user__username', flat=True)
for user in users_with_certificates_in_course:
award_course_certificate.delay(user, str(course_key))

View File

@@ -6,12 +6,17 @@ This module contains tests for programs-related signals and signal handlers.
import mock
from django.test import TestCase
from opaque_keys.edx.keys import CourseKey
from openedx.core.djangoapps.programs.signals import (
handle_course_cert_awarded, handle_course_cert_changed, handle_course_cert_revoked
handle_course_cert_awarded,
handle_course_cert_changed,
handle_course_cert_date_change,
handle_course_cert_revoked
)
from openedx.core.djangoapps.signals.signals import (
COURSE_CERT_AWARDED, COURSE_CERT_CHANGED, COURSE_CERT_REVOKED
COURSE_CERT_AWARDED,
COURSE_CERT_CHANGED,
COURSE_CERT_DATE_CHANGE,
COURSE_CERT_REVOKED
)
from openedx.core.djangoapps.site_configuration.tests.factories import SiteConfigurationFactory
from openedx.core.djangolib.testing.utils import skip_unless_lms
@@ -224,3 +229,59 @@ class CertRevokedReceiverTest(TestCase):
self.assertEqual(mock_is_learner_issuance_enabled.call_count, 1)
self.assertEqual(mock_task.call_count, 1)
self.assertEqual(mock_task.call_args[0], (TEST_USERNAME, TEST_COURSE_KEY))
@skip_unless_lms
@mock.patch('openedx.core.djangoapps.programs.tasks.v1.tasks.update_certificate_visible_date_on_course_update.delay')
@mock.patch(
'openedx.core.djangoapps.credentials.models.CredentialsApiConfig.is_learner_issuance_enabled',
new_callable=mock.PropertyMock,
return_value=False,
)
class CourseCertAvailableDateChangedReceiverTest(TestCase):
"""
Tests for the `handle_course_cert_date_change` signal handler function.
"""
@property
def signal_kwargs(self):
"""
DRY helper.
"""
return {
'sender': self.__class__,
'course_key': TEST_COURSE_KEY,
}
def test_signal_received(self, mock_is_learner_issuance_enabled, mock_task): # pylint: disable=unused-argument
"""
Ensures the receiver function is invoked when COURSE_CERT_DATE_CHANGE is
sent.
Suboptimal: because we cannot mock the receiver function itself (due
to the way django signals work), we mock a configuration call that is
known to take place inside the function.
"""
COURSE_CERT_DATE_CHANGE.send(**self.signal_kwargs)
self.assertEqual(mock_is_learner_issuance_enabled.call_count, 1)
def test_programs_disabled(self, mock_is_learner_issuance_enabled, mock_task):
"""
Ensures that the receiver function does nothing when the credentials API
configuration is not enabled.
"""
handle_course_cert_date_change(**self.signal_kwargs)
self.assertEqual(mock_is_learner_issuance_enabled.call_count, 1)
self.assertEqual(mock_task.call_count, 0)
def test_programs_enabled(self, mock_is_learner_issuance_enabled, mock_task):
"""
Ensures that the receiver function invokes the expected celery task
when the credentials API configuration is enabled.
"""
mock_is_learner_issuance_enabled.return_value = True
handle_course_cert_date_change(**self.signal_kwargs)
self.assertEqual(mock_is_learner_issuance_enabled.call_count, 1)
self.assertEqual(mock_task.call_count, 1)

View File

@@ -14,6 +14,8 @@ COURSE_GRADE_CHANGED = Signal(providing_args=["user", "course_grade", "course_ke
COURSE_CERT_CHANGED = Signal(providing_args=["user", "course_key", "mode", "status"])
COURSE_CERT_AWARDED = Signal(providing_args=["user", "course_key", "mode", "status"])
COURSE_CERT_REVOKED = Signal(providing_args=["user", "course_key", "mode", "status"])
COURSE_CERT_DATE_CHANGE = Signal(providing_args=["course_key"])
# Signal that indicates that a user has passed a course.
COURSE_GRADE_NOW_PASSED = Signal(