Commit c3d94d5f by Zia Fazal

update gradebook asynchronously

fixed failing tests due to recursive import error
parent 214fc702
...@@ -1471,6 +1471,7 @@ class UsersApiTests(ModuleStoreTestCase): ...@@ -1471,6 +1471,7 @@ class UsersApiTests(ModuleStoreTestCase):
user_id = self.user.id user_id = self.user.id
course = CourseFactory.create(org='TUCGLG', run='TUCGLG1', display_name="Robot Super Course") course = CourseFactory.create(org='TUCGLG', run='TUCGLG1', display_name="Robot Super Course")
CourseEnrollmentFactory.create(user=self.user, course_id=course.id)
test_data = '<html>{}</html>'.format(str(uuid.uuid4())) test_data = '<html>{}</html>'.format(str(uuid.uuid4()))
chapter1 = ItemFactory.create( chapter1 = ItemFactory.create(
category="chapter", category="chapter",
...@@ -1642,9 +1643,13 @@ class UsersApiTests(ModuleStoreTestCase): ...@@ -1642,9 +1643,13 @@ class UsersApiTests(ModuleStoreTestCase):
grade_dict = {'value': points_scored, 'max_value': points_possible, 'user_id': user.id} grade_dict = {'value': points_scored, 'max_value': points_possible, 'user_id': user.id}
module.system.publish(module, 'grade', grade_dict) module.system.publish(module, 'grade', grade_dict)
with mock.patch('api_manager.users.views._recalculate_grade') as recalculate_grade:
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))
response = self.do_get(test_uri) response = self.do_get(test_uri)
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
# grades should be calculate in score_changed signal handler of gradebook app
# make sure grades are not recalculated when calling user grades API
self.assertFalse(recalculate_grade.called)
courseware_summary = response.data['courseware_summary'] courseware_summary = response.data['courseware_summary']
self.assertEqual(len(courseware_summary), 2) self.assertEqual(len(courseware_summary), 2)
......
...@@ -187,10 +187,11 @@ def _manage_role(course_descriptor, user, role, action): ...@@ -187,10 +187,11 @@ def _manage_role(course_descriptor, user, role, action):
pass pass
def _recalculate_grade(request, student, course_descriptor): def _recalculate_grade(request, student, course_id):
""" """
Helper method for recalculating gradebook data Helper method for recalculating gradebook data
""" """
course_descriptor, course_key, course_content = get_course(request, student, course_id, depth=None) # pylint: disable=W0612
progress_summary = grades.progress_summary(student, request, course_descriptor, locators_as_strings=True) # pylint: disable=unused-variable progress_summary = grades.progress_summary(student, request, course_descriptor, locators_as_strings=True) # pylint: disable=unused-variable
grade_summary = grades.grade(student, request, course_descriptor) grade_summary = grades.grade(student, request, course_descriptor)
grading_policy = course_descriptor.grading_policy grading_policy = course_descriptor.grading_policy
...@@ -1046,14 +1047,14 @@ class UsersCoursesGradesDetail(SecureAPIView): ...@@ -1046,14 +1047,14 @@ class UsersCoursesGradesDetail(SecureAPIView):
except ObjectDoesNotExist: except ObjectDoesNotExist:
return Response({}, status=status.HTTP_404_NOT_FOUND) return Response({}, status=status.HTTP_404_NOT_FOUND)
# @TODO: Add authorization check here once we get caller identity course_key = get_course_key(course_id)
# Only student can get his/her own information *or* course staff
# can get everyone's grades if not CourseEnrollment.is_enrolled(student, course_key):
# get the full course tree with depth=None which reduces the number of return Response(
# round trips to the database {
course_descriptor, course_key, course_content = get_course(request, student, course_id, depth=None) # pylint: disable=W0612 'message': _("Student not enrolled in given course")
if not course_descriptor: }, status=status.HTTP_404_NOT_FOUND
return Response({}, status=status.HTTP_404_NOT_FOUND) )
queryset = StudentGradebook.objects.filter( queryset = StudentGradebook.objects.filter(
user=student, user=student,
...@@ -1071,7 +1072,7 @@ class UsersCoursesGradesDetail(SecureAPIView): ...@@ -1071,7 +1072,7 @@ class UsersCoursesGradesDetail(SecureAPIView):
grade_summary = json.loads(gradebook_entry.grade_summary) grade_summary = json.loads(gradebook_entry.grade_summary)
grading_policy = json.loads(gradebook_entry.grading_policy) grading_policy = json.loads(gradebook_entry.grading_policy)
else: else:
gradebook_values = _recalculate_grade(request, student, course_descriptor) gradebook_values = _recalculate_grade(request, student, course_id)
current_grade = gradebook_values["current_grade"] current_grade = gradebook_values["current_grade"]
proforma_grade = gradebook_values["proforma_grade"] proforma_grade = gradebook_values["proforma_grade"]
progress_summary = gradebook_values["progress_summary"] progress_summary = gradebook_values["progress_summary"]
...@@ -1084,14 +1085,7 @@ class UsersCoursesGradesDetail(SecureAPIView): ...@@ -1084,14 +1085,7 @@ class UsersCoursesGradesDetail(SecureAPIView):
gradebook_entry.grading_policy = json.dumps(grading_policy, cls=EdxJSONEncoder) gradebook_entry.grading_policy = json.dumps(grading_policy, cls=EdxJSONEncoder)
gradebook_entry.save() gradebook_entry.save()
else: else:
if not CourseEnrollment.is_enrolled(student, course_key): gradebook_values = _recalculate_grade(request, student, course_id)
return Response(
{
'message': _("Student not enrolled in given course")
}, status=status.HTTP_404_NOT_FOUND
)
gradebook_values = _recalculate_grade(request, student, course_descriptor)
current_grade = gradebook_values["current_grade"] current_grade = gradebook_values["current_grade"]
proforma_grade = gradebook_values["proforma_grade"] proforma_grade = gradebook_values["proforma_grade"]
progress_summary = gradebook_values["progress_summary"] progress_summary = gradebook_values["progress_summary"]
......
...@@ -3,66 +3,36 @@ Signal handlers supporting various gradebook use cases ...@@ -3,66 +3,36 @@ 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.signals import score_changed from courseware.signals import score_changed
from xmodule.modulestore import EdxJSONEncoder
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
from gradebook.models import StudentGradebook, StudentGradebookHistory
from edx_notifications.lib.publisher import ( from edx_notifications.lib.publisher import (
publish_notification_to_user, publish_notification_to_user,
get_notification_type get_notification_type
) )
from edx_notifications.data import NotificationMessage from edx_notifications.data import NotificationMessage
from gradebook.models import StudentGradebook, StudentGradebookHistory
from gradebook.tasks import update_user_gradebook
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@receiver(score_changed, dispatch_uid="lms.courseware.score_changed") @receiver(score_changed, dispatch_uid="lms.courseware.score_changed")
def on_score_changed(sender, **kwargs): def on_score_changed(sender, **kwargs):
""" """
Listens for a 'score_changed' signal and when observed Listens for a 'score_changed' signal invoke grade book update task
recalculates the specified user's gradebook entry
""" """
from courseware.views import get_course
user = kwargs['user'] user = kwargs['user']
course_key = kwargs['course_key'] course_key = kwargs['course_key']
course_descriptor = get_course(course_key, depth=None) update_user_gradebook.delay(course_key, user)
request = RequestMockWithoutMiddleware().get('/')
request.user = user
progress_summary = grades.progress_summary(user, request, course_descriptor, locators_as_strings=True)
grade_summary = grades.grade(user, request, course_descriptor)
grading_policy = course_descriptor.grading_policy
grade = grade_summary['percent']
proforma_grade = grades.calculate_proforma_grade(grade_summary, grading_policy)
try:
gradebook_entry = StudentGradebook.objects.get(user=user, course_id=course_key)
if gradebook_entry.grade != grade:
gradebook_entry.grade = 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()
except StudentGradebook.DoesNotExist:
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)
......
"""
This module has implementation of celery tasks for learner gradebook use cases
"""
import json
import logging
from celery.task import task # pylint: disable=import-error,no-name-in-module
from courseware import grades
from xmodule.modulestore import EdxJSONEncoder
from util.request import RequestMockWithoutMiddleware
from gradebook.models import StudentGradebook
log = logging.getLogger('edx.celery.task')
@task(name=u'lms.djangoapps.gradebook.tasks.update_user_gradebook')
def update_user_gradebook(course_key, user):
"""
Taks to recalculate user's gradebook entry
"""
try:
_generate_user_gradebook(course_key, user)
except Exception as ex:
log.exception('An error occurred while generating gradebook: %s', ex.message)
raise
def _generate_user_gradebook(course_key, user):
"""
Recalculates the specified user's gradebook entry
"""
# import is local to avoid recursive import
from courseware.views import get_course
course_descriptor = get_course(course_key, depth=None)
request = RequestMockWithoutMiddleware().get('/')
request.user = user
progress_summary = grades.progress_summary(user, request, course_descriptor, locators_as_strings=True)
grade_summary = grades.grade(user, request, course_descriptor)
grading_policy = course_descriptor.grading_policy
grade = grade_summary['percent']
proforma_grade = grades.calculate_proforma_grade(grade_summary, grading_policy)
try:
gradebook_entry = StudentGradebook.objects.get(user=user, course_id=course_key)
if gradebook_entry.grade != grade:
gradebook_entry.grade = 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()
except StudentGradebook.DoesNotExist:
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)
)
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