Skip to content
Snippets Groups Projects
Commit 52cfe647 authored by SaadYousaf's avatar SaadYousaf
Browse files

syncing certificates on course update on credential side.

parent 4fc7dbd3
Branches
Tags
No related merge requests found
......@@ -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',
......
"""
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)
......@@ -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']
......
......@@ -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)
......@@ -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))
......@@ -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)
......@@ -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(
......
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