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
This commit is contained in:
@@ -36,6 +36,7 @@
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="grade-override-info-container alert alert-info pattern-library-shim" role="alert"></div>
|
||||
<div class="grade-override-menu-buttons">
|
||||
<button class="btn grade-override-modal-save"><%- strLib.save %></button>
|
||||
<button class="btn grade-override-modal-close"><%- strLib.cancel %></button>
|
||||
|
||||
@@ -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):
|
||||
"""
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -74,6 +74,10 @@
|
||||
.grade-override-menu-buttons {
|
||||
padding: 10px;
|
||||
}
|
||||
.grade-override-info-container{
|
||||
margin: 10px;
|
||||
font-size: $small-font-size;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user