Example #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 == 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)
Example #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 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
Example #3
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.

    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