Commit a5637411 by Marko Jevtic Committed by Jonathan Piacenti

(YONK-8) Performance Improvement - UsersCoursesGradesDetail

parent e7c0d0da
...@@ -1390,6 +1390,8 @@ class EdxJSONEncoder(json.JSONEncoder): ...@@ -1390,6 +1390,8 @@ class EdxJSONEncoder(json.JSONEncoder):
def default(self, obj): def default(self, obj):
if isinstance(obj, (CourseKey, UsageKey)): if isinstance(obj, (CourseKey, UsageKey)):
return unicode(obj) return unicode(obj)
elif isinstance(obj, Location):
return obj.dict()
elif isinstance(obj, datetime.datetime): elif isinstance(obj, datetime.datetime):
if obj.tzinfo is not None: if obj.tzinfo is not None:
if obj.utcoffset() is None: if obj.utcoffset() is None:
......
...@@ -1444,6 +1444,12 @@ class UsersApiTests(ModuleStoreTestCase): ...@@ -1444,6 +1444,12 @@ class UsersApiTests(ModuleStoreTestCase):
metadata={'rerandomize': 'always', 'graded': True, 'format': "Homework"}, metadata={'rerandomize': 'always', 'graded': True, 'format': "Homework"},
due=self.course_end_date.replace(tzinfo=timezone.utc) due=self.course_end_date.replace(tzinfo=timezone.utc)
) )
points_scored = 5
points_possible = 10
user = self.user
module = self.get_module_for_user(user, course, item5)
grade_dict = {'value': points_scored, 'max_value': points_possible, 'user_id': user.id}
module.system.publish(module, 'grade', grade_dict)
test_uri = '{}/{}/courses/{}/grades'.format(self.users_base_uri, user_id, unicode(course.id)) test_uri = '{}/{}/courses/{}/grades'.format(self.users_base_uri, user_id, unicode(course.id))
...@@ -1471,16 +1477,16 @@ class UsersApiTests(ModuleStoreTestCase): ...@@ -1471,16 +1477,16 @@ class UsersApiTests(ModuleStoreTestCase):
self.assertGreater(len(grading_policy['GRADER']), 0) self.assertGreater(len(grading_policy['GRADER']), 0)
self.assertIsNotNone(grading_policy['GRADE_CUTOFFS']) self.assertIsNotNone(grading_policy['GRADE_CUTOFFS'])
self.assertEqual(response.data['current_grade'], 0.73) self.assertEqual(response.data['current_grade'], 0.74)
self.assertEqual(response.data['proforma_grade'], 0.9375) self.assertEqual(response.data['proforma_grade'], 0.9174999999999999)
test_uri = '{}/{}/courses/grades'.format(self.users_base_uri, user_id) test_uri = '{}/{}/courses/grades'.format(self.users_base_uri, user_id)
response = self.do_get(test_uri) response = self.do_get(test_uri)
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertEqual(response.data[0]['course_id'], unicode(course.id)) self.assertEqual(response.data[0]['course_id'], unicode(course.id))
self.assertEqual(response.data[0]['current_grade'], 0.73) self.assertEqual(response.data[0]['current_grade'], 0.74)
self.assertEqual(response.data[0]['proforma_grade'], 0.9375) self.assertEqual(response.data[0]['proforma_grade'], 0.9174999999999999)
self.assertEqual(response.data[0]['complete_status'], False) self.assertEqual(response.data[0]['complete_status'], False)
def is_user_profile_created_updated(self, response, data): def is_user_profile_created_updated(self, response, data):
......
""" API implementation for user-oriented interactions. """ """ API implementation for user-oriented interactions. """
import json
import logging import logging
from edx_notifications.lib.consumer import mark_notification_read from edx_notifications.lib.consumer import mark_notification_read
from requests.exceptions import ConnectionError from requests.exceptions import ConnectionError
...@@ -1000,19 +1001,21 @@ class UsersCoursesGradesDetail(SecureAPIView): ...@@ -1000,19 +1001,21 @@ class UsersCoursesGradesDetail(SecureAPIView):
if not course_descriptor: if not course_descriptor:
return Response({}, status=status.HTTP_404_NOT_FOUND) return Response({}, status=status.HTTP_404_NOT_FOUND)
progress_summary = grades.progress_summary(student, request, course_descriptor) # pylint: disable=W0612
grade_summary = grades.grade(student, request, course_descriptor)
grading_policy = course_descriptor.grading_policy
queryset = StudentGradebook.objects.filter( queryset = StudentGradebook.objects.filter(
user=student, user=student,
course_id__exact=course_key, course_id__exact=course_key,
) )
current_grade = 0 current_grade = 0
proforma_grade = 0 proforma_grade = 0
progress_summary = {}
grade_summary = {}
grading_policy = {}
if len(queryset): if len(queryset):
current_grade = queryset[0].grade current_grade = queryset[0].grade
proforma_grade = queryset[0].proforma_grade proforma_grade = queryset[0].proforma_grade
progress_summary = json.loads(queryset[0].progress_summary)
grade_summary = json.loads(queryset[0].grade_summary)
grading_policy = json.loads(queryset[0].grading_policy)
response_data = { response_data = {
'courseware_summary': progress_summary, 'courseware_summary': progress_summary,
......
""" """
One-time data migration script -- shoulen't need to run it again One-time data migration script -- shouldn't need to run it again
""" """
import json
import logging import logging
from optparse import make_option from optparse import make_option
...@@ -10,6 +11,7 @@ from courseware import grades ...@@ -10,6 +11,7 @@ from courseware import grades
from gradebook.models import StudentGradebook from gradebook.models import StudentGradebook
from student.models import CourseEnrollment from student.models import CourseEnrollment
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
from xmodule.modulestore import EdxJSONEncoder
from util.request import RequestMockWithoutMiddleware from util.request import RequestMockWithoutMiddleware
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
...@@ -21,7 +23,7 @@ class Command(BaseCommand): ...@@ -21,7 +23,7 @@ class Command(BaseCommand):
""" """
def handle(self, *args, **options): def handle(self, *args, **options):
help = "Command to creaete or update gradebook entries" help = "Command to create or update gradebook entries"
option_list = BaseCommand.option_list + ( option_list = BaseCommand.option_list + (
make_option( make_option(
"-c", "-c",
...@@ -69,15 +71,28 @@ class Command(BaseCommand): ...@@ -69,15 +71,28 @@ class Command(BaseCommand):
request.user = user request.user = user
grade_data = grades.grade(user, request, course) grade_data = grades.grade(user, request, course)
grade = grade_data['percent'] grade = grade_data['percent']
proforma_grade = grades.calculate_proforma_grade(grade_data, course.grading_policy) grading_policy = course.grading_policy
proforma_grade = grades.calculate_proforma_grade(grade_data, grading_policy)
progress_summary = grades.progress_summary(user, request, course)
try: try:
gradebook_entry = StudentGradebook.objects.get(user=user, course_id=course.id) gradebook_entry = StudentGradebook.objects.get(user=user, course_id=course.id)
if gradebook_entry.grade != grade or gradebook_entry.proforma_grade != proforma_grade: if gradebook_entry.grade != grade or gradebook_entry.proforma_grade != proforma_grade:
gradebook_entry.grade = grade gradebook_entry.grade = grade
gradebook_entry.proforma_grade = proforma_grade gradebook_entry.proforma_grade = proforma_grade
gradebook_entry.progress_summary = json.dumps(progress_summary, cls=EdxJSONEncoder)
gradebook_entry.grade_summary = json.dumps(grade_data, cls=EdxJSONEncoder)
gradebook_entry.grading_policy = json.dumps(grading_policy, cls=EdxJSONEncoder)
gradebook_entry.save() gradebook_entry.save()
except StudentGradebook.DoesNotExist: except StudentGradebook.DoesNotExist:
StudentGradebook.objects.create(user=user, course_id=course.id, grade=grade, proforma_grade=proforma_grade) StudentGradebook.objects.create(
user=user,
course_id=course.id,
grade=grade,
proforma_grade=proforma_grade,
progress_summary=json.dumps(progress_summary, cls=EdxJSONEncoder),
grade_summary=json.dumps(grade_data, cls=EdxJSONEncoder),
grading_policy=json.dumps(grading_policy, cls=EdxJSONEncoder)
)
log_msg = 'Gradebook entry created -- Course: {}, User: {} (grade: {}, proforma_grade: {})'.format(course.id, user.id, grade, proforma_grade) log_msg = 'Gradebook entry created -- Course: {}, User: {} (grade: {}, proforma_grade: {})'.format(course.id, user.id, grade, proforma_grade)
print log_msg print log_msg
log.info(log_msg) log.info(log_msg)
# -*- coding: utf-8 -*-
import datetime
from south.db import db
from south.v2 import SchemaMigration
from django.db import models
class Migration(SchemaMigration):
def forwards(self, orm):
# Adding field 'StudentGradebookHistory.progress_summary'
db.add_column('gradebook_studentgradebookhistory', 'progress_summary',
self.gf('django.db.models.fields.TextField')(default='', blank=True),
keep_default=False)
# Adding field 'StudentGradebookHistory.grade_summary'
db.add_column('gradebook_studentgradebookhistory', 'grade_summary',
self.gf('django.db.models.fields.TextField')(default='', blank=True),
keep_default=False)
# Adding field 'StudentGradebookHistory.grading_policy'
db.add_column('gradebook_studentgradebookhistory', 'grading_policy',
self.gf('django.db.models.fields.TextField')(default='', blank=True),
keep_default=False)
# Adding field 'StudentGradebook.progress_summary'
db.add_column('gradebook_studentgradebook', 'progress_summary',
self.gf('django.db.models.fields.TextField')(default='', blank=True),
keep_default=False)
# Adding field 'StudentGradebook.grade_summary'
db.add_column('gradebook_studentgradebook', 'grade_summary',
self.gf('django.db.models.fields.TextField')(default='', blank=True),
keep_default=False)
# Adding field 'StudentGradebook.grading_policy'
db.add_column('gradebook_studentgradebook', 'grading_policy',
self.gf('django.db.models.fields.TextField')(default='', blank=True),
keep_default=False)
def backwards(self, orm):
# Deleting field 'StudentGradebookHistory.progress_summary'
db.delete_column('gradebook_studentgradebookhistory', 'progress_summary')
# Deleting field 'StudentGradebookHistory.grade_summary'
db.delete_column('gradebook_studentgradebookhistory', 'grade_summary')
# Deleting field 'StudentGradebookHistory.grading_policy'
db.delete_column('gradebook_studentgradebookhistory', 'grading_policy')
# Deleting field 'StudentGradebook.progress_summary'
db.delete_column('gradebook_studentgradebook', 'progress_summary')
# Deleting field 'StudentGradebook.grade_summary'
db.delete_column('gradebook_studentgradebook', 'grade_summary')
# Deleting field 'StudentGradebook.grading_policy'
db.delete_column('gradebook_studentgradebook', 'grading_policy')
models = {
'auth.group': {
'Meta': {'object_name': 'Group'},
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
},
'auth.permission': {
'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
},
'auth.user': {
'Meta': {'object_name': 'User'},
'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
},
'contenttypes.contenttype': {
'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
},
'gradebook.studentgradebook': {
'Meta': {'unique_together': "(('user', 'course_id'),)", 'object_name': 'StudentGradebook'},
'course_id': ('xmodule_django.models.CourseKeyField', [], {'db_index': 'True', 'max_length': '255', 'blank': 'True'}),
'created': ('model_utils.fields.AutoCreatedField', [], {'default': 'datetime.datetime.now'}),
'grade': ('django.db.models.fields.FloatField', [], {}),
'grade_summary': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'grading_policy': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'modified': ('model_utils.fields.AutoLastModifiedField', [], {'default': 'datetime.datetime.now'}),
'proforma_grade': ('django.db.models.fields.FloatField', [], {}),
'progress_summary': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
},
'gradebook.studentgradebookhistory': {
'Meta': {'object_name': 'StudentGradebookHistory'},
'course_id': ('xmodule_django.models.CourseKeyField', [], {'db_index': 'True', 'max_length': '255', 'blank': 'True'}),
'created': ('model_utils.fields.AutoCreatedField', [], {'default': 'datetime.datetime.now'}),
'grade': ('django.db.models.fields.FloatField', [], {}),
'grade_summary': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'grading_policy': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'modified': ('model_utils.fields.AutoLastModifiedField', [], {'default': 'datetime.datetime.now'}),
'proforma_grade': ('django.db.models.fields.FloatField', [], {}),
'progress_summary': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
}
}
complete_apps = ['gradebook']
\ No newline at end of file
...@@ -13,15 +13,19 @@ from model_utils.models import TimeStampedModel ...@@ -13,15 +13,19 @@ from model_utils.models import TimeStampedModel
from student.models import CourseEnrollment from student.models import CourseEnrollment
from xmodule_django.models import CourseKeyField from xmodule_django.models import CourseKeyField
class StudentGradebook(TimeStampedModel): class StudentGradebook(TimeStampedModel):
""" """
StudentGradebook is essentiall a container used to cache calculated StudentGradebook is essentially a container used to cache calculated
grades (see courseware.grades.grade), which can be an expensive operation. grades (see courseware.grades.grade), which can be an expensive operation.
""" """
user = models.ForeignKey(User, db_index=True) user = models.ForeignKey(User, db_index=True)
course_id = CourseKeyField(db_index=True, max_length=255, blank=True) course_id = CourseKeyField(db_index=True, max_length=255, blank=True)
grade = models.FloatField() grade = models.FloatField()
proforma_grade = models.FloatField() proforma_grade = models.FloatField()
progress_summary = models.TextField(blank=True)
grade_summary = models.TextField(blank=True)
grading_policy = models.TextField(blank=True)
class Meta: class Meta:
""" """
...@@ -147,6 +151,7 @@ class StudentGradebook(TimeStampedModel): ...@@ -147,6 +151,7 @@ class StudentGradebook(TimeStampedModel):
return data return data
class StudentGradebookHistory(TimeStampedModel): class StudentGradebookHistory(TimeStampedModel):
""" """
A running audit trail for the StudentGradebook model. Listens for A running audit trail for the StudentGradebook model. Listens for
...@@ -156,6 +161,9 @@ class StudentGradebookHistory(TimeStampedModel): ...@@ -156,6 +161,9 @@ class StudentGradebookHistory(TimeStampedModel):
course_id = CourseKeyField(db_index=True, max_length=255, blank=True) course_id = CourseKeyField(db_index=True, max_length=255, blank=True)
grade = models.FloatField() grade = models.FloatField()
proforma_grade = models.FloatField() proforma_grade = models.FloatField()
progress_summary = models.TextField(blank=True)
grade_summary = models.TextField(blank=True)
grading_policy = models.TextField(blank=True)
@receiver(post_save, sender=StudentGradebook) @receiver(post_save, sender=StudentGradebook)
def save_history(sender, instance, **kwargs): # pylint: disable=no-self-argument, unused-argument def save_history(sender, instance, **kwargs): # pylint: disable=no-self-argument, unused-argument
...@@ -169,7 +177,13 @@ class StudentGradebookHistory(TimeStampedModel): ...@@ -169,7 +177,13 @@ class StudentGradebookHistory(TimeStampedModel):
create_history_entry = False create_history_entry = False
if latest_history_entry is not None: if latest_history_entry is not None:
if (latest_history_entry.grade != instance.grade) or (latest_history_entry.proforma_grade != instance.proforma_grade): if (
latest_history_entry.grade != instance.grade or
latest_history_entry.proforma_grade != instance.proforma_grade or
latest_history_entry.progress_summary != instance.progress_summary or
latest_history_entry.grade_summary != instance.grade_summary or
latest_history_entry.grading_policy != instance.grading_policy
):
create_history_entry = True create_history_entry = True
else: else:
create_history_entry = True create_history_entry = True
...@@ -179,6 +193,9 @@ class StudentGradebookHistory(TimeStampedModel): ...@@ -179,6 +193,9 @@ class StudentGradebookHistory(TimeStampedModel):
user=instance.user, user=instance.user,
course_id=instance.course_id, course_id=instance.course_id,
grade=instance.grade, grade=instance.grade,
proforma_grade=instance.proforma_grade proforma_grade=instance.proforma_grade,
progress_summary=instance.progress_summary,
grade_summary=instance.grade_summary,
grading_policy=instance.grading_policy
) )
new_history_entry.save() new_history_entry.save()
...@@ -3,12 +3,14 @@ Signal handlers supporting various gradebook use cases ...@@ -3,12 +3,14 @@ Signal handlers supporting various gradebook use cases
""" """
import logging import logging
import sys import sys
import json
from django.dispatch import receiver from django.dispatch import receiver
from django.conf import settings from django.conf import settings
from django.db.models.signals import post_save, pre_save from django.db.models.signals import post_save, pre_save
from courseware import grades from courseware import grades
from courseware.signals import score_changed from courseware.signals import score_changed
from xmodule.modulestore import EdxJSONEncoder
from util.request import RequestMockWithoutMiddleware from util.request import RequestMockWithoutMiddleware
from util.signals import course_deleted from util.signals import course_deleted
from student.roles import get_aggregate_exclusion_user_ids from student.roles import get_aggregate_exclusion_user_ids
...@@ -36,17 +38,31 @@ def on_score_changed(sender, **kwargs): ...@@ -36,17 +38,31 @@ def on_score_changed(sender, **kwargs):
course_descriptor = get_course(course_key, depth=None) course_descriptor = get_course(course_key, depth=None)
request = RequestMockWithoutMiddleware().get('/') request = RequestMockWithoutMiddleware().get('/')
request.user = user request.user = user
grade_data = grades.grade(user, request, course_descriptor) progress_summary = grades.progress_summary(user, request, course_descriptor)
grade = grade_data['percent'] grade_summary = grades.grade(user, request, course_descriptor)
proforma_grade = grades.calculate_proforma_grade(grade_data, course_descriptor.grading_policy) grading_policy = course_descriptor.grading_policy
grade = grade_summary['percent']
proforma_grade = grades.calculate_proforma_grade(grade_summary, grading_policy)
try: try:
gradebook_entry = StudentGradebook.objects.get(user=user, course_id=course_key) gradebook_entry = StudentGradebook.objects.get(user=user, course_id=course_key)
if gradebook_entry.grade != grade: if gradebook_entry.grade != grade:
gradebook_entry.grade = grade gradebook_entry.grade = grade
gradebook_entry.proforma_grade = proforma_grade gradebook_entry.proforma_grade = proforma_grade
gradebook_entry.progress_summary = json.dumps(progress_summary, cls=EdxJSONEncoder)
gradebook_entry.grade_summary = json.dumps(grade_summary, cls=EdxJSONEncoder)
gradebook_entry.grading_policy = json.dumps(grading_policy, cls=EdxJSONEncoder)
gradebook_entry.save() gradebook_entry.save()
except StudentGradebook.DoesNotExist: except StudentGradebook.DoesNotExist:
StudentGradebook.objects.create(user=user, course_id=course_key, grade=grade, proforma_grade=proforma_grade) StudentGradebook.objects.create(
user=user,
course_id=course_key,
grade=grade,
proforma_grade=proforma_grade,
progress_summary=json.dumps(progress_summary, cls=EdxJSONEncoder),
grade_summary=json.dumps(grade_summary, cls=EdxJSONEncoder),
grading_policy=json.dumps(grading_policy, cls=EdxJSONEncoder)
)
@receiver(course_deleted) @receiver(course_deleted)
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment