From 4edbbbb66f0a0a7f941255e0c6d0b05e39ebebd5 Mon Sep 17 00:00:00 2001 From: Simon Chen Date: Fri, 26 Oct 2018 11:10:47 -0400 Subject: [PATCH] Update the gradebook functionality to allow grades update. The override modal is now able to update grades for all users for each gradable unit in the course --- .../_gradebook_modal_table.underscore | 1 + lms/djangoapps/grades/api/v1/views.py | 40 +++++++++++----- .../docs/decisions/0001-gradebook-api.rst | 21 +++++--- lms/static/js/writable_gradebook.js | 48 +++++++++++-------- .../instructor/_writable_gradebook.scss | 4 ++ 5 files changed, 77 insertions(+), 37 deletions(-) diff --git a/common/static/common/templates/gradebook/_gradebook_modal_table.underscore b/common/static/common/templates/gradebook/_gradebook_modal_table.underscore index a739818e3b..132eba604e 100644 --- a/common/static/common/templates/gradebook/_gradebook_modal_table.underscore +++ b/common/static/common/templates/gradebook/_gradebook_modal_table.underscore @@ -36,6 +36,7 @@ +
diff --git a/lms/djangoapps/grades/api/v1/views.py b/lms/djangoapps/grades/api/v1/views.py index 98f0b4b758..cc505d7778 100644 --- a/lms/djangoapps/grades/api/v1/views.py +++ b/lms/djangoapps/grades/api/v1/views.py @@ -12,6 +12,7 @@ from rest_framework.generics import GenericAPIView from rest_framework.pagination import CursorPagination from rest_framework.response import Response from six import text_type +from util.date_utils import to_timestamp from courseware.courses import get_course_with_access from edx_rest_framework_extensions import permissions @@ -22,16 +23,23 @@ from lms.djangoapps.grades.config.waffle import waffle_flags, WRITABLE_GRADEBOOK from lms.djangoapps.grades.constants import ScoreDatabaseTableEnum from lms.djangoapps.grades.course_data import CourseData from lms.djangoapps.grades.course_grade_factory import CourseGradeFactory +from lms.djangoapps.grades.events import SUBSECTION_GRADE_CALCULATED, subsection_grade_calculated from lms.djangoapps.grades.models import PersistentSubsectionGrade, PersistentSubsectionGradeOverride from lms.djangoapps.grades.signals import signals from lms.djangoapps.grades.subsection_grade import CreateSubsectionGrade +from lms.djangoapps.grades.tasks import recalculate_subsection_grade_v3 from opaque_keys import InvalidKeyError from opaque_keys.edx.keys import CourseKey, UsageKey from openedx.core.djangoapps.content.course_overviews.models import CourseOverview from openedx.core.lib.api.authentication import OAuth2AuthenticationAllowInactiveUser from openedx.core.lib.api.view_utils import DeveloperErrorViewMixin from student.models import CourseEnrollment - +from track.event_transaction_utils import ( + create_new_event_transaction_id, + get_event_transaction_id, + get_event_transaction_type, + set_event_transaction_type +) log = logging.getLogger(__name__) USER_MODEL = get_user_model() @@ -767,17 +775,27 @@ class GradebookBulkUpdateView(GradeViewMixin, GenericAPIView): grade=subsection_grade_model, defaults=self._clean_override_data(override_data), ) - signals.SUBSECTION_OVERRIDE_CHANGED.send( - sender=None, - user_id=subsection_grade_model.user_id, - course_id=text_type(subsection_grade_model.course_id), - usage_id=text_type(subsection_grade_model.usage_key), - only_if_higher=False, - modified=override.modified, - score_deleted=False, - score_db_table=ScoreDatabaseTableEnum.overrides, - force_update_subsections=True, + + set_event_transaction_type(SUBSECTION_GRADE_CALCULATED) + create_new_event_transaction_id() + + recalculate_subsection_grade_v3.apply( + kwargs=dict( + user_id=subsection_grade_model.user_id, + anonymous_user_id=None, + course_id=text_type(subsection_grade_model.course_id), + usage_id=text_type(subsection_grade_model.usage_key), + only_if_higher=False, + expected_modified_time=to_timestamp(override.modified), + score_deleted=False, + event_transaction_id=unicode(get_event_transaction_id()), + event_transaction_type=unicode(get_event_transaction_type()), + score_db_table=ScoreDatabaseTableEnum.overrides, + force_update_subsections=True, + ) ) + # Emit events to let our tracking system to know we updated subsection grade + subsection_grade_calculated(subsection_grade_model) def _clean_override_data(self, override_data): """ diff --git a/lms/djangoapps/grades/docs/decisions/0001-gradebook-api.rst b/lms/djangoapps/grades/docs/decisions/0001-gradebook-api.rst index 9b930c4865..39db22d19e 100644 --- a/lms/djangoapps/grades/docs/decisions/0001-gradebook-api.rst +++ b/lms/djangoapps/grades/docs/decisions/0001-gradebook-api.rst @@ -71,11 +71,18 @@ Decisions d. A status code of ``422`` will be returned for requests that contain any failed item. This allows a client to easily tell if any item in their request payload was problematic and needs special handling. If all - requested items succeed, a ``202 (accepted)`` is returned. This status code was chosen because an - asynchronous celery task is enqueued for each subsection grade that needs to be updated. + requested items succeed, a ``202 (accepted)`` is returned. This status code was chosen because a + celery task is enqueued and waited for each subsection grade that needs to be updated. - e. We have to thread a ``force_update_subsections`` keyword argument through the Django signal invocation - that enqueues the subsection update task. This is because we may be creating a new subsection grade - with no score data available from either ``courseware.StudentModule`` records or from the `Submissions` API. - In this case, the only score data available exists in the grade override record, and the subsection ``update()`` - call should be forced to read from this record. + e. We have to thread a ``force_update_subsections`` keyword argument into the subsection update task that + we enqueue. This is because we may be creating a new subsection grade with no score data available from + either ``courseware.StudentModule`` records or from the `Submissions` API. In this case, the only score + data available exists in the grade override record, and the subsection ``update()`` call should be forced + to read from this record. + + f. We have to synchronously update each grade record for each user in this endpoint. This means the request + will be left open for longer period than we wanted. The reason is: the primary consumer gradebook UI + would need to display the updated grade result for all users, after update is complete. If we do update + asynchronously, the gradebook UI do not know how to update the table with new values, including agregations + for the user's course grade. This is the lowest effort change to address the UI display problem. We will + need to improve this mechanism as we continue to develop. diff --git a/lms/static/js/writable_gradebook.js b/lms/static/js/writable_gradebook.js index db4d9b840e..bc41ab3aa1 100644 --- a/lms/static/js/writable_gradebook.js +++ b/lms/static/js/writable_gradebook.js @@ -16,26 +16,21 @@ function _templateLoader(templateName, staticPath, callback, errorCallback) { } function courseXblockUpdater(courseID, dataToSend, visibilityData, callback, errorCallback) { - var cleanData = {'users' : {}}; - - if (dataToSend instanceof Array) - for (var i = 0; i < dataToSend.length; i++) { - cleanData.users['id_' + dataToSend.userID] = { - 'block_id' : dataToSend[i].blockID || '', - 'grade' : dataToSend[i].grade || '', - 'max_grade' : dataToSend[i].maxGrade || null, - 'state' : dataToSend[i].state || '{}', - 'user_id' : dataToSend[i].userID || '' - }; - } - else if (dataToSend instanceof Object) - cleanData.users = dataToSend; - - var postUrl = '/api/score/courses/' + courseID; - - if (!_.isEmpty(visibilityData)) - cleanData.visibility = visibilityData; + var cleanData = _.map(dataToSend, function (data) { + return { + user_id: data.user_id, + usage_id: data.block_id, + grade: { + earned_all_override: data.grade || 0, + possible_all_override: data.max_grade || 0, + earned_graded_override: data.grade || 0, + possible_graded_override: data.max_grade || 0 + } + }; + }); + var postUrl = '/api/grades/v1/gradebook/' + courseID + '/bulk-update'; + $('') $.ajax({ url: postUrl, method: 'POST', @@ -314,6 +309,7 @@ $(document).ready(function() { isManualGrading = JSON.parse($(gradeOverrideObject).attr('data-manual-grading')); $modal.find('.assignment-name-placeholder').text(assignmentName); $modal.find('.block-id-placeholder').text(blockID); + $modal.find('.grade-override-info-container').hide(); if ( _.isEmpty(userAutoGrades) ) { $tableWrapper.hide(); $manualGradeVisibilityWrapper.toggle(false); @@ -486,6 +482,18 @@ $(document).ready(function() { }); } + function setInfoMessage(messageText){ + var $messageField = $('.grade-override-modal').find('.grade-override-info-container'); + if(messageText) { + $messageField.text(messageText); + $messageField.show(); + } + else { + $messageField.empty(); + $messageField.hide(); + } + } + $(document).on('click', '.grade-override-modal-save', function() { var visibilityData = {}; if (isManualGrading) { @@ -499,12 +507,14 @@ $(document).ready(function() { return; var validStatus = ValidateAdjustedGradesData(); if (validStatus) { + setInfoMessage(gettext('Update in progress, please wait...')); courseXblockUpdater( courseID, adjustedGradesData, visibilityData, function(data){ gradebookOverrideModalReset(); + setInfoMessage(); renderAllGradebook = false; gradeBookData = []; $gradesTableWrapper.empty(); diff --git a/lms/static/sass/course/instructor/_writable_gradebook.scss b/lms/static/sass/course/instructor/_writable_gradebook.scss index d3c7be5ead..a95130af61 100644 --- a/lms/static/sass/course/instructor/_writable_gradebook.scss +++ b/lms/static/sass/course/instructor/_writable_gradebook.scss @@ -74,6 +74,10 @@ .grade-override-menu-buttons { padding: 10px; } + .grade-override-info-container{ + margin: 10px; + font-size: $small-font-size; + } } }