diff --git a/openedx/core/djangoapps/certificates/api.py b/openedx/core/djangoapps/certificates/api.py index ee01cb2ced..32971aec27 100644 --- a/openedx/core/djangoapps/certificates/api.py +++ b/openedx/core/djangoapps/certificates/api.py @@ -81,9 +81,17 @@ def _course_uses_available_date(course): return can_show_certificate_available_date_field(course) and course.certificate_available_date -def available_date_for_certificate(course, certificate): +def available_date_for_certificate(course, certificate, certificate_available_date=None): + """ + Returns the available date to use with a certificate + + Arguments: + course (CourseOverview): The course we're checking + certificate (GeneratedCertificate): The certificate we're getting the date for + certificate_available_date (datetime): An optional date to override the from the course overview. + """ if _course_uses_available_date(course): - return course.certificate_available_date + return certificate_available_date or course.certificate_available_date return certificate.modified_date diff --git a/openedx/core/djangoapps/content/course_overviews/signals.py b/openedx/core/djangoapps/content/course_overviews/signals.py index 306e4a0002..9282965981 100644 --- a/openedx/core/djangoapps/content/course_overviews/signals.py +++ b/openedx/core/djangoapps/content/course_overviews/signals.py @@ -95,4 +95,8 @@ def _check_for_cert_availability_date_changes(previous_course_overview, updated_ f"{previous_course_overview.certificate_available_date} to " + f"{updated_course_overview.certificate_available_date}. Sending COURSE_CERT_DATE_CHANGE signal." ) - COURSE_CERT_DATE_CHANGE.send_robust(sender=None, course_key=updated_course_overview.id) + COURSE_CERT_DATE_CHANGE.send_robust( + sender=None, + course_key=updated_course_overview.id, + available_date=updated_course_overview.certificate_available_date + ) diff --git a/openedx/core/djangoapps/programs/signals.py b/openedx/core/djangoapps/programs/signals.py index 48d804b93d..d70a8015ed 100644 --- a/openedx/core/djangoapps/programs/signals.py +++ b/openedx/core/djangoapps/programs/signals.py @@ -182,15 +182,15 @@ def handle_course_cert_revoked(sender, user, course_key, mode, status, **kwargs) @receiver(COURSE_CERT_DATE_CHANGE, dispatch_uid='course_certificate_date_change_handler') -def handle_course_cert_date_change(sender, course_key, **kwargs): # lint-amnesty, pylint: disable=unused-argument +def handle_course_cert_date_change(sender, course_key, available_date, **kwargs): # lint-amnesty, pylint: disable=unused-argument """ 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. + course_key (CourseLocator): refers to the course whose certificate_available_date was updated. + available_date (datetime): the date to update the certificate's available date to Returns: None @@ -216,4 +216,4 @@ def handle_course_cert_date_change(sender, course_key, **kwargs): # lint-amnest # 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(str(course_key)) + update_certificate_visible_date_on_course_update.delay(str(course_key), available_date) diff --git a/openedx/core/djangoapps/programs/tasks.py b/openedx/core/djangoapps/programs/tasks.py index b26191f604..5504b6acbd 100644 --- a/openedx/core/djangoapps/programs/tasks.py +++ b/openedx/core/djangoapps/programs/tasks.py @@ -1,7 +1,7 @@ """ This file contains celery tasks for programs-related functionality. """ - +from datetime import datetime from celery import shared_task from celery.exceptions import MaxRetriesExceededError @@ -300,10 +300,17 @@ def post_course_certificate(client, username, certificate, visible_date): @shared_task(bind=True, ignore_result=True) @set_code_owner_attribute -def award_course_certificate(self, username, course_run_key): +def award_course_certificate(self, username, course_run_key, certificate_available_date=None): """ This task is designed to be called whenever a student GeneratedCertificate is updated. It can be called independently for a username and a course_run, but is invoked on each GeneratedCertificate.save. + + Arguments: + username (str): The user to award the Credentials course cert to + course_run_key (str): The course run key to award the certificate for + certificate_available_date (str): A string representation of the datetime for when to make the certificate + available to the user. If not provided, it will calculate the date. + """ def _retry_with_custom_exception(username, course_run_key, reason, countdown): exception = MaxRetriesExceededError( @@ -368,10 +375,19 @@ def award_course_certificate(self, username, course_run_key): username=settings.CREDENTIALS_SERVICE_USERNAME), org=course_key.org, ) - # FIXME This may result in visible dates that do not update alongside the Course Overview if that changes - # This is a known limitation of this implementation and was chosen to reduce the amount of replication, - # endpoints, celery tasks, and jenkins jobs that needed to be written for this functionality - visible_date = available_date_for_certificate(course_overview, certificate) + + # Date is being passed via JSON and is encoded in the EMCA date time string format. The rest of the code + # expects a datetime. + certificate_available_date = datetime.strptime(certificate_available_date, VISIBLE_DATE_FORMAT) + + # Even in the cases where this task is called with a certificate_available_date, we still need to retrieve + # the course overview because it's required to determine if we should use the certificate_available_date or + # the certs modified date + visible_date = available_date_for_certificate( + course_overview, + certificate, + certificate_available_date=certificate_available_date + ) LOGGER.info( "Task award_course_certificate will award certificate for course " f"{course_key} with a visible date of {visible_date}" @@ -588,7 +604,7 @@ def revoke_program_certificates(self, username, course_key): @shared_task(bind=True, ignore_result=True) @set_code_owner_attribute -def update_certificate_visible_date_on_course_update(self, course_key): +def update_certificate_visible_date_on_course_update(self, course_key, certificate_available_date): """ This task is designed to be called whenever a course is updated with certificate_available_date so that visible_date is updated on credential @@ -598,8 +614,10 @@ def update_certificate_visible_date_on_course_update(self, course_key): the credentials API to update all these certificates visible_date value to keep certificates in sync on both sides. - Args: + Arguments: course_key (str): The course identifier + certificate_available_date (str): The date to update the certificate availablity date to. It's a string + representation of a datetime object because task parameters must be JSON-able. Returns: None @@ -626,8 +644,8 @@ def update_certificate_visible_date_on_course_update(self, course_key): ).values_list('user__username', flat=True) LOGGER.info( - "Task update_certificate_visible_date_on_course_update resending course certificates" + "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)) + award_course_certificate.delay(user, str(course_key), certificate_available_date=certificate_available_date) diff --git a/openedx/core/djangoapps/signals/signals.py b/openedx/core/djangoapps/signals/signals.py index 64dae48bcb..5798bf3dcc 100644 --- a/openedx/core/djangoapps/signals/signals.py +++ b/openedx/core/djangoapps/signals/signals.py @@ -14,7 +14,7 @@ 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"]) +COURSE_CERT_DATE_CHANGE = Signal(providing_args=["course_key", "available_date"]) COURSE_ASSESSMENT_GRADE_CHANGED = Signal(