def _listen_for_id_verification_status_changed(sender, user, **kwargs):  # pylint: disable=unused-argument
    """
    Catches a track change signal, determines user status,
    calls fire_ungenerated_certificate_task for passing grades
    """
    if not auto_certificate_generation_enabled():
        return

    user_enrollments = CourseEnrollment.enrollments_for_user(user=user)

    grade_factory = CourseGradeFactory()
    expected_verification_status = IDVerificationService.user_status(user)
    expected_verification_status = expected_verification_status['status']
    for enrollment in user_enrollments:
        if grade_factory.read(user=user, course=enrollment.course_overview).passed:
            if fire_ungenerated_certificate_task(user, enrollment.course_id, expected_verification_status):
                message = (
                    u'Certificate generation task initiated for {user} : {course} via track change ' +
                    u'with verification status of {status}'
                )
                log.info(message.format(
                    user=user.id,
                    course=enrollment.course_id,
                    status=expected_verification_status
                ))
Exemple #2
0
    def __init__(self, site, user, enrollments=None, uuid=None, mobile_only=False):
        self.site = site
        self.user = user
        self.mobile_only = mobile_only

        self.enrollments = enrollments or list(CourseEnrollment.enrollments_for_user(self.user))
        self.enrollments.sort(key=lambda e: e.created, reverse=True)

        self.enrolled_run_modes = {}
        self.course_run_ids = []
        for enrollment in self.enrollments:
            # enrollment.course_id is really a CourseKey (╯ಠ_ಠ)╯︵ ┻━┻
            enrollment_id = unicode(enrollment.course_id)
            mode = enrollment.mode
            if mode == CourseMode.NO_ID_PROFESSIONAL_MODE:
                mode = CourseMode.PROFESSIONAL
            self.enrolled_run_modes[enrollment_id] = mode
            # We can't use dict.keys() for this because the course run ids need to be ordered
            self.course_run_ids.append(enrollment_id)

        self.entitlements = list(CourseEntitlement.unexpired_entitlements_for_user(self.user))
        self.course_uuids = [str(entitlement.course_uuid) for entitlement in self.entitlements]

        self.course_grade_factory = CourseGradeFactory()

        if uuid:
            self.programs = [get_programs(uuid=uuid)]
        else:
            self.programs = attach_program_detail_url(get_programs(self.site), self.mobile_only)
Exemple #3
0
def send_composite_outcome(user_id, course_id, assignment_id, version):
    """
    Calculate and transmit the score for a composite module (such as a
    vertical).

    A composite module may contain multiple problems, so we need to
    calculate the total points earned and possible for all child problems. This
    requires calculating the scores for the whole course, which is an expensive
    operation.

    Callers should be aware that the score calculation code accesses the latest
    scores from the database. This can lead to a race condition between a view
    that updates a user's score and the calculation of the grade. If the Celery
    task attempts to read the score from the database before the view exits (and
    its transaction is committed), it will see a stale value. Care should be
    taken that this task is not triggered until the view exits.

    The GradedAssignment model has a version_number field that is incremented
    whenever the score is updated. It is used by this method for two purposes.
    First, it allows the task to exit if it detects that it has been superseded
    by another task that will transmit the score for the same assignment.
    Second, it prevents a race condition where two tasks calculate different
    scores for a single assignment, and may potentially update the campus LMS
    in the wrong order.
    """
    assignment = GradedAssignment.objects.get(id=assignment_id)
    if version != assignment.version_number:
        log.info(
            u"Score passback for GradedAssignment %s skipped. More recent score available.",
            assignment.id
        )
        return
    course_key = CourseKey.from_string(course_id)
    mapped_usage_key = assignment.usage_key.map_into_course(course_key)
    user = User.objects.get(id=user_id)
    course = modulestore().get_course(course_key, depth=0)
    course_grade = CourseGradeFactory().read(user, course)
    earned, possible = course_grade.score_for_module(mapped_usage_key)
    if possible == 0:
        weighted_score = 0
    else:
        weighted_score = float(earned) / float(possible)

    assignment = GradedAssignment.objects.get(id=assignment_id)
    if assignment.version_number == version:
        outcomes.send_score_update(assignment, weighted_score)
Exemple #4
0
    def add_cert(self,
                 student,
                 course_id,
                 course=None,
                 forced_grade=None,
                 template_file=None,
                 generate_pdf=True):
        """
        Request a new certificate for a student.

        Arguments:
          student   - User.object
          course_id - courseenrollment.course_id (CourseKey)
          forced_grade - a string indicating a grade parameter to pass with
                         the certificate request. If this is given, grading
                         will be skipped.
          generate_pdf - Boolean should a message be sent in queue to generate certificate PDF

        Will change the certificate status to 'generating' or
        `downloadable` in case of web view certificates.

        The course must not be a CCX.

        Certificate must be in the 'unavailable', 'error',
        'deleted' or 'generating' state.

        If a student has a passing grade or is in the whitelist
        table for the course a request will be made for a new cert.

        If a student has allow_certificate set to False in the
        userprofile table the status will change to 'restricted'

        If a student does not have a passing grade the status
        will change to status.notpassing

        Returns the newly created certificate instance
        """

        if hasattr(course_id, 'ccx'):
            LOGGER.warning(
                (u"Cannot create certificate generation task for user %s "
                 u"in the course '%s'; "
                 u"certificates are not allowed for CCX courses."), student.id,
                six.text_type(course_id))
            return None

        valid_statuses = [
            status.generating,
            status.unavailable,
            status.deleted,
            status.error,
            status.notpassing,
            status.downloadable,
            status.auditing,
            status.audit_passing,
            status.audit_notpassing,
            status.unverified,
        ]

        cert_status_dict = certificate_status_for_student(student, course_id)
        cert_status = cert_status_dict.get('status')
        download_url = cert_status_dict.get('download_url')
        cert = None
        if download_url:
            self._log_pdf_cert_generation_discontinued_warning(
                student.id, course_id, cert_status, download_url)
            return None

        if cert_status not in valid_statuses:
            LOGGER.warning(
                (u"Cannot create certificate generation task for user %s "
                 u"in the course '%s'; "
                 u"the certificate status '%s' is not one of %s."), student.id,
                six.text_type(course_id), cert_status,
                six.text_type(valid_statuses))
            return None

        # The caller can optionally pass a course in to avoid
        # re-fetching it from Mongo. If they have not provided one,
        # get it from the modulestore.
        if course is None:
            course = modulestore().get_course(course_id, depth=0)

        profile = UserProfile.objects.get(user=student)
        profile_name = profile.name

        # Needed for access control in grading.
        self.request.user = student
        self.request.session = {}

        is_whitelisted = self.whitelist.filter(user=student,
                                               course_id=course_id,
                                               whitelist=True).exists()
        course_grade = CourseGradeFactory().read(student, course)
        enrollment_mode, __ = CourseEnrollment.enrollment_mode_for_user(
            student, course_id)
        mode_is_verified = enrollment_mode in GeneratedCertificate.VERIFIED_CERTS_MODES
        user_is_verified = IDVerificationService.user_is_verified(student)
        cert_mode = enrollment_mode

        is_eligible_for_certificate = CourseMode.is_eligible_for_certificate(
            enrollment_mode, cert_status)
        if is_whitelisted and not is_eligible_for_certificate:
            # check if audit certificates are enabled for audit mode
            is_eligible_for_certificate = enrollment_mode != CourseMode.AUDIT or \
                not settings.FEATURES['DISABLE_AUDIT_CERTIFICATES']

        unverified = False
        # For credit mode generate verified certificate
        if cert_mode in (CourseMode.CREDIT_MODE, CourseMode.MASTERS):
            cert_mode = CourseMode.VERIFIED

        if template_file is not None:
            template_pdf = template_file
        elif mode_is_verified and user_is_verified:
            template_pdf = "certificate-template-{id.org}-{id.course}-verified.pdf".format(
                id=course_id)
        elif mode_is_verified and not user_is_verified:
            template_pdf = "certificate-template-{id.org}-{id.course}.pdf".format(
                id=course_id)
            if CourseMode.mode_for_course(course_id, CourseMode.HONOR):
                cert_mode = GeneratedCertificate.MODES.honor
            else:
                unverified = True
        else:
            # honor code and audit students
            template_pdf = "certificate-template-{id.org}-{id.course}.pdf".format(
                id=course_id)

        LOGGER.info((
            u"Certificate generated for student %s in the course: %s with template: %s. "
            u"given template: %s, "
            u"user is verified: %s, "
            u"mode is verified: %s,"
            u"generate_pdf is: %s"), student.username,
                    six.text_type(course_id), template_pdf, template_file,
                    user_is_verified, mode_is_verified, generate_pdf)
        cert, created = GeneratedCertificate.objects.get_or_create(
            user=student, course_id=course_id)

        cert.mode = cert_mode
        cert.user = student
        cert.grade = course_grade.percent
        cert.course_id = course_id
        cert.name = profile_name
        cert.download_url = ''

        # Strip HTML from grade range label
        grade_contents = forced_grade or course_grade.letter_grade
        try:
            grade_contents = lxml.html.fromstring(
                grade_contents).text_content()
            passing = True
        except (TypeError, XMLSyntaxError, ParserError) as exc:
            LOGGER.info((u"Could not retrieve grade for student %s "
                         u"in the course '%s' "
                         u"because an exception occurred while parsing the "
                         u"grade contents '%s' as HTML. "
                         u"The exception was: '%s'"), student.id,
                        six.text_type(course_id), grade_contents,
                        six.text_type(exc))

            # Log if the student is whitelisted
            if is_whitelisted:
                LOGGER.info(u"Student %s is whitelisted in '%s'", student.id,
                            six.text_type(course_id))
                passing = True
            else:
                passing = False

        # If this user's enrollment is not eligible to receive a
        # certificate, mark it as such for reporting and
        # analytics. Only do this if the certificate is new, or
        # already marked as ineligible -- we don't want to mark
        # existing audit certs as ineligible.
        cutoff = settings.AUDIT_CERT_CUTOFF_DATE
        if (cutoff and cert.created_date >= cutoff
            ) and not is_eligible_for_certificate:
            cert.status = status.audit_passing if passing else status.audit_notpassing
            cert.save()
            LOGGER.info(
                u"Student %s with enrollment mode %s is not eligible for a certificate.",
                student.id, enrollment_mode)
            return cert
        # If they are not passing, short-circuit and don't generate cert
        elif not passing:
            cert.status = status.notpassing
            cert.save()

            LOGGER.info(
                (u"Student %s does not have a grade for '%s', "
                 u"so their certificate status has been set to '%s'. "
                 u"No certificate generation task was sent to the XQueue."),
                student.id, six.text_type(course_id), cert.status)
            return cert

        # Check to see whether the student is on the the embargoed
        # country restricted list. If so, they should not receive a
        # certificate -- set their status to restricted and log it.
        if self.restricted.filter(user=student).exists():
            cert.status = status.restricted
            cert.save()

            LOGGER.info(
                (u"Student %s is in the embargoed country restricted "
                 u"list, so their certificate status has been set to '%s' "
                 u"for the course '%s'. "
                 u"No certificate generation task was sent to the XQueue."),
                student.id, cert.status, six.text_type(course_id))
            return cert

        if unverified:
            cert.status = status.unverified
            cert.save()
            LOGGER.info(
                (u"User %s has a verified enrollment in course %s "
                 u"but is missing ID verification. "
                 u"Certificate status has been set to unverified"),
                student.id,
                six.text_type(course_id),
            )
            return cert

        # Finally, generate the certificate and send it off.
        return self._generate_cert(cert, course, student, grade_contents,
                                   template_pdf, generate_pdf)
Exemple #5
0
    def get(self, request, *args, **kwargs):
        course_key_string = kwargs.get('course_key_string')
        course_key = CourseKey.from_string(course_key_string)
        student_id = kwargs.get('student_id')

        if not course_home_mfe_progress_tab_is_active(course_key):
            raise Http404

        # Enable NR tracing for this view based on course
        monitoring_utils.set_custom_attribute('course_id', course_key_string)
        monitoring_utils.set_custom_attribute('user_id', request.user.id)
        monitoring_utils.set_custom_attribute('is_staff', request.user.is_staff)
        is_staff = bool(has_access(request.user, 'staff', course_key))

        student = self._get_student_user(request, course_key, student_id, is_staff)
        username = get_enterprise_learner_generic_name(request) or student.username

        course = get_course_with_access(student, 'load', course_key, check_if_enrolled=False)

        course_overview = CourseOverview.get_from_id(course_key)
        enrollment = CourseEnrollment.get_enrollment(student, course_key)
        enrollment_mode = getattr(enrollment, 'mode', None)

        if not (enrollment and enrollment.is_active) and not is_staff:
            return Response('User not enrolled.', status=401)

        # The block structure is used for both the course_grade and has_scheduled content fields
        # So it is called upfront and reused for optimization purposes
        collected_block_structure = get_block_structure_manager(course_key).get_collected()
        course_grade = CourseGradeFactory().read(student, collected_block_structure=collected_block_structure)

        # Get has_scheduled_content data
        transformers = BlockStructureTransformers()
        transformers += [start_date.StartDateTransformer(), ContentTypeGateTransformer()]
        usage_key = collected_block_structure.root_block_usage_key
        course_blocks = get_course_blocks(
            student,
            usage_key,
            transformers=transformers,
            collected_block_structure=collected_block_structure,
            include_has_scheduled_content=True
        )
        has_scheduled_content = course_blocks.get_xblock_field(usage_key, 'has_scheduled_content')

        # Get user_has_passing_grade data
        user_has_passing_grade = False
        if not student.is_anonymous:
            user_grade = course_grade.percent
            user_has_passing_grade = user_grade >= course.lowest_passing_grade

        descriptor = modulestore().get_course(course_key)
        grading_policy = descriptor.grading_policy
        verification_status = IDVerificationService.user_status(student)
        verification_link = None
        if verification_status['status'] is None or verification_status['status'] == 'expired':
            verification_link = IDVerificationService.get_verify_location(course_id=course_key)
        elif verification_status['status'] == 'must_reverify':
            verification_link = IDVerificationService.get_verify_location(course_id=course_key)
        verification_data = {
            'link': verification_link,
            'status': verification_status['status'],
            'status_date': verification_status['status_date'],
        }

        access_expiration = get_access_expiration_data(request.user, course_overview)

        data = {
            'access_expiration': access_expiration,
            'certificate_data': get_cert_data(student, course, enrollment_mode, course_grade),
            'completion_summary': get_course_blocks_completion_summary(course_key, student),
            'course_grade': course_grade,
            'end': course.end,
            'enrollment_mode': enrollment_mode,
            'grading_policy': grading_policy,
            'has_scheduled_content': has_scheduled_content,
            'section_scores': list(course_grade.chapter_grades.values()),
            'studio_url': get_studio_url(course, 'settings/grading'),
            'username': username,
            'user_has_passing_grade': user_has_passing_grade,
            'verification_data': verification_data,
        }
        context = self.get_serializer_context()
        context['staff_access'] = is_staff
        context['course_blocks'] = course_blocks
        context['course_key'] = course_key
        # course_overview and enrollment will be used by VerifiedModeSerializer
        context['course_overview'] = course_overview
        context['enrollment'] = enrollment
        serializer = self.get_serializer_class()(data, context=context)

        return Response(serializer.data)
Exemple #6
0
def _cert_info(user, course_overview, cert_status):
    """
    Implements the logic for cert_info -- split out for testing.

    Arguments:
        user (User): A user.
        course_overview (CourseOverview): A course.
    """
    # simplify the status for the template using this lookup table
    template_state = {
        CertificateStatuses.generating: 'generating',
        CertificateStatuses.downloadable: 'downloadable',
        CertificateStatuses.notpassing: 'notpassing',
        CertificateStatuses.restricted: 'restricted',
        CertificateStatuses.auditing: 'auditing',
        CertificateStatuses.audit_passing: 'auditing',
        CertificateStatuses.audit_notpassing: 'auditing',
        CertificateStatuses.unverified: 'unverified',
    }

    certificate_earned_but_not_available_status = 'certificate_earned_but_not_available'
    default_status = 'processing'

    default_info = {
        'status': default_status,
        'show_survey_button': False,
        'can_unenroll': True,
    }

    if cert_status is None:
        return default_info

    status = template_state.get(cert_status['status'], default_status)
    is_hidden_status = status in ('unavailable', 'processing', 'generating', 'notpassing', 'auditing')

    if (
        not certificates_viewable_for_course(course_overview) and
        (status in CertificateStatuses.PASSED_STATUSES) and
        course_overview.certificate_available_date
    ):
        status = certificate_earned_but_not_available_status

    if (
        course_overview.certificates_display_behavior == 'early_no_info' and
        is_hidden_status
    ):
        return default_info

    status_dict = {
        'status': status,
        'mode': cert_status.get('mode', None),
        'linked_in_url': None,
        'can_unenroll': status not in DISABLE_UNENROLL_CERT_STATES,
    }

    if status != default_status and course_overview.end_of_course_survey_url is not None:
        status_dict.update({
            'show_survey_button': True,
            'survey_url': process_survey_link(course_overview.end_of_course_survey_url, user)})
    else:
        status_dict['show_survey_button'] = False

    if status == 'downloadable':
        # showing the certificate web view button if certificate is downloadable state and feature flags are enabled.
        if has_html_certificates_enabled(course_overview):
            if course_overview.has_any_active_web_certificate:
                status_dict.update({
                    'show_cert_web_view': True,
                    'cert_web_view_url': get_certificate_url(course_id=course_overview.id, uuid=cert_status['uuid'])
                })
            elif cert_status['download_url']:
                status_dict['download_url'] = cert_status['download_url']
            else:
                # don't show download certificate button if we don't have an active certificate for course
                status_dict['status'] = 'unavailable'
        elif 'download_url' not in cert_status:
            log.warning(
                u"User %s has a downloadable cert for %s, but no download url",
                user.username,
                course_overview.id
            )
            return default_info
        else:
            status_dict['download_url'] = cert_status['download_url']

            # If enabled, show the LinkedIn "add to profile" button
            # Clicking this button sends the user to LinkedIn where they
            # can add the certificate information to their profile.
            linkedin_config = LinkedInAddToProfileConfiguration.current()

            # posting certificates to LinkedIn is not currently
            # supported in White Labels
            if linkedin_config.enabled and not theming_helpers.is_request_in_themed_site():
                status_dict['linked_in_url'] = linkedin_config.add_to_profile_url(
                    course_overview.id,
                    course_overview.display_name,
                    cert_status.get('mode'),
                    cert_status['download_url']
                )

    if status in {'generating', 'downloadable', 'notpassing', 'restricted', 'auditing', 'unverified'}:
        cert_grade_percent = -1
        persisted_grade_percent = -1
        persisted_grade = CourseGradeFactory().read(user, course=course_overview, create_if_needed=False)
        if persisted_grade is not None:
            persisted_grade_percent = persisted_grade.percent

        if 'grade' in cert_status:
            cert_grade_percent = float(cert_status['grade'])

        if cert_grade_percent == -1 and persisted_grade_percent == -1:
            # Note: as of 11/20/2012, we know there are students in this state-- cs169.1x,
            # who need to be regraded (we weren't tracking 'notpassing' at first).
            # We can add a log.warning here once we think it shouldn't happen.
            return default_info
        grades_input = [cert_grade_percent, persisted_grade_percent]
        max_grade = (
            None
            if all(grade is None for grade in grades_input)
            else max(filter(lambda x: x is not None, grades_input))
        )
        status_dict['grade'] = text_type(max_grade)

    return status_dict
Exemple #7
0
    def generate(cls, _xmodule_instance_args, _entry_id, course_id,
                 _task_input, action_name):
        """
        Generate a CSV containing all students' problem grades within a given
        `course_id`.
        """
        def log_task_info(message):
            """
            Updates the status on the celery task to the given message.
            Also logs the update.
            """
            fmt = u'Task: {task_id}, InstructorTask ID: {entry_id}, Course: {course_id}, Input: {task_input}'
            task_info_string = fmt.format(task_id=task_id,
                                          entry_id=_entry_id,
                                          course_id=course_id,
                                          task_input=_task_input)
            TASK_LOG.info(u'%s, Task type: %s, %s, %s', task_info_string,
                          action_name, message, task_progress.state)

        start_time = time()
        start_date = datetime.now(UTC)
        status_interval = 100
        task_id = _xmodule_instance_args.get(
            'task_id') if _xmodule_instance_args is not None else None

        enrolled_students = CourseEnrollment.objects.users_enrolled_in(
            course_id,
            include_inactive=True,
            verified_only=problem_grade_report_verified_only(course_id),
        )
        task_progress = TaskProgress(action_name, enrolled_students.count(),
                                     start_time)

        # This struct encapsulates both the display names of each static item in the
        # header row as values as well as the django User field names of those items
        # as the keys.  It is structured in this way to keep the values related.
        header_row = OrderedDict([('id', 'Student ID'), ('email', 'Email'),
                                  ('username', 'Username')])

        course = get_course_by_id(course_id)
        log_task_info(u'Retrieving graded scorable blocks')
        graded_scorable_blocks = cls._graded_scorable_blocks_to_header(course)

        # Just generate the static fields for now.
        rows = [
            list(header_row.values()) + ['Enrollment Status', 'Grade'] +
            _flatten(list(graded_scorable_blocks.values()))
        ]
        error_rows = [list(header_row.values()) + ['error_msg']]

        # Bulk fetch and cache enrollment states so we can efficiently determine
        # whether each user is currently enrolled in the course.
        log_task_info(u'Fetching enrollment status')
        CourseEnrollment.bulk_fetch_enrollment_states(enrolled_students,
                                                      course_id)

        for student, course_grade, error in CourseGradeFactory().iter(
                enrolled_students, course):
            student_fields = [
                getattr(student, field_name) for field_name in header_row
            ]
            task_progress.attempted += 1

            if not course_grade:
                err_msg = text_type(error)
                # There was an error grading this student.
                if not err_msg:
                    err_msg = u'Unknown error'
                error_rows.append(student_fields + [err_msg])
                task_progress.failed += 1
                continue

            enrollment_status = _user_enrollment_status(student, course_id)

            earned_possible_values = []
            for block_location in graded_scorable_blocks:
                try:
                    problem_score = course_grade.problem_scores[block_location]
                except KeyError:
                    earned_possible_values.append(
                        [u'Not Available', u'Not Available'])
                else:
                    if problem_score.first_attempted:
                        earned_possible_values.append(
                            [problem_score.earned, problem_score.possible])
                    else:
                        earned_possible_values.append(
                            [u'Not Attempted', problem_score.possible])

            rows.append(student_fields +
                        [enrollment_status, course_grade.percent] +
                        _flatten(earned_possible_values))

            task_progress.succeeded += 1
            if task_progress.attempted % status_interval == 0:
                step = u'Calculating Grades'
                task_progress.update_task_state(extra_meta={'step': step})
                log_message = u'{0} {1}/{2}'.format(step,
                                                    task_progress.attempted,
                                                    task_progress.total)
                log_task_info(log_message)

        log_task_info('Uploading CSV to store')
        # Perform the upload if any students have been successfully graded
        if len(rows) > 1:
            upload_csv_to_report_store(rows, 'problem_grade_report', course_id,
                                       start_date)
        # If there are any error rows, write them out as well
        if len(error_rows) > 1:
            upload_csv_to_report_store(error_rows, 'problem_grade_report_err',
                                       course_id, start_date)

        return task_progress.update_task_state(
            extra_meta={'step': 'Uploading CSV'})
Exemple #8
0
def get_extra_course_about_context(request, course):
    """
    Get all the extra context for the course_about page

    Arguments:
        request (Request): Request object
        course (CourseOverview): Course Overview object to add data to the context

    Returns:
        dict: Returns an empty dict if it is the testing environment otherwise returns a dict with added context
    """
    if is_testing_environment():
        return {}

    user = request.user
    course_language_names = []
    enrolled_course_group_course = None
    enroll_popup_message = cannot_enroll_message = ''

    multilingual_course = MultilingualCourse.objects.all(
    ).multilingual_course_with_course_id(course.id)
    if multilingual_course:
        course_group_courses = multilingual_course.multilingual_course_group.multilingual_courses
        course_language_codes = course_group_courses.open_multilingual_courses(
        ).language_codes_with_course_ids()
        course_language_names = get_language_names_from_codes(
            course_language_codes)

        enrolled_course_group_course = course_group_courses.all(
        ).enrolled_course(user)
        if enrolled_course_group_course:
            course_display_name = enrolled_course_group_course.course.display_name_with_default

            enroll_popup_message = Text(
                _('Warning: If you wish to change the language of this course, your progress in '
                  'the following course(s) will be erased.{line_break}{course_name}'
                  )).format(course_name=course_display_name,
                            line_break=HTML('<br>'))

    course_enrollment_count = CourseEnrollment.objects.enrollment_counts(
        course.id).get('total')

    course_requirements = course_grade = certificate = None
    if user.is_authenticated:
        course_requirements = get_pre_requisite_courses_not_completed(
            user, [course.id])
        course_grade = CourseGradeFactory().read(user, course_key=course.id)
        certificate = GeneratedCertificate.certificate_for_student(
            user, course.id)

    has_generated_cert_for_any_other_course_group_course = False
    if enrolled_course_group_course and enrolled_course_group_course.course != course:
        has_generated_cert_for_any_other_course_group_course = GeneratedCertificate.objects.filter(
            course_id=enrolled_course_group_course.course.id,
            user=user).exists()

        if has_generated_cert_for_any_other_course_group_course:
            cannot_enroll_message = _(
                'You cannot enroll in this course version as you have already earned a '
                'certificate for another version of this course!')

    context = {
        'course_languages':
        course_language_names,
        'course_requirements':
        course_requirements,
        'total_enrollments':
        course_enrollment_count,
        'self_paced':
        course.self_paced,
        'effort':
        course.effort,
        'is_course_passed':
        course_grade and getattr(course_grade, 'passed', False),
        'has_certificate':
        certificate,
        'has_user_enrolled_in_course_group_courses':
        bool(enrolled_course_group_course),
        'has_generated_cert_for_any_course_group_course':
        has_generated_cert_for_any_other_course_group_course,
        'enroll_popup_message':
        enroll_popup_message,
        'cannot_enroll_message':
        cannot_enroll_message,
    }

    return context
Exemple #9
0
    def get(self, request, *args, **kwargs):
        course_key_string = kwargs.get('course_key_string')
        course_key = CourseKey.from_string(course_key_string)

        # Enable NR tracing for this view based on course
        monitoring_utils.set_custom_metric('course_id', course_key_string)
        monitoring_utils.set_custom_metric('user_id', request.user.id)
        monitoring_utils.set_custom_metric('is_staff', request.user.is_staff)

        _, request.user = setup_masquerade(
            request,
            course_key,
            staff_access=has_access(request.user, 'staff', course_key),
            reset_masquerade_data=True
        )

        user_timezone_locale = user_timezone_locale_prefs(request)
        user_timezone = user_timezone_locale['user_timezone']

        transformers = BlockStructureTransformers()
        transformers += course_blocks_api.get_course_block_access_transformers(request.user)
        transformers += [
            BlocksAPITransformer(None, None, depth=3),
        ]
        course = get_course_with_access(request.user, 'load', course_key, check_if_enrolled=True)

        enrollment_mode, _ = CourseEnrollment.enrollment_mode_for_user(request.user, course_key)

        course_grade = CourseGradeFactory().read(request.user, course)
        courseware_summary = course_grade.chapter_grades.values()

        verification_status = IDVerificationService.user_status(request.user)
        verification_link = None
        if verification_status['status'] is None or verification_status['status'] == 'expired':
            verification_link = IDVerificationService.get_verify_location('verify_student_verify_now',
                                                                          course_id=course_key)
        elif verification_status['status'] == 'must_reverify':
            verification_link = IDVerificationService.get_verify_location('verify_student_reverify',
                                                                          course_id=course_key)
        verification_data = {
            'link': verification_link,
            'status': verification_status['status'],
            'status_date': verification_status['status_date'],
        }

        data = {
            'certificate_data': get_cert_data(request.user, course, enrollment_mode, course_grade),
            'courseware_summary': courseware_summary,
            'credit_course_requirements': credit_course_requirements(course_key, request.user),
            'credit_support_url': CREDIT_SUPPORT_URL,
            'enrollment_mode': enrollment_mode,
            'studio_url': get_studio_url(course, 'settings/grading'),
            'user_timezone': user_timezone,
            'verification_data': verification_data,
        }
        context = self.get_serializer_context()
        context['staff_access'] = bool(has_access(request.user, 'staff', course))
        context['course_key'] = course_key
        serializer = self.get_serializer_class()(data, context=context)

        return Response(serializer.data)
def _has_passing_grade(user, course):
    """
    Check if the user has a passing grade in this course run
    """
    course_grade = CourseGradeFactory().read(user, course)
    return course_grade.passed
Exemple #11
0
def _cert_info(user, enrollment, cert_status):
    """
    Implements the logic for cert_info -- split out for testing.

    TODO: replace with a method that lives in the certificates app and combines this logic with
     lms.djangoapps.certificates.api.can_show_certificate_message and
     lms.djangoapps.courseware.views.get_cert_data

    Arguments:
        user (User): A user.
        enrollment (CourseEnrollment): A course enrollment.
        cert_status (dict): dictionary containing information about certificate status for the user

    Returns:
        dictionary containing:
            'status': one of 'generating', 'downloadable', 'notpassing', 'restricted', 'auditing',
                'processing', 'unverified', 'unavailable', or 'certificate_earned_but_not_available'
            'show_survey_button': bool
            'can_unenroll': if status allows for unenrollment

        The dictionary may also contain:
            'linked_in_url': url to add cert to LinkedIn profile
            'survey_url': url, only if course_overview.end_of_course_survey_url is not None
            'show_cert_web_view': bool if html web certs are enabled and there is an active web cert
            'cert_web_view_url': url if html web certs are enabled and there is an active web cert
            'download_url': url to download a cert
            'grade': if status is in 'generating', 'downloadable', 'notpassing', 'restricted',
                'auditing', or 'unverified'
    """
    # simplify the status for the template using this lookup table
    template_state = {
        CertificateStatuses.generating: 'generating',
        CertificateStatuses.downloadable: 'downloadable',
        CertificateStatuses.notpassing: 'notpassing',
        CertificateStatuses.restricted: 'restricted',
        CertificateStatuses.auditing: 'auditing',
        CertificateStatuses.audit_passing: 'auditing',
        CertificateStatuses.audit_notpassing: 'auditing',
        CertificateStatuses.unverified: 'unverified',
    }

    certificate_earned_but_not_available_status = 'certificate_earned_but_not_available'
    default_status = 'processing'

    default_info = {
        'status': default_status,
        'show_survey_button': False,
        'can_unenroll': True,
    }

    if cert_status is None or enrollment is None:
        return default_info

    course_overview = enrollment.course_overview if enrollment else None
    status = template_state.get(cert_status['status'], default_status)
    is_hidden_status = status in ('processing', 'generating', 'notpassing', 'auditing')

    if _is_certificate_earned_but_not_available(course_overview, status):
        status = certificate_earned_but_not_available_status

    if (
        course_overview.certificates_display_behavior == CertificatesDisplayBehaviors.EARLY_NO_INFO and
        is_hidden_status
    ):
        return default_info

    if not CourseMode.is_eligible_for_certificate(enrollment.mode, status=status):
        return default_info

    if course_overview and access.is_beta_tester(user, course_overview.id):
        # Beta testers are not eligible for a course certificate
        return default_info

    status_dict = {
        'status': status,
        'mode': cert_status.get('mode', None),
        'linked_in_url': None,
        'can_unenroll': status not in DISABLE_UNENROLL_CERT_STATES,
    }

    if status != default_status and course_overview.end_of_course_survey_url is not None:
        status_dict.update({
            'show_survey_button': True,
            'survey_url': process_survey_link(course_overview.end_of_course_survey_url, user)})
    else:
        status_dict['show_survey_button'] = False

    if status == 'downloadable':
        # showing the certificate web view button if certificate is downloadable state and feature flags are enabled.
        if has_html_certificates_enabled(course_overview):
            if course_overview.has_any_active_web_certificate:
                status_dict.update({
                    'show_cert_web_view': True,
                    'cert_web_view_url': get_certificate_url(course_id=course_overview.id, uuid=cert_status['uuid'])
                })
            elif cert_status['download_url']:
                status_dict['download_url'] = cert_status['download_url']
            else:
                # don't show download certificate button if we don't have an active certificate for course
                status_dict['status'] = 'unavailable'
        elif 'download_url' not in cert_status:
            log.warning(
                "User %s has a downloadable cert for %s, but no download url",
                user.username,
                course_overview.id
            )
            return default_info
        else:
            status_dict['download_url'] = cert_status['download_url']

            # If enabled, show the LinkedIn "add to profile" button
            # Clicking this button sends the user to LinkedIn where they
            # can add the certificate information to their profile.
            linkedin_config = LinkedInAddToProfileConfiguration.current()
            if linkedin_config.is_enabled():
                status_dict['linked_in_url'] = linkedin_config.add_to_profile_url(
                    course_overview.display_name, cert_status.get('mode'), cert_status['download_url'],
                )

    if status in {'generating', 'downloadable', 'notpassing', 'restricted', 'auditing', 'unverified'}:
        cert_grade_percent = -1
        persisted_grade_percent = -1
        persisted_grade = CourseGradeFactory().read(user, course=course_overview, create_if_needed=False)
        if persisted_grade is not None:
            persisted_grade_percent = persisted_grade.percent

        if 'grade' in cert_status:
            cert_grade_percent = float(cert_status['grade'])

        if cert_grade_percent == -1 and persisted_grade_percent == -1:
            # Note: as of 11/20/2012, we know there are students in this state-- cs169.1x,
            # who need to be regraded (we weren't tracking 'notpassing' at first).
            # We can add a log.warning here once we think it shouldn't happen.
            return default_info
        grades_input = [cert_grade_percent, persisted_grade_percent]
        max_grade = (
            None
            if all(grade is None for grade in grades_input)
            else max(filter(lambda x: x is not None, grades_input))
        )
        status_dict['grade'] = str(max_grade)

        # If the grade is passing, the status is one of these statuses, and request certificate
        # is enabled for a course then we need to provide the option to the learner
        if (
            status_dict['status'] != CertificateStatuses.downloadable and
            (has_self_generated_certificates_enabled(course_overview.id) or auto_certificate_generation_enabled()) and
            persisted_grade and persisted_grade.passed
        ):
            status_dict['status'] = CertificateStatuses.requesting

    return status_dict
Exemple #12
0
    def _rows_for_users(self, context, users, header_row,
                        graded_scorable_blocks):
        """
        Returns a list of rows for the given users for this report.
        """
        if six.text_type(
                context.course_id) == 'course-v1:MITx+CTL.SC0x+1T2019':
            context.update_status(
                'ProblemGradeReport Investigation log: Start of row creation for users'
            )
        success_rows, error_rows = [], []
        for student, course_grade, error in CourseGradeFactory().iter(
                users, context.course):
            student_fields = [
                getattr(student, field_name) for field_name in header_row
            ]
            context.task_progress.attempted += 1
            if context.task_progress.attempted % 2 == 0 and \
                    six.text_type(context.course_id) == 'course-v1:MITx+CTL.SC0x+1T2019':
                context.update_status(
                    'ProblemGradeReport Investigation log: Processing user {}'.
                    format(context.task_progress.attempted))
            if not course_grade:
                err_msg = text_type(error)
                # There was an error grading this student.
                if not err_msg:
                    err_msg = 'Unknown error'
                error_rows.append(student_fields + [err_msg])
                context.task_progress.failed += 1
                continue

            enrollment_status = _user_enrollment_status(
                student, context.course_id)

            earned_possible_values = []
            for block_location in graded_scorable_blocks:
                try:
                    problem_score = course_grade.problem_scores[block_location]
                except KeyError:
                    earned_possible_values.append(
                        ['Not Available', 'Not Available'])
                else:
                    if problem_score.first_attempted:
                        earned_possible_values.append(
                            [problem_score.earned, problem_score.possible])
                    else:
                        earned_possible_values.append(
                            ['Not Attempted', problem_score.possible])

            context.task_progress.succeeded += 1
            success_rows.append(student_fields +
                                [enrollment_status, course_grade.percent] +
                                _flatten(earned_possible_values))

        context.task_progress.update_task_state(
            extra_meta={
                'step': 'ProblemGradeReport Step 5: Calculating Grades'
            })
        log_message = u'{0} {1}/{2}'.format(
            'ProblemGradeReport Step 5: Calculating Grades',
            context.task_progress.attempted, context.task_progress.total)
        self.log_task_info(context, log_message)
        return success_rows, error_rows
Exemple #13
0
class ProgramProgressMeter(object):
    """Utility for gauging a user's progress towards program completion.

    Arguments:
        user (User): The user for which to find programs.

    Keyword Arguments:
        enrollments (list): List of the user's enrollments.
        uuid (str): UUID identifying a specific program. If provided, the meter
            will only inspect this one program, not all programs the user may be
            engaged with.
    """
    def __init__(self,
                 site,
                 user,
                 enrollments=None,
                 uuid=None,
                 mobile_only=False):
        self.site = site
        self.user = user
        self.mobile_only = mobile_only

        self.enrollments = enrollments or list(
            CourseEnrollment.enrollments_for_user(self.user))
        self.enrollments.sort(key=lambda e: e.created, reverse=True)

        self.enrolled_run_modes = {}
        self.course_run_ids = []
        for enrollment in self.enrollments:
            # enrollment.course_id is really a CourseKey (╯ಠ_ಠ)╯︵ ┻━┻
            enrollment_id = six.text_type(enrollment.course_id)
            mode = enrollment.mode
            if mode == CourseMode.NO_ID_PROFESSIONAL_MODE:
                mode = CourseMode.PROFESSIONAL
            self.enrolled_run_modes[enrollment_id] = mode
            # We can't use dict.keys() for this because the course run ids need to be ordered
            self.course_run_ids.append(enrollment_id)

        self.entitlements = list(
            CourseEntitlement.unexpired_entitlements_for_user(self.user))
        self.course_uuids = [
            str(entitlement.course_uuid) for entitlement in self.entitlements
        ]

        self.course_grade_factory = CourseGradeFactory()

        if uuid:
            self.programs = [get_programs(uuid=uuid)]
        else:
            self.programs = attach_program_detail_url(get_programs(self.site),
                                                      self.mobile_only)

    def invert_programs(self):
        """Intersect programs and enrollments.

        Builds a dictionary of program dict lists keyed by course run ID and by course UUID.
        The resulting dictionary is suitable in applications where programs must be
        filtered by the course runs or courses they contain (e.g., the student dashboard).

        Returns:
            defaultdict, programs keyed by course run ID
        """
        inverted_programs = defaultdict(list)

        for program in self.programs:
            for course in program['courses']:
                course_uuid = course['uuid']
                if course_uuid in self.course_uuids:
                    program_list = inverted_programs[course_uuid]
                    if program not in program_list:
                        program_list.append(program)
                for course_run in course['course_runs']:
                    course_run_id = course_run['key']
                    if course_run_id in self.course_run_ids:
                        program_list = inverted_programs[course_run_id]
                        if program not in program_list:
                            program_list.append(program)

        # Sort programs by title for consistent presentation.
        for program_list in six.itervalues(inverted_programs):
            program_list.sort(key=lambda p: p['title'])

        return inverted_programs

    @cached_property
    def engaged_programs(self):
        """Derive a list of programs in which the given user is engaged.

        Returns:
            list of program dicts, ordered by most recent enrollment
        """
        inverted_programs = self.invert_programs()

        programs = []
        # Remember that these course run ids are derived from a list of
        # enrollments sorted from most recent to least recent. Iterating
        # over the values in inverted_programs alone won't yield a program
        # ordering consistent with the user's enrollments.
        for course_run_id in self.course_run_ids:
            for program in inverted_programs[course_run_id]:
                # Dicts aren't a hashable type, so we can't use a set. Sets also
                # aren't ordered, which is important here.
                if program not in programs:
                    programs.append(program)

        for course_uuid in self.course_uuids:
            for program in inverted_programs[course_uuid]:
                if program not in programs:
                    programs.append(program)

        return programs

    def _is_course_in_progress(self, now, course):
        """Check if course qualifies as in progress as part of the program.

        A course is considered to be in progress if a user is enrolled in a run
        of the correct mode or a run of the correct mode is still available for enrollment.

        Arguments:
            now (datetime): datetime for now
            course (dict): Containing nested course runs.

        Returns:
            bool, indicating whether the course is in progress.
        """
        enrolled_runs = [
            run for run in course['course_runs']
            if run['key'] in self.course_run_ids
        ]

        # Check if the user is enrolled in a required run and mode/seat.
        runs_with_required_mode = [
            run for run in enrolled_runs
            if run['type'] == self.enrolled_run_modes[run['key']]
        ]

        if runs_with_required_mode:
            not_failed_runs = [
                run for run in runs_with_required_mode
                if run not in self.failed_course_runs
            ]
            if not_failed_runs:
                return True

        # Check if seats required for course completion are still available.
        upgrade_deadlines = []
        for run in enrolled_runs:
            for seat in run['seats']:
                if seat['type'] == run['type'] and run[
                        'type'] != self.enrolled_run_modes[run['key']]:
                    upgrade_deadlines.append(seat['upgrade_deadline'])

        # An upgrade deadline of None means the course is always upgradeable.
        return any(not deadline or deadline and parse(deadline) > now
                   for deadline in upgrade_deadlines)

    def progress(self, programs=None, count_only=True):
        """Gauge a user's progress towards program completion.

        Keyword Arguments:
            programs (list): Specific list of programs to check the user's progress
                against. If left unspecified, self.engaged_programs will be used.

            count_only (bool): Whether or not to return counts of completed, in
                progress, and unstarted courses instead of serialized representations
                of the courses.

        Returns:
            list of dict, each containing information about a user's progress
                towards completing a program.
        """
        now = datetime.datetime.now(utc)

        progress = []
        programs = programs or self.engaged_programs
        for program in programs:
            program_copy = deepcopy(program)
            completed, in_progress, not_started = [], [], []

            for course in program_copy['courses']:
                active_entitlement = CourseEntitlement.get_entitlement_if_active(
                    user=self.user, course_uuid=course['uuid'])
                if self._is_course_complete(course):
                    completed.append(course)
                elif self._is_course_enrolled(course) or active_entitlement:
                    # Show all currently enrolled courses and active entitlements as in progress
                    if active_entitlement:
                        course[
                            'course_runs'] = get_fulfillable_course_runs_for_entitlement(
                                active_entitlement, course['course_runs'])
                        course[
                            'user_entitlement'] = active_entitlement.to_dict()
                        course['enroll_url'] = reverse(
                            'entitlements_api:v1:enrollments',
                            args=[str(active_entitlement.uuid)])
                        in_progress.append(course)
                    else:
                        course_in_progress = self._is_course_in_progress(
                            now, course)
                        if course_in_progress:
                            in_progress.append(course)
                        else:
                            course['expired'] = not course_in_progress
                            not_started.append(course)
                else:
                    not_started.append(course)

            grades = {}
            for run in self.course_run_ids:
                grade = self.course_grade_factory.read(
                    self.user, course_key=CourseKey.from_string(run))
                grades[run] = grade.percent

            progress.append({
                'uuid': program_copy['uuid'],
                'completed': len(completed) if count_only else completed,
                'in_progress': len(in_progress) if count_only else in_progress,
                'not_started': len(not_started) if count_only else not_started,
                'grades': grades,
            })

        return progress

    @property
    def completed_programs_with_available_dates(self):
        """
        Calculate the available date for completed programs based on course runs.

        Returns a dict of {uuid_string: available_datetime}
        """
        # Query for all user certs up front, for performance reasons (rather than querying per course run).
        user_certificates = GeneratedCertificate.eligible_available_certificates.filter(
            user=self.user)
        certificates_by_run = {
            cert.course_id: cert
            for cert in user_certificates
        }

        completed = {}
        for program in self.programs:
            available_date = self._available_date_for_program(
                program, certificates_by_run)
            if available_date:
                completed[program['uuid']] = available_date
        return completed

    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 _course_run_mode_translation(self, course_run_mode):
        """
        Returns a canonical mode for a course run (whose data is coming from the program cache).
        This mode must match the certificate mode to be counted as complete.
        """
        mappings = {
            # Runs of type 'credit' are counted as 'verified' since verified
            # certificates are earned when credit runs are completed. LEARNER-1274
            # tracks a cleaner way to do this using the discovery service's
            # applicable_seat_types field.
            CourseMode.CREDIT_MODE:
            CourseMode.VERIFIED,
        }
        return mappings.get(course_run_mode, course_run_mode)

    def _certificate_mode_translation(self, certificate_mode):
        """
        Returns a canonical mode for a certificate (whose data is coming from the database).
        This mode must match the course run mode to be counted as complete.
        """
        mappings = {
            # Treat "no-id-professional" certificates as "professional" certificates
            CourseMode.NO_ID_PROFESSIONAL_MODE:
            CourseMode.PROFESSIONAL,
        }
        return mappings.get(certificate_mode, certificate_mode)

    def _is_course_complete(self, course):
        """Check if a user has completed a course.

        A course is completed if the user has earned a certificate for any of
        the nested course runs.

        Arguments:
            course (dict): Containing nested course runs.

        Returns:
            bool, indicating whether the course is complete.
        """
        def reshape(course_run):
            """
            Modify the structure of a course run dict to facilitate comparison
            with course run certificates.
            """
            return {
                'course_run_id': course_run['key'],
                # A course run's type is assumed to indicate which mode must be
                # completed in order for the run to count towards program completion.
                # This supports the same flexible program construction allowed by the
                # old programs service (e.g., completion of an old honor-only run may
                # count towards completion of a course in a program). This may change
                # in the future to make use of the more rigid set of "applicable seat
                # types" associated with each program type in the catalog.
                'type': self._course_run_mode_translation(course_run['type']),
            }

        return any(
            reshape(course_run) in self.completed_course_runs
            for course_run in course['course_runs'])

    @cached_property
    def completed_course_runs(self):
        """
        Determine which course runs have been completed by the user.

        Returns:
            list of dicts, each representing a course run certificate
        """
        return self.course_runs_with_state['completed']

    @cached_property
    def failed_course_runs(self):
        """
        Determine which course runs have been failed by the user.

        Returns:
            list of dicts, each a course run ID
        """
        return [
            run['course_run_id']
            for run in self.course_runs_with_state['failed']
        ]

    @cached_property
    def course_runs_with_state(self):
        """
        Determine which course runs have been completed and failed by the user.

        Returns:
            dict with a list of completed and failed runs
        """
        course_run_certificates = certificate_api.get_certificates_for_user(
            self.user.username)

        completed_runs, failed_runs = [], []
        for certificate in course_run_certificates:
            course_data = {
                'course_run_id': six.text_type(certificate['course_key']),
                'type':
                self._certificate_mode_translation(certificate['type']),
            }

            if certificate_api.is_passing_status(certificate['status']):
                completed_runs.append(course_data)
            else:
                failed_runs.append(course_data)

        return {'completed': completed_runs, 'failed': failed_runs}

    def _is_course_enrolled(self, course):
        """Check if a user is enrolled in a course.

        A user is considered to be enrolled in a course if
        they're enrolled in any of the nested course runs.

        Arguments:
            course (dict): Containing nested course runs.

        Returns:
            bool, indicating whether the course is in progress.
        """
        return any(course_run['key'] in self.course_run_ids
                   for course_run in course['course_runs'])
Exemple #14
0
 def user_has_passing_grade(self):
     """ Returns a boolean on if the effective_user has a passing grade in the course """
     course = get_course_by_id(self.course_key)
     user_grade = CourseGradeFactory().read(self.effective_user,
                                            course).percent
     return user_grade >= course.lowest_passing_grade
Exemple #15
0
    def generate(cls, _xmodule_instance_args, _entry_id, course_id,
                 _task_input, action_name):
        """
        Generate a CSV containing all students' problem grades within a given
        `course_id`.
        """
        start_time = time()
        start_date = datetime.now(UTC)
        status_interval = 100
        enrolled_students = CourseEnrollment.objects.users_enrolled_in(
            course_id, include_inactive=True)
        task_progress = TaskProgress(action_name, enrolled_students.count(),
                                     start_time)

        # This struct encapsulates both the display names of each static item in the
        # header row as values as well as the django User field names of those items
        # as the keys.  It is structured in this way to keep the values related.
        header_row = OrderedDict([('id', 'Student ID'), ('email', 'Email'),
                                  ('username', 'Username')])

        course = get_course_by_id(course_id)
        graded_scorable_blocks = cls._graded_scorable_blocks_to_header(course)

        # Just generate the static fields for now.
        rows = [
            list(header_row.values()) + ['Enrollment Status', 'Grade'] +
            _flatten(list(graded_scorable_blocks.values()))
        ]
        error_rows = [list(header_row.values()) + ['error_msg']]
        current_step = {'step': 'Calculating Grades'}

        # Bulk fetch and cache enrollment states so we can efficiently determine
        # whether each user is currently enrolled in the course.
        CourseEnrollment.bulk_fetch_enrollment_states(enrolled_students,
                                                      course_id)

        for student, course_grade, error in CourseGradeFactory().iter(
                enrolled_students, course):
            student_fields = [
                getattr(student, field_name) for field_name in header_row
            ]
            task_progress.attempted += 1

            if not course_grade:
                err_msg = text_type(error)
                # There was an error grading this student.
                if not err_msg:
                    err_msg = u'Unknown error'
                error_rows.append(student_fields + [err_msg])
                task_progress.failed += 1
                continue

            enrollment_status = _user_enrollment_status(student, course_id)

            earned_possible_values = []
            for block_location in graded_scorable_blocks:
                try:
                    problem_score = course_grade.problem_scores[block_location]
                except KeyError:
                    earned_possible_values.append(
                        [u'Not Available', u'Not Available'])
                else:
                    if problem_score.first_attempted:
                        earned_possible_values.append(
                            [problem_score.earned, problem_score.possible])
                    else:
                        earned_possible_values.append(
                            [u'Not Attempted', problem_score.possible])

            rows.append(student_fields +
                        [enrollment_status, course_grade.percent] +
                        _flatten(earned_possible_values))

            task_progress.succeeded += 1
            if task_progress.attempted % status_interval == 0:
                task_progress.update_task_state(extra_meta=current_step)

        # Perform the upload if any students have been successfully graded
        if len(rows) > 1:
            upload_csv_to_report_store(rows, 'problem_grade_report', course_id,
                                       start_date)
        # If there are any error rows, write them out as well
        if len(error_rows) > 1:
            upload_csv_to_report_store(error_rows, 'problem_grade_report_err',
                                       course_id, start_date)

        return task_progress.update_task_state(
            extra_meta={'step': 'Uploading CSV'})
Exemple #16
0
def _get_course_grade(user, course_key):
    """
    Get the user's course grade in this course run
    """
    return CourseGradeFactory().read(user, course_key=course_key)
 def get_course_grade(self):
     """
     Return CourseGrade for current user and course.
     """
     return CourseGradeFactory().read(self.student_user, self.course)
    def get(self, request, *args, **kwargs):
        course_key_string = kwargs.get('course_key_string')
        course_key = CourseKey.from_string(course_key_string)

        if not course_home_mfe_progress_tab_is_active(course_key):
            raise Http404

        # Enable NR tracing for this view based on course
        monitoring_utils.set_custom_attribute('course_id', course_key_string)
        monitoring_utils.set_custom_attribute('user_id', request.user.id)
        monitoring_utils.set_custom_attribute('is_staff',
                                              request.user.is_staff)

        _, request.user = setup_masquerade(request,
                                           course_key,
                                           staff_access=has_access(
                                               request.user, 'staff',
                                               course_key),
                                           reset_masquerade_data=True)

        course = get_course_with_access(request.user,
                                        'load',
                                        course_key,
                                        check_if_enrolled=True)

        enrollment_mode, _ = CourseEnrollment.enrollment_mode_for_user(
            request.user, course_key)

        course_grade = CourseGradeFactory().read(request.user, course)\

        descriptor = modulestore().get_course(course_key)
        grading_policy = descriptor.grading_policy

        verification_status = IDVerificationService.user_status(request.user)
        verification_link = None
        if verification_status['status'] is None or verification_status[
                'status'] == 'expired':
            verification_link = IDVerificationService.get_verify_location(
                course_id=course_key)
        elif verification_status['status'] == 'must_reverify':
            verification_link = IDVerificationService.get_verify_location(
                course_id=course_key)
        verification_data = {
            'link': verification_link,
            'status': verification_status['status'],
            'status_date': verification_status['status_date'],
        }

        data = {
            'certificate_data':
            get_cert_data(request.user, course, enrollment_mode, course_grade),
            'completion_summary':
            get_course_blocks_completion_summary(course_key, request.user),
            'course_grade':
            course_grade,
            'section_scores':
            course_grade.chapter_grades.values(),
            'enrollment_mode':
            enrollment_mode,
            'grading_policy':
            grading_policy,
            'studio_url':
            get_studio_url(course, 'settings/grading'),
            'verification_data':
            verification_data,
        }
        context = self.get_serializer_context()
        context['staff_access'] = bool(
            has_access(request.user, 'staff', course))
        context['course_key'] = course_key
        serializer = self.get_serializer_class()(data, context=context)

        return Response(serializer.data)
    def get(self, request, course_key):
        """
        Returns a gradebook entry/entries (i.e. both course and subsection-level grade data)
        for all users enrolled in a course, or a single user enrolled in a course
        if a `username` parameter is provided.

        Args:
            request: A Django request object.
            course_key: The edx course opaque key of a course object.
        """
        course = get_course_by_id(course_key, depth=None)

        # We fetch the entire course structure up-front, and use this when iterating
        # over users to determine their subsection grades.  We purposely avoid fetching
        # the user-specific course structure for each user, because that is very expensive.
        course_data = CourseData(user=None, course=course)
        graded_subsections = list(
            grades_context.graded_subsections_for_course(
                course_data.collected_structure))

        if request.GET.get('username'):
            with self._get_user_or_raise(request, course_key) as grade_user:
                course_grade = CourseGradeFactory().read(grade_user, course)

            entry = self._gradebook_entry(grade_user, course,
                                          graded_subsections, course_grade)
            serializer = StudentGradebookEntrySerializer(entry)
            return Response(serializer.data)
        else:
            q_objects = []
            if request.GET.get('user_contains'):
                search_term = request.GET.get('user_contains')
                q_objects.append(
                    Q(user__username__icontains=search_term) |
                    Q(programcourseenrollment__program_enrollment__external_user_key__icontains
                      =search_term) | Q(user__email__icontains=search_term))
            if request.GET.get('username_contains'):
                q_objects.append(
                    Q(user__username__icontains=request.GET.get(
                        'username_contains')))
            if request.GET.get('cohort_id'):
                cohort = cohorts.get_cohort_by_id(course_key,
                                                  request.GET.get('cohort_id'))
                if cohort:
                    q_objects.append(Q(user__in=cohort.users.all()))
                else:
                    q_objects.append(Q(user__in=[]))
            if request.GET.get('enrollment_mode'):
                q_objects.append(Q(mode=request.GET.get('enrollment_mode')))

            entries = []
            related_models = ['user']
            users = self._paginate_users(course_key, q_objects, related_models)

            with bulk_gradebook_view_context(course_key, users):
                for user, course_grade, exc in CourseGradeFactory().iter(
                        users,
                        course_key=course_key,
                        collected_block_structure=course_data.
                        collected_structure):
                    if not exc:
                        entry = self._gradebook_entry(user, course,
                                                      graded_subsections,
                                                      course_grade)
                        entries.append(entry)

            serializer = StudentGradebookEntrySerializer(entries, many=True)
            return self.get_paginated_response(serializer.data)
Exemple #20
0
def _get_course_grade(user, course_key):
    """
    Get the user's course grade in this course run. Note that this may be None.
    """
    return CourseGradeFactory().read(user, course_key=course_key)
Exemple #21
0
    def get(self, request, course_key):
        """
        Returns a gradebook entry/entries (i.e. both course and subsection-level grade data)
        for all users enrolled in a course, or a single user enrolled in a course
        if a `username` parameter is provided.

        Args:
            request: A Django request object.
            course_key: The edx course opaque key of a course object.
        """
        course = get_course_by_id(course_key, depth=None)

        # We fetch the entire course structure up-front, and use this when iterating
        # over users to determine their subsection grades.  We purposely avoid fetching
        # the user-specific course structure for each user, because that is very expensive.
        course_data = CourseData(user=None, course=course)
        graded_subsections = list(grades_context.graded_subsections_for_course(course_data.collected_structure))

        if request.GET.get('username'):
            with self._get_user_or_raise(request, course_key) as grade_user:
                course_grade = CourseGradeFactory().read(grade_user, course)

            entry = self._gradebook_entry(grade_user, course, graded_subsections, course_grade)
            serializer = StudentGradebookEntrySerializer(entry)
            return Response(serializer.data)
        else:
            q_objects = []
            annotations = {}
            if request.GET.get('user_contains'):
                search_term = request.GET.get('user_contains')
                q_objects.append(
                    Q(user__username__icontains=search_term) |
                    Q(programcourseenrollment__program_enrollment__external_user_key__icontains=search_term) |
                    Q(user__email__icontains=search_term)
                )
            if request.GET.get('username_contains'):
                q_objects.append(Q(user__username__icontains=request.GET.get('username_contains')))
            if request.GET.get('cohort_id'):
                cohort = cohorts.get_cohort_by_id(course_key, request.GET.get('cohort_id'))
                if cohort:
                    q_objects.append(Q(user__in=cohort.users.all()))
                else:
                    q_objects.append(Q(user__in=[]))
            if request.GET.get('enrollment_mode'):
                q_objects.append(Q(mode=request.GET.get('enrollment_mode')))
            if request.GET.get('assignment') and (
                    request.GET.get('assignment_grade_max')
                    or request.GET.get('assignment_grade_min')):
                subqueryset = PersistentSubsectionGrade.objects.annotate(
                    effective_grade_percentage=Case(
                        When(override__isnull=False,
                             then=(
                                 F('override__earned_graded_override')
                                 / F('override__possible_graded_override')
                             ) * 100),
                        default=(F('earned_graded') / F('possible_graded')) * 100
                    )
                )
                grade_conditions = {
                    'effective_grade_percentage__range': (
                        request.GET.get('assignment_grade_min', 0),
                        request.GET.get('assignment_grade_max', 100)
                    )
                }
                annotations['selected_assignment_grade_in_range'] = Exists(
                    subqueryset.filter(
                        course_id=OuterRef('course'),
                        user_id=OuterRef('user'),
                        usage_key=UsageKey.from_string(request.GET.get('assignment')),
                        **grade_conditions
                    )
                )
                q_objects.append(Q(selected_assignment_grade_in_range=True))
            if request.GET.get('course_grade_min') or request.GET.get('course_grade_max'):
                grade_conditions = {}
                q_object = Q()
                course_grade_min = request.GET.get('course_grade_min')
                if course_grade_min:
                    course_grade_min = float(request.GET.get('course_grade_min')) / 100
                    grade_conditions['percent_grade__gte'] = course_grade_min

                if request.GET.get('course_grade_max'):
                    course_grade_max = float(request.GET.get('course_grade_max')) / 100
                    grade_conditions['percent_grade__lte'] = course_grade_max

                if not course_grade_min or course_grade_min == 0:
                    subquery_grade_absent = ~Exists(
                        PersistentCourseGrade.objects.filter(
                            course_id=OuterRef('course'),
                            user_id=OuterRef('user_id'),
                        )
                    )

                    annotations['course_grade_absent'] = subquery_grade_absent
                    q_object |= Q(course_grade_absent=True)

                subquery_grade_in_range = Exists(
                    PersistentCourseGrade.objects.filter(
                        course_id=OuterRef('course'),
                        user_id=OuterRef('user_id'),
                        **grade_conditions
                    )
                )
                annotations['course_grade_in_range'] = subquery_grade_in_range
                q_object |= Q(course_grade_in_range=True)

                q_objects.append(q_object)

            entries = []
            related_models = ['user']
            users = self._paginate_users(course_key, q_objects, related_models, annotations=annotations)

            users_counts = self._get_users_counts(course_key, q_objects, annotations=annotations)

            with bulk_gradebook_view_context(course_key, users):
                for user, course_grade, exc in CourseGradeFactory().iter(
                    users, course_key=course_key, collected_block_structure=course_data.collected_structure
                ):
                    if not exc:
                        entry = self._gradebook_entry(user, course, graded_subsections, course_grade)
                        entries.append(entry)

            serializer = StudentGradebookEntrySerializer(entries, many=True)
            return self.get_paginated_response(serializer.data, **users_counts)
Exemple #22
0
def send_grade_if_interesting(user,
                              course_run_key,
                              mode,
                              status,
                              letter_grade,
                              percent_grade,
                              verbose=False):
    """ Checks if grade is interesting to Credentials and schedules a Celery task if so. """

    if verbose:
        msg = "Starting send_grade_if_interesting with params: "\
            "user [{username}], "\
            "course_run_key [{key}], "\
            "mode [{mode}], "\
            "status [{status}], "\
            "letter_grade [{letter_grade}], "\
            "percent_grade [{percent_grade}], "\
            "verbose [{verbose}]"\
            .format(
                username=getattr(user, 'username', None),
                key=str(course_run_key),
                mode=mode,
                status=status,
                letter_grade=letter_grade,
                percent_grade=percent_grade,
                verbose=verbose
            )
        logger.info(msg)
    # Avoid scheduling new tasks if certification is disabled. (Grades are a part of the records/cert story)
    if not CredentialsApiConfig.current().is_learner_issuance_enabled:
        if verbose:
            logger.info(
                "Skipping send grade: is_learner_issuance_enabled False")
        return

    # Avoid scheduling new tasks if learner records are disabled for this site.
    if not is_learner_records_enabled_for_org(course_run_key.org):
        if verbose:
            logger.info(
                "Skipping send grade: ENABLE_LEARNER_RECORDS False for org [{org}]"
                .format(org=course_run_key.org))
        return

    # Grab mode/status if we don't have them in hand
    if mode is None or status is None:
        try:
            cert = GeneratedCertificate.objects.get(user=user,
                                                    course_id=course_run_key)  # pylint: disable=no-member
            mode = cert.mode
            status = cert.status
        except GeneratedCertificate.DoesNotExist:
            # We only care about grades for which there is a certificate.
            if verbose:
                logger.info(
                    "Skipping send grade: no cert for user [{username}] & course_id [{course_id}]"
                    .format(username=getattr(user, 'username', None),
                            course_id=str(course_run_key)))
            return

    # Don't worry about whether it's available as well as awarded. Just awarded is good enough to record a verified
    # attempt at a course. We want even the grades that didn't pass the class because Credentials wants to know about
    # those too.
    if mode not in INTERESTING_MODES or status not in INTERESTING_STATUSES:
        if verbose:
            logger.info(
                "Skipping send grade: mode/status uninteresting for mode [{mode}] & status [{status}]"
                .format(mode=mode, status=status))
        return

    # If the course isn't in any program, don't bother telling Credentials about it. When Credentials grows support
    # for course records as well as program records, we'll need to open this up.
    if not is_course_run_in_a_program(course_run_key):
        if verbose:
            logger.info(
                f"Skipping send grade: course run not in a program. [{str(course_run_key)}]"
            )
        return

    # Grab grades if we don't have them in hand
    if letter_grade is None or percent_grade is None:
        grade = CourseGradeFactory().read(user,
                                          course_key=course_run_key,
                                          create_if_needed=False)
        if grade is None:
            if verbose:
                logger.info(
                    "Skipping send grade: No grade found for user [{username}] & course_id [{course_id}]"
                    .format(username=getattr(user, 'username', None),
                            course_id=str(course_run_key)))
            return
        letter_grade = grade.letter_grade
        percent_grade = grade.percent

    send_grade_to_credentials.delay(user.username, str(course_run_key), True,
                                    letter_grade, percent_grade)
Exemple #23
0
class ProgramProgressMeter(object):
    """Utility for gauging a user's progress towards program completion.

    Arguments:
        user (User): The user for which to find programs.

    Keyword Arguments:
        enrollments (list): List of the user's enrollments.
        uuid (str): UUID identifying a specific program. If provided, the meter
            will only inspect this one program, not all programs the user may be
            engaged with.
    """
    def __init__(self, site, user, enrollments=None, uuid=None, mobile_only=False):
        self.site = site
        self.user = user
        self.mobile_only = mobile_only

        self.enrollments = enrollments or list(CourseEnrollment.enrollments_for_user(self.user))
        self.enrollments.sort(key=lambda e: e.created, reverse=True)

        self.enrolled_run_modes = {}
        self.course_run_ids = []
        for enrollment in self.enrollments:
            # enrollment.course_id is really a CourseKey (╯ಠ_ಠ)╯︵ ┻━┻
            enrollment_id = unicode(enrollment.course_id)
            mode = enrollment.mode
            if mode == CourseMode.NO_ID_PROFESSIONAL_MODE:
                mode = CourseMode.PROFESSIONAL
            self.enrolled_run_modes[enrollment_id] = mode
            # We can't use dict.keys() for this because the course run ids need to be ordered
            self.course_run_ids.append(enrollment_id)

        self.entitlements = list(CourseEntitlement.unexpired_entitlements_for_user(self.user))
        self.course_uuids = [str(entitlement.course_uuid) for entitlement in self.entitlements]

        self.course_grade_factory = CourseGradeFactory()

        if uuid:
            self.programs = [get_programs(uuid=uuid)]
        else:
            self.programs = attach_program_detail_url(get_programs(self.site), self.mobile_only)

    def invert_programs(self):
        """Intersect programs and enrollments.

        Builds a dictionary of program dict lists keyed by course run ID and by course UUID.
        The resulting dictionary is suitable in applications where programs must be
        filtered by the course runs or courses they contain (e.g., the student dashboard).

        Returns:
            defaultdict, programs keyed by course run ID
        """
        inverted_programs = defaultdict(list)

        for program in self.programs:
            for course in program['courses']:
                course_uuid = course['uuid']
                if course_uuid in self.course_uuids:
                    program_list = inverted_programs[course_uuid]
                    if program not in program_list:
                        program_list.append(program)
                for course_run in course['course_runs']:
                    course_run_id = course_run['key']
                    if course_run_id in self.course_run_ids:
                        program_list = inverted_programs[course_run_id]
                        if program not in program_list:
                            program_list.append(program)

        # Sort programs by title for consistent presentation.
        for program_list in inverted_programs.itervalues():
            program_list.sort(key=lambda p: p['title'])

        return inverted_programs

    @cached_property
    def engaged_programs(self):
        """Derive a list of programs in which the given user is engaged.

        Returns:
            list of program dicts, ordered by most recent enrollment
        """
        inverted_programs = self.invert_programs()

        programs = []
        # Remember that these course run ids are derived from a list of
        # enrollments sorted from most recent to least recent. Iterating
        # over the values in inverted_programs alone won't yield a program
        # ordering consistent with the user's enrollments.
        for course_run_id in self.course_run_ids:
            for program in inverted_programs[course_run_id]:
                # Dicts aren't a hashable type, so we can't use a set. Sets also
                # aren't ordered, which is important here.
                if program not in programs:
                    programs.append(program)

        for course_uuid in self.course_uuids:
            for program in inverted_programs[course_uuid]:
                if program not in programs:
                    programs.append(program)

        return programs

    def _is_course_in_progress(self, now, course):
        """Check if course qualifies as in progress as part of the program.

        A course is considered to be in progress if a user is enrolled in a run
        of the correct mode or a run of the correct mode is still available for enrollment.

        Arguments:
            now (datetime): datetime for now
            course (dict): Containing nested course runs.

        Returns:
            bool, indicating whether the course is in progress.
        """
        enrolled_runs = [run for run in course['course_runs'] if run['key'] in self.course_run_ids]

        # Check if the user is enrolled in a required run and mode/seat.
        runs_with_required_mode = [
            run for run in enrolled_runs
            if run['type'] == self.enrolled_run_modes[run['key']]
        ]

        if runs_with_required_mode:
            not_failed_runs = [run for run in runs_with_required_mode if run not in self.failed_course_runs]
            if not_failed_runs:
                return True

        # Check if seats required for course completion are still available.
        upgrade_deadlines = []
        for run in enrolled_runs:
            for seat in run['seats']:
                if seat['type'] == run['type'] and run['type'] != self.enrolled_run_modes[run['key']]:
                    upgrade_deadlines.append(seat['upgrade_deadline'])

        # An upgrade deadline of None means the course is always upgradeable.
        return any(not deadline or deadline and parse(deadline) > now for deadline in upgrade_deadlines)

    def progress(self, programs=None, count_only=True):
        """Gauge a user's progress towards program completion.

        Keyword Arguments:
            programs (list): Specific list of programs to check the user's progress
                against. If left unspecified, self.engaged_programs will be used.

            count_only (bool): Whether or not to return counts of completed, in
                progress, and unstarted courses instead of serialized representations
                of the courses.

        Returns:
            list of dict, each containing information about a user's progress
                towards completing a program.
        """
        now = datetime.datetime.now(utc)

        progress = []
        programs = programs or self.engaged_programs
        for program in programs:
            program_copy = deepcopy(program)
            completed, in_progress, not_started = [], [], []

            for course in program_copy['courses']:
                active_entitlement = CourseEntitlement.get_entitlement_if_active(
                    user=self.user,
                    course_uuid=course['uuid']
                )
                if self._is_course_complete(course):
                    completed.append(course)
                elif self._is_course_enrolled(course) or active_entitlement:
                    # Show all currently enrolled courses and active entitlements as in progress
                    if active_entitlement:
                        course['course_runs'] = get_fulfillable_course_runs_for_entitlement(
                            active_entitlement,
                            course['course_runs']
                        )
                        course['user_entitlement'] = active_entitlement.to_dict()
                        course['enroll_url'] = reverse(
                            'entitlements_api:v1:enrollments',
                            args=[str(active_entitlement.uuid)]
                        )
                        in_progress.append(course)
                    else:
                        course_in_progress = self._is_course_in_progress(now, course)
                        if course_in_progress:
                            in_progress.append(course)
                        else:
                            course['expired'] = not course_in_progress
                            not_started.append(course)
                else:
                    not_started.append(course)

            grades = {}
            for run in self.course_run_ids:
                grade = self.course_grade_factory.read(self.user, course_key=CourseKey.from_string(run))
                grades[run] = grade.percent

            progress.append({
                'uuid': program_copy['uuid'],
                'completed': len(completed) if count_only else completed,
                'in_progress': len(in_progress) if count_only else in_progress,
                'not_started': len(not_started) if count_only else not_started,
                'grades': grades,
            })

        return progress

    @property
    def completed_programs_with_available_dates(self):
        """
        Calculate the available date for completed programs based on course runs.

        Returns a dict of {uuid_string: available_datetime}
        """
        # Query for all user certs up front, for performance reasons (rather than querying per course run).
        user_certificates = GeneratedCertificate.eligible_available_certificates.filter(user=self.user)
        certificates_by_run = {cert.course_id: cert for cert in user_certificates}

        completed = {}
        for program in self.programs:
            available_date = self._available_date_for_program(program, certificates_by_run)
            if available_date:
                completed[program['uuid']] = available_date
        return completed

    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 _course_run_mode_translation(self, course_run_mode):
        """
        Returns a canonical mode for a course run (whose data is coming from the program cache).
        This mode must match the certificate mode to be counted as complete.
        """
        mappings = {
            # Runs of type 'credit' are counted as 'verified' since verified
            # certificates are earned when credit runs are completed. LEARNER-1274
            # tracks a cleaner way to do this using the discovery service's
            # applicable_seat_types field.
            CourseMode.CREDIT_MODE: CourseMode.VERIFIED,
        }
        return mappings.get(course_run_mode, course_run_mode)

    def _certificate_mode_translation(self, certificate_mode):
        """
        Returns a canonical mode for a certificate (whose data is coming from the database).
        This mode must match the course run mode to be counted as complete.
        """
        mappings = {
            # Treat "no-id-professional" certificates as "professional" certificates
            CourseMode.NO_ID_PROFESSIONAL_MODE: CourseMode.PROFESSIONAL,
        }
        return mappings.get(certificate_mode, certificate_mode)

    def _is_course_complete(self, course):
        """Check if a user has completed a course.

        A course is completed if the user has earned a certificate for any of
        the nested course runs.

        Arguments:
            course (dict): Containing nested course runs.

        Returns:
            bool, indicating whether the course is complete.
        """

        def reshape(course_run):
            """
            Modify the structure of a course run dict to facilitate comparison
            with course run certificates.
            """
            return {
                'course_run_id': course_run['key'],
                # A course run's type is assumed to indicate which mode must be
                # completed in order for the run to count towards program completion.
                # This supports the same flexible program construction allowed by the
                # old programs service (e.g., completion of an old honor-only run may
                # count towards completion of a course in a program). This may change
                # in the future to make use of the more rigid set of "applicable seat
                # types" associated with each program type in the catalog.
                'type': self._course_run_mode_translation(course_run['type']),
            }

        return any(reshape(course_run) in self.completed_course_runs for course_run in course['course_runs'])

    @cached_property
    def completed_course_runs(self):
        """
        Determine which course runs have been completed by the user.

        Returns:
            list of dicts, each representing a course run certificate
        """
        return self.course_runs_with_state['completed']

    @cached_property
    def failed_course_runs(self):
        """
        Determine which course runs have been failed by the user.

        Returns:
            list of dicts, each a course run ID
        """
        return [run['course_run_id'] for run in self.course_runs_with_state['failed']]

    @cached_property
    def course_runs_with_state(self):
        """
        Determine which course runs have been completed and failed by the user.

        Returns:
            dict with a list of completed and failed runs
        """
        course_run_certificates = certificate_api.get_certificates_for_user(self.user.username)

        completed_runs, failed_runs = [], []
        for certificate in course_run_certificates:
            course_data = {
                'course_run_id': unicode(certificate['course_key']),
                'type': self._certificate_mode_translation(certificate['type']),
            }

            if certificate_api.is_passing_status(certificate['status']):
                completed_runs.append(course_data)
            else:
                failed_runs.append(course_data)

        return {'completed': completed_runs, 'failed': failed_runs}

    def _is_course_enrolled(self, course):
        """Check if a user is enrolled in a course.

        A user is considered to be enrolled in a course if
        they're enrolled in any of the nested course runs.

        Arguments:
            course (dict): Containing nested course runs.

        Returns:
            bool, indicating whether the course is in progress.
        """
        return any(course_run['key'] in self.course_run_ids for course_run in course['course_runs'])
Exemple #24
0
 def user_has_passing_grade(self):
     """ Returns a boolean on if the effective_user has a passing grade in the course """
     if not self.effective_user.is_anonymous:
         user_grade = CourseGradeFactory().read(self.effective_user, self.course).percent
         return user_grade >= self.course.lowest_passing_grade
     return False