diff --git a/lms/djangoapps/certificates/models.py b/lms/djangoapps/certificates/models.py index e76849f35a..9044ce3bd3 100644 --- a/lms/djangoapps/certificates/models.py +++ b/lms/djangoapps/certificates/models.py @@ -1239,3 +1239,31 @@ class CertificateDateOverride(TimeStampedModel): def __str__(self): return "Certificate %s, date overridden to %s by %s on %s." % \ (self.generated_certificate, self.date, self.overridden_by, self.created) + + def save(self, *args, **kwargs): # pylint: disable=signature-differs + """ + After the base save() method finishes, fire the COURSE_CERT_CHANGED + signal. + """ + super().save(*args, **kwargs) + COURSE_CERT_CHANGED.send_robust( + sender=self.__class__, + user=self.generated_certificate.user, + course_key=self.generated_certificate.course_id, + mode=self.generated_certificate.mode, + status=self.generated_certificate.status, + ) + + def delete(self, *args, **kwargs): # pylint: disable=signature-differs + """ + After the base delete() method finishes, fire the COURSE_CERT_CHANGED + signal. + """ + super().delete(*args, **kwargs) + COURSE_CERT_CHANGED.send_robust( + sender=self.__class__, + user=self.generated_certificate.user, + course_key=self.generated_certificate.course_id, + mode=self.generated_certificate.mode, + status=self.generated_certificate.status, + ) diff --git a/openedx/core/djangoapps/programs/tasks.py b/openedx/core/djangoapps/programs/tasks.py index b757633801..179f660384 100644 --- a/openedx/core/djangoapps/programs/tasks.py +++ b/openedx/core/djangoapps/programs/tasks.py @@ -9,6 +9,7 @@ from celery.utils.log import get_task_logger from django.conf import settings from django.contrib.auth.models import User # lint-amnesty, pylint: disable=imported-auth-user from django.contrib.sites.models import Site +from django.core.exceptions import ObjectDoesNotExist from edx_django_utils.monitoring import set_code_owner_attribute from edx_rest_api_client import exceptions from opaque_keys.edx.keys import CourseKey @@ -293,7 +294,7 @@ def post_course_certificate_configuration(client, cert_config, certificate_avail }) -def post_course_certificate(client, username, certificate, visible_date): +def post_course_certificate(client, username, certificate, visible_date, date_override=None): """ POST a certificate that has been updated to Credentials """ @@ -305,6 +306,7 @@ def post_course_certificate(client, username, certificate, visible_date): 'mode': certificate.mode, 'type': COURSE_CERTIFICATE, }, + 'date_override': date_override.strftime(VISIBLE_DATE_FORMAT) if date_override else None, 'attributes': [ { 'name': 'visible_date', @@ -458,7 +460,19 @@ def award_course_certificate(self, username, course_run_key, certificate_availab "Task award_course_certificate will award certificate for course " f"{course_key} with a visible date of {visible_date}" ) - post_course_certificate(credentials_client, username, certificate, visible_date) + + # If the certificate has an associated CertificateDateOverride, send + # it along + try: + date_override = certificate.date_override.date + LOGGER.info( + "Task award_course_certificate will award certificate for " + f"course {course_key} with a date override of {date_override}" + ) + except ObjectDoesNotExist: + date_override = None + + post_course_certificate(credentials_client, username, certificate, visible_date, date_override) LOGGER.info(f"Awarded certificate for course {course_key} to user {username}") except Exception as exc: diff --git a/openedx/core/djangoapps/programs/tests/test_tasks.py b/openedx/core/djangoapps/programs/tests/test_tasks.py index 70a991c43f..832c547819 100644 --- a/openedx/core/djangoapps/programs/tests/test_tasks.py +++ b/openedx/core/djangoapps/programs/tests/test_tasks.py @@ -20,7 +20,7 @@ from edx_rest_api_client.client import EdxRestApiClient from common.djangoapps.course_modes.tests.factories import CourseModeFactory from common.djangoapps.student.tests.factories import UserFactory -from lms.djangoapps.certificates.tests.factories import GeneratedCertificateFactory +from lms.djangoapps.certificates.tests.factories import CertificateDateOverrideFactory, GeneratedCertificateFactory from openedx.core.djangoapps.catalog.tests.mixins import CatalogIntegrationMixin from openedx.core.djangoapps.content.course_overviews.tests.factories import CourseOverviewFactory from openedx.core.djangoapps.credentials.tests.mixins import CredentialsApiConfigMixin @@ -493,6 +493,7 @@ class PostCourseCertificateTestCase(TestCase): 'mode': self.certificate.mode, 'type': tasks.COURSE_CERTIFICATE, }, + 'date_override': None, 'attributes': [{ 'name': 'visible_date', 'value': visible_date.strftime('%Y-%m-%dT%H:%M:%SZ') # text representation of date @@ -536,6 +537,15 @@ class AwardCourseCertificatesTestCase(CredentialsApiConfigMixin, TestCase): ApplicationFactory.create(name='credentials') UserFactory.create(username=settings.CREDENTIALS_SERVICE_USERNAME) + def _add_certificate_date_override(self): + """ + Creates a mock CertificateDateOverride and adds it to the certificate + """ + self.certificate.date_override = CertificateDateOverrideFactory.create( + generated_certificate=self.certificate, + overridden_by=UserFactory.create(username='test-admin'), + ) + @ddt.data( 'verified', 'no-id-professional', @@ -564,6 +574,18 @@ class AwardCourseCertificatesTestCase(CredentialsApiConfigMixin, TestCase): assert call_args[2] == self.certificate assert call_args[3] == self.available_date + def test_award_course_certificates_override_date(self, mock_post_course_certificate): + """ + Tests the API POST method is called with date override when present + """ + self._add_certificate_date_override() + tasks.award_course_certificate.delay(self.student.username, str(self.course.id)).get() + call_args, _ = mock_post_course_certificate.call_args + assert call_args[1] == self.student.username + assert call_args[2] == self.certificate + assert call_args[3] == self.certificate.modified_date + assert call_args[4] == self.certificate.date_override.date.date() + def test_award_course_cert_not_called_if_disabled(self, mock_post_course_certificate): """ Test that the post method is never called if the config is disabled