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 == api.available_date_for_certificate( self.course, self.certificate) assert self.certificate.modified_date == api.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) maybe_avail = self.course.certificate_available_date if uses_avail_date else self.certificate.modified_date assert maybe_avail == api.available_date_for_certificate( self.course, self.certificate) assert maybe_avail == api.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 == api.available_date_for_certificate( self.course, self.certificate) assert self.certificate.modified_date == api.display_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 certificate_api.is_passing_status( certificate.status): course_overview = CourseOverview.get_from_id(key) available_date = 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 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 self.assertEqual(self.certificate.modified_date, api.available_date_for_certificate(self.course, self.certificate)) self.assertEqual(self.certificate.modified_date, api.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) maybe_avail = self.course.certificate_available_date if uses_avail_date else self.certificate.modified_date self.assertEqual(maybe_avail, api.available_date_for_certificate(self.course, self.certificate)) self.assertEqual(maybe_avail, api.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 self.assertEqual(maybe_avail, api.available_date_for_certificate(self.course, self.certificate)) self.assertEqual(self.certificate.modified_date, api.display_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 certificate_api.is_passing_status(certificate.status): course_overview = CourseOverview.get_from_id(key) available_date = available_date_for_certificate(course_overview, certificate) earliest_course_run_date = min(filter(None, [available_date, earliest_course_run_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(filter(None, [earliest_course_run_date, program_available_date])) return program_available_date
def award_course_certificate(self, username, course_run_key): """ 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. """ LOGGER.info(u'Running task award_course_certificate for username %s', 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: LOGGER.warning( 'Task award_course_certificate cannot be executed when credentials issuance is disabled in API config', ) raise self.retry(countdown=countdown, max_retries=MAX_RETRIES) try: course_key = CourseKey.from_string(course_run_key) try: user = User.objects.get(username=username) except User.DoesNotExist: LOGGER.exception(u'Task award_course_certificate was called with invalid username %s', 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( u'Task award_course_certificate was called without Certificate found for %s to user %s', course_key, username ) return if certificate.mode in CourseMode.CERTIFICATE_RELEVANT_MODES: try: course_overview = CourseOverview.get_from_id(course_key) except (CourseOverview.DoesNotExist, IOError): LOGGER.exception( u'Task award_course_certificate was called without course overview data for course %s', course_key ) return credentials_client = get_credentials_api_client(User.objects.get( 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) post_course_certificate(credentials_client, username, certificate, visible_date) LOGGER.info(u'Awarded certificate for course %s to user %s', course_key, username) except Exception as exc: LOGGER.exception(u'Failed to determine course certificates to be awarded for user %s', username) raise self.retry(exc=exc, countdown=countdown, max_retries=MAX_RETRIES)
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( 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, IOError): 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), org=course_key.org, ) # 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, 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}" ) post_course_certificate(credentials_client, username, certificate, visible_date) 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
def award_course_certificate(self, username, course_run_key): """ 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. """ LOGGER.info('Running task award_course_certificate for username %s', 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: LOGGER.warning( 'Task award_course_certificate cannot be executed when credentials issuance is disabled in API config', ) raise self.retry(countdown=countdown, max_retries=MAX_RETRIES) try: course_key = CourseKey.from_string(course_run_key) try: user = User.objects.get(username=username) except User.DoesNotExist: LOGGER.exception( 'Task award_course_certificate was called with invalid username %s', 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 for %s to user %s', course_key, username) return if certificate.mode in CourseMode.CREDIT_ELIGIBLE_MODES + CourseMode.CREDIT_MODES: try: course_overview = CourseOverview.get_from_id(course_key) except (CourseOverview.DoesNotExist, IOError): LOGGER.exception( 'Task award_course_certificate was called without course overview data for course %s', course_key) return credentials_client = get_credentials_api_client( User.objects.get( 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) post_course_certificate(credentials_client, username, certificate, visible_date) LOGGER.info('Awarded certificate for course %s to user %s', course_key, username) except Exception as exc: LOGGER.exception( 'Failed to determine course certificates to be awarded for user %s', username) raise self.retry(exc=exc, countdown=countdown, max_retries=MAX_RETRIES)