models.py 6.36 KB
Newer Older
1 2 3 4 5 6 7
"""
Django database models supporting the gradebook app
"""
from django.utils import timezone

from django.contrib.auth.models import User
from django.db import models
8
from django.db.models import Avg, Max, Min, Count
9 10 11 12
from django.db.models.signals import post_save
from django.dispatch import receiver

from model_utils.models import TimeStampedModel
13
from student.models import CourseEnrollment
14 15 16 17 18 19 20 21 22 23 24
from xmodule_django.models import CourseKeyField


class StudentGradebook(TimeStampedModel):
    """
    StudentGradebook is essentiall a container used to cache calculated
    grades (see courseware.grades.grade), which can be an expensive operation.
    """
    user = models.ForeignKey(User, db_index=True)
    course_id = CourseKeyField(db_index=True, max_length=255, blank=True)
    grade = models.FloatField()
25
    proforma_grade = models.FloatField()
26 27 28 29 30 31 32 33

    class Meta:
        """
        Meta information for this Django model
        """
        unique_together = (('user', 'course_id'),)

    @classmethod
34
    def generate_leaderboard(cls, course_key, user_id=None, count=3, exclude_users=[]):
35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52
        """
        Assembles a data set representing the Top N users, by grade, for a given course.
        Optionally provide a user_id to include user-specific info.  For example, you
        may want to view the Top 5 users, but also need the data for the logged-in user
        who may actually be currently located in position #10.

        data = {
            'course_avg': 0.873,
            'queryset': [
                {'id': 123, 'username': 'testuser1', 'title', 'Engineer', 'avatar_url': 'http://gravatar.com/123/', 'grade': 0.92, 'created': '2014-01-15 06:27:54'},
                {'id': 983, 'username': 'testuser2', 'title', 'Analyst', 'avatar_url': 'http://gravatar.com/983/', 'grade': 0.91, 'created': '2014-06-27 01:15:54'},
                {'id': 246, 'username': 'testuser3', 'title', 'Product Owner', 'avatar_url': 'http://gravatar.com/246/', 'grade': 0.90, 'created': '2014-03-19 04:54:54'},
                {'id': 357, 'username': 'testuser4', 'title', 'Director', 'avatar_url': 'http://gravatar.com/357/', 'grade': 0.89, 'created': '2014-12-01 08:38:54'},
            ]
            ### IF USER ID SPECIFIED (in this case user_id=246) ###
            'user_position': 4,
            'user_grade': 0.89
        }
53 54 55 56 57

        If there is a discrepancy between the number of gradebook entries and the overall number of enrolled
        users (excluding any users who should be excluded), then we modify the course average to account for
        those users who currently lack gradebook entries.  We assume zero grades for these users because they
        have not yet submitted a response to a scored assessment which means no grade has been calculated.
58 59
        """
        data = {}
60 61 62 63 64 65
        data['course_avg'] = 0
        data['course_max'] = 0
        data['course_min'] = 0
        data['course_count'] = 0
        data['queryset'] = []

66 67
        enrolled_users_not_excluded = CourseEnrollment.users_enrolled_in(course_key).exclude(id__in=exclude_users)
        total_user_count = enrolled_users_not_excluded.count()
68

69
        if total_user_count:
70 71
            # Generate the base data set we're going to work with
            queryset = StudentGradebook.objects.select_related('user')\
72
                .filter(course_id__exact=course_key, user__is_active=True, user__in=enrolled_users_not_excluded)
73
            gradebook_user_count = len(queryset)
74

75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112
            if gradebook_user_count:
                # Calculate the class average
                course_avg = queryset.aggregate(Avg('grade'))['grade__avg']
                if course_avg is not None:
                    # Take into account any ungraded students (assumes zeros for grades...)
                    course_avg = course_avg / total_user_count * gradebook_user_count

                    # Fill up the response container
                    data['course_avg'] = float("{0:.3f}".format(course_avg))
                    data['course_max'] = queryset.aggregate(Max('grade'))['grade__max']
                    data['course_min'] = queryset.aggregate(Min('grade'))['grade__min']
                    data['course_count'] = queryset.aggregate(Count('grade'))['grade__count']

                # Construct the leaderboard as a queryset
                data['queryset'] = queryset.values(
                    'user__id',
                    'user__username',
                    'user__profile__title',
                    'user__profile__avatar_url',
                    'grade',
                    'modified')\
                    .order_by('-grade', 'modified')[:count]
                # If a user_id value was provided, we need to provide some additional user-specific data to the caller
                if user_id:
                    user_grade = 0
                    user_time_scored = timezone.now()
                    try:
                        user_queryset = StudentGradebook.objects.get(course_id__exact=course_key, user__id=user_id)
                    except StudentGradebook.DoesNotExist:
                        user_queryset = None
                    if user_queryset:
                        user_grade = user_queryset.grade
                        user_time_scored = user_queryset.created
                    users_above = queryset.filter(grade__gte=user_grade)\
                        .exclude(user__id=user_id)\
                        .exclude(grade=user_grade, modified__gt=user_time_scored)
                    data['user_position'] = len(users_above) + 1
                    data['user_grade'] = user_grade
113 114 115 116 117 118 119 120 121 122 123 124

        return data


class StudentGradebookHistory(TimeStampedModel):
    """
    A running audit trail for the StudentGradebook model.  Listens for
    post_save events and creates/stores copies of gradebook entries.
    """
    user = models.ForeignKey(User, db_index=True)
    course_id = CourseKeyField(db_index=True, max_length=255, blank=True)
    grade = models.FloatField()
125
    proforma_grade = models.FloatField()
126 127 128 129 130 131 132 133 134

    @receiver(post_save, sender=StudentGradebook)
    def save_history(sender, instance, **kwargs):  # pylint: disable=no-self-argument, unused-argument
        """
        Event hook for creating gradebook entry copies
        """
        history_entry = StudentGradebookHistory(
            user=instance.user,
            course_id=instance.course_id,
135 136
            grade=instance.grade,
            proforma_grade=instance.proforma_grade
137 138
        )
        history_entry.save()