Example #1
0
class StudentModuleHistoryExtended(BaseStudentModuleHistory):
    """Keeps a complete history of state changes for a given XModule for a given
    Student. Right now, we restrict this to problems so that the table doesn't
    explode in size.

    This new extended CSMH has a larger primary key that won't run out of space
    so quickly."""
    class Meta(object):
        app_label = 'coursewarehistoryextended'
        get_latest_by = "created"
        index_together = ['student_module']

    id = UnsignedBigIntAutoField(primary_key=True)  # pylint: disable=invalid-name

    student_module = models.ForeignKey(StudentModule,
                                       db_index=True,
                                       db_constraint=False,
                                       on_delete=models.DO_NOTHING)

    @receiver(post_save, sender=StudentModule)
    def save_history(sender, instance, **kwargs):  # pylint: disable=no-self-argument, unused-argument
        """
        Checks the instance's module_type, and creates & saves a
        StudentModuleHistoryExtended entry if the module_type is one that
        we save.
        """
        if instance.module_type in StudentModuleHistoryExtended.HISTORY_SAVING_TYPES:
            history_entry = StudentModuleHistoryExtended(
                student_module=instance,
                version=None,
                created=instance.modified,
                state=instance.state,
                grade=instance.grade,
                max_grade=instance.max_grade)
            history_entry.save()

    @receiver(post_delete, sender=StudentModule)
    def delete_history(sender, instance, **kwargs):  # pylint: disable=no-self-argument, unused-argument
        """
        Django can't cascade delete across databases, so we tell it at the model level to
        on_delete=DO_NOTHING and then listen for post_delete so we can clean up the CSMHE rows.
        """
        StudentModuleHistoryExtended.objects.filter(
            student_module=instance).all().delete()

    def __unicode__(self):
        return unicode(repr(self))
Example #2
0
class PersistentCourseGrade(TimeStampedModel):
    """
    A django model tracking persistent course grades.

    .. no_pii:
    """
    class Meta(object):
        app_label = "grades"
        # Indices:
        # (course_id, user_id) for individual grades
        # (course_id) for instructors to see all course grades, implicitly created via the unique_together constraint
        # (user_id) for course dashboard; explicitly declared as an index below
        # (passed_timestamp, course_id) for tracking when users first earned a passing grade.
        # (modified): find all the grades updated within a certain timespan
        # (modified, course_id): find all the grades updated within a certain timespan for a course
        unique_together = [
            ('course_id', 'user_id'),
        ]
        index_together = [('passed_timestamp', 'course_id'),
                          ('modified', 'course_id')]

    # primary key will need to be large for this table
    id = UnsignedBigIntAutoField(primary_key=True)  # pylint: disable=invalid-name
    user_id = models.IntegerField(blank=False, db_index=True)
    course_id = CourseKeyField(blank=False, max_length=255)

    # Information relating to the state of content when grade was calculated
    course_edited_timestamp = models.DateTimeField(
        u'Last content edit timestamp', blank=True, null=True)
    course_version = models.CharField(u'Course content version identifier',
                                      blank=True,
                                      max_length=255)
    grading_policy_hash = models.CharField(u'Hash of grading policy',
                                           blank=False,
                                           max_length=255)

    # Information about the course grade itself
    percent_grade = models.FloatField(blank=False)
    letter_grade = models.CharField(u'Letter grade for course',
                                    blank=False,
                                    max_length=255)

    # Information related to course completion
    passed_timestamp = models.DateTimeField(
        u'Date learner earned a passing grade', blank=True, null=True)

    _CACHE_NAMESPACE = u"grades.models.PersistentCourseGrade"

    def __unicode__(self):
        """
        Returns a string representation of this model.
        """
        return u', '.join([
            u"{} user: {}".format(type(self).__name__, self.user_id),
            u"course version: {}".format(self.course_version),
            u"grading policy: {}".format(self.grading_policy_hash),
            u"percent grade: {}%".format(self.percent_grade),
            u"letter grade: {}".format(self.letter_grade),
            u"passed timestamp: {}".format(self.passed_timestamp),
        ])

    @classmethod
    def prefetch(cls, course_id, users):
        """
        Prefetches grades for the given users for the given course.
        """
        get_cache(cls._CACHE_NAMESPACE)[cls._cache_key(course_id)] = {
            grade.user_id: grade
            for grade in cls.objects.filter(
                user_id__in=[user.id for user in users], course_id=course_id)
        }

    @classmethod
    def clear_prefetched_data(cls, course_key):
        """
        Clears prefetched grades for this course from the RequestCache.
        """
        get_cache(cls._CACHE_NAMESPACE).pop(cls._cache_key(course_key), None)

    @classmethod
    def read(cls, user_id, course_id):
        """
        Reads a grade from database

        Arguments:
            user_id: The user associated with the desired grade
            course_id: The id of the course associated with the desired grade

        Raises PersistentCourseGrade.DoesNotExist if applicable
        """
        try:
            prefetched_grades = get_cache(
                cls._CACHE_NAMESPACE)[cls._cache_key(course_id)]
            try:
                return prefetched_grades[user_id]
            except KeyError:
                # user's grade is not in the prefetched dict, so
                # assume they have no grade
                raise cls.DoesNotExist
        except KeyError:
            # grades were not prefetched for the course, so fetch it
            return cls.objects.get(user_id=user_id, course_id=course_id)

    @classmethod
    def update_or_create(cls, user_id, course_id, **kwargs):
        """
        Creates a course grade in the database.
        Returns a PersistedCourseGrade object.
        """
        passed = kwargs.pop('passed')

        if kwargs.get('course_version', None) is None:
            kwargs['course_version'] = ""

        grade, _ = cls.objects.update_or_create(user_id=user_id,
                                                course_id=course_id,
                                                defaults=kwargs)
        if passed and not grade.passed_timestamp:
            grade.passed_timestamp = now()
            grade.save()

        cls._emit_grade_calculated_event(grade)
        cls._update_cache(course_id, user_id, grade)
        return grade

    @classmethod
    def _update_cache(cls, course_id, user_id, grade):
        course_cache = get_cache(cls._CACHE_NAMESPACE).get(
            cls._cache_key(course_id))
        if course_cache is not None:
            course_cache[user_id] = grade

    @classmethod
    def _cache_key(cls, course_id):
        return u"grades_cache.{}".format(course_id)

    @staticmethod
    def _emit_grade_calculated_event(grade):
        events.course_grade_calculated(grade)
Example #3
0
class PersistentSubsectionGrade(TimeStampedModel):
    """
    A django model tracking persistent grades at the subsection level.

    .. no_pii:
    """
    class Meta(object):
        app_label = "grades"
        unique_together = [
            # * Specific grades can be pulled using all three columns,
            # * Progress page can pull all grades for a given (course_id, user_id)
            # * Course staff can see all grades for a course using (course_id,)
            ('course_id', 'user_id', 'usage_key'),
        ]
        # Allows querying in the following ways:
        # (modified): find all the grades updated within a certain timespan
        # (modified, course_id): find all the grades updated within a timespan for a certain course
        # (modified, course_id, usage_key): find all the grades updated within a timespan for a subsection
        #   in a course
        # (first_attempted, course_id, user_id): find all attempted subsections in a course for a user
        # (first_attempted, course_id): find all attempted subsections in a course for all users
        index_together = [('modified', 'course_id', 'usage_key'),
                          ('first_attempted', 'course_id', 'user_id')]

    # primary key will need to be large for this table
    id = UnsignedBigIntAutoField(primary_key=True)  # pylint: disable=invalid-name

    user_id = models.IntegerField(blank=False)
    course_id = CourseKeyField(blank=False, max_length=255)

    # note: the usage_key may not have the run filled in for
    # old mongo courses.  Use the full_usage_key property
    # instead when you want to use/compare the usage_key.
    usage_key = UsageKeyField(blank=False, max_length=255)

    # Information relating to the state of content when grade was calculated
    subtree_edited_timestamp = models.DateTimeField(
        u'Last content edit timestamp', blank=True, null=True)
    course_version = models.CharField(u'Guid of latest course version',
                                      blank=True,
                                      max_length=255)

    # earned/possible refers to the number of points achieved and available to achieve.
    # graded refers to the subset of all problems that are marked as being graded.
    earned_all = models.FloatField(blank=False)
    possible_all = models.FloatField(blank=False)
    earned_graded = models.FloatField(blank=False)
    possible_graded = models.FloatField(blank=False)

    # timestamp for the learner's first attempt at content in
    # this subsection. If null, indicates no attempt
    # has yet been made.
    first_attempted = models.DateTimeField(null=True, blank=True)

    # track which blocks were visible at the time of grade calculation
    visible_blocks = models.ForeignKey(VisibleBlocks,
                                       db_column='visible_blocks_hash',
                                       to_field='hashed',
                                       on_delete=models.CASCADE)

    _CACHE_NAMESPACE = u'grades.models.PersistentSubsectionGrade'

    @property
    def full_usage_key(self):
        """
        Returns the "correct" usage key value with the run filled in.
        """
        if self.usage_key.run is None:
            # pylint: disable=unexpected-keyword-arg,no-value-for-parameter
            return self.usage_key.replace(course_key=self.course_id)
        else:
            return self.usage_key

    def __unicode__(self):
        """
        Returns a string representation of this model.
        """
        return (
            u"{} user: {}, course version: {}, subsection: {} ({}). {}/{} graded, {}/{} all, first_attempted: {}"
        ).format(
            type(self).__name__,
            self.user_id,
            self.course_version,
            self.usage_key,
            self.visible_blocks_id,
            self.earned_graded,
            self.possible_graded,
            self.earned_all,
            self.possible_all,
            self.first_attempted,
        )

    @classmethod
    def prefetch(cls, course_key, users):
        """
        Prefetches grades for the given users in the given course.
        """
        cache_key = cls._cache_key(course_key)
        get_cache(cls._CACHE_NAMESPACE)[cache_key] = defaultdict(list)
        cached_grades = get_cache(cls._CACHE_NAMESPACE)[cache_key]
        queryset = cls.objects.select_related(
            'visible_blocks', 'override').filter(
                user_id__in=[user.id for user in users],
                course_id=course_key,
            )
        for record in queryset:
            cached_grades[record.user_id].append(record)

    @classmethod
    def clear_prefetched_data(cls, course_key):
        """
        Clears prefetched grades for this course from the RequestCache.
        """
        get_cache(cls._CACHE_NAMESPACE).pop(cls._cache_key(course_key), None)

    @classmethod
    def read_grade(cls, user_id, usage_key):
        """
        Reads a grade from database

        Arguments:
            user_id: The user associated with the desired grade
            usage_key: The location of the subsection associated with the desired grade

        Raises PersistentSubsectionGrade.DoesNotExist if applicable
        """
        return cls.objects.select_related('visible_blocks', 'override').get(
            user_id=user_id,
            course_id=usage_key.
            course_key,  # course_id is included to take advantage of db indexes
            usage_key=usage_key,
        )

    @classmethod
    def bulk_read_grades(cls, user_id, course_key):
        """
        Reads all grades for the given user and course.

        Arguments:
            user_id: The user associated with the desired grades
            course_key: The course identifier for the desired grades
        """
        try:
            prefetched_grades = get_cache(
                cls._CACHE_NAMESPACE)[cls._cache_key(course_key)]
            try:
                return prefetched_grades[user_id]
            except KeyError:
                # The user's grade is not in the cached dict of subsection grades,
                # so return an empty list.
                return []
        except KeyError:
            # subsection grades were not prefetched for the course, so get them from the DB
            return cls.objects.select_related('visible_blocks',
                                              'override').filter(
                                                  user_id=user_id,
                                                  course_id=course_key,
                                              )

    @classmethod
    def update_or_create_grade(cls, **params):
        """
        Wrapper for objects.update_or_create.
        """
        cls._prepare_params(params)
        VisibleBlocks.cached_get_or_create(params['user_id'],
                                           params['visible_blocks'])
        cls._prepare_params_visible_blocks_id(params)

        # TODO: do we NEED to pop these?
        first_attempted = params.pop('first_attempted')
        user_id = params.pop('user_id')
        usage_key = params.pop('usage_key')

        grade, _ = cls.objects.update_or_create(
            user_id=user_id,
            course_id=usage_key.course_key,
            usage_key=usage_key,
            defaults=params,
        )
        grade.override = PersistentSubsectionGradeOverride.get_override(
            user_id, usage_key)
        if first_attempted is not None and grade.first_attempted is None:
            grade.first_attempted = first_attempted
            grade.save()

        cls._emit_grade_calculated_event(grade)
        return grade

    @classmethod
    def bulk_create_grades(cls, grade_params_iter, user_id, course_key):
        """
        Bulk creation of grades.
        """
        if not grade_params_iter:
            return

        PersistentSubsectionGradeOverride.prefetch(user_id, course_key)

        list(map(cls._prepare_params, grade_params_iter))
        VisibleBlocks.bulk_get_or_create(
            user_id, course_key,
            [params['visible_blocks'] for params in grade_params_iter])
        list(map(cls._prepare_params_visible_blocks_id, grade_params_iter))

        grades = [
            PersistentSubsectionGrade(**params) for params in grade_params_iter
        ]
        grades = cls.objects.bulk_create(grades)
        for grade in grades:
            cls._emit_grade_calculated_event(grade)
        return grades

    @classmethod
    def _prepare_params(cls, params):
        """
        Prepares the fields for the grade record.
        """
        if not params.get('course_id', None):
            params['course_id'] = params['usage_key'].course_key
        params['course_version'] = params.get('course_version', None) or ""
        params['visible_blocks'] = BlockRecordList.from_list(
            params['visible_blocks'], params['course_id'])

    @classmethod
    def _prepare_params_visible_blocks_id(cls, params):
        """
        Prepares the visible_blocks_id field for the grade record,
        using the hash of the visible_blocks field.  Specifying
        the hashed field eliminates extra queries to get the
        VisibleBlocks record.  Use this variation of preparing
        the params when you are sure of the existence of the
        VisibleBlock.
        """
        params['visible_blocks_id'] = params['visible_blocks'].hash_value
        del params['visible_blocks']

    @staticmethod
    def _emit_grade_calculated_event(grade):
        events.subsection_grade_calculated(grade)

    @classmethod
    def _cache_key(cls, course_id):
        return u"subsection_grades_cache.{}".format(course_id)
Example #4
0
class PersistentSubsectionGrade(DeleteGradesMixin, TimeStampedModel):
    """
    A django model tracking persistent grades at the subsection level.
    """

    class Meta(object):
        app_label = "grades"
        unique_together = [
            # * Specific grades can be pulled using all three columns,
            # * Progress page can pull all grades for a given (course_id, user_id)
            # * Course staff can see all grades for a course using (course_id,)
            ('course_id', 'user_id', 'usage_key'),
        ]
        # Allows querying in the following ways:
        # (modified): find all the grades updated within a certain timespan
        # (modified, course_id): find all the grades updated within a timespan for a certain course
        # (modified, course_id, usage_key): find all the grades updated within a timespan for a subsection
        #   in a course
        # (first_attempted, course_id, user_id): find all attempted subsections in a course for a user
        # (first_attempted, course_id): find all attempted subsections in a course for all users
        index_together = [
            ('modified', 'course_id', 'usage_key'),
            ('first_attempted', 'course_id', 'user_id')
        ]

    # primary key will need to be large for this table
    id = UnsignedBigIntAutoField(primary_key=True)  # pylint: disable=invalid-name

    user_id = models.IntegerField(blank=False)
    course_id = CourseKeyField(blank=False, max_length=255)

    # note: the usage_key may not have the run filled in for
    # old mongo courses.  Use the full_usage_key property
    # instead when you want to use/compare the usage_key.
    usage_key = UsageKeyField(blank=False, max_length=255)

    # Information relating to the state of content when grade was calculated
    subtree_edited_timestamp = models.DateTimeField(u'Last content edit timestamp', blank=True, null=True)
    course_version = models.CharField(u'Guid of latest course version', blank=True, max_length=255)

    # earned/possible refers to the number of points achieved and available to achieve.
    # graded refers to the subset of all problems that are marked as being graded.
    earned_all = models.FloatField(blank=False)
    possible_all = models.FloatField(blank=False)
    earned_graded = models.FloatField(blank=False)
    possible_graded = models.FloatField(blank=False)

    # timestamp for the learner's first attempt at content in
    # this subsection. If null, indicates no attempt
    # has yet been made.
    first_attempted = models.DateTimeField(null=True, blank=True)

    # track which blocks were visible at the time of grade calculation
    visible_blocks = models.ForeignKey(VisibleBlocks, db_column='visible_blocks_hash', to_field='hashed')

    @property
    def full_usage_key(self):
        """
        Returns the "correct" usage key value with the run filled in.
        """
        if self.usage_key.run is None:  # pylint: disable=no-member
            return self.usage_key.replace(course_key=self.course_id)
        else:
            return self.usage_key

    def __unicode__(self):
        """
        Returns a string representation of this model.
        """
        return (
            u"{} user: {}, course version: {}, subsection: {} ({}). {}/{} graded, {}/{} all, first_attempted: {}"
        ).format(
            type(self).__name__,
            self.user_id,
            self.course_version,
            self.usage_key,
            self.visible_blocks_id,
            self.earned_graded,
            self.possible_graded,
            self.earned_all,
            self.possible_all,
            self.first_attempted,
        )

    @classmethod
    def read_grade(cls, user_id, usage_key):
        """
        Reads a grade from database

        Arguments:
            user_id: The user associated with the desired grade
            usage_key: The location of the subsection associated with the desired grade

        Raises PersistentSubsectionGrade.DoesNotExist if applicable
        """
        return cls.objects.select_related('visible_blocks').get(
            user_id=user_id,
            course_id=usage_key.course_key,  # course_id is included to take advantage of db indexes
            usage_key=usage_key,
        )

    @classmethod
    def bulk_read_grades(cls, user_id, course_key):
        """
        Reads all grades for the given user and course.

        Arguments:
            user_id: The user associated with the desired grades
            course_key: The course identifier for the desired grades
        """
        return cls.objects.select_related('visible_blocks').filter(
            user_id=user_id,
            course_id=course_key,
        )

    @classmethod
    def update_or_create_grade(cls, **params):
        """
        Wrapper for objects.update_or_create.
        """
        cls._prepare_params_and_visible_blocks(params)

        first_attempted = params.pop('first_attempted')
        user_id = params.pop('user_id')
        usage_key = params.pop('usage_key')

        grade, _ = cls.objects.update_or_create(
            user_id=user_id,
            course_id=usage_key.course_key,
            usage_key=usage_key,
            defaults=params,
        )
        if first_attempted is not None and grade.first_attempted is None:
            if waffle.waffle().is_enabled(waffle.ESTIMATE_FIRST_ATTEMPTED):
                grade.first_attempted = first_attempted
            else:
                grade.first_attempted = now()
            grade.save()

        cls._emit_grade_calculated_event(grade)
        return grade

    @classmethod
    def _prepare_first_attempted_for_create(cls, params):
        """
        Update the value of 'first_attempted' to now() if we aren't
        using score-based estimates.
        """
        if params['first_attempted'] is not None and not waffle.waffle().is_enabled(waffle.ESTIMATE_FIRST_ATTEMPTED):
            params['first_attempted'] = now()

    @classmethod
    def create_grade(cls, **params):
        """
        Wrapper for objects.create.
        """
        cls._prepare_params_and_visible_blocks(params)
        cls._prepare_first_attempted_for_create(params)

        grade = cls.objects.create(**params)
        cls._emit_grade_calculated_event(grade)
        return grade

    @classmethod
    def bulk_create_grades(cls, grade_params_iter, course_key):
        """
        Bulk creation of grades.
        """
        if not grade_params_iter:
            return

        map(cls._prepare_params, grade_params_iter)
        VisibleBlocks.bulk_get_or_create([params['visible_blocks'] for params in grade_params_iter], course_key)
        map(cls._prepare_params_visible_blocks_id, grade_params_iter)
        map(cls._prepare_first_attempted_for_create, grade_params_iter)
        grades = [PersistentSubsectionGrade(**params) for params in grade_params_iter]
        grades = cls.objects.bulk_create(grades)
        for grade in grades:
            cls._emit_grade_calculated_event(grade)
        return grades

    @classmethod
    def _prepare_params_and_visible_blocks(cls, params):
        """
        Prepares the fields for the grade record, while
        creating the related VisibleBlocks, if needed.
        """
        cls._prepare_params(params)
        params['visible_blocks'] = VisibleBlocks.objects.create_from_blockrecords(params['visible_blocks'])

    @classmethod
    def _prepare_params(cls, params):
        """
        Prepares the fields for the grade record.
        """
        if not params.get('course_id', None):
            params['course_id'] = params['usage_key'].course_key
        params['course_version'] = params.get('course_version', None) or ""
        params['visible_blocks'] = BlockRecordList.from_list(params['visible_blocks'], params['course_id'])

    @classmethod
    def _prepare_params_visible_blocks_id(cls, params):
        """
        Prepares the visible_blocks_id field for the grade record,
        using the hash of the visible_blocks field.  Specifying
        the hashed field eliminates extra queries to get the
        VisibleBlocks record.  Use this variation of preparing
        the params when you are sure of the existence of the
        VisibleBlock.
        """
        params['visible_blocks_id'] = params['visible_blocks'].hash_value
        del params['visible_blocks']

    @staticmethod
    def _emit_grade_calculated_event(grade):
        """
        Emits an edx.grades.subsection.grade_calculated event
        with data from the passed grade.
        """
        # TODO: remove this context manager after completion of AN-6134
        event_name = u'edx.grades.subsection.grade_calculated'
        context = contexts.course_context_from_course_id(grade.course_id)
        with tracker.get_tracker().context(event_name, context):
            tracker.emit(
                event_name,
                {
                    'user_id': unicode(grade.user_id),
                    'course_id': unicode(grade.course_id),
                    'block_id': unicode(grade.usage_key),
                    'course_version': unicode(grade.course_version),
                    'weighted_total_earned': grade.earned_all,
                    'weighted_total_possible': grade.possible_all,
                    'weighted_graded_earned': grade.earned_graded,
                    'weighted_graded_possible': grade.possible_graded,
                    'first_attempted': unicode(grade.first_attempted),
                    'subtree_edited_timestamp': unicode(grade.subtree_edited_timestamp),
                    'event_transaction_id': unicode(get_event_transaction_id()),
                    'event_transaction_type': unicode(get_event_transaction_type()),
                    'visible_blocks_hash': unicode(grade.visible_blocks_id),
                }
            )
Example #5
0
class PersistentSubsectionGrade(TimeStampedModel):
    """
    A django model tracking persistent grades at the subsection level.
    """
    class Meta(object):
        unique_together = [
            # * Specific grades can be pulled using all three columns,
            # * Progress page can pull all grades for a given (course_id, user_id)
            # * Course staff can see all grades for a course using (course_id,)
            ('course_id', 'user_id', 'usage_key'),
        ]

    # primary key will need to be large for this table
    id = UnsignedBigIntAutoField(primary_key=True)  # pylint: disable=invalid-name

    # uniquely identify this particular grade object
    user_id = models.IntegerField(blank=False)
    course_id = CourseKeyField(blank=False, max_length=255)
    usage_key = UsageKeyField(blank=False, max_length=255)

    # Information relating to the state of content when grade was calculated
    subtree_edited_timestamp = models.DateTimeField(
        'last content edit timestamp', blank=False)
    course_version = models.CharField('guid of latest course version',
                                      blank=True,
                                      max_length=255)

    # earned/possible refers to the number of points achieved and available to achieve.
    # graded refers to the subset of all problems that are marked as being graded.
    earned_all = models.FloatField(blank=False)
    possible_all = models.FloatField(blank=False)
    earned_graded = models.FloatField(blank=False)
    possible_graded = models.FloatField(blank=False)

    # track which blocks were visible at the time of grade calculation
    visible_blocks = models.ForeignKey(VisibleBlocks,
                                       db_column='visible_blocks_hash',
                                       to_field='hashed')

    # use custom manager
    objects = PersistentSubsectionGradeQuerySet.as_manager()

    def __unicode__(self):
        """
        Returns a string representation of this model.
        """
        return u"{} user: {}, course version: {}, subsection {} ({}). {}/{} graded, {}/{} all".format(
            type(self).__name__,
            self.user_id,
            self.course_version,
            self.usage_key,
            self.visible_blocks_id,
            self.earned_graded,
            self.possible_graded,
            self.earned_all,
            self.possible_all,
        )

    @classmethod
    def save_grade(cls, **kwargs):
        """
        Wrapper for create_grade or update_grade, depending on which applies.
        Takes the same arguments as both of those methods.
        """
        user_id = kwargs.pop('user_id')
        usage_key = kwargs.pop('usage_key')

        try:
            with transaction.atomic():
                grade, is_created = cls.objects.get_or_create(
                    user_id=user_id,
                    course_id=usage_key.course_key,
                    usage_key=usage_key,
                    defaults=kwargs,
                )
                log.info(
                    u"Persistent Grades: Grade model saved: {0}".format(grade))
        except IntegrityError:
            cls.update_grade(user_id=user_id, usage_key=usage_key, **kwargs)
            log.warning(
                u"Persistent Grades: Integrity error trying to save grade for user: {0}, usage key: {1}, defaults: {2}"
                .format(user_id, usage_key, **kwargs))
        else:
            if not is_created:
                grade.update(**kwargs)

    @classmethod
    def read_grade(cls, user_id, usage_key):
        """
        Reads a grade from database

        Arguments:
            user_id: The user associated with the desired grade
            usage_key: The location of the subsection associated with the desired grade

        Raises PersistentSubsectionGrade.DoesNotExist if applicable
        """
        return cls.objects.get(
            user_id=user_id,
            course_id=usage_key.
            course_key,  # course_id is included to take advantage of db indexes
            usage_key=usage_key,
        )

    @classmethod
    def update_grade(
        cls,
        user_id,
        usage_key,
        course_version,
        subtree_edited_timestamp,
        earned_all,
        possible_all,
        earned_graded,
        possible_graded,
        visible_blocks,
    ):
        """
        Updates a previously existing grade.

        This is distinct from update() in that `grade.update()` operates on an
        existing grade object, while this is a classmethod that pulls the grade
        from the database, and then updates it.  If you already have a grade
        object, use the update() method on that object to avoid an extra
        round-trip to the database.  Use this classmethod if all you have are a
        user and the usage key of an existing grade.

        Requires all the arguments listed in docstring for create_grade
        """
        grade = cls.read_grade(
            user_id=user_id,
            usage_key=usage_key,
        )

        grade.update(
            course_version=course_version,
            subtree_edited_timestamp=subtree_edited_timestamp,
            earned_all=earned_all,
            possible_all=possible_all,
            earned_graded=earned_graded,
            possible_graded=possible_graded,
            visible_blocks=visible_blocks,
        )

    def update(
        self,
        course_version,
        subtree_edited_timestamp,
        earned_all,
        possible_all,
        earned_graded,
        possible_graded,
        visible_blocks,
    ):
        """
        Modify an existing PersistentSubsectionGrade object, saving the new
        version.
        """
        visible_blocks_hash = VisibleBlocks.objects.hash_from_blockrecords(
            BlockRecordList.from_list(visible_blocks))

        self.course_version = course_version or ""
        self.subtree_edited_timestamp = subtree_edited_timestamp
        self.earned_all = earned_all
        self.possible_all = possible_all
        self.earned_graded = earned_graded
        self.possible_graded = possible_graded
        self.visible_blocks_id = visible_blocks_hash  # pylint: disable=attribute-defined-outside-init
        self.save()
        log.info(u"Persistent Grades: Grade model updated: {0}".format(self))
Example #6
0
class PersistentCourseGrade(TimeStampedModel):
    """
    A django model tracking persistent course grades.
    """

    class Meta(object):
        # Indices:
        # (course_id, user_id) for individual grades
        # (course_id) for instructors to see all course grades, implicitly created via the unique_together constraint
        # (user_id) for course dashboard; explicitly declared as an index below
        # (passed_timestamp, course_id) for tracking when users first earned a passing grade.
        unique_together = [
            ('course_id', 'user_id'),
        ]
        index_together = [
            ('passed_timestamp', 'course_id'),
        ]

    # primary key will need to be large for this table
    id = UnsignedBigIntAutoField(primary_key=True)  # pylint: disable=invalid-name
    user_id = models.IntegerField(blank=False, db_index=True)
    course_id = CourseKeyField(blank=False, max_length=255)

    # Information relating to the state of content when grade was calculated
    course_edited_timestamp = models.DateTimeField(u'Last content edit timestamp', blank=False)
    course_version = models.CharField(u'Course content version identifier', blank=True, max_length=255)
    grading_policy_hash = models.CharField(u'Hash of grading policy', blank=False, max_length=255)

    # Information about the course grade itself
    percent_grade = models.FloatField(blank=False)
    letter_grade = models.CharField(u'Letter grade for course', blank=False, max_length=255)

    # Information related to course completion
    passed_timestamp = models.DateTimeField(u'Date learner earned a passing grade', blank=True, null=True)

    def __unicode__(self):
        """
        Returns a string representation of this model.
        """
        return u', '.join([
            u"{} user: {}".format(type(self).__name__, self.user_id),
            u"course version: {}".format(self.course_version),
            u"grading policy: {}".format(self.grading_policy_hash),
            u"percent grade: {}%".format(self.percent_grade),
            u"letter grade: {}".format(self.letter_grade),
            u"passed_timestamp: {}".format(self.passed_timestamp),
        ])

    @classmethod
    def read_course_grade(cls, user_id, course_id):
        """
        Reads a grade from database

        Arguments:
            user_id: The user associated with the desired grade
            course_id: The id of the course associated with the desired grade

        Raises PersistentCourseGrade.DoesNotExist if applicable
        """
        return cls.objects.get(user_id=user_id, course_id=course_id)

    @classmethod
    def update_or_create_course_grade(cls, user_id, course_id, **kwargs):
        """
        Creates a course grade in the database.
        Returns a PersistedCourseGrade object.
        """
        passed = kwargs.pop('passed')
        if kwargs.get('course_version', None) is None:
            kwargs['course_version'] = ""

        grade, _ = cls.objects.update_or_create(
            user_id=user_id,
            course_id=course_id,
            defaults=kwargs
        )
        if passed and not grade.passed_timestamp:
            grade.passed_timestamp = now()
            grade.save()
        return grade
Example #7
0
class PersistentSubsectionGrade(TimeStampedModel):
    """
    A django model tracking persistent grades at the subsection level.
    """

    class Meta(object):
        unique_together = [
            # * Specific grades can be pulled using all three columns,
            # * Progress page can pull all grades for a given (course_id, user_id)
            # * Course staff can see all grades for a course using (course_id,)
            ('course_id', 'user_id', 'usage_key'),
        ]

    # primary key will need to be large for this table
    id = UnsignedBigIntAutoField(primary_key=True)  # pylint: disable=invalid-name

    user_id = models.IntegerField(blank=False)
    course_id = CourseKeyField(blank=False, max_length=255)

    # note: the usage_key may not have the run filled in for
    # old mongo courses.  Use the full_usage_key property
    # instead when you want to use/compare the usage_key.
    usage_key = UsageKeyField(blank=False, max_length=255)

    # Information relating to the state of content when grade was calculated
    subtree_edited_timestamp = models.DateTimeField('last content edit timestamp', blank=False)
    course_version = models.CharField('guid of latest course version', blank=True, max_length=255)

    # earned/possible refers to the number of points achieved and available to achieve.
    # graded refers to the subset of all problems that are marked as being graded.
    earned_all = models.FloatField(blank=False)
    possible_all = models.FloatField(blank=False)
    earned_graded = models.FloatField(blank=False)
    possible_graded = models.FloatField(blank=False)

    # timestamp for the learner's first attempt at content in
    # this subsection. If null, indicates no attempt
    # has yet been made.
    first_attempted = models.DateTimeField(null=True, blank=True)

    # track which blocks were visible at the time of grade calculation
    visible_blocks = models.ForeignKey(VisibleBlocks, db_column='visible_blocks_hash', to_field='hashed')

    @property
    def full_usage_key(self):
        """
        Returns the "correct" usage key value with the run filled in.
        """
        if self.usage_key.run is None:  # pylint: disable=no-member
            return self.usage_key.replace(course_key=self.course_id)
        else:
            return self.usage_key

    def __unicode__(self):
        """
        Returns a string representation of this model.
        """
        return (
            u"{} user: {}, course version: {}, subsection: {} ({}). {}/{} graded, {}/{} all, first_attempted: {}"
        ).format(
            type(self).__name__,
            self.user_id,
            self.course_version,
            self.usage_key,
            self.visible_blocks_id,
            self.earned_graded,
            self.possible_graded,
            self.earned_all,
            self.possible_all,
            self.first_attempted,
        )

    @classmethod
    def read_grade(cls, user_id, usage_key):
        """
        Reads a grade from database

        Arguments:
            user_id: The user associated with the desired grade
            usage_key: The location of the subsection associated with the desired grade

        Raises PersistentSubsectionGrade.DoesNotExist if applicable
        """
        return cls.objects.select_related('visible_blocks').get(
            user_id=user_id,
            course_id=usage_key.course_key,  # course_id is included to take advantage of db indexes
            usage_key=usage_key,
        )

    @classmethod
    def bulk_read_grades(cls, user_id, course_key):
        """
        Reads all grades for the given user and course.

        Arguments:
            user_id: The user associated with the desired grades
            course_key: The course identifier for the desired grades
        """
        return cls.objects.select_related('visible_blocks').filter(
            user_id=user_id,
            course_id=course_key,
        )

    @classmethod
    def update_or_create_grade(cls, **kwargs):
        """
        Wrapper for objects.update_or_create.
        """
        cls._prepare_params_and_visible_blocks(kwargs)

        user_id = kwargs.pop('user_id')
        usage_key = kwargs.pop('usage_key')

        grade, _ = cls.objects.update_or_create(
            user_id=user_id,
            course_id=usage_key.course_key,
            usage_key=usage_key,
            defaults=kwargs,
        )
        return grade

    @classmethod
    def create_grade(cls, **kwargs):
        """
        Wrapper for objects.create.
        """
        cls._prepare_params_and_visible_blocks(kwargs)
        return cls.objects.create(**kwargs)

    @classmethod
    def bulk_create_grades(cls, grade_params_iter, course_key):
        """
        Bulk creation of grades.
        """
        if not grade_params_iter:
            return

        map(cls._prepare_params, grade_params_iter)
        VisibleBlocks.bulk_get_or_create([params['visible_blocks'] for params in grade_params_iter], course_key)
        map(cls._prepare_params_visible_blocks_id, grade_params_iter)

        return cls.objects.bulk_create([PersistentSubsectionGrade(**params) for params in grade_params_iter])

    @classmethod
    def _prepare_params_and_visible_blocks(cls, params):
        """
        Prepares the fields for the grade record, while
        creating the related VisibleBlocks, if needed.
        """
        cls._prepare_params(params)
        params['visible_blocks'] = VisibleBlocks.objects.create_from_blockrecords(params['visible_blocks'])

    @classmethod
    def _prepare_params(cls, params):
        """
        Prepares the fields for the grade record.
        """
        if not params.get('course_id', None):
            params['course_id'] = params['usage_key'].course_key
        params['course_version'] = params.get('course_version', None) or ""
        params['visible_blocks'] = BlockRecordList.from_list(params['visible_blocks'], params['course_id'])

    @classmethod
    def _prepare_params_visible_blocks_id(cls, params):
        """
        Prepares the visible_blocks_id field for the grade record,
        using the hash of the visible_blocks field.  Specifying
        the hashed field eliminates extra queries to get the
        VisibleBlocks record.  Use this variation of preparing
        the params when you are sure of the existence of the
        VisibleBlock.
        """
        params['visible_blocks_id'] = params['visible_blocks'].hash_value
        del params['visible_blocks']