def test_available_vs_display_date( self, feature_enabled, is_self_paced, uses_avail_date ): self.course.self_paced = is_self_paced with configure_waffle_namespace(feature_enabled): # With no available_date set, both return modified_date assert self.certificate.modified_date == available_date_for_certificate(self.course, self.certificate) assert self.certificate.modified_date == display_date_for_certificate(self.course, self.certificate) # With an available date set in the past, both return the available date (if configured) self.course.certificate_available_date = datetime(2017, 2, 1, tzinfo=pytz.UTC) self.course.certificates_display_behavior = CertificatesDisplayBehaviors.END_WITH_DATE maybe_avail = self.course.certificate_available_date if uses_avail_date else self.certificate.modified_date assert maybe_avail == available_date_for_certificate(self.course, self.certificate) assert maybe_avail == display_date_for_certificate(self.course, self.certificate) # With a future available date, they each return a different date self.course.certificate_available_date = datetime.max.replace(tzinfo=pytz.UTC) maybe_avail = self.course.certificate_available_date if uses_avail_date else self.certificate.modified_date assert maybe_avail == available_date_for_certificate(self.course, self.certificate) assert self.certificate.modified_date == display_date_for_certificate(self.course, self.certificate) # With a certificate date override, display date returns the override, available date ignores it self.certificate.date_override = MockCertificateDateOverride() date = self.certificate.date_override.date assert date == display_date_for_certificate(self.course, self.certificate) assert maybe_avail == available_date_for_certificate(self.course, self.certificate)
def _available_date_for_program(self, program_data, certificates): """ Calculate the available date for the program based on the courses within it. Arguments: program_data (dict): nested courses and course runs certificates (dict): course run key -> certificate mapping Returns a datetime object or None if the program is not complete. """ program_available_date = None for course in program_data['courses']: earliest_course_run_date = None for course_run in course['course_runs']: key = CourseKey.from_string(course_run['key']) # Get a certificate if one exists certificate = certificates.get(key) if certificate is None: continue # Modes must match (see _is_course_complete() comments for why) course_run_mode = self._course_run_mode_translation( course_run['type']) certificate_mode = self._certificate_mode_translation( certificate.mode) modes_match = course_run_mode == certificate_mode # Grab the available date and keep it if it's the earliest one for this catalog course. if modes_match and CertificateStatuses.is_passing_status( certificate.status): course_overview = CourseOverview.get_from_id(key) available_date = certificate_api.available_date_for_certificate( course_overview, certificate) earliest_course_run_date = min([ date for date in [available_date, earliest_course_run_date] if date ]) # If we're missing a cert for a course, the program isn't completed and we should just bail now if earliest_course_run_date is None: return None # Keep the catalog course date if it's the latest one program_available_date = max([ date for date in [earliest_course_run_date, program_available_date] if date ]) return program_available_date
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. If this function is moved, make sure to update it's entry in EXPLICIT_QUEUES in the settings files so it runs in the correct queue. 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( f"Failed to award course certificate for user {username} for course {course_run_key}. Reason: {reason}" ) return self.retry(exc=exception, countdown=countdown, max_retries=MAX_RETRIES) LOGGER.info( f"Running task award_course_certificate for username {username}") 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: error_msg = ( "Task award_course_certificate cannot be executed when credentials issuance is disabled in API config" ) LOGGER.warning(error_msg) raise _retry_with_custom_exception(username=username, course_run_key=course_run_key, reason=error_msg, countdown=countdown) try: course_key = CourseKey.from_string(course_run_key) try: user = User.objects.get(username=username) except User.DoesNotExist: LOGGER.exception( f"Task award_course_certificate was called with invalid username {username}" ) # Don't retry for this case - just conclude the task. return # Get the cert for the course key and username if it's both passing and available in professional/verified try: certificate = GeneratedCertificate.eligible_certificates.get( user=user.id, course_id=course_key) except GeneratedCertificate.DoesNotExist: LOGGER.exception( "Task award_course_certificate was called without Certificate found " f"for {course_key} to user {username}") return if certificate.mode in CourseMode.CERTIFICATE_RELEVANT_MODES: try: course_overview = CourseOverview.get_from_id(course_key) except (CourseOverview.DoesNotExist, OSError): LOGGER.exception( f"Task award_course_certificate was called without course overview data for course {course_key}" ) return credentials_client = get_credentials_api_client( User.objects.get( username=settings.CREDENTIALS_SERVICE_USERNAME), ) # Date is being passed via JSON and is encoded in the EMCA date time string format. The rest of the code # expects a datetime. if certificate_available_date: certificate_available_date = datetime.strptime( certificate_available_date, 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}") # 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, org=course_key.org) LOGGER.info( f"Awarded certificate for course {course_key} to user {username}" ) except Exception as exc: error_msg = f"Failed to determine course certificates to be awarded for user {username}." LOGGER.exception(error_msg) raise _retry_with_custom_exception(username=username, course_run_key=course_run_key, reason=error_msg, countdown=countdown) from exc