Esempio n. 1
0
    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)
Esempio n. 2
0
    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
Esempio n. 3
0
    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))
Esempio n. 4
0
    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
Esempio n. 5
0
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)
Esempio n. 6
0
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)