diff --git a/openedx/core/djangoapps/content/course_overviews/signals.py b/openedx/core/djangoapps/content/course_overviews/signals.py index 474391c4ebc2189a29b5b214855d0558b9416bd5..65d9e617b1981d859a229b7c5c1cb4b5ebc10b7c 100644 --- a/openedx/core/djangoapps/content/course_overviews/signals.py +++ b/openedx/core/djangoapps/content/course_overviews/signals.py @@ -8,6 +8,7 @@ import logging from django.dispatch import Signal from django.dispatch.dispatcher import receiver +from openedx.core.djangoapps.signals.signals import COURSE_CERT_DATE_CHANGE from xmodule.modulestore.django import SignalHandler from .models import CourseOverview @@ -50,6 +51,7 @@ def _check_for_course_changes(previous_course_overview, updated_course_overview) if previous_course_overview: _check_for_course_date_changes(previous_course_overview, updated_course_overview) _check_for_pacing_changes(previous_course_overview, updated_course_overview) + _check_for_cert_availability_date_changes(previous_course_overview, updated_course_overview) def _check_for_course_date_changes(previous_course_overview, updated_course_overview): @@ -83,3 +85,8 @@ def _check_for_pacing_changes(previous_course_overview, updated_course_overview) updated_course_overview=updated_course_overview, previous_self_paced=previous_course_overview.self_paced, ) + + +def _check_for_cert_availability_date_changes(previous_course_overview, updated_course_overview): + if previous_course_overview.certificate_available_date != updated_course_overview.certificate_available_date: + COURSE_CERT_DATE_CHANGE.send_robust(sender=None, course_key=updated_course_overview.id) diff --git a/openedx/core/djangoapps/content/course_overviews/tests/test_signals.py b/openedx/core/djangoapps/content/course_overviews/tests/test_signals.py index a83a85dcb0927b8e4e30334aa22264aff938c133..ee1696b1b42f908ff935634ddb5371d9e759a502 100644 --- a/openedx/core/djangoapps/content/course_overviews/tests/test_signals.py +++ b/openedx/core/djangoapps/content/course_overviews/tests/test_signals.py @@ -90,3 +90,7 @@ class CourseOverviewSignalsTestCase(ModuleStoreTestCase): @patch('openedx.core.djangoapps.content.course_overviews.signals.COURSE_PACING_CHANGED.send') def test_pacing_changed(self, mock_signal): self.assert_changed_signal_sent('self_paced', True, False, mock_signal) + + @patch('openedx.core.djangoapps.content.course_overviews.signals.COURSE_CERT_DATE_CHANGE.send_robust') + def test_cert_date_changed(self, mock_signal): + self.assert_changed_signal_sent('certificate_available_date', self.TODAY, self.NEXT_WEEK, mock_signal) diff --git a/openedx/core/djangoapps/courseware_api/views.py b/openedx/core/djangoapps/courseware_api/views.py index ed7fff4a30cce313df9919eaa2123954001d97a9..0594840b9459a65edc46acc2e0dea59fb9961257 100644 --- a/openedx/core/djangoapps/courseware_api/views.py +++ b/openedx/core/djangoapps/courseware_api/views.py @@ -6,7 +6,6 @@ import json from completion.exceptions import UnavailableCompletionData from completion.utilities import get_key_to_last_completed_block -from django.conf import settings from django.urls import reverse from django.utils.translation import ugettext as _ from edx_rest_framework_extensions.auth.jwt.authentication import JwtAuthentication diff --git a/openedx/core/djangoapps/models/course_details.py b/openedx/core/djangoapps/models/course_details.py index 1aa0713263745ba47301cc607d8dc700fe1ef5cc..0d6fd3d543ae7d70029c60912feb300886450dff 100644 --- a/openedx/core/djangoapps/models/course_details.py +++ b/openedx/core/djangoapps/models/course_details.py @@ -6,11 +6,8 @@ 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 @@ -246,8 +243,6 @@ 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'] diff --git a/openedx/core/djangoapps/programs/signals.py b/openedx/core/djangoapps/programs/signals.py index a5f566c79bed47150a19a76505f77fe450470587..48d804b93da3c107f754c35a2ee3c2e897a0daef 100644 --- a/openedx/core/djangoapps/programs/signals.py +++ b/openedx/core/djangoapps/programs/signals.py @@ -8,6 +8,7 @@ import logging from django.dispatch import receiver from openedx.core.djangoapps.credentials.helpers import is_learner_records_enabled_for_org +from openedx.core.djangoapps.models.config.waffle import enable_course_detail_update_certificate_date from openedx.core.djangoapps.signals.signals import ( COURSE_CERT_AWARDED, COURSE_CERT_CHANGED, @@ -82,8 +83,7 @@ def handle_course_cert_changed(sender, user, course_key, mode, status, **kwargs) 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 @@ -196,6 +196,10 @@ def handle_course_cert_date_change(sender, course_key, **kwargs): # lint-amnest None """ + # Stop if cert date updating isn't in effect for the course + if not enable_course_detail_update_certificate_date(course_key): + return + # 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 @@ -209,6 +213,7 @@ def handle_course_cert_date_change(sender, course_key, **kwargs): # lint-amnest '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 import update_certificate_visible_date_on_course_update - update_certificate_visible_date_on_course_update.delay(course_key) + update_certificate_visible_date_on_course_update.delay(str(course_key)) diff --git a/openedx/core/djangoapps/programs/tasks.py b/openedx/core/djangoapps/programs/tasks.py index 8183dc469ad77908057506033bc1d55037a30255..9af43dbdde7b37e3fce7c30caf31c4a2e59c0a76 100644 --- a/openedx/core/djangoapps/programs/tasks.py +++ b/openedx/core/djangoapps/programs/tasks.py @@ -606,7 +606,6 @@ def update_certificate_visible_date_on_course_update(self, course_key): # 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: error_msg = ( "Task update_certificate_visible_date_on_course_update cannot be executed when credentials issuance is " @@ -622,5 +621,9 @@ def update_certificate_visible_date_on_course_update(self, course_key): course_id=course_key ).values_list('user__username', flat=True) + LOGGER.info( + "Task update_certificate_visible_date_on_course_update resending course certificates" + f"for {len(users_with_certificates_in_course)} users in course {course_key}." + ) for user in users_with_certificates_in_course: award_course_certificate.delay(user, str(course_key)) diff --git a/openedx/core/djangoapps/programs/tests/test_signals.py b/openedx/core/djangoapps/programs/tests/test_signals.py index 07e73792699dd9ef8f77c5acfffe162a43b13f4f..506dedfd52ea75a1ff7ff24636ffd99cb96ed082 100644 --- a/openedx/core/djangoapps/programs/tests/test_signals.py +++ b/openedx/core/djangoapps/programs/tests/test_signals.py @@ -238,6 +238,7 @@ class CertRevokedReceiverTest(TestCase): new_callable=mock.PropertyMock, return_value=False, ) +@mock.patch("openedx.core.djangoapps.programs.signals.enable_course_detail_update_certificate_date") class CourseCertAvailableDateChangedReceiverTest(TestCase): """ Tests for the `handle_course_cert_date_change` signal handler function. @@ -253,7 +254,7 @@ class CourseCertAvailableDateChangedReceiverTest(TestCase): 'course_key': TEST_COURSE_KEY, } - def test_signal_received(self, mock_is_learner_issuance_enabled, mock_task): # pylint: disable=unused-argument + def test_signal_received(self, mock_enable_update, mock_is_learner_issuance_enabled, mock_task): # pylint: disable=unused-argument """ Ensures the receiver function is invoked when COURSE_CERT_DATE_CHANGE is sent. @@ -262,23 +263,26 @@ class CourseCertAvailableDateChangedReceiverTest(TestCase): to the way django signals work), we mock a configuration call that is known to take place inside the function. """ + mock_enable_update.return_value = True COURSE_CERT_DATE_CHANGE.send(**self.signal_kwargs) assert mock_is_learner_issuance_enabled.call_count == 1 - def test_programs_disabled(self, mock_is_learner_issuance_enabled, mock_task): + def test_programs_disabled(self, mock_enable_update, mock_is_learner_issuance_enabled, mock_task): """ Ensures that the receiver function does nothing when the credentials API configuration is not enabled. """ + mock_enable_update.return_value = True handle_course_cert_date_change(**self.signal_kwargs) assert mock_is_learner_issuance_enabled.call_count == 1 assert mock_task.call_count == 0 - def test_programs_enabled(self, mock_is_learner_issuance_enabled, mock_task): + def test_programs_enabled(self, mock_enable_update, 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_enable_update.return_value = True mock_is_learner_issuance_enabled.return_value = True handle_course_cert_date_change(**self.signal_kwargs)