Ejemplo n.º 1
0
    def get_progress_data(self, course_enrollment):
        """
        TODO: Add this to metrics, then we'll need to store per-user progress data
        For initial implementation, we get the

        TODO: We will cache course grades, so we'll refactor this method to  use
        the cache, so we'll likely change the call to LearnerCourseGrades
        """
        cert = GeneratedCertificate.objects.filter(
            user=course_enrollment.user,
            course_id=course_enrollment.course_id,
            )

        if cert:
            course_completed = cert[0].created_date
        else:
            course_completed = False

        # Default values if we can't retrieve progress data
        progress_percent = 0.0
        course_progress_details = None

        try:
            obj = LearnerCourseGradeMetrics.objects.most_recent_for_learner_course(
                user=course_enrollment.user,
                course_id=str(course_enrollment.course_id))
            if obj:
                progress_percent = obj.progress_percent
                course_progress_details = obj.progress_details
        except Exception as e:  # pylint: disable=broad-except
            # TODO: Use more specific database-related exception
            error_data = dict(
                msg='Exception trying to get learner course metrics',
                username=course_enrollment.user.username,
                course_id=str(course_enrollment.course_id),
                exception=str(e)
                )
            log_error(
                error_data=error_data,
                error_type=PipelineError.UNSPECIFIED_DATA,
                )

        # Empty list initially, then will fill after we implement capturing
        # learner specific progress
        course_progress_history = []

        data = dict(
            course_completed=course_completed,
            course_progress=progress_percent,
            course_progress_details=course_progress_details,
            course_progress_history=course_progress_history,
            )
        return data
Ejemplo n.º 2
0
def bulk_calculate_course_progress_data(course_id, date_for=None):
    """Calculates the average progress for a set of course enrollments

    How it works
    1. collects progress percent for each course enrollment
        1.1. If up to date enrollment metrics record already exists, use that
    2. calculate and return the average of these enrollments

    TODO: Update to filter on active users

    Questions:
    - What makes a learner an active learner?

    """
    progress_percentages = []

    if not date_for:
        date_for = datetime.utcnow().replace(tzinfo=utc).date()

    site = get_site_for_course(course_id)
    if not site:
        raise UnlinkedCourseError(
            'No site found for course "{}"'.format(course_id))

    course_sm = get_student_modules_for_course_in_site(site=site,
                                                       course_id=course_id)
    for ce in course_enrollments_for_course(course_id):
        metrics = collect_metrics_for_enrollment(site=site,
                                                 course_enrollment=ce,
                                                 course_sm=course_sm,
                                                 date_for=date_for)
        if metrics:
            progress_percentages.append(metrics.progress_percent)
        else:
            # Log this for troubleshooting
            error_data = dict(
                msg=('Unable to create or retrieve enrollment metrics ' +
                     'for user {} and course {}'.format(
                         ce.user.username, str(ce.course_id))))
            log_error(error_data=error_data,
                      error_type=PipelineError.COURSE_DATA,
                      user=ce.user,
                      course_id=str(course_id))

    return dict(
        average_progress=calculate_average_progress(progress_percentages), )
Ejemplo n.º 3
0
def get_average_progress_deprecated(course_id, date_for, course_enrollments):
    """Collects and aggregates raw course grades data
    """
    progress = []
    for ce in course_enrollments:
        try:
            course_progress = figures.metrics.LearnerCourseGrades.course_progress(
                ce)
            figures.pipeline.loaders.save_learner_course_grades(
                site=figures.sites.get_site_for_course(course_id),
                date_for=date_for,
                course_enrollment=ce,
                course_progress_details=course_progress[
                    'course_progress_details'])
        # TODO: Use more specific database-related exception
        except Exception as e:  # pylint: disable=broad-except
            error_data = dict(
                msg='Unable to get course blocks',
                username=ce.user.username,
                course_id=str(ce.course_id),
                exception=str(e),
            )
            log_error(
                error_data=error_data,
                error_type=PipelineError.GRADES_DATA,
                user=ce.user,
                course_id=ce.course_id,
            )
            course_progress = dict(progress_percent=0.0,
                                   course_progress_details=None)
        if course_progress:
            progress.append(course_progress)

    if progress:
        progress_percent = [rec['progress_percent'] for rec in progress]
        average_progress = float(sum(progress_percent)) / float(
            len(progress_percent))
        average_progress = float(
            Decimal(average_progress).quantize(Decimal('.00')))
    else:
        average_progress = 0.0

    return average_progress
Ejemplo n.º 4
0
def get_average_progress(course_id, date_for, course_enrollments):
    """Collects and aggregates raw course grades data
    """
    progress = []

    for ce in course_enrollments:
        try:
            course_progress = figures.metrics.LearnerCourseGrades.course_progress(
                ce)
            figures.pipeline.loaders.save_learner_course_grades(
                date_for=date_for,
                course_enrollment=ce,
                course_progress_details=course_progress[
                    'course_progress_details'])
        except Exception as e:
            error_data = dict(
                msg='Unable to get course blocks',
                username=ce.user.username,
                course_id=str(ce.course_id),
                exception=str(e),
            )
            log_error(
                error_data=error_data,
                error_type=PipelineError.GRADES_DATA,
                user=ce.user,
                course_id=ce.course_id,
            )
            course_progress = dict(progress_percent=0.0,
                                   course_progress_details=None)
        if course_progress:
            progress.append(course_progress)
    if len(progress):
        progress_percent = [rec['progress_percent'] for rec in progress]
        average_progress = float(sum(progress_percent)) / float(
            len(progress_percent))
    else:
        average_progress = 0.0

    return average_progress
Ejemplo n.º 5
0
def _enrollment_metrics_needs_update(most_recent_lcgm, most_recent_sm):
    """Returns True if we need to update our learner progress, False otherwise

    See the #Logic section in this module's docstring

    If we need to check that the records match the same user and course, we
    can do something like:

    ```
    class RecordMismatchError(Exception):
        pass


    def rec_matches_user_and_course(lcgm, sm):
        return lcgm.user == sm.student and lcgm.course_id == sm.course_id
    ```

    And in this function add the check when we have both records:

    ```
        if not rec_matches_user_and_course(most_recent_lcgm, most_recent_sm):
            rec_msg = '{}(user_id={}, course_id="{}"'
            msg1 = rec_msg.format('lcgm',
                                  most_recent_lcgm.user.id,
                                  most_recent_lcgm.course_id)
            msg2 = rec_msg.format('sm',
                                  most_recent_sm.student.id,
                                  most_recent_sm.course_id)
            raise RecordMismatchError(msg1 + ':' + msg2)
    ```
    """
    # First assume we need to update the enrollment metrics record
    needs_update = True
    if not most_recent_lcgm and not most_recent_sm:
        # Learner has not started coursework
        needs_update = False
    elif most_recent_lcgm and most_recent_sm:
        # Learner has past course activity
        needs_update = most_recent_lcgm.date_for < most_recent_sm.modified.date(
        )
    elif not most_recent_lcgm and most_recent_sm:
        # No LCGM recs, so Learner started on course after last collection
        # This could also happen
        # If this is the irst time collection is run for the learner+course
        # if an unhandled error prevents LCGM from saving
        # if the learner's LCGM recs were deleted
        needs_update = True
    elif most_recent_lcgm and not most_recent_sm:
        # This shouldn't happen. We log this state as an error

        # using 'COURSE_DATA' for the pipeline error type. Although we should
        # revisit logging and error tracking in Figures to define a clear
        # approach that has clear an intuitive contexts for the logging event
        #
        # We neede to decide:
        #
        # 1. Is this always an error state? Could this condition happen naturally?
        #
        # 2. Which error type should be created and what is the most applicable
        #    context
        #    Candidates
        #    - Enrollment (learner+course) context
        #    - Data state context - Why would we have an LCGM
        #
        # So we hold off updating PipelineError error choises initially until
        # we can think carefully on how we tag pipeline errors
        #
        error_data = dict(
            msg='LearnerCourseGradeMetrics record exists without StudentModule',
        )
        log_error(error_data=error_data,
                  error_type=PipelineError.COURSE_DATA,
                  user=most_recent_lcgm.user,
                  course_id=str(most_recent_lcgm.course_id))
        needs_update = False
    return needs_update
Ejemplo n.º 6
0
 def test_logging_to_model_with_kwargs(self, dict_args):
     assert PipelineError.objects.count() == 0
     logger.log_error(self.error_data, **dict_args)
Ejemplo n.º 7
0
 def test_logging_to_model(self):
     assert PipelineError.objects.count() == 0
     logger.log_error(self.error_data)
Ejemplo n.º 8
0
 def test_logging_to_logger(self):
     assert PipelineError.objects.count() == 0
     env_tokens = {'LOG_PIPELINE_ERRORS_TO_DB': False}
     with mock.patch('figures.settings.env_tokens', env_tokens):
         logger.log_error(self.error_data)
         assert PipelineError.objects.count() == 0
Ejemplo n.º 9
0
 def test_logging_to_logger(self):
     assert PipelineError.objects.count() == 0
     features = {'FIGURES_LOG_PIPELINE_ERRORS_TO_DB': False}
     with mock.patch('figures.helpers.settings.FEATURES', features):
         logger.log_error(self.error_data)
         assert PipelineError.objects.count() == 0